import Foundation

public enum HybridMarkdownLineKind: Hashable, Sendable {
    case paragraph
    case heading(level: Int, markerRange: NSRange, textRange: NSRange)
}

public enum HybridMarkdownSpanKind: Hashable, Sendable {
    case bold
    case italic
    case inlineCode
    case markdownDelimiter
}

public struct HybridMarkdownSpan: Hashable, Sendable {
    public var range: NSRange
    public var kind: HybridMarkdownSpanKind

    public init(range: NSRange, kind: HybridMarkdownSpanKind) {
        self.range = range
        self.kind = kind
    }
}

public struct HybridMarkdownLineRenderPlan: Hashable, Sendable {
    public var line: EditorLine
    public var kind: HybridMarkdownLineKind
    public var spans: [HybridMarkdownSpan]

    public init(line: EditorLine, kind: HybridMarkdownLineKind, spans: [HybridMarkdownSpan]) {
        self.line = line
        self.kind = kind
        self.spans = spans
    }
}

public struct HybridMarkdownLineRenderer: Sendable {
    public init() {}

    public func renderPlan(for line: EditorLine) -> HybridMarkdownLineRenderPlan {
        let kind = lineKind(for: line)
        let spans = inlineSpans(in: line)
        return HybridMarkdownLineRenderPlan(line: line, kind: kind, spans: spans)
    }

    private func lineKind(for line: EditorLine) -> HybridMarkdownLineKind {
        let markerCount = line.source.prefix { $0 == "#" }.count
        guard (1...6).contains(markerCount),
              line.source.dropFirst(markerCount).first == " "
        else {
            return .paragraph
        }

        let textOffset = markerCount + 1
        return .heading(
            level: markerCount,
            markerRange: NSRange(location: line.range.location, length: markerCount),
            textRange: NSRange(
                location: line.range.location + textOffset,
                length: max(0, line.range.length - textOffset)
            )
        )
    }

    private func inlineSpans(in line: EditorLine) -> [HybridMarkdownSpan] {
        guard line.range.length > 0 else { return [] }

        var spans: [HybridMarkdownSpan] = []
        var excludedRanges: [NSRange] = []

        collectMatches("`([^`\\n]+)`", in: line, excluding: excludedRanges).forEach { match in
            spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .inlineCode))
            spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
            excludedRanges.append(match.fullRange)
        }

        collectMatches("\\*\\*([^*\\n]+)\\*\\*", in: line, excluding: excludedRanges).forEach { match in
            spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .bold))
            spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2))
            excludedRanges.append(match.fullRange)
        }

        collectMatches("(?<!\\*)\\*([^*\\n]+)\\*(?!\\*)", in: line, excluding: excludedRanges).forEach { match in
            spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .italic))
            spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1))
            excludedRanges.append(match.fullRange)
        }

        return spans.sorted {
            if $0.range.location == $1.range.location {
                return $0.range.length < $1.range.length
            }
            return $0.range.location < $1.range.location
        }
    }

    private func collectMatches(
        _ pattern: String,
        in line: EditorLine,
        excluding excludedRanges: [NSRange]
    ) -> [InlineMatch] {
        guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
        return regex.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count))
            .compactMap { match in
                guard match.numberOfRanges > 1 else { return nil }
                let shiftedRange = NSRange(
                    location: line.range.location + match.range.location,
                    length: match.range.length
                )
                guard !excludedRanges.contains(where: { $0.intersects(shiftedRange) }) else { return nil }
                return InlineMatch(
                    fullRange: shiftedRange,
                    contentRange: NSRange(
                        location: line.range.location + match.range(at: 1).location,
                        length: match.range(at: 1).length
                    )
                )
            }
    }

    private func delimiterRanges(
        _ fullRange: NSRange,
        leading: Int,
        trailing: Int
    ) -> [HybridMarkdownSpan] {
        [
            HybridMarkdownSpan(range: NSRange(location: fullRange.location, length: leading), kind: .markdownDelimiter),
            HybridMarkdownSpan(
                range: NSRange(location: fullRange.upperBound - trailing, length: trailing),
                kind: .markdownDelimiter
            )
        ]
    }
}

private struct InlineMatch {
    var fullRange: NSRange
    var contentRange: NSRange
}

private extension NSRange {
    var upperBound: Int {
        location + length
    }

    func intersects(_ other: NSRange) -> Bool {
        location < other.upperBound && other.location < upperBound
    }
}
