import UIKit
import CoreText

// MARK: - Telegram-Exact Text Layout (Pre-calculated)

/// Pre-calculated text layout using Telegram's exact CoreText pipeline.
///
/// Telegram uses CTTypesetter (NOT CTFramesetter) for manual line breaking,
/// custom inter-line spacing (12% of fontLineHeight), and CTRunDraw for rendering.
/// UILabel/TextKit produce different line breaks and density — this class
/// reproduces Telegram's exact algorithm from:
///   - InteractiveTextComponent.swift (lines 1480-1548) — line breaking
///   - TextNode.swift (lines 1723-1726) — font metrics & line spacing
///   - InteractiveTextComponent.swift (lines 2358-2573) — rendering
///
/// Two-phase pattern (matches Telegram asyncLayout):
///   1. `CoreTextTextLayout.calculate()` — runs on ANY thread (background-safe)
///   2. `CoreTextLabel.draw()` — runs on main thread, renders pre-calculated lines
final class CoreTextTextLayout: @unchecked Sendable {

    // MARK: - Line

    /// A single laid-out line with position and metrics.
    struct Line {
        let ctLine: CTLine
        let origin: CGPoint       // Top-left corner in UIKit (top-down) coordinates
        let width: CGFloat        // Typographic advance width (CTLineGetTypographicBounds)
        let ascent: CGFloat       // Distance from baseline to top of tallest glyph
        let descent: CGFloat      // Distance from baseline to bottom of lowest glyph
        let stringRange: NSRange  // Character range this line covers in the original string
    }

    // MARK: - Link Detection

    /// A detected URL with bounding rects for hit testing.
    struct LinkInfo {
        let url: URL
        let range: NSRange
        var rects: [CGRect]
    }

    // MARK: - Mention Detection

    /// A detected @username with bounding rects for hit testing.
    struct MentionInfo {
        let username: String   // without the "@" prefix
        let range: NSRange     // range of "@username" in the original string
        var rects: [CGRect]    // populated during layout
    }

    /// Cached mention regex (compilation is expensive).
    private static let mentionDetector: NSRegularExpression? = {
        // Match @username at start of string or after whitespace.
        // Username: 1-32 alphanumeric or underscore characters.
        try? NSRegularExpression(pattern: "(?:^|(?<=\\s))@([A-Za-z0-9_]{1,32})", options: [])
    }()

    /// TLD whitelist — desktop parity (desktop/app/constants.ts lines 38-63).
    static let allowedTLDs: Set<String> = [
        "com", "ru", "ua", "org", "net", "edu", "gov", "io", "tech", "info",
        "biz", "me", "online", "site", "app", "dev", "chat", "gg", "fm", "tv",
        "im", "sc", "su", "by"
    ]

    /// Cached data detector (regex compilation is expensive).
    private static let linkDetector: NSDataDetector? = {
        try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
    }()

    /// Link color matching Telegram accent blue (#3390EC).
    static let linkColor = UIColor(red: 0x33/255, green: 0x90/255, blue: 0xEC/255, alpha: 1)

    // MARK: - Properties

    let lines: [Line]
    let size: CGSize              // Bounding box (ceil'd max-line-width × total height)
    let lastLineWidth: CGFloat    // Width of the final line — for inline timestamp decisions
    let lastLineHasRTL: Bool
    let lastLineHasBlockQuote: Bool
    let textColor: UIColor
    let links: [LinkInfo]
    let mentions: [MentionInfo]

    private init(
        lines: [Line],
        size: CGSize,
        lastLineWidth: CGFloat,
        lastLineHasRTL: Bool,
        lastLineHasBlockQuote: Bool,
        textColor: UIColor,
        links: [LinkInfo] = [],
        mentions: [MentionInfo] = []
    ) {
        self.lines = lines
        self.size = size
        self.lastLineWidth = lastLineWidth
        self.lastLineHasRTL = lastLineHasRTL
        self.lastLineHasBlockQuote = lastLineHasBlockQuote
        self.textColor = textColor
        self.links = links
        self.mentions = mentions
    }

    /// Returns the URL at the given point, or nil if no link at that position.
    func linkAt(point: CGPoint) -> URL? {
        for link in links {
            for rect in link.rects {
                if rect.insetBy(dx: -4, dy: -4).contains(point) {
                    return link.url
                }
            }
        }
        return nil
    }

    /// Returns the username at the given point, or nil if no mention at that position.
    func mentionAt(point: CGPoint) -> String? {
        for mention in mentions {
            for rect in mention.rects {
                if rect.insetBy(dx: -4, dy: -4).contains(point) {
                    return mention.username
                }
            }
        }
        return nil
    }

    // MARK: - Telegram Line Spacing

    /// Telegram default: 12% of font line height.
    /// Source: TextNode.swift line 277, InteractiveTextComponent.swift line 299.
    static let telegramLineSpacingFactor: CGFloat = 0.12

    // MARK: - Layout Calculation (Thread-Safe)

    /// Calculate text layout using Telegram's exact algorithm.
    ///
    /// Algorithm (InteractiveTextComponent.swift lines 1480-1548):
    /// 1. `CTTypesetterCreateWithAttributedString` — create typesetter
    /// 2. Loop: `CTTypesetterSuggestLineBreak` → `CTTypesetterCreateLine`
    /// 3. `CTLineGetTypographicBounds` for per-line ascent/descent/width
    /// 4. Accumulate height with `floor(fontLineHeight * 0.12)` inter-line spacing
    ///
    /// - Parameters:
    ///   - text: Raw message text
    ///   - maxWidth: Maximum line width constraint (bubbleW - leftPad - rightPad)
    ///   - font: Text font (default: system 17pt, matching Telegram)
    ///   - textColor: Foreground color baked into attributed string
    ///   - lineSpacingFactor: Inter-line spacing as fraction of font line height (default: 0.12)
    /// - Returns: Pre-calculated layout with lines, size, and lastLineWidth
    static func calculate(
        text: String,
        maxWidth: CGFloat,
        font: UIFont = .systemFont(ofSize: 17),
        textColor: UIColor = .white,
        linkColor: UIColor = CoreTextTextLayout.linkColor,
        lineSpacingFactor: CGFloat = telegramLineSpacingFactor
    ) -> CoreTextTextLayout {
        // Guard: empty text, non-positive width, or NaN → return zero layout
        let safeMaxWidth = maxWidth.isNaN ? 100 : max(maxWidth, 10)
        guard !text.isEmpty, safeMaxWidth >= 10 else {
            return CoreTextTextLayout(
                lines: [],
                size: .zero,
                lastLineWidth: 0,
                lastLineHasRTL: false,
                lastLineHasBlockQuote: false,
                textColor: textColor
            )
        }

        // ── Attributed string (Telegram: StringWithAppliedEntities.swift) ──
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: textColor
        ]
        let attrString = NSMutableAttributedString(string: text, attributes: attributes)
        let stringLength = attrString.length

        // ── Link detection (desktop parity: TextParser.tsx + constants.ts TLD whitelist) ──
        var detectedLinks: [(url: URL, range: NSRange)] = []
        if let detector = linkDetector {
            let fullRange = NSRange(location: 0, length: stringLength)
            detector.enumerateMatches(in: text, options: [], range: fullRange) { result, _, _ in
                guard let result, let url = result.url else { return }
                let host = url.host?.lowercased() ?? ""
                let tld = host.split(separator: ".").last.map(String.init) ?? ""
                guard allowedTLDs.contains(tld) else { return }
                attrString.addAttributes([
                    .foregroundColor: linkColor as UIColor,
                    .underlineStyle: NSUnderlineStyle.single.rawValue
                ], range: result.range)
                detectedLinks.append((url: url, range: result.range))
            }
        }

        // ── Mention detection (@username — desktop/Android parity) ──
        var detectedMentions: [(username: String, range: NSRange)] = []
        if let mentionRegex = mentionDetector {
            let fullRange = NSRange(location: 0, length: stringLength)
            mentionRegex.enumerateMatches(in: text, options: [], range: fullRange) { result, _, _ in
                guard let result else { return }
                let matchRange = result.range           // full "@username"
                let usernameRange = result.range(at: 1) // "username" capture group
                guard usernameRange.location != NSNotFound else { return }
                // Skip if overlaps with a detected link
                let overlapsLink = detectedLinks.contains { link in
                    NSIntersectionRange(link.range, matchRange).length > 0
                }
                guard !overlapsLink else { return }
                let username = (text as NSString).substring(with: usernameRange)
                // Blue color + underline (Telegram parity)
                attrString.addAttribute(.foregroundColor, value: linkColor as UIColor, range: matchRange)
                attrString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: matchRange)
                attrString.addAttribute(.underlineColor, value: linkColor as UIColor, range: matchRange)
                detectedMentions.append((username: username, range: matchRange))
            }
        }

        // ── Typesetter (Telegram: InteractiveTextComponent line 1481) ──
        let typesetter = CTTypesetterCreateWithAttributedString(attrString as CFAttributedString)

        // ── Font metrics (Telegram: TextNode.swift lines 1723-1726) ──
        let ctFont = font as CTFont
        let fontAscent = CTFontGetAscent(ctFont)
        let fontDescent = CTFontGetDescent(ctFont)
        let fontLineHeight = floor(fontAscent + fontDescent)
        let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor)

        var resultLines: [Line] = []
        var currentIndex: CFIndex = 0
        var currentY: CGFloat = 0
        var maxLineWidth: CGFloat = 0
        var lastLineWidth: CGFloat = 0
        var lastLineRange = NSRange(location: 0, length: 0)

        // ── Line breaking loop (Telegram: InteractiveTextComponent lines 1490-1548) ──
        // Safety: cap iterations to stringLength to prevent infinite loop if
        // CTTypesetterSuggestLineBreak returns non-advancing counts.
        var iterations = 0
        while currentIndex < stringLength {
            iterations += 1
            if iterations > stringLength + 1 { break } // infinite loop guard

            // Suggest line break (word boundary)
            let lineCharCount = CTTypesetterSuggestLineBreak(
                typesetter, currentIndex, Double(safeMaxWidth)
            )
            guard lineCharCount > 0 else { break }

            // Create line from typesetter
            let ctLine = CTTypesetterCreateLine(
                typesetter, CFRange(location: currentIndex, length: lineCharCount)
            )

            // Measure line (Telegram: CTLineGetTypographicBounds)
            var lineAscent: CGFloat = 0
            var lineDescent: CGFloat = 0
            let lineWidth = CGFloat(CTLineGetTypographicBounds(
                ctLine, &lineAscent, &lineDescent, nil
            ))

            // Guard against NaN from CoreText (observed with certain Cyrillic strings)
            guard !lineWidth.isNaN, !lineAscent.isNaN, !lineDescent.isNaN else { break }

            let clampedWidth = min(lineWidth, safeMaxWidth)

            // Inter-line spacing (applied BETWEEN lines, not before first)
            if !resultLines.isEmpty {
                currentY += fontLineSpacing
            }

            resultLines.append(Line(
                ctLine: ctLine,
                origin: CGPoint(x: 0, y: currentY),
                width: clampedWidth,
                ascent: lineAscent,
                descent: lineDescent,
                stringRange: NSRange(location: currentIndex, length: lineCharCount)
            ))

            // Advance by font line height (Telegram uses font-level, not per-line)
            currentY += fontLineHeight

            maxLineWidth = max(maxLineWidth, clampedWidth)
            lastLineWidth = clampedWidth
            lastLineRange = NSRange(location: currentIndex, length: lineCharCount)
            currentIndex += lineCharCount
        }

        let nsText = text as NSString
        let safeLastRange = NSIntersectionRange(lastLineRange, NSRange(location: 0, length: nsText.length))
        let lastLineText = safeLastRange.length > 0 ? nsText.substring(with: safeLastRange) : ""
        let lastLineHasRTL = containsRTLCharacters(in: lastLineText)
        let lastLineHasBlockQuote = lastLineText
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .hasPrefix(">")

        // ── Compute link bounding rects ──
        var linkInfos: [LinkInfo] = []
        for detected in detectedLinks {
            var rects: [CGRect] = []
            for line in resultLines {
                let overlap = NSIntersectionRange(line.stringRange, detected.range)
                guard overlap.length > 0 else { continue }
                let lineStartInLink = overlap.location - line.stringRange.location
                let lineEndInLink = lineStartInLink + overlap.length
                var xStart: CGFloat = 0
                var xEnd: CGFloat = 0
                // CTLineGetOffsetForStringIndex uses UTF-16 offsets relative to line start
                xStart = CGFloat(CTLineGetOffsetForStringIndex(
                    line.ctLine, overlap.location, nil
                ))
                xEnd = CGFloat(CTLineGetOffsetForStringIndex(
                    line.ctLine, overlap.location + overlap.length, nil
                ))
                if xEnd < xStart { swap(&xStart, &xEnd) }
                let lineH = line.ascent + line.descent
                rects.append(CGRect(x: xStart, y: line.origin.y, width: xEnd - xStart, height: lineH))
            }
            if !rects.isEmpty {
                linkInfos.append(LinkInfo(url: detected.url, range: detected.range, rects: rects))
            }
        }

        // ── Compute mention bounding rects (same algorithm as links) ──
        var mentionInfos: [MentionInfo] = []
        for detected in detectedMentions {
            var rects: [CGRect] = []
            for line in resultLines {
                let overlap = NSIntersectionRange(line.stringRange, detected.range)
                guard overlap.length > 0 else { continue }
                var xStart = CGFloat(CTLineGetOffsetForStringIndex(
                    line.ctLine, overlap.location, nil
                ))
                var xEnd = CGFloat(CTLineGetOffsetForStringIndex(
                    line.ctLine, overlap.location + overlap.length, nil
                ))
                if xEnd < xStart { swap(&xStart, &xEnd) }
                let lineH = line.ascent + line.descent
                rects.append(CGRect(x: xStart, y: line.origin.y, width: xEnd - xStart, height: lineH))
            }
            if !rects.isEmpty {
                mentionInfos.append(MentionInfo(username: detected.username, range: detected.range, rects: rects))
            }
        }

        return CoreTextTextLayout(
            lines: resultLines,
            size: CGSize(width: ceil(maxLineWidth), height: ceil(currentY)),
            lastLineWidth: ceil(lastLineWidth),
            lastLineHasRTL: lastLineHasRTL,
            lastLineHasBlockQuote: lastLineHasBlockQuote,
            textColor: textColor,
            links: linkInfos,
            mentions: mentionInfos
        )
    }

    private static func containsRTLCharacters(in text: String) -> Bool {
        for scalar in text.unicodeScalars {
            switch scalar.value {
            case 0x0590...0x08FF, 0xFB1D...0xFDFD, 0xFE70...0xFEFC:
                return true
            default:
                continue
            }
        }
        return false
    }
}

// MARK: - CoreText Label (Custom Rendering View)

/// Custom UIView that renders `CoreTextTextLayout` via CoreText.
/// Drop-in replacement for UILabel in message body text rendering.
///
/// Rendering matches Telegram (InteractiveTextComponent lines 2358-2573):
/// - Flips text matrix `CGAffineTransform(scaleX: 1.0, y: -1.0)` for UIKit coords
/// - Positions each line at its baseline via `context.textPosition`
/// - Draws each CTRun individually via `CTRunDraw`
final class CoreTextLabel: UIView {

    /// Pre-calculated layout to render. Setting triggers redraw.
    var textLayout: CoreTextTextLayout? {
        didSet { setNeedsDisplay() }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        isOpaque = false
        backgroundColor = .clear
        contentMode = .redraw  // Redraw on bounds change
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError() }

    // MARK: - Rendering

    override func draw(_ rect: CGRect) {
        guard let layout = textLayout,
              let context = UIGraphicsGetCurrentContext()
        else { return }

        // Save context state
        let savedTextMatrix = context.textMatrix
        let savedTextPosition = context.textPosition

        // Flip text matrix for UIKit coordinates (Telegram: scaleX: 1.0, y: -1.0).
        // UIKit context has origin top-left (Y down). CoreText expects bottom-left (Y up).
        // Flipping the text matrix makes glyphs render right-side-up in UIKit.
        context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)

        for line in layout.lines {
            // Baseline position in UIKit coordinates:
            //   line.origin.y = top of line frame
            //   + line.ascent  = baseline (distance from top to baseline)
            // Telegram: context.textPosition = CGPoint(x: minX, y: maxY - descent)
            //   which equals origin.y + ascent (since maxY = origin.y + ascent + descent)
            let baselineY = line.origin.y + line.ascent
            context.textPosition = CGPoint(x: line.origin.x, y: baselineY)

            // Draw each glyph run (Telegram: CTRunDraw per run)
            let glyphRuns = CTLineGetGlyphRuns(line.ctLine) as! [CTRun]
            for run in glyphRuns {
                CTRunDraw(run, context, CFRangeMake(0, 0))

                // Draw underline for link runs (CTRunDraw doesn't render underlines)
                let attrs = CTRunGetAttributes(run) as? [NSAttributedString.Key: Any] ?? [:]
                if let underline = attrs[.underlineStyle] as? Int, underline != 0,
                   let color = attrs[.foregroundColor] as? UIColor {
                    var runAscent: CGFloat = 0
                    let runWidth = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, nil, nil))
                    var origin = CGPoint.zero
                    CTRunGetPositions(run, CFRangeMake(0, 1), &origin)
                    let underlineY = baselineY + 2
                    context.setStrokeColor(color.cgColor)
                    context.setLineWidth(0.5)
                    context.move(to: CGPoint(x: line.origin.x + origin.x, y: underlineY))
                    context.addLine(to: CGPoint(x: line.origin.x + origin.x + runWidth, y: underlineY))
                    context.strokePath()
                }
            }
        }

        // Restore context state
        context.textMatrix = savedTextMatrix
        context.textPosition = savedTextPosition
    }
}
