package org.thoughtcrime.securesms.loki.views

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.animation.DecelerateInterpolator
import androidx.core.math.MathUtils
import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.utilities.byteToNormalizedFloat
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

class WaveformSeekBar : View {

    companion object {
        @JvmStatic
        fun dp(context: Context, dp: Float): Float {
            return TypedValue.applyDimension(
                    TypedValue.COMPLEX_UNIT_DIP,
                    dp,
                    context.resources.displayMetrics
            )
        }
    }

    private val sampleDataHolder = SampleDataHolder(::invalidate)
    /** An array of signed byte values representing the audio signal. */
    var sampleData: ByteArray?
        get() {
            return sampleDataHolder.getSamples()
        }
        set(value) {
            sampleDataHolder.setSamples(value)
            invalidate()
        }

    /** Indicates whether the user is currently interacting with the view and performing a seeking gesture. */
    private var userSeeking = false
    private var _progress: Float = 0f
    /** In [0..1] range. */
    var progress: Float
        set(value) {
            // Do not let to modify the progress value from the outside
            // when the user is currently interacting with the view.
            if (userSeeking) return

            _progress = value
            invalidate()
            progressChangeListener?.onProgressChanged(this, _progress, false)
        }
        get() {
            return _progress
        }

    var barBackgroundColor: Int = Color.LTGRAY
        set(value) {
            field = value
            invalidate()
        }

    var barProgressColor: Int = Color.WHITE
        set(value) {
            field = value
            invalidate()
        }

    var barGap: Float = dp(context, 2f)
        set(value) {
            field = value
            invalidate()
        }

    var barWidth: Float = dp(context, 5f)
        set(value) {
            field = value
            invalidate()
        }

    var barMinHeight: Float = barWidth
        set(value) {
            field = value
            invalidate()
        }

    var barCornerRadius: Float = dp(context, 2.5f)
        set(value) {
            field = value
            invalidate()
        }

    var barGravity: WaveGravity = WaveGravity.CENTER
        set(value) {
            field = value
            invalidate()
        }

    var progressChangeListener: ProgressChangeListener? = null

    private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val barRect = RectF()

    private var canvasWidth = 0
    private var canvasHeight = 0

    private var touchDownX = 0f
    private var touchDownProgress: Float = 0f
    private var scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

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

        val typedAttrs = context.obtainStyledAttributes(attrs, R.styleable.WaveformSeekBar)
        barWidth = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_width, barWidth)
        barGap = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_gap, barGap)
        barCornerRadius = typedAttrs.getDimension(
                R.styleable.WaveformSeekBar_bar_corner_radius,
                barCornerRadius)
        barMinHeight =
                typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_min_height, barMinHeight)
        barBackgroundColor = typedAttrs.getColor(
                R.styleable.WaveformSeekBar_bar_background_color,
                barBackgroundColor)
        barProgressColor =
                typedAttrs.getColor(R.styleable.WaveformSeekBar_bar_progress_color, barProgressColor)
        progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_progress, progress)
        barGravity = WaveGravity.fromString(
                typedAttrs.getString(R.styleable.WaveformSeekBar_bar_gravity))

        typedAttrs.recycle()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        canvasWidth = w
        canvasHeight = h
        invalidate()
    }

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

        val totalWidth = getAvailableWidth()
        val barAmount = (totalWidth / (barWidth + barGap)).toInt()

        var lastBarRight = paddingLeft.toFloat()

        (0 until barAmount).forEach { barIdx ->
            // Convert a signed byte to a [0..1] float.
            val barValue = byteToNormalizedFloat(sampleDataHolder.computeBarValue(barIdx, barAmount))

            val barHeight = max(barMinHeight, getAvailableHeight() * barValue)

            val top: Float = when (barGravity) {
                WaveGravity.TOP -> paddingTop.toFloat()
                WaveGravity.CENTER -> paddingTop + getAvailableHeight() * 0.5f - barHeight * 0.5f
                WaveGravity.BOTTOM -> canvasHeight - paddingBottom - barHeight
            }

            barRect.set(lastBarRight, top, lastBarRight + barWidth, top + barHeight)

            barPaint.color = if (barRect.right <= totalWidth * progress)
                barProgressColor else barBackgroundColor

            canvas.drawRoundRect(barRect, barCornerRadius, barCornerRadius, barPaint)

            lastBarRight = barRect.right + barGap
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (!isEnabled) return false

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                userSeeking = true
                touchDownX = event.x
                touchDownProgress = progress
                updateProgress(event, false)
            }
            MotionEvent.ACTION_MOVE -> {
                // Prevent any parent scrolling if the user scrolled more
                // than scaledTouchSlop on horizontal axis.
                if (abs(event.x - touchDownX) > scaledTouchSlop) {
                    parent.requestDisallowInterceptTouchEvent(true)
                }
                updateProgress(event, false)
            }
            MotionEvent.ACTION_UP -> {
                userSeeking = false
                updateProgress(event, true)
                performClick()
            }
            MotionEvent.ACTION_CANCEL -> {
                updateProgress(touchDownProgress, false)
                userSeeking = false
            }
        }
        return true
    }

    private fun updateProgress(event: MotionEvent, notify: Boolean) {
        updateProgress(event.x / getAvailableWidth(), notify)
    }

    private fun updateProgress(progress: Float, notify: Boolean) {
        _progress = MathUtils.clamp(progress, 0f, 1f)
        invalidate()

        if (notify) {
            progressChangeListener?.onProgressChanged(this, _progress, true)
        }
    }

    override fun performClick(): Boolean {
        super.performClick()
        return true
    }

    private fun getAvailableWidth() = canvasWidth - paddingLeft - paddingRight
    private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom

    private class SampleDataHolder(private val invalidateDelegate: () -> Any) {

        private var sampleDataFrom: ByteArray? = null
        private var sampleDataTo: ByteArray? = null
        private var progress = 1f // Mix between from and to values.

        private var animation: ValueAnimator? = null

        fun computeBarValue(barIdx: Int, barAmount: Int): Byte {
            /** @return The array's value at the interpolated index. */
            fun getSampleValue(sampleData: ByteArray?): Byte {
                if (sampleData == null || sampleData.isEmpty())
                    return Byte.MIN_VALUE
                else {
                    val sampleIdx = (barIdx * (sampleData.size / barAmount.toFloat())).toInt()
                    return sampleData[sampleIdx]
                }
            }

            if (progress == 1f) {
                return getSampleValue(sampleDataTo)
            }

            val fromValue = getSampleValue(sampleDataFrom)
            val toValue = getSampleValue(sampleDataTo)
            val rawResultValue = fromValue * (1f - progress) + toValue * progress
            return rawResultValue.roundToInt().toByte()
        }

        fun setSamples(sampleData: ByteArray?) {
            /** @return a mix between [sampleDataFrom] and [sampleDataTo] arrays according to the current [progress] value. */
            fun computeNewDataFromArray(): ByteArray? {
                if (sampleDataTo == null) return null
                if (sampleDataFrom == null) return sampleDataTo

                val sampleSize = min(sampleDataFrom!!.size, sampleDataTo!!.size)
                return ByteArray(sampleSize) { i -> computeBarValue(i, sampleSize) }
            }

            sampleDataFrom = computeNewDataFromArray()
            sampleDataTo = sampleData
            progress = 0f

            animation?.cancel()
            animation = ValueAnimator.ofFloat(0f, 1f).apply {
                addUpdateListener { animation ->
                    progress = animation.animatedValue as Float
                    invalidateDelegate()
                }
                interpolator = DecelerateInterpolator(3f)
                duration = 500
                start()
            }
        }

        fun getSamples(): ByteArray? {
            return sampleDataTo
        }
    }

    enum class WaveGravity {
        TOP,
        CENTER,
        BOTTOM,
        ;

        companion object {
            @JvmStatic
            fun fromString(gravity: String?): WaveGravity = when (gravity) {
                "1" -> TOP
                "2" -> CENTER
                else -> BOTTOM
            }
        }
    }

    interface ProgressChangeListener {
        fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean)
    }
}