package com.erwan.revwatch.model.face

import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.Canvas
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Typeface
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin

/**
 * Renders a [FaceDef] onto an Android [Canvas].
 *
 * Supports 21 primitive types:
 *   Static:  circle, ellipse, arc, rect, line, text, ticks, ring, polygon, shape, dot, gradient_ring
 *   Dynamic: progress_arc, progress_bar, progress_ring, needle, value_dot,
 *            zone_arc, zone_ring, sweep_ticks, digital_bar
 *
 * All coordinates in the DSL are percentages (0–100) of the gauge bounds.
 * Angles are in degrees: 0° = 3 o'clock, positive = clockwise.
 */
class FaceRenderer(private val context: Context? = null) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val tmpRect = RectF()
    /** Custom @var values resolved for the current render pass. */
    private var cv: Map<String, String> = emptyMap()

    /** Font cache: DSL key → Typeface. */
    private val fontCache = mutableMapOf<String, Typeface>()
    /** Reusable map for custom vars to avoid allocation on every render. */
    private val reusableVars = mutableMapOf<String, String>()

    // ── Static segment cache (preserves z-order) ──────────────────────
    private data class Segment(val startIdx: Int, val endIdx: Int, val isStatic: Boolean)
    private var segments: List<Segment> = emptyList()
    private var segmentBitmaps: Array<android.graphics.Bitmap?> = emptyArray()
    private var cacheFace: FaceDef? = null
    private var cacheW = 0
    private var cacheH = 0

    /** Types that implicitly read runtime values ($progress, $zone, etc.) in their draw function. */
    private val INHERENTLY_DYNAMIC_TYPES = setOf(
        "progress_arc", "progress_bar", "progress_ring",
        "needle", "value_dot", "sweep_ticks", "digital_bar",
        "zone_arc", "zone_ring"
    )

    /** Check if a node is fully static (no $ references anywhere). */
    private fun isStaticNode(node: FaceNode, dynamicVars: Set<String>): Boolean = when (node) {
        is FaceNode.Conditional -> false
        is FaceNode.ForLoop -> false
        is FaceNode.Group -> node.children.all { isStaticNode(it, dynamicVars) }
        is FaceNode.Element -> {
            val hasDynamic = node.type in INHERENTLY_DYNAMIC_TYPES ||
                    node.args.any { a -> a.contains('$') || dynamicVars.any { a.contains(it) } } ||
                    node.props.values.any { v -> v.contains('$') || dynamicVars.any { v.contains(it) } } ||
                    node.props.containsKey("blink")
            !hasDynamic
        }
    }

    /** Find @var names that transitively depend on $ runtime variables. */
    private fun findDynamicVars(face: FaceDef): Set<String> {
        val dynamic = mutableSetOf<String>()
        var changed = true
        while (changed) {
            changed = false
            for ((name, expr) in face.vars) {
                if (name in dynamic) continue
                if (expr.contains('$') || dynamic.any { expr.contains(it) }) {
                    dynamic.add(name)
                    changed = true
                }
            }
        }
        return dynamic
    }

    fun render(canvas: Canvas, face: FaceDef, ctx: FaceRenderContext, bounds: RectF) {
        val size = min(bounds.width(), bounds.height())

        // Resolve custom vars (reuse map to avoid allocation)
        reusableVars.clear()
        val customVars = reusableVars

        // Inject extra vars from multi-value data bindings first
        // so that face @var declarations can reference them
        if (ctx.extraVars.isNotEmpty()) {
            customVars.putAll(ctx.extraVars)
        }

        for ((name, expr) in face.vars) {
            var resolved = expr
            // Substitute previous vars (longest first to avoid partial matches)
            if (customVars.isNotEmpty() && (resolved.contains('$') || resolved.contains('@'))) {
                val sorted = customVars.entries.sortedByDescending { it.key.length }
                for ((n, v) in sorted) {
                    if (resolved.contains(n)) {
                        resolved = resolved.replace(n, v)
                        if (!resolved.contains('$') && !resolved.contains('@')) break
                    }
                }
            }
            val f = FaceExpr.tryResolveFloat(resolved, ctx, customVars)
            customVars[name] = f?.toString() ?: resolved
        }
        cv = customVars

        val bw = bounds.width().toInt()
        val bh = bounds.height().toInt()
        if (bw <= 0 || bh <= 0) return

        // Rebuild segment cache if face or size changed
        if (cacheFace !== face || cacheW != bw || cacheH != bh) {
            val dynVars = findDynamicVars(face)
            val flags = BooleanArray(face.elements.size) { isStaticNode(face.elements[it], dynVars) }

            // Build segments: runs of consecutive static or dynamic elements
            val segs = mutableListOf<Segment>()
            var i = 0
            while (i < face.elements.size) {
                val isStatic = flags[i]
                val start = i
                while (i < face.elements.size && flags[i] == isStatic) i++
                segs.add(Segment(start, i, isStatic))
            }
            segments = segs

            // Recycle old bitmaps
            segmentBitmaps.forEach { it?.recycle() }
            segmentBitmaps = Array(segs.size) { null }

            // Pre-render each static segment to its own bitmap
            for (si in segs.indices) {
                if (!segs[si].isStatic) continue
                val bmp = android.graphics.Bitmap.createBitmap(bw, bh, android.graphics.Bitmap.Config.ARGB_8888).apply {
                    density = canvas.density
                }
                val sc = Canvas(bmp)
                sc.translate(-bounds.left, -bounds.top)
                for (ei in segs[si].startIdx until segs[si].endIdx) {
                    drawNode(sc, face.elements[ei], ctx, bounds, size)
                }
                segmentBitmaps[si] = bmp
            }

            cacheFace = face
            cacheW = bw
            cacheH = bh
        }

        // Draw background
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null
        paint.isAntiAlias = true; paint.style = Paint.Style.FILL; paint.alpha = 255
        paint.color = FaceExpr.resolveColor(face.bg, ctx, cv)
        canvas.drawRect(bounds, paint)

        // Draw segments in order — preserves z-order
        for (si in segments.indices) {
            val seg = segments[si]
            if (seg.isStatic) {
                segmentBitmaps[si]?.let { canvas.drawBitmap(it, bounds.left, bounds.top, null) }
            } else {
                for (ei in seg.startIdx until seg.endIdx) {
                    drawNode(canvas, face.elements[ei], ctx, bounds, size)
                }
            }
        }
    }

    // ── Node dispatch ───────────────────────────────────────────────

    private fun drawNode(canvas: Canvas, node: FaceNode, ctx: FaceRenderContext, b: RectF, s: Float) {
        when (node) {
            is FaceNode.Group -> {
                for (child in node.children) drawNode(canvas, child, ctx, b, s)
            }
            is FaceNode.Conditional -> {
                if (FaceExpr.evalCondition(node.condition, ctx, cv)) {
                    for (child in node.children) drawNode(canvas, child, ctx, b, s)
                }
            }
            is FaceNode.ForLoop -> {
                val fromVal = FaceExpr.resolveFloat(node.from, ctx, cv).toDouble()
                val toVal = FaceExpr.resolveFloat(node.to, ctx, cv).toDouble()
                val stepVal = FaceExpr.resolveFloat(node.step, ctx, cv).toDouble().let { if (it == 0.0) 1.0 else it }
                // Guard against wrong direction
                if ((stepVal > 0 && fromVal > toVal + 0.0001) || (stepVal < 0 && fromVal < toVal - 0.0001)) return
                val savedVar = reusableVars[node.varName]
                var v = fromVal
                var iterations = 0
                while (if (stepVal > 0) v <= toVal + 0.0001 else v >= toVal - 0.0001) {
                    if (++iterations > 1000) break
                    reusableVars[node.varName] = v.toFloat().toString()
                    for (child in node.children) drawNode(canvas, child, ctx, b, s)
                    v += stepVal
                }
                if (savedVar != null) reusableVars[node.varName] = savedVar
                else reusableVars.remove(node.varName)
            }
            is FaceNode.Element -> {
                // Blink: skip drawing during the "off" phase
                val blinkMs = node.props["blink"]?.let { FaceExpr.resolveFloat(it, ctx, cv) }
                if (blinkMs != null && blinkMs > 0f) {
                    val phase = (System.currentTimeMillis() % blinkMs.toLong()).toFloat() / blinkMs
                    if (phase > 0.5f) return
                }
                drawElement(canvas, node, ctx, b, s)
            }
        }
    }

    /** Returns true if any element in the face uses blink. */
    fun hasBlink(face: FaceDef): Boolean = hasBlinkNodes(face.elements)

    /** Returns true if the face uses the $time variable (needs continuous rendering). */
    fun hasTimeVar(face: FaceDef): Boolean {
        if (face.vars.values.any { it.contains("\$time") }) return true
        return hasTimeInNodes(face.elements)
    }

    private fun hasTimeInNodes(nodes: List<FaceNode>): Boolean = nodes.any { node ->
        when (node) {
            is FaceNode.Element -> node.args.any { it.contains("\$time") } ||
                    node.props.values.any { it.contains("\$time") }
            is FaceNode.Conditional -> node.condition.contains("\$time") || hasTimeInNodes(node.children)
            is FaceNode.Group -> hasTimeInNodes(node.children)
            is FaceNode.ForLoop -> hasTimeInNodes(node.children)
        }
    }

    private fun hasBlinkNodes(nodes: List<FaceNode>): Boolean {
        for (node in nodes) {
            when (node) {
                is FaceNode.Element -> if (node.props.containsKey("blink")) return true
                is FaceNode.Conditional -> if (hasBlinkNodes(node.children)) return true
                is FaceNode.Group -> if (hasBlinkNodes(node.children)) return true
                is FaceNode.ForLoop -> if (hasBlinkNodes(node.children)) return true
            }
        }
        return false
    }

    private fun drawElement(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        when (el.type) {
            "circle" -> drawCircle(canvas, el, ctx, b, s)
            "arc" -> drawArc(canvas, el, ctx, b, s)
            "rect" -> drawRect(canvas, el, ctx, b, s)
            "line" -> drawLine(canvas, el, ctx, b, s)
            "text" -> drawText(canvas, el, ctx, b, s)
            "ticks" -> drawTicks(canvas, el, ctx, b, s)
            "ring" -> drawRing(canvas, el, ctx, b, s)
            "polygon" -> drawPolygon(canvas, el, ctx, b, s)
            "shape" -> drawShape(canvas, el, ctx, b, s)
            "ellipse" -> drawEllipse(canvas, el, ctx, b, s)
            "dot" -> drawDot(canvas, el, ctx, b, s)
            "gradient_ring" -> drawGradientRing(canvas, el, ctx, b, s)
            "progress_arc" -> drawProgressArc(canvas, el, ctx, b, s)
            "progress_bar" -> drawProgressBar(canvas, el, ctx, b, s)
            "progress_ring" -> drawProgressRing(canvas, el, ctx, b, s)
            "needle" -> drawNeedle(canvas, el, ctx, b, s)
            "value_dot" -> drawValueDot(canvas, el, ctx, b, s)
            "zone_arc" -> drawZoneArc(canvas, el, ctx, b, s)
            "zone_ring" -> drawZoneRing(canvas, el, ctx, b, s)
            "sweep_ticks" -> drawSweepTicks(canvas, el, ctx, b, s)
            "digital_bar" -> drawDigitalBar(canvas, el, ctx, b, s)
        }
    }

    // ── Helpers ──────────────────────────────────────────────────────

    /** Degrees (not percentage) */
    private fun deg(arg: String, ctx: FaceRenderContext): Float =
        FaceExpr.resolveFloat(arg, ctx, cv)

    /** Property → pixels via percentage */
    private fun propPct(el: FaceNode.Element, key: String, s: Float, ctx: FaceRenderContext, default: Float = 0f): Float {
        val v = el.props[key] ?: return default
        return FaceExpr.resolveFloat(v, ctx, cv) / 100f * s
    }

    /** Property → raw float */
    private fun propFloat(el: FaceNode.Element, key: String, ctx: FaceRenderContext, default: Float = 0f): Float {
        val v = el.props[key] ?: return default
        return FaceExpr.resolveFloat(v, ctx, cv)
    }

    /** Property → ARGB color */
    private fun propColor(el: FaceNode.Element, key: String, ctx: FaceRenderContext, default: Int = 0xFFFFFFFF.toInt()): Int {
        val v = el.props[key] ?: return default
        return FaceExpr.resolveColor(v, ctx, cv)
    }

    /** Canvas position from percentage. */
    private fun posX(arg: String, b: RectF, s: Float, ctx: FaceRenderContext): Float =
        b.left + FaceExpr.resolveFloat(arg, ctx, cv) / 100f * s

    private fun posY(arg: String, b: RectF, s: Float, ctx: FaceRenderContext): Float =
        b.top + FaceExpr.resolveFloat(arg, ctx, cv) / 100f * s

    private fun resolveSize(arg: String, s: Float, ctx: FaceRenderContext): Float =
        FaceExpr.resolveFloat(arg, ctx, cv) / 100f * s

    private fun arcRect(cx: Float, cy: Float, r: Float): RectF {
        tmpRect.set(cx - r, cy - r, cx + r, cy + r)
        return tmpRect
    }

    private fun resolveCap(value: String?): Paint.Cap = when (value) {
        "round" -> Paint.Cap.ROUND
        "square" -> Paint.Cap.SQUARE
        else -> Paint.Cap.BUTT
    }

    private fun applyAlpha(el: FaceNode.Element, ctx: FaceRenderContext) {
        val a = el.props["alpha"]?.let { FaceExpr.resolveFloat(it, ctx, cv) } ?: 1f
        paint.alpha = (a.coerceIn(0f, 1f) * 255).toInt()
    }

    private fun applyDash(el: FaceNode.Element, s: Float) {
        val dashStr = el.props["dash"] ?: return
        val parts = dashStr.split(",")
        if (parts.size >= 2) {
            val on = (parts[0].toFloatOrNull() ?: 4f) / 100f * s
            val off = (parts[1].toFloatOrNull() ?: 2f) / 100f * s
            paint.pathEffect = DashPathEffect(floatArrayOf(on, off), 0f)
        }
    }

    /** Resolve a DSL font key to a Typeface, with caching. */
    private fun resolveTypeface(fontKey: String?, style: Int): Typeface {
        if (fontKey == null || fontKey == "sans-serif" || fontKey == "sans-serif-condensed") {
            return Typeface.create(fontKey ?: "sans-serif-condensed", style)
        }
        val key = fontKey.lowercase()
        val base = fontCache.getOrPut(key) {
            if (context != null) {
                try {
                    Typeface.createFromAsset(context.assets, "fonts/$key.ttf")
                } catch (_: Exception) {
                    Typeface.create("sans-serif", Typeface.NORMAL)
                }
            } else {
                Typeface.create("sans-serif", Typeface.NORMAL)
            }
        }
        return Typeface.create(base, style)
    }

    private fun applyGlow(el: FaceNode.Element, ctx: FaceRenderContext, s: Float): Boolean {
        val glowPct = el.props["glow"]?.toFloatOrNull() ?: return false
        if (glowPct <= 0f) return false
        val px = (glowPct / 100f * s).coerceAtLeast(1f)
        paint.maskFilter = BlurMaskFilter(px, BlurMaskFilter.Blur.NORMAL)
        // Apply glow_color if specified (changes paint color for the glow pass)
        el.props["glow_color"]?.let { paint.color = FaceExpr.resolveColor(it, ctx, cv) }
        return true
    }

    private fun clearEffects() {
        paint.pathEffect = null
        paint.maskFilter = null
    }

    private fun deg2rad(deg: Float): Float = Math.toRadians(deg.toDouble()).toFloat()

    // ── Rounded-rect helpers for corner property ────────────────────

    /** Build a clockwise rounded-rect Path centered at (cx,cy) with half-size r and corner radius cr. */
    private fun buildRoundedRectPath(cx: Float, cy: Float, r: Float, cr: Float): Path {
        val path = Path()
        path.moveTo(cx + r, cy)
        path.lineTo(cx + r, cy + r - cr)
        if (cr > 0.1f) {
            tmpRect.set(cx + r - 2f * cr, cy + r - 2f * cr, cx + r, cy + r)
            path.arcTo(tmpRect, 0f, 90f)
        } else path.lineTo(cx + r, cy + r)
        path.lineTo(cx - r + cr, cy + r)
        if (cr > 0.1f) {
            tmpRect.set(cx - r, cy + r - 2f * cr, cx - r + 2f * cr, cy + r)
            path.arcTo(tmpRect, 90f, 90f)
        } else path.lineTo(cx - r, cy + r)
        path.lineTo(cx - r, cy - r + cr)
        if (cr > 0.1f) {
            tmpRect.set(cx - r, cy - r, cx - r + 2f * cr, cy - r + 2f * cr)
            path.arcTo(tmpRect, 180f, 90f)
        } else path.lineTo(cx - r, cy - r)
        path.lineTo(cx + r - cr, cy - r)
        if (cr > 0.1f) {
            tmpRect.set(cx + r - 2f * cr, cy - r, cx + r, cy - r + 2f * cr)
            path.arcTo(tmpRect, 270f, 90f)
        } else path.lineTo(cx + r, cy - r)
        path.close()
        return path
    }

    /** Compute total perimeter of a rounded rect with half-size r and corner radius cr. */
    private fun roundedRectPerim(r: Float, cr: Float): Float {
        val straight = maxOf(0f, r - cr)
        return 8f * straight + 2f * Math.PI.toFloat() * cr
    }

    /**
     * Map an angle (0=right, CW) to a point on the rounded rect perimeter + inward normal.
     * Returns FloatArray(4): [x, y, nx, ny] where (nx,ny) is the unit vector pointing inward.
     */
    private fun pointOnRoundedRect(cx: Float, cy: Float, r: Float, cr: Float, angleDeg: Float): FloatArray {
        val straight = maxOf(0f, r - cr)
        val straight2 = 2f * straight
        val arcLen = Math.PI.toFloat() * cr / 2f
        val segLens = floatArrayOf(straight, arcLen, straight2, arcLen, straight2, arcLen, straight2, arcLen, straight)
        val totalPerim = segLens.sum()
        if (totalPerim <= 0f) return floatArrayOf(cx + r, cy, -1f, 0f)

        val normAngle = ((angleDeg % 360f) + 360f) % 360f
        var dist = (normAngle / 360f) * totalPerim

        for (i in segLens.indices) {
            if (dist <= segLens[i] + 0.001f || i == segLens.size - 1) {
                val t = if (segLens[i] > 0f) minOf(1f, dist / segLens[i]) else 0f
                return when (i) {
                    0 -> floatArrayOf(cx + r, cy + t * straight, -1f, 0f)
                    1 -> { val a = t * Math.PI.toFloat() / 2f; floatArrayOf(cx + r - cr + cr * cos(a), cy + r - cr + cr * sin(a), -cos(a), -sin(a)) }
                    2 -> floatArrayOf(cx + r - cr - t * straight2, cy + r, 0f, -1f)
                    3 -> { val a = Math.PI.toFloat() / 2f + t * Math.PI.toFloat() / 2f; floatArrayOf(cx - r + cr + cr * cos(a), cy + r - cr + cr * sin(a), -cos(a), -sin(a)) }
                    4 -> floatArrayOf(cx - r, cy + r - cr - t * straight2, 1f, 0f)
                    5 -> { val a = Math.PI.toFloat() + t * Math.PI.toFloat() / 2f; floatArrayOf(cx - r + cr + cr * cos(a), cy - r + cr + cr * sin(a), -cos(a), -sin(a)) }
                    6 -> floatArrayOf(cx - r + cr + t * straight2, cy - r, 0f, 1f)
                    7 -> { val a = 3f * Math.PI.toFloat() / 2f + t * Math.PI.toFloat() / 2f; floatArrayOf(cx + r - cr + cr * cos(a), cy - r + cr + cr * sin(a), -cos(a), -sin(a)) }
                    8 -> floatArrayOf(cx + r, cy - r + cr + t * straight, -1f, 0f)
                    else -> floatArrayOf(cx + r, cy, -1f, 0f)
                }
            }
            dist -= segLens[i]
        }
        return floatArrayOf(cx + r, cy, -1f, 0f)
    }

    // ── 1. CIRCLE ───────────────────────────────────────────────────
    // circle cx cy r [fill:color] [stroke:color] [w:width] [alpha:a] [glow:radius]

    private fun drawCircle(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 3) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)

        val fill = el.props["fill"]
        val stroke = el.props["stroke"]

        if (fill != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.FILL
            paint.color = FaceExpr.resolveColor(fill, ctx, cv)
            applyAlpha(el, ctx)
            if (applyGlow(el, ctx, s)) {
                canvas.drawCircle(cx, cy, r, paint)
                clearEffects()
                paint.color = FaceExpr.resolveColor(fill, ctx, cv)
                applyAlpha(el, ctx)
            }
            canvas.drawCircle(cx, cy, r, paint)
        }
        if (stroke != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
            paint.strokeWidth = propPct(el, "w", s, ctx, 1f / 100f * s)
            applyAlpha(el, ctx)
            applyDash(el, s)
            canvas.drawCircle(cx, cy, r, paint)
            clearEffects()
        }
    }

    // ── ELLIPSE ───────────────────────────────────────────────────
    // ellipse cx cy rx ry [fill:color] [stroke:color] [w:width] [rotation:deg] [alpha:a] [glow:radius]

    private fun drawEllipse(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 4) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val rx = resolveSize(el.args[2], s, ctx)
        val ry = resolveSize(el.args[3], s, ctx)
        val rot = el.props["rotation"]?.let { deg(it, ctx) } ?: 0f

        val oval = RectF(cx - rx, cy - ry, cx + rx, cy + ry)

        canvas.save()
        if (rot != 0f) canvas.rotate(rot, cx, cy)

        val fill = el.props["fill"]
        if (fill != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.FILL
            paint.color = FaceExpr.resolveColor(fill, ctx, cv)
            applyAlpha(el, ctx)
            if (applyGlow(el, ctx, s)) {
                canvas.drawOval(oval, paint)
                clearEffects()
                paint.color = FaceExpr.resolveColor(fill, ctx, cv)
                applyAlpha(el, ctx)
            }
            canvas.drawOval(oval, paint)
        }
        val stroke = el.props["stroke"]
        if (stroke != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
            paint.strokeWidth = propPct(el, "w", s, ctx, 1f / 100f * s)
            applyAlpha(el, ctx)
            canvas.drawOval(oval, paint)
            clearEffects()
        }

        canvas.restore()
    }

    // ── 2. ARC ──────────────────────────────────────────────────────
    // arc cx cy r startAngle sweepAngle [stroke:color] [fill:color] [w:width] [cap:] [glow:]

    private fun drawArc(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 5) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)
        val start = deg(el.args[3], ctx)
        val sweep = deg(el.args[4], ctx)

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (r * cornerPct / 100f).coerceIn(0f, r)
            val totalPerim = roundedRectPerim(r, cr)
            if (totalPerim <= 0f) return
            val normStart = ((start % 360f) + 360f) % 360f
            val startDist = normStart / 360f * totalPerim
            val sweepDist = kotlin.math.abs(sweep) / 360f * totalPerim
            val path = buildRoundedRectPath(cx, cy, r, cr)
            val stroke = el.props["stroke"] ?: el.props["color"]
            if (stroke != null) {
                paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
                paint.style = Paint.Style.STROKE
                paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
                paint.strokeWidth = propPct(el, "w", s, ctx, 1f / 100f * s)
                paint.strokeCap = resolveCap(el.props["cap"])
                applyAlpha(el, ctx)
                applyGlow(el, ctx, s)
                paint.pathEffect = DashPathEffect(floatArrayOf(sweepDist, totalPerim), -startDist)
                canvas.drawPath(path, paint)
                clearEffects()
            }
            return
        }

        val rect = arcRect(cx, cy, r)

        val fill = el.props["fill"]
        if (fill != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.FILL
            paint.color = FaceExpr.resolveColor(fill, ctx, cv)
            applyAlpha(el, ctx)
            canvas.drawArc(rect, start, sweep, true, paint)
        }

        val stroke = el.props["stroke"] ?: el.props["color"]
        if (stroke != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
            paint.strokeWidth = propPct(el, "w", s, ctx, 1f / 100f * s)
            paint.strokeCap = resolveCap(el.props["cap"])
            applyAlpha(el, ctx)
            applyDash(el, s)
            applyGlow(el, ctx, s)
            canvas.drawArc(rect, start, sweep, false, paint)
            clearEffects()
        }
    }

    // ── 3. RECT ─────────────────────────────────────────────────────
    // rect x y w h [fill:color] [stroke:color] [sw:width] [corner:r] [alpha:a]

    private fun drawRect(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 4) return
        val x = posX(el.args[0], b, s, ctx)
        val y = posY(el.args[1], b, s, ctx)
        val w = resolveSize(el.args[2], s, ctx)
        val h = resolveSize(el.args[3], s, ctx)
        val corner = propPct(el, "corner", s, ctx)
        val drawRect = RectF(x, y, x + w, y + h)

        val fill = el.props["fill"] ?: el.props["bg"]
        if (fill != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.FILL
            paint.color = FaceExpr.resolveColor(fill, ctx, cv)
            applyAlpha(el, ctx)
            if (corner > 0f) canvas.drawRoundRect(drawRect, corner, corner, paint)
            else canvas.drawRect(drawRect, paint)
        }

        val stroke = el.props["stroke"]
        if (stroke != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
            paint.strokeWidth = propPct(el, "sw", s, ctx, 1f / 100f * s)
            applyAlpha(el, ctx)
            if (corner > 0f) canvas.drawRoundRect(drawRect, corner, corner, paint)
            else canvas.drawRect(drawRect, paint)
        }
    }

    // ── 4. LINE ─────────────────────────────────────────────────────
    // line x1 y1 x2 y2 [stroke:color] [w:width] [cap:] [dash:on,off]

    private fun drawLine(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 4) return
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.color = propColor(el, "stroke", ctx, propColor(el, "color", ctx))
        paint.strokeWidth = propPct(el, "w", s, ctx, 0.5f / 100f * s)
        paint.strokeCap = resolveCap(el.props["cap"])
        applyAlpha(el, ctx)
        applyDash(el, s)
        canvas.drawLine(
            posX(el.args[0], b, s, ctx), posY(el.args[1], b, s, ctx),
            posX(el.args[2], b, s, ctx), posY(el.args[3], b, s, ctx), paint
        )
        clearEffects()
    }

    // ── 5. TEXT ──────────────────────────────────────────────────────
    // text content x y [size:s] [color:color] [bold] [italic] [align:center|left|right]

    private fun drawText(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 3) return
        val content = FaceExpr.resolveText(el.args[0], ctx, cv)
        val x = posX(el.args[1], b, s, ctx)
        val y = posY(el.args[2], b, s, ctx)
        val textSize = propPct(el, "size", s, ctx, 10f / 100f * s)

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.FILL
        paint.color = propColor(el, "color", ctx)
        paint.textSize = textSize
        applyAlpha(el, ctx)

        val bold = el.props.containsKey("bold")
        val italic = el.props.containsKey("italic")
        val style = when {
            bold && italic -> Typeface.BOLD_ITALIC
            bold -> Typeface.BOLD
            italic -> Typeface.ITALIC
            else -> Typeface.NORMAL
        }
        paint.typeface = resolveTypeface(el.props["font"], style)
        paint.textAlign = when (el.props["align"]) {
            "left" -> Paint.Align.LEFT
            "right" -> Paint.Align.RIGHT
            else -> Paint.Align.CENTER
        }

        // Glow on text via shadow layer
        val glowPct = el.props["glow"]?.toFloatOrNull()
        if (glowPct != null && glowPct > 0f) {
            val glowPx = (glowPct / 100f * s).coerceAtLeast(1f)
            val glowColor = propColor(el, "glow_color", ctx, paint.color)
            paint.setShadowLayer(glowPx, 0f, 0f, glowColor)
        }

        // Vertical centering: offset by half ascent
        val yOff = y - (paint.ascent() + paint.descent()) / 2f
        canvas.drawText(content, x, yOff, paint)
        paint.setShadowLayer(0f, 0f, 0f, 0)
    }

    // ── 6. TICKS ────────────────────────────────────────────────────
    // ticks cx cy r count len [start:angle] [sweep:angle] [stroke:color] [w:width]

    private fun drawTicks(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 5) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)
        val count = el.args[3].toIntOrNull() ?: return
        val len = resolveSize(el.args[4], s, ctx)
        val startAngle = propFloat(el, "start", ctx, -225f)
        val sweepAngle = propFloat(el, "sweep", ctx, 270f)

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.color = propColor(el, "stroke", ctx)
        paint.strokeWidth = propPct(el, "w", s, ctx, 0.5f / 100f * s)
        paint.strokeCap = resolveCap(el.props["cap"])
        applyAlpha(el, ctx)

        if (count < 2) return

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (r * cornerPct / 100f).coerceIn(0f, r)
            for (i in 0 until count) {
                val angle = startAngle + sweepAngle * i / (count - 1)
                val pt = pointOnRoundedRect(cx, cy, r, cr, angle)
                canvas.drawLine(pt[0] + pt[2] * len, pt[1] + pt[3] * len, pt[0], pt[1], paint)
            }
            return
        }

        for (i in 0 until count) {
            val angle = startAngle + sweepAngle * i / (count - 1)
            val rad = deg2rad(angle)
            val x1 = cx + (r - len) * cos(rad)
            val y1 = cy + (r - len) * sin(rad)
            val x2 = cx + r * cos(rad)
            val y2 = cy + r * sin(rad)
            canvas.drawLine(x1, y1, x2, y2, paint)
        }
    }

    // ── 7. RING ─────────────────────────────────────────────────────
    // ring cx cy r_outer r_inner start sweep [fill:color] [stroke:color] [w:width]

    private fun drawRing(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 6) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val rOuter = resolveSize(el.args[2], s, ctx)
        val rInner = resolveSize(el.args[3], s, ctx)
        val start = deg(el.args[4], ctx)
        val sweep = deg(el.args[5], ctx)

        // Draw as thick arc: stroke width = rOuter - rInner, radius = midpoint
        val midR = (rOuter + rInner) / 2f
        val thickness = rOuter - rInner

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (midR * cornerPct / 100f).coerceIn(0f, midR)
            val totalPerim = roundedRectPerim(midR, cr)
            if (totalPerim <= 0f) return
            val normStart = ((start % 360f) + 360f) % 360f
            val startDist = normStart / 360f * totalPerim
            val sweepDist = kotlin.math.abs(sweep) / 360f * totalPerim
            val path = buildRoundedRectPath(cx, cy, midR, cr)
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.strokeWidth = thickness
            paint.color = propColor(el, "fill", ctx, propColor(el, "stroke", ctx))
            paint.strokeCap = Paint.Cap.BUTT
            applyAlpha(el, ctx)
            paint.pathEffect = DashPathEffect(floatArrayOf(sweepDist, totalPerim), -startDist)
            canvas.drawPath(path, paint)
            paint.pathEffect = null
            return
        }

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = thickness
        paint.color = propColor(el, "fill", ctx, propColor(el, "stroke", ctx))
        paint.strokeCap = Paint.Cap.BUTT
        applyAlpha(el, ctx)
        canvas.drawArc(arcRect(cx, cy, midR), start, sweep, false, paint)
    }

    // ── 8. POLYGON ──────────────────────────────────────────────────
    // polygon cx cy r sides [rotation:deg] [fill:color] [stroke:color] [w:width]

    private fun drawPolygon(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 4) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)
        val sides = FaceExpr.resolveFloat(el.args[3], ctx, cv).toInt()
        if (sides < 3) return
        val rotation = propFloat(el, "rotation", ctx, -90f)

        val path = Path()
        for (i in 0 until sides) {
            val angle = rotation + 360f * i / sides
            val rad = deg2rad(angle)
            val px = cx + r * cos(rad)
            val py = cy + r * sin(rad)
            if (i == 0) path.moveTo(px, py) else path.lineTo(px, py)
        }
        path.close()

        val fill = el.props["fill"]
        if (fill != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.FILL
            paint.color = FaceExpr.resolveColor(fill, ctx, cv)
            applyAlpha(el, ctx)
            if (applyGlow(el, ctx, s)) {
                canvas.drawPath(path, paint)
                clearEffects()
                paint.color = FaceExpr.resolveColor(fill, ctx, cv)
                applyAlpha(el, ctx)
            }
            canvas.drawPath(path, paint)
        }
        val stroke = el.props["stroke"]
        if (stroke != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
            paint.strokeWidth = propPct(el, "w", s, ctx, 0.5f / 100f * s)
            applyAlpha(el, ctx)
            if (applyGlow(el, ctx, s)) {
                canvas.drawPath(path, paint)
                clearEffects()
                paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
                applyAlpha(el, ctx)
            }
            canvas.drawPath(path, paint)
        }
    }

    // ── 8b. SHAPE (free-form polygon) ──────────────────────────────
    // shape x1 y1 x2 y2 x3 y3 ... [fill:color] [stroke:color] [w:width]

    private fun drawShape(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 6) return // min 3 points
        val path = Path()
        var i = 0
        var first = true
        var compound = false
        while (i < el.args.size) {
            if (el.args[i] == "M") { path.close(); first = true; compound = true; i++; continue }
            if (i + 1 >= el.args.size) break
            val px = posX(el.args[i], b, s, ctx)
            val py = posY(el.args[i + 1], b, s, ctx)
            if (first) { path.moveTo(px, py); first = false } else path.lineTo(px, py)
            i += 2
        }
        path.close()
        if (compound) path.fillType = Path.FillType.EVEN_ODD

        val fill = el.props["fill"]
        if (fill != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.FILL
            paint.color = FaceExpr.resolveColor(fill, ctx, cv)
            applyAlpha(el, ctx)
            if (applyGlow(el, ctx, s)) {
                canvas.drawPath(path, paint)
                clearEffects()
                paint.color = FaceExpr.resolveColor(fill, ctx, cv)
                applyAlpha(el, ctx)
            }
            canvas.drawPath(path, paint)
        }
        val stroke = el.props["stroke"]
        if (stroke != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
            paint.strokeWidth = propPct(el, "w", s, ctx, 0.5f / 100f * s)
            applyAlpha(el, ctx)
            if (applyGlow(el, ctx, s)) {
                canvas.drawPath(path, paint)
                clearEffects()
                paint.color = FaceExpr.resolveColor(stroke, ctx, cv)
                applyAlpha(el, ctx)
            }
            canvas.drawPath(path, paint)
        }
    }

    // ── 9. DOT ──────────────────────────────────────────────────────
    // dot cx cy r [fill:color] [glow:radius] [glow_color:color]

    private fun drawDot(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 3) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)

        val dotColor = propColor(el, "fill", ctx, propColor(el, "color", ctx))
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.FILL
        paint.color = dotColor
        applyAlpha(el, ctx)

        if (applyGlow(el, ctx, s)) {
            canvas.drawCircle(cx, cy, r, paint)
            clearEffects()
            paint.color = dotColor
            applyAlpha(el, ctx)
        }
        canvas.drawCircle(cx, cy, r, paint)
    }

    // ── 10. GRADIENT_RING ───────────────────────────────────────────
    // gradient_ring cx cy r_outer r_inner start sweep count [from:color] [to:color]

    private fun drawGradientRing(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 7) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val rOuter = resolveSize(el.args[2], s, ctx)
        val rInner = resolveSize(el.args[3], s, ctx)
        val start = deg(el.args[4], ctx)
        val sweep = deg(el.args[5], ctx)
        val count = el.args[6].toIntOrNull()?.coerceIn(2, 360) ?: 36

        val fromColor = propColor(el, "from", ctx, propColor(el, "color", ctx))
        val toColor = propColor(el, "to", ctx, propColor(el, "color", ctx))

        val midR = (rOuter + rInner) / 2f
        val thickness = rOuter - rInner
        val segSweep = sweep / count

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = thickness
        paint.strokeCap = Paint.Cap.BUTT
        applyAlpha(el, ctx)

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (midR * cornerPct / 100f).coerceIn(0f, midR)
            val totalPerim = roundedRectPerim(midR, cr)
            if (totalPerim <= 0f) return
            val path = buildRoundedRectPath(cx, cy, midR, cr)
            for (i in 0 until count) {
                val t = i.toFloat() / (count - 1)
                paint.color = lerpColor(fromColor, toColor, t)
                val sa = start + segSweep * i
                val segSweepWithOverlap = segSweep + 0.3f
                val normSa = ((sa % 360f) + 360f) % 360f
                val segStartDist = normSa / 360f * totalPerim
                val segLen = kotlin.math.abs(segSweepWithOverlap) / 360f * totalPerim
                paint.pathEffect = DashPathEffect(floatArrayOf(segLen, totalPerim), -segStartDist)
                canvas.drawPath(path, paint)
            }
            paint.pathEffect = null
            return
        }

        for (i in 0 until count) {
            val t = i.toFloat() / (count - 1)
            paint.color = lerpColor(fromColor, toColor, t)
            canvas.drawArc(arcRect(cx, cy, midR), start + segSweep * i, segSweep + 0.3f, false, paint)
        }
    }

    // ── 11. PROGRESS_ARC ────────────────────────────────────────────
    // progress_arc cx cy r start maxSweep [stroke:color] [w:width] [cap:] [glow:] [bg:color]

    private fun drawProgressArc(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 5) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)
        val start = deg(el.args[3], ctx)
        val maxSweep = deg(el.args[4], ctx)
        val sweep = maxSweep * ctx.progress.coerceIn(0f, 1f)
        val strokeW = propPct(el, "w", s, ctx, 2f / 100f * s)

        // Rounded rect mode: corner:0 = square, corner:100 = circle
        if (el.props.containsKey("corner")) {
            drawProgressArcRounded(canvas, el, ctx, b, s, cx, cy, r, start, maxSweep, sweep, strokeW)
            return
        }

        val rect = arcRect(cx, cy, r)

        // Background track
        val bgColor = el.props["bg"]
        if (bgColor != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(bgColor, ctx, cv)
            paint.strokeWidth = strokeW
            paint.strokeCap = resolveCap(el.props["cap"])
            applyAlpha(el, ctx)
            canvas.drawArc(rect, start, maxSweep, false, paint)
        }

        // Progress arc
        val arcColor = propColor(el, "stroke", ctx, propColor(el, "color", ctx))
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.color = arcColor
        paint.strokeWidth = strokeW
        paint.strokeCap = resolveCap(el.props["cap"])
        applyAlpha(el, ctx)

        // Glow pass
        if (applyGlow(el, ctx, s)) {
            val savedAlpha = paint.alpha
            paint.alpha = (savedAlpha * 0.5f).toInt()
            canvas.drawArc(rect, start, sweep, false, paint)
            clearEffects()
            paint.color = arcColor
            paint.alpha = savedAlpha
        }
        canvas.drawArc(rect, start, sweep, false, paint)
    }

    /** Rounded-rect progress arc using DashPathEffect.
     *  Path traces clockwise from right-center (angle 0°). */
    private fun drawProgressArcRounded(
        canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext,
        b: RectF, s: Float, cx: Float, cy: Float, r: Float,
        startAngle: Float, maxSweep: Float, sweep: Float, strokeW: Float
    ) {
        val cornerPct = propFloat(el, "corner", ctx, 100f)
        val cr = (r * cornerPct / 100f).coerceIn(0f, r)

        val totalPerim = roundedRectPerim(r, cr)
        if (totalPerim <= 0f) return

        val normStart = ((startAngle % 360f) + 360f) % 360f
        val startDist = normStart / 360f * totalPerim
        val sweepDist = kotlin.math.abs(maxSweep) / 360f * totalPerim
        val progressDist = kotlin.math.abs(sweep) / 360f * totalPerim

        val path = buildRoundedRectPath(cx, cy, r, cr)

        val cap = resolveCap(el.props["cap"])

        // Background track
        val bgColor = el.props["bg"]
        if (bgColor != null) {
            paint.shader = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = FaceExpr.resolveColor(bgColor, ctx, cv)
            paint.strokeWidth = strokeW
            paint.strokeCap = cap
            paint.pathEffect = android.graphics.DashPathEffect(floatArrayOf(sweepDist, totalPerim), -startDist)
            applyAlpha(el, ctx)
            canvas.drawPath(path, paint)
        }

        // Progress
        if (progressDist > 0.5f) {
            val arcColor = propColor(el, "stroke", ctx, propColor(el, "color", ctx))
            paint.shader = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.color = arcColor
            paint.strokeWidth = strokeW
            paint.strokeCap = cap
            paint.pathEffect = android.graphics.DashPathEffect(floatArrayOf(progressDist, totalPerim), -startDist)
            applyAlpha(el, ctx)

            if (applyGlow(el, ctx, s)) {
                val savedAlpha = paint.alpha
                paint.alpha = (savedAlpha * 0.5f).toInt()
                canvas.drawPath(path, paint)
                clearEffects()
                paint.color = arcColor
                paint.alpha = savedAlpha
                paint.pathEffect = android.graphics.DashPathEffect(floatArrayOf(progressDist, totalPerim), -startDist)
            }
            canvas.drawPath(path, paint)
        }
        paint.pathEffect = null
    }

    // ── 12. PROGRESS_BAR ────────────────────────────────────────────
    // progress_bar x y w h [dir:right|left|up|down] [fill:color] [bg:color] [corner:r]

    private fun drawProgressBar(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 4) return
        val x = posX(el.args[0], b, s, ctx)
        val y = posY(el.args[1], b, s, ctx)
        val w = resolveSize(el.args[2], s, ctx)
        val h = resolveSize(el.args[3], s, ctx)
        val corner = propPct(el, "corner", s, ctx)
        val dir = el.props["dir"] ?: "right"
        val p = ctx.progress.coerceIn(0f, 1f)

        // Background
        val bgColor = el.props["bg"]
        if (bgColor != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.FILL
            paint.color = FaceExpr.resolveColor(bgColor, ctx, cv)
            applyAlpha(el, ctx)
            val bgRect = RectF(x, y, x + w, y + h)
            if (corner > 0f) canvas.drawRoundRect(bgRect, corner, corner, paint)
            else canvas.drawRect(bgRect, paint)
        }

        // Progress fill
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.FILL
        paint.color = propColor(el, "fill", ctx, propColor(el, "color", ctx))
        applyAlpha(el, ctx)

        val fillRect = when (dir) {
            "left" -> RectF(x + w * (1f - p), y, x + w, y + h)
            "up" -> RectF(x, y + h * (1f - p), x + w, y + h)
            "down" -> RectF(x, y, x + w, y + h * p)
            else -> RectF(x, y, x + w * p, y + h) // right
        }
        if (corner > 0f) canvas.drawRoundRect(fillRect, corner, corner, paint)
        else canvas.drawRect(fillRect, paint)
    }

    // ── 13. PROGRESS_RING ───────────────────────────────────────────
    // progress_ring cx cy r_outer r_inner start maxSweep [fill:color] [bg:color]

    private fun drawProgressRing(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 6) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val rOuter = resolveSize(el.args[2], s, ctx)
        val rInner = resolveSize(el.args[3], s, ctx)
        val start = deg(el.args[4], ctx)
        val maxSweep = deg(el.args[5], ctx)
        val midR = (rOuter + rInner) / 2f
        val thickness = rOuter - rInner

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (midR * cornerPct / 100f).coerceIn(0f, midR)
            val totalPerim = roundedRectPerim(midR, cr)
            if (totalPerim <= 0f) return
            val normStart = ((start % 360f) + 360f) % 360f
            val startDist = normStart / 360f * totalPerim
            val sweepDist = kotlin.math.abs(maxSweep) / 360f * totalPerim
            val sweep = maxSweep * ctx.progress.coerceIn(0f, 1f)
            val progressDist = kotlin.math.abs(sweep) / 360f * totalPerim
            val path = buildRoundedRectPath(cx, cy, midR, cr)

            val bgColor = el.props["bg"]
            if (bgColor != null) {
                paint.shader = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
                paint.style = Paint.Style.STROKE
                paint.strokeWidth = thickness
                paint.strokeCap = Paint.Cap.BUTT
                paint.color = FaceExpr.resolveColor(bgColor, ctx, cv)
                applyAlpha(el, ctx)
                paint.pathEffect = DashPathEffect(floatArrayOf(sweepDist, totalPerim), -startDist)
                canvas.drawPath(path, paint)
            }
            if (progressDist > 0.5f) {
                paint.shader = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
                paint.style = Paint.Style.STROKE
                paint.strokeWidth = thickness
                paint.strokeCap = Paint.Cap.BUTT
                paint.color = propColor(el, "fill", ctx, propColor(el, "color", ctx))
                applyAlpha(el, ctx)
                paint.pathEffect = DashPathEffect(floatArrayOf(progressDist, totalPerim), -startDist)
                canvas.drawPath(path, paint)
            }
            paint.pathEffect = null
            return
        }

        // Background ring
        val bgColor = el.props["bg"]
        if (bgColor != null) {
            paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
            paint.style = Paint.Style.STROKE
            paint.strokeWidth = thickness
            paint.strokeCap = Paint.Cap.BUTT
            paint.color = FaceExpr.resolveColor(bgColor, ctx, cv)
            applyAlpha(el, ctx)
            canvas.drawArc(arcRect(cx, cy, midR), start, maxSweep, false, paint)
        }

        // Progress ring
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = thickness
        paint.strokeCap = Paint.Cap.BUTT
        paint.color = propColor(el, "fill", ctx, propColor(el, "color", ctx))
        applyAlpha(el, ctx)
        canvas.drawArc(arcRect(cx, cy, midR), start, maxSweep * ctx.progress.coerceIn(0f, 1f), false, paint)
    }

    // ── 14. NEEDLE ──────────────────────────────────────────────────
    // needle cx cy len [from:angle] [sweep:angle] [w:width] [color:color]
    //   [tail:len] [glow:radius] [cap:round|butt]

    private fun drawNeedle(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 3) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val len = resolveSize(el.args[2], s, ctx)
        val from = propFloat(el, "from", ctx, -225f)
        val sweep = propFloat(el, "sweep", ctx, 270f)
        val tail = propPct(el, "tail", s, ctx)

        val angle = from + sweep * ctx.progress.coerceIn(0f, 1f)
        val rad = deg2rad(angle)

        val needleColor = propColor(el, "color", ctx)
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.color = needleColor
        paint.strokeWidth = propPct(el, "w", s, ctx, 1f / 100f * s)
        paint.strokeCap = resolveCap(el.props["cap"] ?: "round")
        applyAlpha(el, ctx)

        // Rounded rect mode: needle tip on rounded rect perimeter
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (len * cornerPct / 100f).coerceIn(0f, len)
            val pt = pointOnRoundedRect(cx, cy, len, cr, angle)
            val x2 = pt[0]; val y2 = pt[1]
            val dx = x2 - cx; val dy = y2 - cy
            val d = kotlin.math.sqrt(dx * dx + dy * dy).coerceAtLeast(1f)
            val ux = dx / d; val uy = dy / d
            val x1 = if (tail > 0f) cx - tail * ux else cx
            val y1 = if (tail > 0f) cy - tail * uy else cy
            if (applyGlow(el, ctx, s)) {
                val savedAlpha = paint.alpha
                paint.alpha = (savedAlpha * 0.5f).toInt()
                canvas.drawLine(x1, y1, x2, y2, paint)
                clearEffects()
                paint.color = needleColor
                paint.alpha = savedAlpha
            }
            canvas.drawLine(x1, y1, x2, y2, paint)
            val dotR = propPct(el, "dot", s, ctx)
            if (dotR > 0f) {
                paint.style = Paint.Style.FILL
                canvas.drawCircle(cx, cy, dotR, paint)
            }
            return
        }

        val x1 = if (tail > 0f) cx - tail * cos(rad) else cx
        val y1 = if (tail > 0f) cy - tail * sin(rad) else cy
        val x2 = cx + len * cos(rad)
        val y2 = cy + len * sin(rad)

        // Glow pass
        if (applyGlow(el, ctx, s)) {
            val savedAlpha = paint.alpha
            paint.alpha = (savedAlpha * 0.5f).toInt()
            canvas.drawLine(x1, y1, x2, y2, paint)
            clearEffects()
            paint.color = needleColor
            paint.alpha = savedAlpha
        }
        canvas.drawLine(x1, y1, x2, y2, paint)

        // Center dot
        val dotR = propPct(el, "dot", s, ctx)
        if (dotR > 0f) {
            paint.style = Paint.Style.FILL
            canvas.drawCircle(cx, cy, dotR, paint)
        }
    }

    // ── 15. VALUE_DOT ───────────────────────────────────────────────
    // value_dot cx cy r start maxSweep dot_r [fill:color] [glow:radius]

    private fun drawValueDot(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 6) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)
        val start = deg(el.args[3], ctx)
        val maxSweep = deg(el.args[4], ctx)
        val dotR = resolveSize(el.args[5], s, ctx)

        val angle = start + maxSweep * ctx.progress.coerceIn(0f, 1f)

        val vdColor = propColor(el, "fill", ctx, propColor(el, "color", ctx))
        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.FILL
        paint.color = vdColor
        applyAlpha(el, ctx)

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (r * cornerPct / 100f).coerceIn(0f, r)
            val pt = pointOnRoundedRect(cx, cy, r, cr, angle)
            val dx = pt[0]; val dy = pt[1]
            if (applyGlow(el, ctx, s)) {
                canvas.drawCircle(dx, dy, dotR, paint)
                clearEffects()
                paint.color = vdColor
                applyAlpha(el, ctx)
            }
            canvas.drawCircle(dx, dy, dotR, paint)
            return
        }

        val rad = deg2rad(angle)
        val dx = cx + r * cos(rad)
        val dy = cy + r * sin(rad)

        if (applyGlow(el, ctx, s)) {
            canvas.drawCircle(dx, dy, dotR, paint)
            clearEffects()
            paint.color = vdColor
            applyAlpha(el, ctx)
        }
        canvas.drawCircle(dx, dy, dotR, paint)
    }

    // ── 16. ZONE_ARC ────────────────────────────────────────────────
    // zone_arc cx cy r start sweep w [normal:color] [warning:color] [danger:color]

    private fun drawZoneArc(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 6) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)
        val start = deg(el.args[3], ctx)
        val sweep = deg(el.args[4], ctx)
        val strokeW = resolveSize(el.args[5], s, ctx)

        val normalC = propColor(el, "normal", ctx, 0xFF444444.toInt())
        val warningC = propColor(el, "warning", ctx, 0xFFFFD96B.toInt())
        val dangerC = propColor(el, "danger", ctx, 0xFFFF6B6B.toInt())

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = strokeW
        paint.strokeCap = Paint.Cap.BUTT
        applyAlpha(el, ctx)

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (r * cornerPct / 100f).coerceIn(0f, r)
            drawZoneSegmentsRounded(canvas, cx, cy, r, cr, start, sweep, normalC, warningC, dangerC, ctx)
            return
        }

        val rect = arcRect(cx, cy, r)
        drawZoneSegments(canvas, rect, start, sweep, normalC, warningC, dangerC, ctx)
    }

    // ── 17. ZONE_RING ───────────────────────────────────────────────
    // zone_ring cx cy r_outer r_inner start sweep [normal:color] [warning:color] [danger:color]

    private fun drawZoneRing(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 6) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val rOuter = resolveSize(el.args[2], s, ctx)
        val rInner = resolveSize(el.args[3], s, ctx)
        val start = deg(el.args[4], ctx)
        val sweep = deg(el.args[5], ctx)

        val normalC = propColor(el, "normal", ctx, 0xFF444444.toInt())
        val warningC = propColor(el, "warning", ctx, 0xFFFFD96B.toInt())
        val dangerC = propColor(el, "danger", ctx, 0xFFFF6B6B.toInt())

        val midR = (rOuter + rInner) / 2f
        val thickness = rOuter - rInner

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = thickness
        paint.strokeCap = Paint.Cap.BUTT
        applyAlpha(el, ctx)

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (midR * cornerPct / 100f).coerceIn(0f, midR)
            drawZoneSegmentsRounded(canvas, cx, cy, midR, cr, start, sweep, normalC, warningC, dangerC, ctx)
            return
        }

        drawZoneSegments(canvas, arcRect(cx, cy, midR), start, sweep, normalC, warningC, dangerC, ctx)
    }

    private fun drawZoneSegments(
        canvas: Canvas, rect: RectF, start: Float, sweep: Float,
        normalC: Int, warningC: Int, dangerC: Int, ctx: FaceRenderContext
    ) {
        // Collect zone boundaries as progress fractions
        data class Seg(val from: Float, val to: Float, val color: Int)
        val segments = mutableListOf<Seg>()

        val wl = ctx.warnLowProgress
        val wh = ctx.warnHighProgress
        val dl = ctx.dangerLowProgress
        val dh = ctx.dangerHighProgress

        // Build segments from 0..1 based on thresholds
        var cursor = 0f
        if (dl != null && dl > cursor) { segments.add(Seg(cursor, dl, dangerC)); cursor = dl }
        if (wl != null && wl > cursor) { segments.add(Seg(cursor, wl, warningC)); cursor = wl }
        val highStart = wh ?: dh ?: 1f
        if (highStart > cursor) { segments.add(Seg(cursor, highStart, normalC)); cursor = highStart }
        if (wh != null && dh != null && dh > wh) {
            segments.add(Seg(cursor, dh, warningC)); cursor = dh
        }
        if (cursor < 1f) { segments.add(Seg(cursor, 1f, if (dh != null) dangerC else if (wh != null) warningC else normalC)) }

        if (segments.isEmpty()) {
            paint.color = normalC
            canvas.drawArc(rect, start, sweep, false, paint)
            return
        }
        for (seg in segments) {
            paint.color = seg.color
            canvas.drawArc(rect, start + sweep * seg.from, sweep * (seg.to - seg.from), false, paint)
        }
    }

    private fun drawZoneSegmentsRounded(
        canvas: Canvas, cx: Float, cy: Float, r: Float, cr: Float,
        start: Float, sweep: Float,
        normalC: Int, warningC: Int, dangerC: Int, ctx: FaceRenderContext
    ) {
        data class Seg(val from: Float, val to: Float, val color: Int)
        val segments = mutableListOf<Seg>()
        val wl = ctx.warnLowProgress
        val wh = ctx.warnHighProgress
        val dl = ctx.dangerLowProgress
        val dh = ctx.dangerHighProgress
        var cursor = 0f
        if (dl != null && dl > cursor) { segments.add(Seg(cursor, dl, dangerC)); cursor = dl }
        if (wl != null && wl > cursor) { segments.add(Seg(cursor, wl, warningC)); cursor = wl }
        val highStart = wh ?: dh ?: 1f
        if (highStart > cursor) { segments.add(Seg(cursor, highStart, normalC)); cursor = highStart }
        if (wh != null && dh != null && dh > wh) { segments.add(Seg(cursor, dh, warningC)); cursor = dh }
        if (cursor < 1f) { segments.add(Seg(cursor, 1f, if (dh != null) dangerC else if (wh != null) warningC else normalC)) }

        val totalPerim = roundedRectPerim(r, cr)
        if (totalPerim <= 0f) return
        val path = buildRoundedRectPath(cx, cy, r, cr)

        if (segments.isEmpty()) {
            paint.color = normalC
            val normStart = ((start % 360f) + 360f) % 360f
            val startDist = normStart / 360f * totalPerim
            val sweepDist = kotlin.math.abs(sweep) / 360f * totalPerim
            paint.pathEffect = DashPathEffect(floatArrayOf(sweepDist, totalPerim), -startDist)
            canvas.drawPath(path, paint)
            paint.pathEffect = null
            return
        }
        for (seg in segments) {
            paint.color = seg.color
            val sa = start + sweep * seg.from
            val sw = sweep * (seg.to - seg.from)
            val normSa = ((sa % 360f) + 360f) % 360f
            val segStartDist = normSa / 360f * totalPerim
            val segLen = kotlin.math.abs(sw) / 360f * totalPerim
            paint.pathEffect = DashPathEffect(floatArrayOf(segLen, totalPerim), -segStartDist)
            canvas.drawPath(path, paint)
        }
        paint.pathEffect = null
    }

    // ── 18. SWEEP_TICKS ─────────────────────────────────────────────
    // sweep_ticks cx cy r count len [start:angle] [sweep:angle] [on:color] [off:color] [w:width]

    private fun drawSweepTicks(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 5) return
        val cx = posX(el.args[0], b, s, ctx)
        val cy = posY(el.args[1], b, s, ctx)
        val r = resolveSize(el.args[2], s, ctx)
        val count = el.args[3].toIntOrNull() ?: return
        val len = resolveSize(el.args[4], s, ctx)
        val startAngle = propFloat(el, "start", ctx, -225f)
        val sweepAngle = propFloat(el, "sweep", ctx, 270f)
        val onColor = propColor(el, "on", ctx, propColor(el, "color", ctx))
        val offColor = propColor(el, "off", ctx, 0xFF333333.toInt())

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = propPct(el, "w", s, ctx, 0.8f / 100f * s)
        paint.strokeCap = resolveCap(el.props["cap"])
        applyAlpha(el, ctx)

        val activeCount = (count * ctx.progress.coerceIn(0f, 1f)).toInt()
        if (count < 2) return

        // Rounded rect mode
        if (el.props.containsKey("corner")) {
            val cornerPct = propFloat(el, "corner", ctx, 100f)
            val cr = (r * cornerPct / 100f).coerceIn(0f, r)
            for (i in 0 until count) {
                paint.color = if (i < activeCount) onColor else offColor
                val angle = startAngle + sweepAngle * i / (count - 1)
                val pt = pointOnRoundedRect(cx, cy, r, cr, angle)
                canvas.drawLine(pt[0] + pt[2] * len, pt[1] + pt[3] * len, pt[0], pt[1], paint)
            }
            return
        }

        for (i in 0 until count) {
            paint.color = if (i < activeCount) onColor else offColor
            val angle = startAngle + sweepAngle * i / (count - 1)
            val rad = deg2rad(angle)
            canvas.drawLine(
                cx + (r - len) * cos(rad), cy + (r - len) * sin(rad),
                cx + r * cos(rad), cy + r * sin(rad), paint
            )
        }
    }

    // ── 19. DIGITAL_BAR ─────────────────────────────────────────────
    // digital_bar x y w h count gap [fill:color] [bg:color] [corner:r]

    private fun drawDigitalBar(canvas: Canvas, el: FaceNode.Element, ctx: FaceRenderContext, b: RectF, s: Float) {
        if (el.args.size < 6) return
        val x = posX(el.args[0], b, s, ctx)
        val y = posY(el.args[1], b, s, ctx)
        val totalW = resolveSize(el.args[2], s, ctx)
        val h = resolveSize(el.args[3], s, ctx)
        val count = el.args[4].toIntOrNull() ?: return
        val gap = resolveSize(el.args[5], s, ctx)
        val corner = propPct(el, "corner", s, ctx)

        val fillColor = propColor(el, "fill", ctx, propColor(el, "color", ctx))
        val bgColor = propColor(el, "bg", ctx, 0xFF222222.toInt())

        val segW = (totalW - gap * (count - 1)) / count
        val activeCount = (count * ctx.progress.coerceIn(0f, 1f)).toInt()

        paint.shader = null; paint.pathEffect = null; paint.maskFilter = null; paint.isAntiAlias = true; paint.alpha = 255
        paint.style = Paint.Style.FILL
        applyAlpha(el, ctx)

        for (i in 0 until count) {
            paint.color = if (i < activeCount) fillColor else bgColor
            val sx = x + i * (segW + gap)
            val segRect = RectF(sx, y, sx + segW, y + h)
            if (corner > 0f) canvas.drawRoundRect(segRect, corner, corner, paint)
            else canvas.drawRect(segRect, paint)
        }
    }

    // ── Color utilities ─────────────────────────────────────────────

    private fun lerpColor(from: Int, to: Int, t: Float): Int {
        val ft = t.coerceIn(0f, 1f)
        val a = lerp((from shr 24) and 0xFF, (to shr 24) and 0xFF, ft)
        val r = lerp((from shr 16) and 0xFF, (to shr 16) and 0xFF, ft)
        val g = lerp((from shr 8) and 0xFF, (to shr 8) and 0xFF, ft)
        val bl = lerp(from and 0xFF, to and 0xFF, ft)
        return (a shl 24) or (r shl 16) or (g shl 8) or bl
    }

    private fun lerp(a: Int, b: Int, t: Float): Int = (a + (b - a) * t).toInt().coerceIn(0, 255)
}
