package wang.yaojia.webterm.wiring

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import com.termux.terminal.TerminalEmulator
import com.termux.terminal.TerminalOutput
import com.termux.terminal.TerminalSession
import com.termux.terminal.TerminalSessionClient
import com.termux.terminal.TextStyle
import wang.yaojia.webterm.api.models.SessionPreview
import kotlin.math.ceil

/**
 * The off-screen preview painter (plan §6.7) — the device-verified half of the thumbnail pipeline.
 *
 * Instantiates a [TerminalEmulator] with NO `TerminalView` and NO subprocess (the same "remove the JNI
 * process, keep the VT parser + `TerminalBuffer`" seam A16 proved), appends the preview bytes, then
 * rasterises the buffer cell-by-cell onto a `Bitmap`-backed [Canvas] with a monospace [Paint] — a direct
 * port of Termux `TerminalRenderer.render`'s per-cell draw: for each cell, fill its background rect then
 * stroke its glyph in the cell's foreground colour, decoding fg/bg/effect from the cell's `TextStyle`
 * (24-bit truecolor / 256 / ANSI, with the bold→bright bump and inverse-video swap).
 *
 * `TerminalRenderer`'s own `mFontWidth`/`mFontLineSpacing` are package-private, so this computes its own
 * cell metrics from the [Paint] (`ceil(measureText("X"))` and the ascent-to-descent span), exactly as
 * `TerminalRenderer` derives them.
 *
 * android.graphics is stubbed in JVM unit tests, so this class is exercised by device QA (plan §7,
 * `:terminal-view` instrumented "thumbnail rasterisation non-null"); the pipeline policy around it is
 * JVM-tested via the [ThumbnailRasterizer] seam.
 */
public class CanvasThumbnailRasterizer(
    private val fontSizePx: Float = DEFAULT_FONT_SIZE_PX,
    typeface: Typeface = Typeface.MONOSPACE,
) : ThumbnailRasterizer {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        this.typeface = typeface
        textSize = fontSizePx
    }
    private val cellWidth: Int = ceil(paint.measureText("X").toDouble()).toInt().coerceAtLeast(1)
    private val cellHeight: Int = (paint.fontMetricsInt.descent - paint.fontMetricsInt.ascent).coerceAtLeast(1)
    private val baselineOffset: Int = -paint.fontMetricsInt.ascent

    override fun rasterize(preview: SessionPreview): Bitmap {
        val cols = preview.cols.coerceIn(1, MAX_COLS)
        val rows = preview.rows.coerceIn(1, MAX_ROWS)
        val emulator = TerminalEmulator(NoOpOutput, cols, rows, TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MIN, NoOpClient)
        val bytes = preview.data.toByteArray(Charsets.UTF_8)
        emulator.append(bytes, bytes.size)

        val bitmap = Bitmap.createBitmap(cols * cellWidth, rows * cellHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        val palette = emulator.mColors.mCurrentColors
        val defaultBg = palette[TextStyle.COLOR_INDEX_BACKGROUND]
        canvas.drawColor(defaultBg)
        for (row in 0 until rows) {
            drawRow(canvas, emulator, row, cols, palette, defaultBg)
        }
        return bitmap
    }

    override fun placeholder(): Bitmap {
        val bitmap = Bitmap.createBitmap(cellWidth, cellHeight, Bitmap.Config.ARGB_8888)
        bitmap.eraseColor(PLACEHOLDER_COLOR)
        return bitmap
    }

    private fun drawRow(canvas: Canvas, emulator: TerminalEmulator, row: Int, cols: Int, palette: IntArray, defaultBg: Int) {
        val screen = emulator.screen
        val line = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row))
        val text = line.mText
        val top = (row * cellHeight).toFloat()
        val baseline = (row * cellHeight + baselineOffset).toFloat()
        for (col in 0 until cols) {
            val style = line.getStyle(col)
            val effect = TextStyle.decodeEffect(style)
            val bold = (effect and (TextStyle.CHARACTER_ATTRIBUTE_BOLD or TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0
            val inverse = (effect and TextStyle.CHARACTER_ATTRIBUTE_INVERSE) != 0
            val invisible = (effect and TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) != 0
            var fg = resolveColor(TextStyle.decodeForeColor(style), palette, boldBright = bold)
            var bg = resolveColor(TextStyle.decodeBackColor(style), palette, boldBright = false)
            if (inverse) { val swap = fg; fg = bg; bg = swap }

            val left = (col * cellWidth).toFloat()
            if (bg != defaultBg) {
                paint.color = bg
                paint.style = Paint.Style.FILL
                canvas.drawRect(left, top, left + cellWidth, top + cellHeight, paint)
            }
            val start = line.findStartOfColumn(col)
            val end = line.findStartOfColumn(col + 1)
            if (!invisible && end > start) {
                paint.color = fg
                paint.isFakeBoldText = bold
                canvas.drawText(text, start, end - start, left, baseline, paint)
            }
        }
    }

    /**
     * Resolve a decoded colour to an ARGB int: a truecolor value (its `0xff000000` marker set) passes
     * through; an indexed value is looked up in the emulator's live [palette], with a bold foreground in
     * the first 8 ANSI colours bumped to its bright (8..15) variant — matching `TerminalRenderer`.
     */
    private fun resolveColor(color: Int, palette: IntArray, boldBright: Boolean): Int {
        if ((color and TRUECOLOR_MASK) == TRUECOLOR_MASK) return color
        val index = if (boldBright && color in 0..7) color + 8 else color
        return palette[index]
    }

    public companion object {
        /** Default glyph size for a thumbnail (px). Small — the card downscales anyway. */
        public const val DEFAULT_FONT_SIZE_PX: Float = 12f

        /** Clamp the off-screen grid so a rogue preview geometry can't allocate an enormous bitmap. */
        public const val MAX_COLS: Int = 400
        public const val MAX_ROWS: Int = 200

        /** Opaque dark-grey placeholder for the fetch/render-failure fallback. */
        public const val PLACEHOLDER_COLOR: Int = -0xdfdfe0 // 0xFF202020

        /** A truecolor `TextStyle` value carries the 0xff000000 marker in its high byte. */
        private const val TRUECOLOR_MASK: Int = -0x1000000 // 0xFF000000
    }
}

/** Off-screen emulator sink: the preview emulator produces no wire output and needs no delegates. */
private object NoOpOutput : TerminalOutput() {
    override fun write(data: ByteArray?, offset: Int, count: Int) = Unit
    override fun titleChanged(oldTitle: String?, newTitle: String?) = Unit
    override fun onCopyTextToClipboard(text: String?) = Unit
    override fun onPasteTextFromClipboard() = Unit
    override fun onBell() = Unit
    override fun onColorsChanged() = Unit
}

/**
 * No-op [TerminalSessionClient] for the off-screen emulator (the constructor requires a non-null client;
 * Termux calls it on rare diagnostic escape paths without a null-check). Logs NOTHING — preview bytes are
 * attacker-influenced and must never reach logcat.
 */
private object NoOpClient : TerminalSessionClient {
    override fun onTextChanged(changedSession: TerminalSession?) = Unit
    override fun onTitleChanged(changedSession: TerminalSession?) = Unit
    override fun onSessionFinished(finishedSession: TerminalSession?) = Unit
    override fun onCopyTextToClipboard(session: TerminalSession?, text: String?) = Unit
    override fun onPasteTextFromClipboard(session: TerminalSession?) = Unit
    override fun onBell(session: TerminalSession?) = Unit
    override fun onColorsChanged(session: TerminalSession?) = Unit
    override fun onTerminalCursorStateChange(state: Boolean) = Unit
    override fun getTerminalCursorStyle(): Int = TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK
    override fun logError(tag: String?, message: String?) = Unit
    override fun logWarn(tag: String?, message: String?) = Unit
    override fun logInfo(tag: String?, message: String?) = Unit
    override fun logDebug(tag: String?, message: String?) = Unit
    override fun logVerbose(tag: String?, message: String?) = Unit
    override fun logStackTraceWithMessage(tag: String?, message: String?, e: Exception?) = Unit
    override fun logStackTrace(tag: String?, e: Exception?) = Unit
}
