// 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.LabelShape
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

private const val SEP       = " | "
private const val MARGIN_MM = 1f
private const val K_HEADER_H = 0.12f   // header height as fraction of usable height
private const val K_GAP      = 0.04f   // column-edge margin as fraction of column width

@Singleton
class RectLabelRenderer @Inject constructor() {

    // ── Public entry point — dispatches on format ─────────────────────────────

    fun render(
        format:    LabelFormat,
        entries:   List<LabelEntry>,
        windLabel: WindLabel?,
        targetDpi: Int,
        options:   RenderOptions = RenderOptions(),
    ): Bitmap {
        val shape       = format.shape as LabelShape.Rectangle
        val rawEntries  = entries.take(format.maxEntries)
        val safeEntries = if (options.useMoa) rawEntries.map { it.copy(elevationMil = it.elevationMoa, windageMil = it.windageMoa) } else rawEntries
        return when (format.columns) {
            1    -> renderSingleColumn(shape, safeEntries, windLabel, targetDpi, options)
            2    -> render2Col(shape, safeEntries, windLabel, targetDpi, options)
            3    -> render3Col(shape, safeEntries, windLabel, targetDpi, options)
            else -> error("RectLabelRenderer: unsupported column count ${format.columns}")
        }
    }

    // ── Single-column (portrait and landscape 1-col) ──────────────────────────

    private fun renderSingleColumn(
        shape:     LabelShape.Rectangle,
        entries:   List<LabelEntry>,
        windLabel: WindLabel?,
        targetDpi: Int,
        options:   RenderOptions,
    ): Bitmap {
        val bitmapW = (shape.widthMm  / 25.4f * targetDpi).roundToInt()
        val bitmapH = (shape.heightMm / 25.4f * targetDpi).roundToInt()
        val marginPx = MARGIN_MM / 25.4f * targetDpi

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

        val left   = marginPx
        val right  = bitmapW - marginPx
        val top    = marginPx
        val bottom = bitmapH - marginPx
        val usableW = right - left
        val usableH = bottom - top

        if (entries.isEmpty()) return bitmap

        val semantic = options.useSemanticColors

        // ── Header ────────────────────────────────────────────────────────────
        var contentTop = top
        if (windLabel != null) {
            val headerH   = usableH * K_HEADER_H
            val headerFont = headerH * 0.65f
            val headerText = if (options.useMetric)
                "${(windLabel.speedMph * 1.60934).roundToInt()} km/h ${windLabel.angleDeg.roundToInt()}°"
            else
                "${windLabel.speedMph.roundToInt()} mph ${windLabel.angleDeg.roundToInt()}°"
            val p = buildPaint(Typeface.DEFAULT, headerFont, if (semantic) STICKER_GRAY else Color.BLACK, Paint.Align.CENTER)
            canvas.drawText(headerText, left + usableW / 2f, top + headerH * 0.75f, p)
            contentTop = top + headerH
        }

        // ── Row geometry ──────────────────────────────────────────────────────
        val nRows      = entries.size
        val contentH   = bottom - contentTop
        val rowH       = contentH / nRows
        val decimals   = options.adjDecimals
        val elevSym    = options.elevationSymbol

        // Auto-fit font
        val fontCapPx = rowH * 0.68f
        var fontSize  = fontCapPx
        val probe = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.DEFAULT_BOLD }
        while (fontSize >= 5f) {
            probe.textSize = fontSize
            val allFit = entries.all { entry ->
                rowFitsSingleCol(probe, entry, usableW, decimals, elevSym)
            }
            if (allFit) break
            fontSize -= 0.25f
        }
        if (fontSize < 5f) fontSize = 5f
        val windFontPx = max(fontSize * 0.70f, 5f)

        // ── Gridlines ─────────────────────────────────────────────────────────
        if (options.showGridlines) {
            val lw = max(1.5f, bitmapH * 0.003f)
            val lp = Paint(Paint.ANTI_ALIAS_FLAG).apply {
                style       = Paint.Style.STROKE
                color       = Color.BLACK
                strokeWidth = lw
            }
            for (i in 1 until nRows) {
                val sepY = contentTop + i * rowH
                canvas.drawLine(left, sepY, right, sepY, lp)
            }
        }

        // ── Rows ──────────────────────────────────────────────────────────────
        for ((i, entry) in entries.withIndex()) {
            val rowTopY   = contentTop + i * rowH
            val baselineY = rowTopY + rowH * 0.5f + fontSize * 0.32f
            drawPortraitRow(canvas, left, right, baselineY, fontSize, windFontPx, entry, decimals, elevSym, semantic)
        }

        return bitmap
    }

    private fun drawPortraitRow(
        canvas:    Canvas,
        left:      Float,
        right:     Float,
        baselineY: Float,
        fontSize:  Float,
        windFont:  Float,
        entry:     LabelEntry,
        decimals:  Int,
        elevSym:   ElevationSymbol,
        semantic:  Boolean,
    ) {
        val usableW = right - left
        val distStr = fmtDist(entry.distanceYards)
        val adjStr  = fmtAdj(entry.elevationMil, decimals, elevSym)
        val windStr = fmtWind(entry.windageMil, decimals)

        val p    = buildPaint(Typeface.DEFAULT_BOLD, fontSize)
        val sepW = p.measureText(SEP)

        val distW = p.measureText(distStr)
        val adjW  = p.measureText(adjStr)
        val windW = if (windStr.isEmpty()) 0f else {
            buildPaint(Typeface.DEFAULT_BOLD, windFont).measureText(windStr)
        }

        // Center the whole block: dist | adj  or  dist | adj | wind
        val totalW = distW + sepW + adjW + (if (windStr.isEmpty()) 0f else sepW + windW)
        var x = left + (usableW - totalW) / 2f

        p.color = if (semantic) STICKER_BLUE else Color.BLACK
        canvas.drawText(distStr, x, baselineY, p)
        x += distW

        p.color = Color.BLACK
        canvas.drawText(SEP, x, baselineY, p)
        x += sepW

        p.color = adjColor(entry.elevationMil, semantic)
        canvas.drawText(adjStr, x, baselineY, p)
        x += adjW

        if (windStr.isNotEmpty()) {
            p.color = Color.BLACK
            canvas.drawText(SEP, x, baselineY, p)
            x += sepW
            val wp = buildPaint(Typeface.DEFAULT_BOLD, windFont, if (semantic) STICKER_AMBER else Color.BLACK)
            canvas.drawText(windStr, x, baselineY, wp)
        }
    }

    // ── 2-column landscape ────────────────────────────────────────────────────

    private fun render2Col(
        shape:     LabelShape.Rectangle,
        entries:   List<LabelEntry>,
        windLabel: WindLabel?,
        targetDpi: Int,
        options:   RenderOptions,
    ): Bitmap {
        val bitmapW = (shape.widthMm  / 25.4f * targetDpi).roundToInt()
        val bitmapH = (shape.heightMm / 25.4f * targetDpi).roundToInt()
        val marginPx = MARGIN_MM / 25.4f * targetDpi

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

        val left   = marginPx
        val right  = bitmapW - marginPx
        val top    = marginPx
        val bottom = bitmapH - marginPx
        val usableW = right - left
        val usableH = bottom - top

        if (entries.isEmpty()) return bitmap

        val semantic = options.useSemanticColors

        // ── Header ────────────────────────────────────────────────────────────
        var contentTop = top
        if (windLabel != null) {
            val headerH   = usableH * K_HEADER_H
            val headerFont = headerH * 0.65f
            val headerText = if (options.useMetric)
                "${(windLabel.speedMph * 1.60934).roundToInt()} km/h ${windLabel.angleDeg.roundToInt()}°"
            else
                "${windLabel.speedMph.roundToInt()} mph ${windLabel.angleDeg.roundToInt()}°"
            val p = buildPaint(Typeface.DEFAULT, headerFont, if (semantic) STICKER_GRAY else Color.BLACK, Paint.Align.CENTER)
            canvas.drawText(headerText, left + usableW / 2f, top + headerH * 0.75f, p)
            contentTop = top + headerH
        }

        // ── Entry distribution ────────────────────────────────────────────────
        // col2 = right column (normal field order), col1 = left column (optionally reversed fields)
        val (col2Entries, col1Entries) = distributeEntries(entries, options.columnFillInterleaved)

        val nRows    = max(col2Entries.size, col1Entries.size)
        val contentH = bottom - contentTop
        val rowH     = if (nRows > 0) contentH / nRows else contentH
        val colW     = usableW / 2f
        val cx       = left + colW   // center divider x

        val decimals = options.adjDecimals
        val elevSym  = options.elevationSymbol
        val col1Rev  = options.landscapeCol1Reversed

        // Auto-fit font: probe worst-case across both columns
        val fontCapPx = min(rowH * 0.68f, bitmapH * 0.22f)
        var fontSize  = fontCapPx
        val probe = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.DEFAULT_BOLD }
        while (fontSize >= 5f) {
            probe.textSize = fontSize
            val allFit =
                col2Entries.all { rowFitsLandscape(probe, it, colW, decimals, elevSym, reversed = false) } &&
                col1Entries.all { rowFitsLandscape(probe, it, colW, decimals, elevSym, reversed = col1Rev) }
            if (allFit) break
            fontSize -= 0.25f
        }
        if (fontSize < 5f) fontSize = 5f
        val windFontPx = max(fontSize * 0.70f, 5f)

        // ── Gridlines ─────────────────────────────────────────────────────────
        if (options.showGridlines) {
            val lw = max(1.5f, bitmapH * 0.003f)
            val lp = Paint(Paint.ANTI_ALIAS_FLAG).apply {
                style       = Paint.Style.STROKE
                color       = Color.BLACK
                strokeWidth = lw
            }
            // Vertical center divider
            canvas.drawLine(cx, contentTop, cx, bottom, lp)
            // Horizontal row separators
            for (i in 1 until nRows) {
                val sepY = contentTop + i * rowH
                canvas.drawLine(left, sepY, right, sepY, lp)
            }
        }

        // ── Rows ──────────────────────────────────────────────────────────────
        for (i in 0 until nRows) {
            val rowTopY   = contentTop + i * rowH
            val baselineY = rowTopY + rowH * 0.5f + fontSize * 0.32f

            if (i < col2Entries.size) {
                drawLandscapeHalf(
                    canvas, cx, right, baselineY, fontSize, windFontPx,
                    col2Entries[i], decimals, elevSym, reversed = false, leftAlign = true, semantic = semantic,
                )
            }
            if (i < col1Entries.size) {
                drawLandscapeHalf(
                    canvas, left, cx, baselineY, fontSize, windFontPx,
                    col1Entries[i], decimals, elevSym, reversed = col1Rev, leftAlign = false, semantic = semantic,
                )
            }
        }

        return bitmap
    }

    // ── 3-column landscape ────────────────────────────────────────────────────

    private fun render3Col(
        shape:     LabelShape.Rectangle,
        entries:   List<LabelEntry>,
        windLabel: WindLabel?,
        targetDpi: Int,
        options:   RenderOptions,
    ): Bitmap {
        val bitmapW = (shape.widthMm  / 25.4f * targetDpi).roundToInt()
        val bitmapH = (shape.heightMm / 25.4f * targetDpi).roundToInt()
        val marginPx = MARGIN_MM / 25.4f * targetDpi

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

        val left    = marginPx
        val right   = bitmapW - marginPx
        val top     = marginPx
        val bottom  = bitmapH - marginPx
        val usableW = right - left
        val usableH = bottom - top

        if (entries.isEmpty()) return bitmap

        val semantic = options.useSemanticColors

        // ── Header ────────────────────────────────────────────────────────────
        var contentTop = top
        if (windLabel != null) {
            val headerH    = usableH * K_HEADER_H
            val headerFont = headerH * 0.65f
            val headerText = if (options.useMetric)
                "${(windLabel.speedMph * 1.60934).roundToInt()} km/h ${windLabel.angleDeg.roundToInt()}°"
            else
                "${windLabel.speedMph.roundToInt()} mph ${windLabel.angleDeg.roundToInt()}°"
            val p = buildPaint(Typeface.DEFAULT, headerFont, if (semantic) STICKER_GRAY else Color.BLACK, Paint.Align.CENTER)
            canvas.drawText(headerText, left + usableW / 2f, top + headerH * 0.75f, p)
            contentTop = top + headerH
        }

        // ── Entry distribution ────────────────────────────────────────────────
        // col1 = left (optionally reversed), col2 = middle, col3 = right
        val (col1Entries, col2Entries, col3Entries) =
            distributeEntries3(entries, options.columnFillInterleaved)

        val nRows    = maxOf(col1Entries.size, col2Entries.size, col3Entries.size)
        val contentH = bottom - contentTop
        val rowH     = if (nRows > 0) contentH / nRows else contentH
        val colW     = usableW / 3f
        val cx1      = left + colW          // divider between col1 and col2
        val cx2      = left + colW * 2f     // divider between col2 and col3

        val decimals = options.adjDecimals
        val elevSym  = options.elevationSymbol
        val col1Rev  = options.landscapeCol1Reversed

        // Auto-fit font: probe worst-case across all three columns
        val fontCapPx = min(rowH * 0.68f, bitmapH * 0.22f)
        var fontSize  = fontCapPx
        val probe = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.DEFAULT_BOLD }
        while (fontSize >= 5f) {
            probe.textSize = fontSize
            val allFit =
                col1Entries.all { rowFitsLandscape(probe, it, colW, decimals, elevSym, col1Rev) } &&
                col2Entries.all { rowFitsLandscape(probe, it, colW, decimals, elevSym, false)   } &&
                col3Entries.all { rowFitsLandscape(probe, it, colW, decimals, elevSym, false)   }
            if (allFit) break
            fontSize -= 0.25f
        }
        if (fontSize < 5f) fontSize = 5f
        val windFontPx = max(fontSize * 0.70f, 5f)

        // ── Gridlines ─────────────────────────────────────────────────────────
        if (options.showGridlines) {
            val lw = max(1.5f, bitmapH * 0.003f)
            val lp = Paint(Paint.ANTI_ALIAS_FLAG).apply {
                style       = Paint.Style.STROKE
                color       = Color.BLACK
                strokeWidth = lw
            }
            canvas.drawLine(cx1, contentTop, cx1, bottom, lp)
            canvas.drawLine(cx2, contentTop, cx2, bottom, lp)
            for (i in 1 until nRows) {
                val sepY = contentTop + i * rowH
                canvas.drawLine(left, sepY, right, sepY, lp)
            }
        }

        // ── Rows ──────────────────────────────────────────────────────────────
        for (i in 0 until nRows) {
            val rowTopY   = contentTop + i * rowH
            val baselineY = rowTopY + rowH * 0.5f + fontSize * 0.32f

            if (i < col1Entries.size) {
                drawLandscapeHalf(
                    canvas, left, cx1, baselineY, fontSize, windFontPx,
                    col1Entries[i], decimals, elevSym, reversed = col1Rev, leftAlign = false, semantic = semantic,
                )
            }
            if (i < col2Entries.size) {
                drawLandscapeHalf(
                    canvas, cx1, cx2, baselineY, fontSize, windFontPx,
                    col2Entries[i], decimals, elevSym, reversed = false, leftAlign = true, semantic = semantic,
                )
            }
            if (i < col3Entries.size) {
                drawLandscapeHalf(
                    canvas, cx2, right, baselineY, fontSize, windFontPx,
                    col3Entries[i], decimals, elevSym, reversed = false, leftAlign = true, semantic = semantic,
                )
            }
        }

        return bitmap
    }

    // ── Entry distribution helpers ────────────────────────────────────────────

    private fun distributeEntries3(
        entries:     List<LabelEntry>,
        interleaved: Boolean,
    ): Triple<List<LabelEntry>, List<LabelEntry>, List<LabelEntry>> {
        return if (interleaved) {
            // Left-to-right: col1=0,3,6,…  col2=1,4,7,…  col3=2,5,8,…
            val col1 = entries.filterIndexed { i, _ -> i % 3 == 0 }
            val col2 = entries.filterIndexed { i, _ -> i % 3 == 1 }
            val col3 = entries.filterIndexed { i, _ -> i % 3 == 2 }
            Triple(col1, col2, col3)
        } else {
            // Fill col1 first, then col2, then col3
            val third = (entries.size + 2) / 3
            val col1  = entries.take(third)
            val col2  = entries.drop(third).take(third)
            val col3  = entries.drop(third * 2)
            Triple(col1, col2, col3)
        }
    }

    private fun distributeEntries(
        entries: List<LabelEntry>,
        interleaved: Boolean,
    ): Pair<List<LabelEntry>, List<LabelEntry>> {
        return if (interleaved) {
            // Pairs per row: col2=even-indexed(0,2,4,6), col1=odd-indexed(1,3,5,7)
            val col2 = entries.filterIndexed { i, _ -> i % 2 == 0 }
            val col1 = entries.filterIndexed { i, _ -> i % 2 == 1 }
            Pair(col2, col1)
        } else {
            // Fill col1 (left) first, then col2 (right)
            val half = (entries.size + 1) / 2
            Pair(entries.drop(half), entries.take(half))
        }
    }

    // ── Landscape half-column draw ────────────────────────────────────────────

    private fun drawLandscapeHalf(
        canvas:     Canvas,
        colLeft:    Float,
        colRight:   Float,
        baselineY:  Float,
        fontSize:   Float,
        windFont:   Float,
        entry:      LabelEntry,
        decimals:   Int,
        elevSym:    ElevationSymbol,
        reversed:   Boolean,
        leftAlign:  Boolean,   // true = col2 (right half), false = col1 (left half)
        semantic:   Boolean,
    ) {
        val colW  = colRight - colLeft
        val gapPx = colW * K_GAP

        val distStr = fmtDist(entry.distanceYards)
        val adjStr  = fmtAdj(entry.elevationMil, decimals, elevSym)
        val windStr = fmtWind(entry.windageMil, decimals)

        val p    = buildPaint(Typeface.DEFAULT_BOLD, fontSize)
        val sepW = p.measureText(SEP)
        val distW = p.measureText(distStr)
        val adjW  = p.measureText(adjStr)
        val windW = if (windStr.isEmpty()) 0f else {
            buildPaint(Typeface.DEFAULT_BOLD, windFont).measureText(windStr)
        }

        // Fields: dist | adj | wind?  (normal) or  wind? | adj | dist  (reversed)
        if (!reversed) {
            var x = colLeft + gapPx

            p.color = if (semantic) STICKER_BLUE else Color.BLACK
            canvas.drawText(distStr, x, baselineY, p)
            x += distW

            p.color = Color.BLACK
            canvas.drawText(SEP, x, baselineY, p)
            x += sepW

            p.color = adjColor(entry.elevationMil, semantic)
            canvas.drawText(adjStr, x, baselineY, p)
            x += adjW

            if (windStr.isNotEmpty()) {
                p.color = Color.BLACK
                canvas.drawText(SEP, x, baselineY, p)
                x += sepW
                val wp = buildPaint(Typeface.DEFAULT_BOLD, windFont, if (semantic) STICKER_AMBER else Color.BLACK)
                canvas.drawText(windStr, x, baselineY, wp)
            }
        } else {
            // Right-aligned within column, reading toward center
            val totalW = distW + sepW + adjW + (if (windStr.isEmpty()) 0f else windW + sepW)
            var x = colRight - gapPx - totalW

            if (windStr.isNotEmpty()) {
                val wp = buildPaint(Typeface.DEFAULT_BOLD, windFont, if (semantic) STICKER_AMBER else Color.BLACK)
                canvas.drawText(windStr, x, baselineY, wp)
                x += windW
                p.color = Color.BLACK
                canvas.drawText(SEP, x, baselineY, p)
                x += sepW
            }

            p.color = adjColor(entry.elevationMil, semantic)
            canvas.drawText(adjStr, x, baselineY, p)
            x += adjW

            p.color = Color.BLACK
            canvas.drawText(SEP, x, baselineY, p)
            x += sepW

            p.color = if (semantic) STICKER_BLUE else Color.BLACK
            canvas.drawText(distStr, x, baselineY, p)
        }
    }

    // ── Fit checks ────────────────────────────────────────────────────────────

    private fun rowFitsSingleCol(
        paint:    Paint,
        entry:    LabelEntry,
        usableW:  Float,
        decimals: Int,
        elevSym:  ElevationSymbol,
    ): Boolean {
        val sepW    = paint.measureText(SEP)
        val distW   = paint.measureText(fmtDist(entry.distanceYards))
        val adjW    = paint.measureText(fmtAdj(entry.elevationMil, decimals, elevSym))
        val windStr = fmtWind(entry.windageMil, decimals)
        val windW   = if (windStr.isEmpty()) 0f else paint.measureText(windStr) * 0.70f + sepW
        return (distW + sepW + adjW + windW) <= usableW
    }

    private fun rowFitsLandscape(
        paint:    Paint,
        entry:    LabelEntry,
        colW:     Float,
        decimals: Int,
        elevSym:  ElevationSymbol,
        reversed: Boolean,
    ): Boolean {
        val gapPx   = colW * K_GAP   // column-edge margin
        val sepW    = paint.measureText(SEP)
        val distW   = paint.measureText(fmtDist(entry.distanceYards))
        val adjW    = paint.measureText(fmtAdj(entry.elevationMil, decimals, elevSym))
        val windStr = fmtWind(entry.windageMil, decimals)
        val windW   = if (windStr.isEmpty()) 0f else paint.measureText(windStr) * 0.70f + sepW
        return (gapPx + distW + sepW + adjW + windW + gapPx) <= colW
    }

}
