package com.erwan.revwatch.ui

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
import com.erwan.revwatch.model.GaugeLimits
import com.erwan.revwatch.model.face.FaceDef
import com.erwan.revwatch.model.face.FaceRenderContext
import com.erwan.revwatch.model.face.FaceRenderer
import kotlin.math.min

class GaugeView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    /** Custom face definition. */
    var faceDef: FaceDef? = null
        set(value) {
            field = value
            cacheDirty = true
            checkBlinkTimer()
        }

    /** Text data for face rendering. */
    var formattedValue: String = "--"
        set(value) {
            if (field != value) {
                field = value
                cacheDirty = true
                if (value == "--") startLoadingAnimation() else stopLoadingAnimation()
            }
        }
    var unitLabel: String = ""
        set(value) { if (field != value) { field = value; cacheDirty = true } }
    var gaugeLabel: String = ""
        set(value) { if (field != value) { field = value; cacheDirty = true } }

    /** Extra DSL variables injected by multi-value data bindings. */
    var extraVars: Map<String, String> = emptyMap()
        set(value) { if (field != value) { field = value; cacheDirty = true } }

    private val faceRenderer = FaceRenderer(context)

    private var value: Double? = null
    private var minValue: Double = 0.0
    private var maxValue: Double = 100.0
    private var accentColor: Int = Color.CYAN
    private var limits: GaugeLimits = GaugeLimits()
    private var currentZone: GaugeLimits.Zone = GaugeLimits.Zone.NORMAL
    private var animatedProgress: Float = 0f
    private var blinkAlpha: Float = 1f

    private var hasBeenSet = false
    private var progressAnimator: ValueAnimator? = null
    private var blinkAnimator: ValueAnimator? = null
    private var dslBlinkRunnable: Runnable? = null
    private var lastSetGaugeTime = 0L

    // ──────── Loading animation (dots) ────────
    private var loadingFrame = 0
    private var loadingRunnable: Runnable? = null

    private val loadingFrames = arrayOf("--", " ")

    private fun startLoadingAnimation() {
        if (loadingRunnable != null) return
        loadingFrame = 0
        loadingRunnable = object : Runnable {
            override fun run() {
                loadingFrame = (loadingFrame + 1) % loadingFrames.size
                cacheDirty = true
                invalidate()
                postDelayed(this, 400)
            }
        }
        postDelayed(loadingRunnable!!, 400)
    }

    private fun stopLoadingAnimation() {
        loadingRunnable?.let { removeCallbacks(it) }
        loadingRunnable = null
    }

    private val isLoading: Boolean
        get() = formattedValue == "--"

    private fun displayValue(): String {
        return if (isLoading && loadingRunnable != null) loadingFrames[loadingFrame] else formattedValue
    }

    // ──────── Bitmap cache ────────
    private var cacheBitmap: Bitmap? = null
    private var cacheCanvas: Canvas? = null
    private var cacheDirty = true
    private val reusableBounds = RectF()
    private val reusableCtx = FaceRenderContext()

    init {
        // Layer type is set dynamically: HARDWARE for $time animated faces
        // (isolates GPU compositing from the heavy VP2/RV view hierarchy),
        // NONE for static faces (avoids texture upload overhead).
    }

    fun setGauge(
        value: Double?,
        minValue: Double,
        maxValue: Double,
        accentColor: Int,
        limits: GaugeLimits = GaugeLimits()
    ) {
        this.value = value
        this.minValue = minValue
        this.maxValue = maxValue
        this.accentColor = accentColor
        this.limits = limits

        val zone = if (value != null) limits.zoneFor(value) else GaugeLimits.Zone.NORMAL
        if (zone != currentZone) {
            currentZone = zone
            updateBlinkAnimation()
            updateBlinkTimer()
        }

        val now = System.currentTimeMillis()
        val interval = now - lastSetGaugeTime
        lastSetGaugeTime = now

        animateToProgress(progress(), interval)
    }

    // ──────── Animation ────────

    private fun animateToProgress(target: Float, updateIntervalMs: Long) {
        progressAnimator?.cancel()
        if (!hasBeenSet) {
            hasBeenSet = true
            animatedProgress = target
            cacheDirty = true
            invalidate()
            return
        }
        val delta = kotlin.math.abs(animatedProgress - target)
        if (delta < 0.0001f) return

        // For $time animated faces: snap directly — the timer already redraws continuously,
        // no need for a ValueAnimator that would add redundant invalidate() calls.
        if (faceHasTime) {
            animatedProgress = target
            // No invalidate needed — the $time timer handles it
            return
        }

        // High-frequency updates (>5 fps / <200ms interval): snap directly for real-time display.
        // Low-frequency updates: animate for smooth visual transitions.
        if (updateIntervalMs < 200 || delta < 0.005f) {
            animatedProgress = target
            cacheDirty = true
            invalidate()
            return
        }

        progressAnimator = ValueAnimator.ofFloat(animatedProgress, target).apply {
            duration = (updateIntervalMs * 0.7f).toLong().coerceIn(100L, 300L)
            interpolator = DecelerateInterpolator(1.8f)
            addUpdateListener {
                val newVal = it.animatedValue as Float
                if (kotlin.math.abs(newVal - animatedProgress) > 0.001f) {
                    animatedProgress = newVal
                    cacheDirty = true
                    invalidate()
                }
            }
            start()
        }
    }

    private var lastBlinkInvalidate = 0L

    private fun updateBlinkAnimation() {
        blinkAnimator?.cancel()
        if (currentZone == GaugeLimits.Zone.DANGER) {
            blinkAnimator = ValueAnimator.ofFloat(1f, 0.35f).apply {
                duration = 600L
                repeatMode = ValueAnimator.REVERSE
                repeatCount = ValueAnimator.INFINITE
                interpolator = LinearInterpolator()
                addUpdateListener {
                    blinkAlpha = it.animatedValue as Float
                    // Throttle blink redraws to ~20fps (50ms)
                    val now = System.currentTimeMillis()
                    if (now - lastBlinkInvalidate >= 50) {
                        lastBlinkInvalidate = now
                        cacheDirty = true
                        invalidate()
                    }
                }
                start()
            }
        } else {
            blinkAlpha = 1f
        }
    }

    private fun progress(): Float {
        val current = value ?: return 0f
        val range = (maxValue - minValue).takeIf { it > 0.0001 } ?: return 0f
        return ((current - minValue) / range).toFloat().coerceIn(0f, 1f)
    }

    // ──────── Draw ────────

    override fun onDraw(canvas: Canvas) {
        val w = width
        val h = height
        if (w <= 0 || h <= 0) return

        val face = faceDef ?: return

        // Ensure cache bitmap exists and matches size
        var bmp = cacheBitmap
        var cv = cacheCanvas
        if (bmp == null || bmp.width != w || bmp.height != h) {
            bmp?.recycle()
            bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).apply {
                density = resources.displayMetrics.densityDpi
            }
            cv = Canvas(bmp)
            cacheBitmap = bmp
            cacheCanvas = cv
            cacheDirty = true
        }
        if (cv == null) return

        if (faceHasTime || cacheDirty) {
            cacheDirty = false
            bmp.eraseColor(Color.TRANSPARENT)

            val p = animatedProgress
            val zone = when (currentZone) {
                GaugeLimits.Zone.NORMAL -> FaceRenderContext.Zone.NORMAL
                GaugeLimits.Zone.WARNING -> FaceRenderContext.Zone.WARNING
                GaugeLimits.Zone.DANGER -> FaceRenderContext.Zone.DANGER
            }
            val range = maxValue - minValue
            fun frac(v: Double?): Float? = if (v != null && range > 0.0001)
                ((v - minValue) / range).toFloat().coerceIn(0f, 1f) else null

            reusableCtx.set(
                progress = p, rawValue = value, formattedValue = displayValue(),
                unit = unitLabel, label = gaugeLabel,
                minValue = minValue, maxValue = maxValue,
                accentColor = accentColor, zone = zone,
                warnLowProgress = frac(limits.warningLow),
                warnHighProgress = frac(limits.warningHigh),
                dangerLowProgress = frac(limits.dangerLow),
                dangerHighProgress = frac(limits.dangerHigh),
                extraVars = extraVars
            )

            val size = min(w.toFloat(), h.toFloat())
            reusableBounds.set((w - size) / 2f, (h - size) / 2f, (w + size) / 2f, (h + size) / 2f)
            faceRenderer.render(cv, face, reusableCtx, reusableBounds)
        }

        canvas.drawBitmap(bmp, 0f, 0f, null)
    }

    /** Whether this face structurally has blink elements (checked once per face). */
    private var faceHasBlink = false
    private var faceHasTime = false

    /** Start/stop a periodic invalidation timer for DSL blink or $time animations. */
    private fun checkBlinkTimer() {
        val face = faceDef
        faceHasBlink = face != null && faceRenderer.hasBlink(face)
        faceHasTime = face != null && faceRenderer.hasTimeVar(face)
        updateBlinkTimer()
        updateLayerType()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        cacheDirty = true
        updateLayerType()
    }

    private fun updateLayerType() {
        // Always NONE — HARDWARE layer adds texture upload overhead that hurts animated faces.
        if (layerType != LAYER_TYPE_NONE) setLayerType(LAYER_TYPE_NONE, null)
    }

    /** Re-evaluate whether the blink timer should be running based on face + zone. */
    private fun updateBlinkTimer() {
        val needsTimer = faceHasTime ||
                (faceHasBlink && currentZone != GaugeLimits.Zone.NORMAL)
        if (needsTimer) {
            // Stop existing timer before (re)starting
            dslBlinkRunnable?.let { handler?.removeCallbacks(it) }
            val h = handler ?: return  // Not attached yet — onAttachedToWindow will retry
            dslBlinkRunnable = object : Runnable {
                override fun run() {
                    if (!isShown) {
                        handler?.postDelayed(this, 100)
                        return
                    }
                    cacheDirty = true
                    postInvalidateOnAnimation()
                    handler?.postDelayed(this, animInterval())
                }
            }
            h.post(dslBlinkRunnable!!)
        } else {
            dslBlinkRunnable?.let { handler?.removeCallbacks(it) }
            dslBlinkRunnable = null
        }
    }

    /** Animation interval: 60fps for $time faces (like ValueAnimator), 20fps for blink only. */
    private fun animInterval(): Long = if (faceHasTime) 16L else 50L

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        checkBlinkTimer()
        if (isLoading) startLoadingAnimation()
    }

    override fun onDetachedFromWindow() {
        progressAnimator?.cancel()
        blinkAnimator?.cancel()
        stopLoadingAnimation()
        dslBlinkRunnable?.let { handler?.removeCallbacks(it) }
        dslBlinkRunnable = null
        cacheBitmap?.recycle()
        cacheBitmap = null
        cacheCanvas = null
        super.onDetachedFromWindow()
    }
}
