import UIKit

/// Generates stretchable bubble images using Telegram's **exact** raster approach.
///
/// Telegram draws the tail via two ellipses + `.copy` blend mode erase in a CGContext.
/// This cannot be replicated with vector paths (CAShapeLayer / UIBezierPath) because
/// non-zero winding fill rule treats CCW subpaths as winding=-1 (filled, not erased).
///
/// Source: `ChatMessageBubbleImages.swift` lines 146-428 in Telegram-iOS.
///
/// Pipeline:
///   1. Draw body (rounded rect) + tail (bottomEllipse QuadCurves + topEllipse erase) in fixed 33pt system
///   2. Mirror horizontally for incoming
///   3. Make stretchable via 9-slice (`stretchableImage(withLeftCapWidth:topCapHeight:)`)
///   4. Cache all variants (mergeType × direction)
enum BubbleImageFactory {

    // MARK: - Public

    /// Pre-rendered stretchable images for all merge types, keyed by `BubbleMergeType`.
    struct ImageSet {
        let outgoing: [BubbleMergeType: UIImage]
        let incoming: [BubbleMergeType: UIImage]

        func image(outgoing: Bool, mergeType: BubbleMergeType) -> UIImage? {
            outgoing ? self.outgoing[mergeType] : self.incoming[mergeType]
        }
    }

    /// Generate all bubble images for the given colors.
    static func generate(
        outgoingColor: UIColor,
        incomingColor: UIColor,
        mainRadius: CGFloat = 16,
        auxiliaryRadius: CGFloat = 8
    ) -> ImageSet {
        let types: [BubbleMergeType] = [
            .none,               // single message — has tail
            .top(side: false),   // first in group — no tail
            .both,               // middle in group — no tail
            .bottom,             // last in group — has tail
        ]

        var outgoing: [BubbleMergeType: UIImage] = [:]
        var incoming: [BubbleMergeType: UIImage] = [:]

        for type in types {
            outgoing[type] = renderBubble(
                mergeType: type, incoming: false,
                fillColor: outgoingColor,
                maxRadius: mainRadius, minRadius: auxiliaryRadius
            )
            incoming[type] = renderBubble(
                mergeType: type, incoming: true,
                fillColor: incomingColor,
                maxRadius: mainRadius, minRadius: auxiliaryRadius
            )
        }

        return ImageSet(outgoing: outgoing, incoming: incoming)
    }

    // MARK: - Private: Telegram-exact rendering

    /// Render a single stretchable bubble image.
    /// Exact port of `messageBubbleImage()` from `ChatMessageBubbleImages.swift`.
    private static func renderBubble(
        mergeType: BubbleMergeType,
        incoming: Bool,
        fillColor: UIColor,
        maxRadius: CGFloat,
        minRadius: CGFloat
    ) -> UIImage {
        // ── Constants from Telegram ──
        let fixedDiameter: CGFloat = 33.0
        let strokeInset: CGFloat = 1.0
        let additionalInset: CGFloat = 1.0

        let innerW = fixedDiameter + 6.0              // 39
        let innerH = fixedDiameter                     // 33
        let rawW = innerW + strokeInset * 2            // 41
        let rawH = innerH + strokeInset * 2            // 35
        let imgW = rawW + additionalInset * 2          // 43
        let imgH = rawH + additionalInset * 2          // 37

        let imageSize = CGSize(width: imgW, height: imgH)

        // Stretch points (9-slice)
        let outStretchX = Int(additionalInset + strokeInset + round(fixedDiameter / 2.0)) - 1 // 18
        let outStretchY = Int(additionalInset + strokeInset + round(fixedDiameter / 2.0))      // 19
        let inStretchX = Int(rawW) - outStretchX + Int(additionalInset)                         // 24

        // Corner radii in OUTGOING orientation
        let (tl, tr, bl, br) = radii(for: mergeType, max: maxRadius, min: minRadius)
        let drawTail = BubbleGeometryEngine.hasTail(for: mergeType)

        // Telegram ellipses
        let bottomEllipse = CGRect(x: 24, y: 16, width: 27, height: 17)
        let topEllipse = CGRect(x: 33, y: 14, width: 23, height: 21)

        let scale = UIScreen.main.scale
        let format = UIGraphicsImageRendererFormat()
        format.scale = scale
        format.opaque = false

        let renderer = UIGraphicsImageRenderer(size: imageSize, format: format)

        let image = renderer.image { rendererCtx in
            let ctx = rendererCtx.cgContext

            // ── Mirror for incoming (horizontal flip) ──
            // Telegram: scaleBy(x: incoming ? -1 : 1, y: -1)
            // The y:-1 is for Core Graphics coordinate flip.
            // In UIGraphicsImageRenderer the context is already UIKit-y-down,
            // so we only need horizontal flip for incoming.
            if incoming {
                ctx.translateBy(x: imageSize.width, y: 0)
                ctx.scaleBy(x: -1, y: 1)
            }

            ctx.translateBy(x: additionalInset + strokeInset, y: additionalInset + strokeInset)

            // ── Fill body + tail with the bubble color ──
            ctx.setFillColor(fillColor.cgColor)

            // Body: rounded rect 33×33
            ctx.move(to: CGPoint(x: 0, y: tl))
            ctx.addArc(tangent1End: .zero, tangent2End: CGPoint(x: tl, y: 0), radius: tl)
            ctx.addLine(to: CGPoint(x: fixedDiameter - tr, y: 0))
            ctx.addArc(tangent1End: CGPoint(x: fixedDiameter, y: 0),
                        tangent2End: CGPoint(x: fixedDiameter, y: tr), radius: tr)
            ctx.addLine(to: CGPoint(x: fixedDiameter, y: fixedDiameter - br))
            ctx.addArc(tangent1End: CGPoint(x: fixedDiameter, y: fixedDiameter),
                        tangent2End: CGPoint(x: fixedDiameter - br, y: fixedDiameter), radius: br)
            ctx.addLine(to: CGPoint(x: bl, y: fixedDiameter))
            ctx.addArc(tangent1End: CGPoint(x: 0, y: fixedDiameter),
                        tangent2End: CGPoint(x: 0, y: fixedDiameter - bl), radius: bl)
            ctx.addLine(to: CGPoint(x: 0, y: tl))
            ctx.fillPath()

            // Tail (only for .none and .bottom)
            if drawTail {
                // bottomEllipse lower half — two QuadCurves
                if maxRadius >= 14.0 {
                    ctx.move(to: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.midY))
                    ctx.addQuadCurve(
                        to: CGPoint(x: bottomEllipse.midX, y: bottomEllipse.maxY),
                        control: CGPoint(x: bottomEllipse.minX, y: bottomEllipse.maxY))
                    ctx.addQuadCurve(
                        to: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.midY),
                        control: CGPoint(x: bottomEllipse.maxX, y: bottomEllipse.maxY))
                    ctx.fillPath()
                } else {
                    ctx.fill(CGRect(
                        x: bottomEllipse.minX - 2, y: bottomEllipse.midY,
                        width: bottomEllipse.width + 2, height: bottomEllipse.height / 2))
                }

                // Connecting rect (fills gap between body center and bottomEllipse)
                ctx.fill(CGRect(
                    x: fixedDiameter / 2, y: floor(fixedDiameter / 2),
                    width: fixedDiameter / 2,
                    height: ceil(bottomEllipse.midY) - floor(fixedDiameter / 2)))

                // topEllipse ERASE — the key trick that only works in raster!
                ctx.setFillColor(UIColor.clear.cgColor)
                ctx.setBlendMode(.copy)
                ctx.fillEllipse(in: topEllipse)
            }
        }

        // ── Make stretchable (9-slice) ──
        let stretchX = incoming ? inStretchX : outStretchX
        return image.stretchableImage(withLeftCapWidth: stretchX, topCapHeight: outStretchY)
    }

    /// Corner radii in outgoing orientation (Telegram convention).
    /// Same logic as `messageBubbleArguments()` in ChatMessageBubbleImages.swift.
    private static func radii(
        for mergeType: BubbleMergeType,
        max maxR: CGFloat,
        min minR: CGFloat
    ) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
        switch mergeType {
        case .none:     return (maxR, maxR, maxR, maxR)
        case .both:     return (maxR, minR, maxR, minR)
        case .bottom:   return (maxR, minR, maxR, maxR)
        case .top(let side):
            return (maxR, maxR, side ? minR : maxR, minR)
        case .side:     return (maxR, maxR, minR, minR)
        case .extracted: return (maxR, maxR, maxR, maxR)
        }
    }
}
