// Copyright (C) 2026 Jason M. Schwefel. All Rights Reserved.
package com.coldboreballisticsllc.scopedope.label.renderer

import android.graphics.*
import com.coldboreballisticsllc.scopedope.domain.model.LabelEntry
import com.coldboreballisticsllc.scopedope.domain.model.WindLabel
import com.coldboreballisticsllc.scopedope.label.format.LabelFormat
import com.coldboreballisticsllc.scopedope.label.format.LabelFormats
import com.coldboreballisticsllc.scopedope.label.format.LabelShape
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt

// ── Content circle ────────────────────────────────────────────────────────────
// Content is drawn within a circle inset from the label edge by LABEL_MARGIN_MM
// on each side (e.g. 50 mm label → 38 mm content circle, 6 mm white annulus).
// A hairline at the content boundary serves as a punch/cut guide.
// An additional 1 mm inset keeps text clear of the circle edge in both modes.
private const val LABEL_MARGIN_MM = 6f   // white annulus on each side of content circle
private const val MARGIN_MM       = 1f

// ── Layout proportions (relative to content radius) ──────────────────────────
//
// K_USABLE_H kept smaller than full diameter so all rows stay in the wider
// central zone of the circle (better horizontal chord ≈ more text room).
//
//   usable_h     = contentR * 1.50   (vertical table height)
//   max_half_w   = contentR * 0.88   (cap on half-column width — never exceed)
//   gap          = contentR * 0.053  (space between distance and adjustment)
//   labelFont    = contentR * 0.111  (wind-label font)
//   fontCap_wind = contentR * 0.176  (font size cap when windage present)
//   fontCap_elev = contentR * 0.204  (font size cap without windage)

private const val SEP = " | "

private const val K_USABLE_H      = 1.50f
private const val K_HALF_W        = 0.88f
private const val K_GAP           = 0.053f
private const val K_LABEL_FONT    = 0.111f
private const val K_FONT_CAP_WIND = 0.176f
private const val K_FONT_CAP_ELEV = 0.204f

@Singleton
class CircleLabelRenderer @Inject constructor() : LabelRenderer {

    override val format: LabelFormat = LabelFormats.CIRCLE_50MM

    override fun render(
        entries:   List<LabelEntry>,
        windLabel: WindLabel?,
        targetDpi: Int,
        options:   RenderOptions,
    ): Bitmap {
        val adjDecimals = options.adjDecimals
        val fullCircle  = options.fullCircle
        val semantic    = options.useSemanticColors

        // Full bitmap = physical label size (50 mm)
        val labelDiamMm = (format.shape as LabelShape.Circle).diameterMm
        val bitmapPx    = (labelDiamMm / 25.4f * targetDpi).roundToInt()
        val cx          = bitmapPx / 2f
        val cy          = bitmapPx / 2f

        // Content circle: label minus margin on each side, with punch guide; or full label.
        val contentDiamMm = if (fullCircle) labelDiamMm else (labelDiamMm - LABEL_MARGIN_MM * 2)
        val contentR      = (contentDiamMm / 25.4f * targetDpi) / 2f

        // 1 mm inset keeps all text and lines clear of the circle boundary.
        val marginPx  = MARGIN_MM / 25.4f * targetDpi
        val layoutR   = contentR - marginPx

        val bitmap = Bitmap.createBitmap(bitmapPx, bitmapPx, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        canvas.drawColor(Color.WHITE)

        // Clip drawing to the content circle
        val clip = Path().apply { addCircle(cx, cy, contentR, Path.Direction.CW) }
        canvas.save()
        canvas.clipPath(clip)

        val showGridlines = options.showGridlines
        val elevSymbol    = options.elevationSymbol

        val limited = entries.take(8)
        if (limited.isEmpty()) {
            canvas.restore()
            if (!fullCircle) drawPunchGuide(canvas, cx, cy, contentR, bitmapPx)
            return bitmap
        }

        val hasWind      = limited.any { abs(it.windageMil) >= 0.05 }
        val drawEntries  = if (options.useMoa) limited.map { it.copy(elevationMil = it.elevationMoa, windageMil = it.windageMoa) } else limited
        val gapPx    = layoutR * K_GAP
        val halfWMax = layoutR * K_HALF_W
        var usableH  = layoutR * K_USABLE_H

        // ── Wind label ────────────────────────────────────────────────────────
        val labelFontPx = layoutR * K_LABEL_FONT
        var labelGap    = 0f
        var labelText: String? = null

        if (windLabel != null) {
            labelText = if (options.useMetric)
                "${(windLabel.speedMph * 1.60934).roundToInt()} km/h ${windLabel.angleDeg.roundToInt()}°"
            else
                "${windLabel.speedMph.roundToInt()} mph ${windLabel.angleDeg.roundToInt()}°"
            labelGap  = labelFontPx * 1.6f
            usableH  -= labelGap
        }

        // ── Row geometry ──────────────────────────────────────────────────────
        val pairs  = drawEntries.chunked(2)
        val nRows  = pairs.size
        val rowH   = usableH / nRows
        val tableH = nRows * rowH
        val topY   = cy - tableH / 2f + labelGap / 2f

        // ── Auto-fit: use per-row chord width to prevent clipping ─────────────
        // Each row's available half-width = chord of content circle at that row's
        // vertical midpoint, capped at halfWMax.
        val fontCap  = if (hasWind) layoutR * K_FONT_CAP_WIND else layoutR * K_FONT_CAP_ELEV
        var fontSize = min(rowH * 0.72f, fontCap)

        val probe = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.DEFAULT_BOLD }
        while (fontSize >= 5f) {
            probe.textSize = fontSize
            val allFit = pairs.withIndex().all { (i, pair) ->
                val rowHalfW = rowChordHalf(topY, i, rowH, cy, layoutR, halfWMax)
                halfRowFits(probe, pair[0], gapPx, rowHalfW, adjDecimals, elevSymbol) &&
                    (pair.size < 2 || halfRowFits(probe, pair[1], gapPx, rowHalfW, adjDecimals, elevSymbol))
            }
            if (allFit) break
            fontSize -= 0.25f
        }
        if (fontSize < 5f) fontSize = 5f
        val windFontPx = max(fontSize * 0.70f, 5f)

        // ── Grid lines (center divider + row separators) ──────────────────────
        if (showGridlines) {
            val lineWidth = max(2f, contentR * 0.018f)
            val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
                style       = Paint.Style.STROKE
                color       = Color.BLACK
                strokeWidth = lineWidth
            }

            val tableTopChord  = chordHalfAt(topY,           cy, layoutR)
            val tableBotChord  = chordHalfAt(topY + tableH,  cy, layoutR)
            canvas.drawLine(cx, topY - tableTopChord * 0.02f, cx, topY + tableH + tableBotChord * 0.02f, linePaint)

            for (i in 1 until nRows) {
                val sepY     = topY + i * rowH
                val sepChord = chordHalfAt(sepY, cy, layoutR).coerceAtMost(halfWMax)
                canvas.drawLine(cx - sepChord, sepY, cx + sepChord, sepY, linePaint)
            }
        }

        // ── Wind label ────────────────────────────────────────────────────────
        if (labelText != null) {
            val p = buildPaint(Typeface.DEFAULT, labelFontPx, if (semantic) STICKER_GRAY else Color.BLACK, Paint.Align.CENTER)
            val baselineY = topY - labelGap * 0.5f + labelFontPx * 0.32f
            canvas.drawText(labelText, cx, baselineY, p)
        }

        // ── Draw each row ─────────────────────────────────────────────────────
        for ((i, pair) in pairs.withIndex()) {
            val rowTopY   = topY + i * rowH
            val baselineY = if (hasWind) {
                rowTopY + rowH * 0.38f
            } else {
                rowTopY + rowH * 0.5f + fontSize * 0.32f
            }
            drawLeft( canvas, cx, baselineY, fontSize, gapPx, pair[0], windFontPx, hasWind, adjDecimals, elevSymbol, semantic)
            if (pair.size > 1) {
                drawRight(canvas, cx, baselineY, fontSize, gapPx, pair[1], windFontPx, hasWind, adjDecimals, elevSymbol, semantic)
            }
        }

        canvas.restore()

        // ── Punch-guide hairline — only for 38 mm content mode ────────────────
        if (!fullCircle) drawPunchGuide(canvas, cx, cy, contentR, bitmapPx)

        return bitmap
    }

    // ── Geometry helpers ──────────────────────────────────────────────────────

    /** Half-width of circle chord at vertical position y. */
    private fun chordHalfAt(y: Float, cy: Float, r: Float): Float {
        val sq = r * r - (y - cy) * (y - cy)
        return if (sq <= 0f) 0f else sqrt(sq)
    }

    /** Available half-width for row i, capped at halfWMax. */
    private fun rowChordHalf(
        topY: Float, i: Int, rowH: Float, cy: Float, contentR: Float, halfWMax: Float,
    ): Float {
        val rowMidY = topY + (i + 0.5f) * rowH
        return chordHalfAt(rowMidY, cy, contentR).coerceAtMost(halfWMax)
    }

    /** Thin circle at the content boundary — guides punching / cutting. */
    private fun drawPunchGuide(canvas: Canvas, cx: Float, cy: Float, contentR: Float, bitmapPx: Int) {
        val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
            style       = Paint.Style.STROKE
            color       = Color.BLACK
            strokeWidth = max(1.5f, bitmapPx * 0.003f)
        }
        canvas.drawCircle(cx, cy, contentR, paint)
    }

    // ── Half-row layout helpers ───────────────────────────────────────────────

    private fun drawLeft(
        canvas:     Canvas,
        cx:         Float,
        baselineY:  Float,
        fontSize:   Float,
        gap:        Float,
        entry:      LabelEntry,
        windFontPx: Float,
        hasWind:    Boolean,
        decimals:   Int,
        elevSym:    ElevationSymbol,
        semantic:   Boolean,
    ) {
        val distStr = fmtDist(entry.distanceYards)
        val adjStr  = fmtAdj(entry.elevationMil, decimals, elevSym)
        val p = buildPaint(Typeface.DEFAULT_BOLD, fontSize)

        val distW = p.measureText(distStr)
        val sepW  = p.measureText(SEP)
        val adjW  = p.measureText(adjStr)

        // Right-to-left from center: gap | dist | SEP | adj
        val distX = cx - gap - distW
        val sepX  = distX - sepW
        val adjX  = sepX - adjW

        p.color = if (semantic) STICKER_BLUE else Color.BLACK
        canvas.drawText(distStr, distX, baselineY, p)
        p.color = Color.BLACK
        canvas.drawText(SEP, sepX, baselineY, p)
        p.color = adjColor(entry.elevationMil, semantic)
        canvas.drawText(adjStr, adjX, baselineY, p)

        if (hasWind) drawWindSub(canvas, adjX, baselineY, fontSize, windFontPx, entry.windageMil, decimals, semantic)
    }

    private fun drawRight(
        canvas:     Canvas,
        cx:         Float,
        baselineY:  Float,
        fontSize:   Float,
        gap:        Float,
        entry:      LabelEntry,
        windFontPx: Float,
        hasWind:    Boolean,
        decimals:   Int,
        elevSym:    ElevationSymbol,
        semantic:   Boolean,
    ) {
        val distStr = fmtDist(entry.distanceYards)
        val adjStr  = fmtAdj(entry.elevationMil, decimals, elevSym)
        val p = buildPaint(Typeface.DEFAULT_BOLD, fontSize)

        val distW = p.measureText(distStr)
        val sepW  = p.measureText(SEP)

        // Left-to-right from center: gap | dist | SEP | adj
        val distX = cx + gap
        val sepX  = distX + distW
        val adjX  = sepX + sepW

        p.color = if (semantic) STICKER_BLUE else Color.BLACK
        canvas.drawText(distStr, distX, baselineY, p)
        p.color = Color.BLACK
        canvas.drawText(SEP, sepX, baselineY, p)
        p.color = adjColor(entry.elevationMil, semantic)
        canvas.drawText(adjStr, adjX, baselineY, p)

        if (hasWind) drawWindSub(canvas, adjX, baselineY, fontSize, windFontPx, entry.windageMil, decimals, semantic)
    }

    private fun drawWindSub(
        canvas:     Canvas,
        x:          Float,
        mainY:      Float,
        mainFont:   Float,
        windFont:   Float,
        windageMil: Double,
        decimals:   Int,
        semantic:   Boolean,
    ) {
        val str = fmtWind(windageMil, decimals)
        if (str.isEmpty()) return
        val p = buildPaint(Typeface.DEFAULT_BOLD, windFont, if (semantic) STICKER_AMBER else Color.BLACK)
        canvas.drawText(str, x, mainY + mainFont * 0.88f, p)
    }

    // ── Width-fit check ───────────────────────────────────────────────────────

    private fun halfRowFits(
        paint:   Paint,
        entry:   LabelEntry,
        gap:     Float,
        halfW:   Float,
        decimals: Int,
        elevSym: ElevationSymbol = ElevationSymbol.PLUS_MINUS,
    ): Boolean {
        val adjW  = paint.measureText(fmtAdj(entry.elevationMil, decimals, elevSym))
        val distW = paint.measureText(fmtDist(entry.distanceYards))
        val sepW  = paint.measureText(SEP)
        return (adjW + sepW + distW + gap) <= halfW
    }

}
