package com.erwan.revwatch.model.face

import kotlin.math.pow

/**
 * Expression evaluator for the Face DSL.
 *
 * Resolves dynamic variables ($progress, $accent, …) and evaluates
 * simple math expressions (270*$progress, -225+270*$progress, …).
 */
object FaceExpr {

    // ── Public API ──────────────────────────────────────────────────

    /** Resolve a string to a Float: "50" → 50f, "270*$progress" → evaluated. */
    fun resolveFloat(expr: String, ctx: FaceRenderContext, customVars: Map<String, String> = emptyMap()): Float {
        // Fast path: single custom var lookup (avoids full string scan for simple $varname refs)
        if (customVars.isNotEmpty() && expr.length > 1 && expr[0] == '$') {
            val cv = customVars[expr]
            if (cv != null) return cv.toFloatOrNull() ?: run {
                val substituted = substituteVars(cv, ctx, customVars)
                evalMath(substituted)
            }
        }
        // Fast path: plain number (no $ or @ references)
        if (!expr.contains('$') && !expr.contains('@') && !expr.startsWith("iif(")) {
            return expr.toFloatOrNull() ?: evalMath(expr)
        }
        val resolved = resolveIif(expr, ctx, customVars)
        val substituted = substituteVars(resolved, ctx, customVars)
        return evalMath(substituted)
    }

    /** Try to resolve as Float; returns null if expression is non-numeric (color, text). */
    fun tryResolveFloat(expr: String, ctx: FaceRenderContext, customVars: Map<String, String> = emptyMap()): Float? {
        // Fast path: single custom var lookup
        if (customVars.isNotEmpty() && expr.length > 1 && expr[0] == '$') {
            val cv = customVars[expr]
            if (cv != null) return cv.toFloatOrNull() ?: tryResolveFloat(cv, ctx, customVars)
        }
        // Fast path: plain number
        if (!expr.contains('$') && !expr.contains('@') && !expr.startsWith("iif(")) {
            return expr.toFloatOrNull()
        }
        val resolved = resolveIif(expr, ctx, customVars)
        val substituted = substituteVars(resolved, ctx, customVars)
        val cleaned = substituted.replace(" ", "")
        if (cleaned.isEmpty()) return null
        return try {
            val pos = intArrayOf(0)
            val result = parseAddSub(cleaned, pos)
            if (pos[0] >= cleaned.length) result else null
        } catch (_: Exception) {
            cleaned.toFloatOrNull()
        }
    }

    /** Resolve a color token to an ARGB int. */
    fun resolveColor(expr: String, ctx: FaceRenderContext, customVars: Map<String, String> = emptyMap()): Int {
        // Fast path: single custom var lookup (e.g. fill:$S1 where $S1 is already resolved to #hex)
        if (customVars.isNotEmpty() && expr.length > 1 && expr[0] == '$') {
            val cv = customVars[expr]
            if (cv != null) return resolveColor(cv, ctx, customVars)
        }
        val resolved = resolveIif(expr, ctx, customVars)
        if (resolved != expr) return resolveColor(resolved, ctx, customVars)
        // Substitute custom vars (@my_color → iif(...) or #hex) — longest first
        if (customVars.isNotEmpty()) {
            var sub = expr
            for ((name, value) in sortedVars(customVars)) {
                sub = sub.replace(name, value)
            }
            if (sub != expr) return resolveColor(sub, ctx, customVars)
        }
        return when (expr) {
            "\$accent" -> ctx.accentColor
            "\$zone_color" -> zoneColor(ctx)
            "white" -> 0xFFFFFFFF.toInt()
            "black" -> 0xFF000000.toInt()
            "muted" -> 0xFF888888.toInt()
            "warning" -> 0xFFFFD96B.toInt()
            "danger" -> 0xFFFF6B6B.toInt()
            "success" -> 0xFF4CAF50.toInt()
            "red" -> 0xFFFF0000.toInt()
            "green" -> 0xFF00FF00.toInt()
            "blue" -> 0xFF0000FF.toInt()
            "yellow" -> 0xFFFFFF00.toInt()
            "cyan" -> 0xFF00FFFF.toInt()
            "orange" -> 0xFFFF9800.toInt()
            else -> if (expr.startsWith("#")) parseHex(expr) else 0xFFFFFFFF.toInt()
        }
    }

    /** Resolve a text content token ($value, $unit, "literal", …). */
    fun resolveText(expr: String, ctx: FaceRenderContext, customVars: Map<String, String> = emptyMap()): String {
        val resolved = resolveIif(expr, ctx, customVars)
        if (resolved != expr) return resolveText(resolved, ctx, customVars)
        // Substitute custom vars (@my_text → iif(...) or literal)
        if (customVars.isNotEmpty()) {
            var sub = expr
            for ((name, value) in customVars) {
                sub = sub.replace(name, value)
            }
            if (sub != expr) return resolveText(sub, ctx, customVars)
        }
        return when (expr) {
            "\$value" -> ctx.formattedValue
            "\$unit" -> ctx.unit
            "\$label" -> ctx.label
            "\$min" -> fmtNum(ctx.minValue)
            "\$max" -> fmtNum(ctx.maxValue)
            "\$raw" -> ctx.rawValue?.let { fmtNum(it) } ?: "--"
            "\$percent" -> "${(ctx.progress * 100).toInt()}%"
            else -> {
                // String interpolation: replace $var tokens within text
                var text = expr.removeSurrounding("\"")
                if (text.contains('$')) {
                    text = text.replace("\$value", ctx.formattedValue)
                    text = text.replace("\$unit", ctx.unit)
                    text = text.replace("\$label", ctx.label)
                    text = text.replace("\$percent", "${(ctx.progress * 100).toInt()}%")
                    text = text.replace("\$min", fmtNum(ctx.minValue))
                    text = text.replace("\$max", fmtNum(ctx.maxValue))
                    text = text.replace("\$raw", ctx.rawValue?.let { fmtNum(it) } ?: "--")
                }
                text
            }
        }
    }

    /** Evaluate a condition string: "$zone==danger", "$progress>0.8", etc. */
    fun evalCondition(condition: String, ctx: FaceRenderContext, customVars: Map<String, String> = emptyMap()): Boolean {
        var s = condition.trim()
        // Substitute custom @vars so e.g. @my_zone==danger → $zone==danger
        if (s.contains('@') && customVars.isNotEmpty()) {
            for ((name, value) in customVars) {
                s = s.replace(name, value)
            }
        }
        // Handle || (OR) - lowest priority
        if (s.contains("||")) {
            return s.split("||").any { evalCondition(it, ctx, customVars) }
        }
        // Handle && (AND)
        if (s.contains("&&")) {
            return s.split("&&").all { evalCondition(it, ctx, customVars) }
        }
        // Single condition
        return when {
            s.contains("!=") -> {
                val (l, r) = s.split("!=", limit = 2)
                // Try numeric first (handles $progress!=0, $raw!=50, etc.)
                val lf = tryResolveFloat(l.trim(), ctx, customVars)
                val rf = tryResolveFloat(r.trim(), ctx, customVars)
                if (lf != null && rf != null) lf != rf
                else resolveString(l.trim(), ctx) != resolveString(r.trim(), ctx)
            }
            s.contains("==") -> {
                val (l, r) = s.split("==", limit = 2)
                // Try numeric first (handles $progress==0, $raw==50, etc.)
                val lf = tryResolveFloat(l.trim(), ctx, customVars)
                val rf = tryResolveFloat(r.trim(), ctx, customVars)
                if (lf != null && rf != null) lf == rf
                else resolveString(l.trim(), ctx) == resolveString(r.trim(), ctx)
            }
            s.contains(">=") -> {
                val (l, r) = s.split(">=", limit = 2)
                resolveFloat(l.trim(), ctx, customVars) >= resolveFloat(r.trim(), ctx, customVars)
            }
            s.contains("<=") -> {
                val (l, r) = s.split("<=", limit = 2)
                resolveFloat(l.trim(), ctx, customVars) <= resolveFloat(r.trim(), ctx, customVars)
            }
            s.contains(">") -> {
                val (l, r) = s.split(">", limit = 2)
                resolveFloat(l.trim(), ctx, customVars) > resolveFloat(r.trim(), ctx, customVars)
            }
            s.contains("<") -> {
                val (l, r) = s.split("<", limit = 2)
                resolveFloat(l.trim(), ctx, customVars) < resolveFloat(r.trim(), ctx, customVars)
            }
            else -> false
        }
    }

    // ── Inline-if: iif(condition,trueVal,falseVal) ─────────────────

    private fun resolveIif(expr: String, ctx: FaceRenderContext, customVars: Map<String, String>): String {
        if (!expr.startsWith("iif(") || !expr.endsWith(")")) return expr
        val inner = expr.substring(4, expr.length - 1)
        val parts = mutableListOf<String>()
        var depth = 0
        var start = 0
        for (i in inner.indices) {
            when (inner[i]) {
                '(' -> depth++
                ')' -> depth--
                ',' -> if (depth == 0) {
                    parts.add(inner.substring(start, i).trim())
                    start = i + 1
                }
            }
        }
        parts.add(inner.substring(start).trim())
        if (parts.size != 3) return expr
        val result = if (evalCondition(parts[0], ctx, customVars)) parts[1] else parts[2]
        return resolveIif(result, ctx, customVars)
    }

    // ── Variable substitution ───────────────────────────────────────

    /** Cache sorted entries: same Map instance → same sorted list (avoids re-sort per element). */
    private var lastSortedVarsMap: Map<String, String>? = null
    private var lastSortedVarsList: List<Map.Entry<String, String>> = emptyList()

    private fun sortedVars(customVars: Map<String, String>): List<Map.Entry<String, String>> {
        if (customVars === lastSortedVarsMap) return lastSortedVarsList
        val sorted = customVars.entries.sortedByDescending { it.key.length }
        lastSortedVarsMap = customVars
        lastSortedVarsList = sorted
        return sorted
    }

    private fun substituteVars(expr: String, ctx: FaceRenderContext, customVars: Map<String, String> = emptyMap()): String {
        // Fast path: no variables to substitute
        if (!expr.contains('$') && !expr.contains('@')) return expr

        var r = expr
        // Custom vars: substitute longest names first to avoid partial matches ($ab before $a)
        if (customVars.isNotEmpty() && (r.contains('$') || r.contains('@'))) {
            for ((name, value) in sortedVars(customVars)) {
                if (r.contains(name)) {
                    r = r.replace(name, value.toString())
                    // Early exit if no more var references
                    if (!r.contains('$') && !r.contains('@')) return r
                }
            }
        }
        // Built-in vars — only replace if the var is present
        if (r.contains('$')) {
            if (r.contains("\$progress")) r = r.replace("\$progress", ctx.progress.toString())
            if (r.contains("\$percent")) r = r.replace("\$percent", (ctx.progress * 100).toString())
            if (r.contains("\$raw")) r = r.replace("\$raw", (ctx.rawValue ?: 0.0).toString())
            if (r.contains("\$min")) r = r.replace("\$min", ctx.minValue.toString())
            if (r.contains("\$max")) r = r.replace("\$max", ctx.maxValue.toString())
            if (r.contains("\$whp")) r = r.replace("\$whp", (ctx.warnHighProgress ?: 1f).toString())
            if (r.contains("\$wlp")) r = r.replace("\$wlp", (ctx.warnLowProgress ?: 0f).toString())
            if (r.contains("\$dhp")) r = r.replace("\$dhp", (ctx.dangerHighProgress ?: 1f).toString())
            if (r.contains("\$dlp")) r = r.replace("\$dlp", (ctx.dangerLowProgress ?: 0f).toString())
            if (r.contains("\$time")) {
                val timeSec = (System.nanoTime() / 1_000_000L % 3_600_000L) / 1000f
                r = r.replace("\$time", timeSec.toString())
            }
        }
        return r
    }

    private fun resolveString(expr: String, ctx: FaceRenderContext): String {
        return when (expr) {
            "\$zone" -> ctx.zone.name.lowercase()
            else -> expr
        }
    }

    private fun zoneColor(ctx: FaceRenderContext): Int = when (ctx.zone) {
        FaceRenderContext.Zone.DANGER -> 0xFFFF6B6B.toInt()
        FaceRenderContext.Zone.WARNING -> 0xFFFFD96B.toInt()
        FaceRenderContext.Zone.NORMAL -> ctx.accentColor
    }

    // ── Math evaluator (supports +, -, *, /, parentheses) ───────────

    private fun evalMath(expr: String): Float {
        val cleaned = expr.replace(" ", "")
        return try {
            val pos = intArrayOf(0)
            val result = parseAddSub(cleaned, pos)
            result
        } catch (_: Exception) {
            cleaned.toFloatOrNull() ?: 0f
        }
    }

    private fun parseAddSub(e: String, p: IntArray): Float {
        var result = parseMulDiv(e, p)
        while (p[0] < e.length && (e[p[0]] == '+' || e[p[0]] == '-')) {
            val op = e[p[0]]; p[0]++
            val rhs = parseMulDiv(e, p)
            result = if (op == '+') result + rhs else result - rhs
        }
        return result
    }

    private fun parseMulDiv(e: String, p: IntArray): Float {
        var result = parseAtom(e, p)
        while (p[0] < e.length && (e[p[0]] == '*' || e[p[0]] == '/')) {
            val op = e[p[0]]; p[0]++
            val rhs = parseAtom(e, p)
            result = if (op == '*') result * rhs else if (rhs != 0f) result / rhs else 0f
        }
        return result
    }

    private fun parseAtom(e: String, p: IntArray): Float {
        // Parentheses
        if (p[0] < e.length && e[p[0]] == '(') {
            p[0]++
            val result = parseAddSub(e, p)
            if (p[0] < e.length && e[p[0]] == ')') p[0]++
            return result
        }
        // Function call: name(args...)
        val funcStart = p[0]
        if (p[0] < e.length && e[p[0]].isLetter()) {
            while (p[0] < e.length && (e[p[0]].isLetter() || e[p[0]] == '_')) p[0]++
            val fname = e.substring(funcStart, p[0])
            if (p[0] < e.length && e[p[0]] == '(') {
                p[0]++ // skip (
                val args = mutableListOf<Float>()
                args.add(parseAddSub(e, p))
                while (p[0] < e.length && e[p[0]] == ',') {
                    p[0]++
                    args.add(parseAddSub(e, p))
                }
                if (p[0] < e.length && e[p[0]] == ')') p[0]++
                return callFunc(fname, args)
            }
            // Not a function call, reset position
            p[0] = funcStart
        }
        // Unary minus
        val neg = p[0] < e.length && e[p[0]] == '-'
        if (neg) p[0]++
        // Number (including scientific notation like 2.5E-16, 1e10)
        val start = p[0]
        while (p[0] < e.length && (e[p[0]].isDigit() || e[p[0]] == '.')) p[0]++
        if (p[0] > start && p[0] < e.length && (e[p[0]] == 'E' || e[p[0]] == 'e')) {
            p[0]++ // skip E/e
            if (p[0] < e.length && (e[p[0]] == '+' || e[p[0]] == '-')) p[0]++ // skip sign
            while (p[0] < e.length && e[p[0]].isDigit()) p[0]++ // exponent digits
        }
        val num = if (p[0] > start) e.substring(start, p[0]).toFloatOrNull() ?: 0f else 0f
        return if (neg) -num else num
    }

    private fun callFunc(name: String, args: List<Float>): Float {
        return when (name) {
            "clamp" -> args.getOrElse(0) { 0f }.coerceIn(args.getOrElse(1) { 0f }, args.getOrElse(2) { 0f })
            "min" -> minOf(args.getOrElse(0) { 0f }, args.getOrElse(1) { 0f })
            "max" -> maxOf(args.getOrElse(0) { 0f }, args.getOrElse(1) { 0f })
            "abs" -> kotlin.math.abs(args.getOrElse(0) { 0f })
            "round" -> kotlin.math.round(args.getOrElse(0) { 0f })
            "floor" -> kotlin.math.floor(args.getOrElse(0) { 0f })
            "ceil" -> kotlin.math.ceil(args.getOrElse(0) { 0f })
            "lerp" -> {
                val a = args.getOrElse(0) { 0f }
                val b = args.getOrElse(1) { 0f }
                val t = args.getOrElse(2) { 0f }
                a + (b - a) * t
            }
            "sin" -> kotlin.math.sin(Math.toRadians(args.getOrElse(0) { 0f }.toDouble())).toFloat()
            "cos" -> kotlin.math.cos(Math.toRadians(args.getOrElse(0) { 0f }.toDouble())).toFloat()
            "mod" -> {
                val a = args.getOrElse(0) { 0f }
                val b = args.getOrElse(1) { 0f }
                if (b != 0f) ((a % b) + b) % b else 0f
            }
            "pow" -> args.getOrElse(0) { 0f }.pow(args.getOrElse(1) { 0f })
            else -> 0f
        }
    }

    // ── Color parsing ───────────────────────────────────────────────

    private fun parseHex(hex: String): Int {
        val c = hex.removePrefix("#")
        return try {
            when (c.length) {
                3 -> {
                    val r = c[0].toString().repeat(2)
                    val g = c[1].toString().repeat(2)
                    val b = c[2].toString().repeat(2)
                    (0xFF000000 or "$r$g$b".toLong(16)).toInt()
                }
                6 -> (0xFF000000 or c.toLong(16)).toInt()
                8 -> c.toLong(16).toInt()
                else -> 0xFFFFFFFF.toInt()
            }
        } catch (_: Exception) {
            0xFFFFFFFF.toInt()
        }
    }

    private fun fmtNum(v: Double): String =
        if (v == v.toLong().toDouble()) v.toLong().toString() else "%.1f".format(v)
}
