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 {

    // 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
    }

    // 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

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

    // 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,
        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 = NSAttributedString(string: text, attributes: attributes)
        let stringLength = attrString.length

        // ── 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
            ))

            // 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(">")

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

    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)
            context.textPosition = CGPoint(
                x: line.origin.x,
                y: line.origin.y + line.ascent
            )

            // 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)) // 0,0 = all glyphs
            }
        }

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