package ch.threema.app.ui

import android.content.Context
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.widget.AppCompatSeekBar
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.withSave
import androidx.transition.ChangeClipBounds
import androidx.transition.Transition
import androidx.transition.TransitionManager
import ch.threema.app.ExecutorServices
import ch.threema.app.R
import ch.threema.app.cache.ThumbnailCache
import ch.threema.app.utils.RuntimeUtil
import ch.threema.base.utils.getThreemaLogger
import ch.threema.storage.models.AbstractMessageModel
import kotlin.math.min
import kotlin.math.roundToInt

private val logger = getThreemaLogger("AudioProgressBarView")

class AudioProgressBarView :
    AppCompatSeekBar,
    AudioWaveformGeneratorTask.AudioWaveformGeneratorListener {
    private var barHeight = 20
    private var barWidth = 5
    private var spaceWidth: Int = 3
    private var barMinHeight = 4
    private var halfBarMinHeight = 2F
    private var state = 0

    private lateinit var colorStateListBar: ColorStateList
    private lateinit var colorStateListBarProgress: ColorStateList

    private lateinit var barPaint: Paint
    private lateinit var barPaintActivated: Paint
    private lateinit var barPaintProgress: Paint
    private lateinit var barPaintProgressActivated: Paint

    private var viewWidth: Int = 0
    private var viewHeight: Int = 0
    private var numSamples: Int = 24
    private var waveBitmap: Bitmap? = null
    private var emptyBitmap: Bitmap? = null
    private var waveFormTask: AudioWaveformGeneratorTask? = null
    private var thumbnailCache: ThumbnailCache<Any>? = null
    private var messageModel: AbstractMessageModel? = null
    private var changeBounds: Transition = ChangeClipBounds()

    private var numPreCalculatedSamples: Int = 0

    // radius of bar edges in px
    private val radius = 4F

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr,
    ) {
        init(attrs)
    }

    fun init(attrs: AttributeSet?) {
        val typedArray: TypedArray = context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.AudioProgressBarView,
            0,
            0,
        )
        with(typedArray) {
            barHeight = getDimensionPixelSize(R.styleable.AudioProgressBarView_barHeight, barHeight)
            barWidth = getDimensionPixelSize(R.styleable.AudioProgressBarView_barWidth, barWidth)
            spaceWidth = getDimensionPixelSize(R.styleable.AudioProgressBarView_spaceWidth, spaceWidth)
            barMinHeight = getDimensionPixelSize(R.styleable.AudioProgressBarView_barMinHeight, barMinHeight)
            halfBarMinHeight = barMinHeight / 2F
            colorStateListBar = getColorStateList(R.styleable.AudioProgressBarView_colorBar)
                ?: context.getColorStateList(R.color.audio_bar_colorstatelist)
            colorStateListBarProgress = getColorStateList(R.styleable.AudioProgressBarView_colorBarProgress)
                ?: context.getColorStateList(R.color.audio_bar_progress_colorstatelist)
            recycle()
        }

        numPreCalculatedSamples = guessSuitableAmountOfSamples(context)

        barPaint = Paint().apply {
            isAntiAlias = true
            color = colorStateListBar.defaultColor
        }

        barPaintActivated = Paint().apply {
            isAntiAlias = true
            val colorActivated: Int = colorStateListBar.getColorForState(
                intArrayOf(android.R.attr.state_activated),
                colorStateListBar.defaultColor,
            )
            colorFilter = PorterDuffColorFilter(colorActivated, PorterDuff.Mode.SRC_IN)
        }

        barPaintProgress = Paint().apply {
            isAntiAlias = true
            colorFilter = PorterDuffColorFilter(colorStateListBarProgress.defaultColor, PorterDuff.Mode.SRC_IN)
        }

        barPaintProgressActivated = Paint().apply {
            isAntiAlias = true
            val colorProgressActivated: Int = colorStateListBarProgress.getColorForState(
                intArrayOf(android.R.attr.state_activated),
                colorStateListBarProgress.defaultColor,
            )
            colorFilter = PorterDuffColorFilter(colorProgressActivated, PorterDuff.Mode.SRC_IN)
        }

        changeBounds.duration = 800
        changeBounds.interpolator = DecelerateInterpolator()
        changeBounds.addTarget(this)

        visibility = INVISIBLE
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)

        viewWidth = right - left
        viewHeight = bottom - top

        numSamples = viewWidth / (barWidth + spaceWidth)
    }

    override fun onDraw(canvas: Canvas) {
        if (!isAttachedToWindow) {
            return
        }

        if (max == 0) {
            max = 100
        }

        var drawBitmap = waveBitmap
        if (drawBitmap == null) {
            if (emptyBitmap != null) {
                drawBitmap = emptyBitmap
            } else {
                emptyBitmap = createEmptyBitmap()
                drawBitmap = emptyBitmap
            }
        }

        if (drawBitmap != null) {
            if (state == 1) {
                canvas.withSave {
                    clipRect((viewWidth * progress / max), 0, viewWidth, viewHeight)
                    drawBitmap(drawBitmap, 0F, 0F, barPaintActivated)
                }
                canvas.withSave {
                    clipRect(0, 0, (viewWidth * progress / max), viewHeight)
                    drawBitmap(drawBitmap, 0F, 0F, barPaintProgressActivated)
                }
            } else {
                canvas.withSave {
                    clipRect((viewWidth * progress / max), 0, viewWidth, viewHeight)
                    drawBitmap(drawBitmap, 0F, 0F, barPaint)
                }
                canvas.withSave {
                    clipRect(0, 0, (viewWidth * progress / max), viewHeight)
                    drawBitmap(drawBitmap, 0F, 0F, barPaintProgress)
                }
            }
        }
        super.onDraw(canvas)
    }

    /**
     *  We calculate a sufficiently large number of samples upfront so we don't need to wait for onLayout
     *  to tell us the width of our view.
     *  The number will adapt based on the devices screen width. Because on tablets we want to present more samples.
     */
    private fun guessSuitableAmountOfSamples(context: Context): Int {
        return (30f + (context.resources.displayMetrics.widthPixels / 40f)).roundToInt()
    }

    private fun createEmptyBitmap(): Bitmap {
        val tmpBitmap = Bitmap.createBitmap(viewWidth, barHeight, Bitmap.Config.ARGB_8888)
        val unusedHeight: Float = (barHeight / 2F) - halfBarMinHeight
        val halfSpace = spaceWidth.toFloat() / 2F

        for (i: Int in 0 until numSamples) {
            tmpBitmap.applyCanvas {
                drawRoundRect(
                    RectF(
                        halfSpace + (i * (barWidth + spaceWidth)),
                        unusedHeight,
                        halfSpace + (i * (barWidth + spaceWidth)) + barWidth,
                        barHeight - unusedHeight,
                    ),
                    radius,
                    radius,
                    barPaint,
                )
            }
        }
        return tmpBitmap
    }

    private fun createWaveformBitmap(samplesData: List<Float>): Bitmap {
        val tmpBitmap = Bitmap.createBitmap(viewWidth, barHeight, Bitmap.Config.ARGB_8888)
        if (samplesData.size < numSamples) {
            logger.warn(
                "Insufficient amount of calculated samples: {} < {}",
                samplesData.size,
                numSamples,
            )
            return tmpBitmap
        }
        val factor: Float = samplesData.size.toFloat() / numSamples.toFloat()
        val halfSpace = spaceWidth.toFloat() / 2F
        val halfBarHeight = barHeight / 2F

        for (i: Int in 0 until numSamples) {
            val sample = samplesData[(i * factor).roundToInt()]
            val unusedHeight: Float =
                min(halfBarHeight * (1F - sample), halfBarHeight - halfBarMinHeight)

            tmpBitmap.applyCanvas {
                drawRoundRect(
                    RectF(
                        halfSpace + (i * (barWidth + spaceWidth)),
                        unusedHeight,
                        halfSpace + (i * (barWidth + spaceWidth)) + barWidth,
                        barHeight - unusedHeight,
                    ),
                    radius,
                    radius,
                    barPaint,
                )
            }
        }
        return tmpBitmap
    }

    override fun onDataReady(newMessageModel: AbstractMessageModel, sampleData: List<Float>) {
        if (!isAttachedToWindow) {
            return
        }

        if (viewHeight == 0 || barHeight == 0) {
            return
        }

        waveFormTask = null
        if (messageModel?.id == newMessageModel.id) {
            if (sampleData.isNotEmpty()) {
                waveBitmap = createWaveformBitmap(sampleData)
                thumbnailCache?.set(messageModel?.id, waveBitmap)
                postInvalidate()
                show()
            }
        }
    }

    override fun onError(errorMessageModel: AbstractMessageModel, errorMessage: String?) {
        waveFormTask = null
    }

    override fun onCanceled(cancelMessageModel: AbstractMessageModel) {
        waveFormTask = null
    }

    override fun onDetachedFromWindow() {
        waveFormTask?.cancel()
        waveFormTask = null

        super.onDetachedFromWindow()
    }

    override fun onCreateDrawableState(extraSpace: Int): IntArray {
        val drawableState = super.onCreateDrawableState(extraSpace)

        if (::barPaint.isInitialized) {
            state = if (drawableState.contains(android.R.attr.state_activated)) 1 else 0
        }
        return drawableState
    }

    fun setMessageModel(
        newMessageModel: AbstractMessageModel?,
        thumbnailCache: ThumbnailCache<Any>?,
    ) {
        if (newMessageModel == null) {
            return
        }

        if (thumbnailCache == null) {
            return
        }

        this.thumbnailCache = thumbnailCache

        if (messageModel?.id != newMessageModel.id) {
            // recycled view
            waveFormTask?.let { task ->
                if (task.getMessageId() == newMessageModel.id) {
                    return
                } else {
                    task.cancel()
                }
            }
        }

        val cachedBitmap = thumbnailCache.get(newMessageModel.id)
        if (cachedBitmap != null) {
            messageModel = newMessageModel
            waveBitmap = cachedBitmap

            postInvalidate()
            visibility = VISIBLE
        } else {
            waveFormTask?.let { task ->
                if (task.getMessageId() == newMessageModel.id) {
                    return
                }
            }

            waveBitmap = null
            messageModel = newMessageModel
            waveFormTask = AudioWaveformGeneratorTask(
                newMessageModel,
                numPreCalculatedSamples,
                this@AudioProgressBarView,
            )

            ExecutorServices.voiceMessageThumbnailExecutorService.execute(
                Thread(
                    waveFormTask,
                    "WaveformGenerator",
                ),
            )
        }
    }

    private fun show() {
        if (messageModel == null) {
            return
        }

        RuntimeUtil.runOnUiThread {
            if (viewWidth > 0) {
                if (messageModel != null) {
                    clipBounds = Rect(
                        0,
                        ((viewHeight / 2F) - halfBarMinHeight).toInt(),
                        viewWidth,
                        ((viewHeight / 2F) + halfBarMinHeight).toInt(),
                    )
                    TransitionManager.beginDelayedTransition(parent as ViewGroup, changeBounds)
                }
                clipBounds = Rect(0, 0, viewWidth, viewHeight)
                visibility = VISIBLE
            }
        }
    }
}
