import Foundation

#if os(macOS)
import AppKit

struct MarkdownPresentationSnapshot: Hashable {
    var lines: [String]

    var signature: String {
        lines.joined(separator: "\n")
    }

    static func make(
        source: String,
        activeLineIndex: Int,
        containerWidth: CGFloat = 420,
        usesRenderedControls: Bool = true
    ) -> MarkdownPresentationSnapshot {
        let storage = NSTextStorage(string: source)
        let lineIndex = DocumentLineIndex(source: source)
        MarkdownTextStyler.apply(
            to: storage,
            lineIndex: lineIndex,
            invalidationPlan: EditorDirtyLineInvalidator.plan(
                previousText: nil,
                currentLineIndex: lineIndex,
                edit: nil,
                previousActiveLineIndex: nil,
                currentActiveLineIndex: activeLineIndex
            ),
            activeLineIndex: activeLineIndex,
            backgroundColor: .textBackgroundColor,
            activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
            textColor: .labelColor,
            secondaryTextColor: .secondaryLabelColor,
            accentColor: .controlAccentColor,
            usesRenderedControls: usesRenderedControls
        )
        return make(from: storage, lineIndex: lineIndex, containerWidth: containerWidth)
    }

    static func make(
        from storage: NSTextStorage,
        lineIndex: DocumentLineIndex,
        containerWidth: CGFloat = 420
    ) -> MarkdownPresentationSnapshot {
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: NSSize(width: containerWidth, height: CGFloat.greatestFiniteMagnitude))
        textContainer.lineFragmentPadding = 0
        layoutManager.addTextContainer(textContainer)
        storage.addLayoutManager(layoutManager)
        defer {
            storage.removeLayoutManager(layoutManager)
        }

        layoutManager.ensureLayout(for: textContainer)

        var lines: [String] = []
        for boundary in lineIndex.boundaries {
            let contentRange = boundary.contentRange
            let paragraphRange = NSRange(
                location: boundary.contentRange.location,
                length: min(boundary.nextLineLocation, storage.length) - boundary.contentRange.location
            )
            let glyphRange = layoutManager.glyphRange(forCharacterRange: paragraphRange, actualCharacterRange: nil)
            let fragment = glyphRange.length > 0
                ? layoutManager.lineFragmentRect(forGlyphAt: glyphRange.location, effectiveRange: nil)
                : .zero
            let paragraphStyle = storage.attribute(.paragraphStyle, at: min(paragraphRange.location, max(0, storage.length - 1)), effectiveRange: nil) as? NSParagraphStyle
            let font = contentRange.length > 0
                ? storage.attribute(.font, at: contentRange.location, effectiveRange: nil) as? NSFont
                : nil

            lines.append([
                "line=\(boundary.index)",
                "range=\(paragraphRange.location):\(paragraphRange.length)",
                "fragment=\(rounded(fragment.minX)),\(rounded(fragment.minY)),\(rounded(fragment.width)),\(rounded(fragment.height))",
                "font=\(fontDescription(font))",
                "paragraph=\(paragraphDescription(paragraphStyle))",
                "runs=\(attributeRuns(in: storage, range: paragraphRange).joined(separator: ","))"
            ].joined(separator: "|"))
        }

        return MarkdownPresentationSnapshot(lines: lines)
    }

    static func contentOrigin(
        source: String,
        text: String,
        activeLineIndex: Int,
        containerWidth: CGFloat = 420,
        usesRenderedControls: Bool = true
    ) -> CGPoint? {
        let storage = NSTextStorage(string: source)
        let lineIndex = DocumentLineIndex(source: source)
        MarkdownTextStyler.apply(
            to: storage,
            lineIndex: lineIndex,
            invalidationPlan: EditorDirtyLineInvalidator.plan(
                previousText: nil,
                currentLineIndex: lineIndex,
                edit: nil,
                previousActiveLineIndex: nil,
                currentActiveLineIndex: activeLineIndex
            ),
            activeLineIndex: activeLineIndex,
            backgroundColor: .textBackgroundColor,
            activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
            textColor: .labelColor,
            secondaryTextColor: .secondaryLabelColor,
            accentColor: .controlAccentColor,
            usesRenderedControls: usesRenderedControls
        )

        let textRange = (source as NSString).range(of: text)
        guard textRange.location != NSNotFound else { return nil }

        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: NSSize(width: containerWidth, height: CGFloat.greatestFiniteMagnitude))
        textContainer.lineFragmentPadding = 0
        layoutManager.addTextContainer(textContainer)
        storage.addLayoutManager(layoutManager)
        defer {
            storage.removeLayoutManager(layoutManager)
        }

        layoutManager.ensureLayout(for: textContainer)
        let glyphRange = layoutManager.glyphRange(forCharacterRange: NSRange(location: textRange.location, length: 1), actualCharacterRange: nil)
        guard glyphRange.length > 0 else { return nil }

        let lineFragment = layoutManager.lineFragmentRect(forGlyphAt: glyphRange.location, effectiveRange: nil)
        let glyphLocation = layoutManager.location(forGlyphAt: glyphRange.location)
        return CGPoint(x: lineFragment.minX + glyphLocation.x, y: lineFragment.minY + glyphLocation.y)
    }

    private static func attributeRuns(in storage: NSTextStorage, range: NSRange) -> [String] {
        guard range.length > 0 else { return [] }

        var runs: [String] = []
        storage.enumerateAttributes(in: range) { attributes, effectiveRange, _ in
            let font = attributes[.font] as? NSFont
            let foreground = attributes[.foregroundColor] as? NSColor
            let background = attributes[.backgroundColor] as? NSColor
            let paragraph = attributes[.paragraphStyle] as? NSParagraphStyle
            let hidden = foreground?.alphaComponent == 0 && (font?.pointSize ?? 0) < 1
            runs.append([
                "\(effectiveRange.location):\(effectiveRange.length)",
                fontDescription(font),
                "fg=\(rounded(foreground?.alphaComponent ?? 1))",
                "bg=\(rounded(background?.alphaComponent ?? 0))",
                "hidden=\(hidden)",
                paragraphDescription(paragraph)
            ].joined(separator: "/"))
        }
        return runs
    }

    private static func fontDescription(_ font: NSFont?) -> String {
        guard let font else { return "nil" }
        let isMono = font.fontDescriptor.symbolicTraits.contains(.monoSpace)
        return "\(isMono ? "mono" : "system"):\(rounded(font.pointSize))"
    }

    private static func paragraphDescription(_ paragraph: NSParagraphStyle?) -> String {
        guard let paragraph else { return "nil" }
        return [
            "ls=\(rounded(paragraph.lineSpacing))",
            "psb=\(rounded(paragraph.paragraphSpacingBefore))",
            "ps=\(rounded(paragraph.paragraphSpacing))",
            "first=\(rounded(paragraph.firstLineHeadIndent))",
            "head=\(rounded(paragraph.headIndent))"
        ].joined(separator: ":")
    }

    private static func rounded(_ value: CGFloat) -> String {
        String(format: "%.3f", Double(value))
    }
}
#endif
