package jp.juggler.subwaytooter.view

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.SparseIntArray
import androidx.appcompat.widget.AppCompatTextView
import jp.juggler.util.LogCategory
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sign

class Blurhash(blurhash: String, punch: Float = 1f) {

    companion object {

        // map from base83 character to index
        private val base83Map = SparseIntArray().apply {
            "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
                .forEachIndexed { index, c ->
                    put(c.code, index)
                }
        }

        // convert from base83 chars(1..4 length) to integer
        private fun String.decodeBase83(start: Int, length: Int): Int {
            var v = 0
            for (i in start until start + length) {
                val ci = this[i].code
                val idx = base83Map.get(ci, -1)
                if (idx == -1) error("decodeBase83: incorrect char code $ci")
                v = v * 83 + idx
            }
            return v
        }

        // array to convert gamma curve from sRGB(0..255) to linear(0..1f)

        private val arraySRGB2Linear = FloatArray(256) { i ->
            val v = i.toDouble() / 255.0
            if (v <= 0.04045) {
                v / 12.92
            } else {
                ((v + 0.055) / 1.055).pow(2.4)
            }
                .toFloat()
        }

        @Suppress("SameParameterValue")
        private fun clip(min: Int, max: Int, value: Int) =
            if (value < min) min else if (value > max) max else value

        private fun sRGBToLinear(value: Int) = arraySRGB2Linear[clip(0, 255, value)]

        private fun linearTosRGB(value: Float): Int {
            // binary search in arraySRGB2Linear to avoid using pow()
            var start = 0
            var end = 256
            while (end - start > 1) {
                val mid = (start + end) shr 1
                val midValue = arraySRGB2Linear[mid]
                when {
                    value < midValue -> end = mid
                    value > midValue -> start = mid + 1
                    else -> return mid
                }
            }
            return clip(0, 255, start)
        }

        private fun signPow2(v: Float): Float = sign(v) * (v * v)

        private fun decodeACSub(maximumValue: Float, v: Int): Float =
            signPow2((v - 9).toFloat() / 9f) * maximumValue

        private fun decodeAC(value: Int, maximumValue: Float) = floatArrayOf(
            decodeACSub(maximumValue, value / 361), // 19*19
            decodeACSub(maximumValue, ((value / 19) % 19)),
            decodeACSub(maximumValue, value % 19)
        )

        private fun decodeDC(value: Int) = floatArrayOf(
            sRGBToLinear((value ushr 16) and 255),
            sRGBToLinear((value ushr 8) and 255),
            sRGBToLinear((value) and 255)
        )
    }

    private val height: Int
    private val width: Int
    private val colors: Array<FloatArray>

    init {
        if (blurhash.length < 6) {
            error("blurhash: too short $blurhash")
        }

        val sizeFlag = blurhash.decodeBase83(0, 1)
        this.height = (sizeFlag / 9) + 1
        this.width = (sizeFlag % 9) + 1

        val lengthExpect = 4 + 2 * width * height
        if (blurhash.length != lengthExpect) {
            error("'blurhash length mismatch. expect=$lengthExpect,actual=${blurhash.length}")
        }

        val quantisedMaximumValue = blurhash.decodeBase83(1, 1)
        val maximumValue = ((quantisedMaximumValue + 1).toFloat() / 166f) * punch

        this.colors = Array(width * height) { i ->
            when (i) {
                0 -> decodeDC(blurhash.decodeBase83(2, 4))
                else -> decodeAC(blurhash.decodeBase83(4 + i * 2, 2), maximumValue)
            }
        }
    }

    // render to IntArray that can be used to Bitmap.setPixels()
    fun render(pixels: IntArray, pixelWidth: Int, pixelHeight: Int) {
        var pos = 0
        for (y in 0 until pixelHeight) {
            val ky = Math.PI * y.toDouble() / pixelHeight.toDouble()
            for (x in 0 until pixelWidth) {
                val kx = Math.PI * x.toDouble() / pixelWidth.toDouble()
                var r = 0f
                var g = 0f
                var b = 0f
                for (j in 0 until height) {
                    for (i in 0 until width) {
                        val basis = (cos(kx * i) * cos(ky * j)).toFloat()
                        val color = colors[i + j * width]
                        r += color[0] * basis
                        g += color[1] * basis
                        b += color[2] * basis
                    }
                }
                pixels[pos++] = Color.argb(
                    255,
                    linearTosRGB(r),
                    linearTosRGB(g),
                    linearTosRGB(b)
                )
            }
        }
    }
}

class BlurhashView : AppCompatTextView {

    companion object {
        val log = LogCategory("BlurhashView")

        const val bitmapWidth = 16
        const val bitmapHeight = 16
    }

    constructor(context: Context) :
        super(context)

    constructor(context: Context, attrs: AttributeSet) :
        super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
        super(context, attrs, defStyleAttr)

    // keep bitmap and IntArray to reuse it.
    private val pixels = IntArray(bitmapWidth * bitmapHeight)
    private val blurhashBitmap = Bitmap.createBitmap(
        bitmapWidth,
        bitmapHeight,
        Bitmap.Config.ARGB_8888
    )

    private var blurhashDecodeOk = false

    private val rectSrc = Rect()
    private val rectDst = Rect()
    private val paint = Paint().apply {
        isFilterBitmap = true
    }

    var errorColor: Int = 0
        set(v) {
            field = v
            invalidate()
        }

    var blurhash: String? = null
        set(v) {
            if (v == field) return

            blurhashDecodeOk = if (v?.isEmpty() != false) {
                false
            } else try {
                Blurhash(v).render(pixels, bitmapWidth, bitmapHeight)
                blurhashBitmap.setPixels(
                    pixels,
                    0,
                    bitmapWidth,
                    0,
                    0,
                    bitmapWidth,
                    bitmapHeight
                )
                true
            } catch (ex: Throwable) {
                log.e(ex, "blurhash decode failed.")
                false
            }

            field = v
            invalidate()
        }

    private fun drawBlurhash(canvas: Canvas) {

        val viewW = width
        val viewH = height

        val b = blurhashBitmap
        if (b != null && !b.isRecycled && blurhashDecodeOk) {
            rectSrc.set(0, 0, b.width, b.height)
            rectDst.set(0, 0, viewW, viewH)
            canvas.drawBitmap(b, rectSrc, rectDst, paint)
        } else {
            paint.color = errorColor
            rectDst.set(0, 0, viewW, viewH)
            canvas.drawRect(rectDst, paint)
        }
    }

    override fun onDraw(canvas: Canvas) {
        drawBlurhash(canvas)
        super.onDraw(canvas)
    }

    override fun isOpaque(): Boolean {
        return true
    }
}
