package org.thoughtcrime.securesms.conversationlist.chatfilter

import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.Px
import androidx.core.content.ContextCompat
import com.google.android.material.animation.ArgbEvaluatorCompat
import org.signal.core.util.dp
import kotlin.math.max
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import org.signal.core.ui.R as CoreUiR

/**
 * Renders the filter-circle at any given position
 *
 * Animation Spec:
 *
 * @ 35dp display open, we want to start animating the first stroke:
 *  - duration 100ms
 *  - curve Quad in/out
 *
 * @ 50dp display open, we want to start animating the second stroke:
 *  - duration 150ms
 *  - curve Quad in/out
 *
 * @ 75dp display open, we want to start animating the third stroke:
 *  - duration 150ms
 *  - curve Quad in/out
 *
 * @ 100dp display open, we want to apply "active" coloring.
 *
 * On release, if active, we transform  into a rounded rectangle
 *  - 38pt circle
 *  - rectangle width 154, height 32
 *  - duration 100ms
 *  - fade in button and text 300ms *after* circle-rectangle animation has completed.
 */
class FilterCircleView @JvmOverloads constructor(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

  companion object {
    private val CIRCLE_Y_EVALUATOR = FloatEvaluator()
    private val COLOR_EVALUATOR = ArgbEvaluatorCompat.getInstance()

    private val STROKES = listOf(
      Stroke(
        triggerPoint = 0.35f,
        width = 4.dp,
        distanceFromBottomOfCircle = 11.dp,
        animationDuration = 100.milliseconds
      ),
      Stroke(
        triggerPoint = 0.5f,
        width = 12.dp,
        distanceFromBottomOfCircle = 17.dp,
        animationDuration = 150.milliseconds
      ),
      Stroke(
        triggerPoint = 0.75f,
        width = 18.dp,
        distanceFromBottomOfCircle = 23.dp,
        animationDuration = 150.milliseconds
      )
    )
  }

  private val circleRadius = 38.dp / 2f
  private val circleBackgroundColor = ContextCompat.getColor(context, CoreUiR.color.signal_colorSurface1)
  private val strokeColor = ContextCompat.getColor(context, CoreUiR.color.signal_colorSecondary)
  private val circleActiveBackgroundColor = ContextCompat.getColor(context, CoreUiR.color.signal_colorSecondaryContainer)
  private val strokeActiveColor = ContextCompat.getColor(context, CoreUiR.color.signal_colorPrimary)

  private var circleColorAnimator: ValueAnimator? = null
  private var strokeColorAnimator: ValueAnimator? = null
  private var circleToRectangleAnimator: ValueAnimator? = null

  private val runningStrokeAnimations = mutableMapOf<Stroke, ValueAnimator>()

  private val circlePaint = Paint().apply {
    isAntiAlias = true
    color = circleBackgroundColor
    style = Paint.Style.FILL
  }

  private val strokePaint = Paint().apply {
    isAntiAlias = true
    color = strokeColor
    style = Paint.Style.FILL
  }

  var progress: Float = 0f
    set(value) {
      field = value
      onStateChange()
    }

  var state: FilterPullState = FilterPullState.CLOSED
    set(value) {
      field = value
      onStateChange()
    }

  var textFieldMetrics: Pair<Int, Int> = Pair(0, 0)

  private val rect = Rect()
  private val rectF = RectF()
  var bottomOffset: Float = evaluateBottomOffset(0f, FilterPullState.CLOSED)

  override fun draw(canvas: Canvas) {
    super.draw(canvas)
    canvas.getClipBounds(rect)

    val centerX = rect.width() / 2f
    val circleBottom = rect.height() - bottomOffset
    val circleCenterY = circleBottom - circleRadius

    val circleShapeAnimator = circleToRectangleAnimator
    if (circleShapeAnimator != null) {
      val (textWidth, textHeight) = textFieldMetrics
      rectF.set(
        CIRCLE_Y_EVALUATOR.evaluate(circleShapeAnimator.animatedValue as Float, centerX - circleRadius, centerX - (textWidth / 2)),
        CIRCLE_Y_EVALUATOR.evaluate(circleShapeAnimator.animatedFraction, circleCenterY - circleRadius, circleCenterY - (textHeight / 2)),
        CIRCLE_Y_EVALUATOR.evaluate(circleShapeAnimator.animatedValue as Float, centerX + circleRadius, centerX + (textWidth / 2)),
        CIRCLE_Y_EVALUATOR.evaluate(circleShapeAnimator.animatedFraction, circleCenterY + circleRadius, circleCenterY + (textHeight / 2))
      )

      canvas.drawRoundRect(
        rectF,
        CIRCLE_Y_EVALUATOR.evaluate(circleShapeAnimator.animatedFraction, circleRadius, 8.dp),
        CIRCLE_Y_EVALUATOR.evaluate(circleShapeAnimator.animatedFraction, circleRadius, 8.dp),
        getCirclePaint()
      )
    } else {
      rectF.set(
        centerX - circleRadius,
        circleBottom - circleRadius * 2,
        centerX + circleRadius,
        circleBottom
      )

      canvas.drawRoundRect(
        rectF,
        circleRadius,
        circleRadius,
        getCirclePaint()
      )
    }

    runningStrokeAnimations.forEach { (stroke, animator) ->
      stroke.fillRect(rect, centerX, circleBottom, animator.animatedFraction)
      rectF.set(rect)
      canvas.drawRoundRect(rectF, 50f, 50f, getStrokePaint())
    }
  }

  private fun onStateChange() {
    bottomOffset = evaluateBottomOffset(progress, state)
    checkStrokeTriggers(progress)
    checkColorAnimators(state)
    checkCircleToRectangleAnimator(state)
    invalidate()
  }

  private fun evaluateBottomOffset(progress: Float, state: FilterPullState): Float {
    return when (state) {
      FilterPullState.OPENING, FilterPullState.OPEN, FilterPullState.CLOSE_APEX, FilterPullState.CANCELING, FilterPullState.OPEN_APEX -> FilterLerp.getOpenCircleBottomPadLerp(progress)
      FilterPullState.CLOSED, FilterPullState.CLOSING -> FilterLerp.getClosedCircleBottomPadLerp(progress)
    }
  }

  private fun checkColorAnimators(state: FilterPullState) {
    if (state != FilterPullState.CLOSED && state != FilterPullState.CANCELING) {
      if (circleColorAnimator == null) {
        circleColorAnimator = ValueAnimator
          .ofInt(circleBackgroundColor, circleActiveBackgroundColor).apply {
            addUpdateListener { invalidate() }
            setEvaluator(COLOR_EVALUATOR)
            duration = 200
            start()
          }
      }

      if (strokeColorAnimator == null) {
        strokeColorAnimator = ValueAnimator
          .ofInt(strokeColor, strokeActiveColor).apply {
            addUpdateListener { invalidate() }
            setEvaluator(COLOR_EVALUATOR)
            duration = 200
            start()
          }
      }
    } else {
      circleColorAnimator?.cancel()
      circleColorAnimator = null

      strokeColorAnimator?.cancel()
      strokeColorAnimator = null
    }
  }

  private fun checkStrokeTriggers(progress: Float) {
    if (progress <= 0f) {
      runningStrokeAnimations.forEach { it.value.cancel() }
      runningStrokeAnimations.clear()
      return
    }

    STROKES
      .filter { it.triggerPoint <= progress && !runningStrokeAnimations.containsKey(it) }
      .forEach {
        runningStrokeAnimations[it] = ValueAnimator.ofFloat(0f, 1f).apply {
          addUpdateListener { invalidate() }
          duration = it.animationDuration.inWholeMilliseconds
          start()
        }
      }
  }

  private fun checkCircleToRectangleAnimator(state: FilterPullState) {
    if (state == FilterPullState.OPENING && circleToRectangleAnimator == null) {
      require(textFieldMetrics != Pair(0, 0))
      circleToRectangleAnimator = ValueAnimator.ofFloat(1f).apply {
        addUpdateListener { invalidate() }
        interpolator = OvershootInterpolator()
        startDelay = 100
        duration = 200
        start()
      }
    } else if (state == FilterPullState.CLOSED) {
      circleToRectangleAnimator?.cancel()
      circleToRectangleAnimator = null
    }
  }

  private fun getCirclePaint(): Paint {
    val circleAlpha = when (state) {
      FilterPullState.CLOSED -> 255
      FilterPullState.OPEN_APEX -> 255
      FilterPullState.CANCELING -> 255
      FilterPullState.OPENING -> 255
      FilterPullState.OPEN -> 0
      FilterPullState.CLOSE_APEX -> 0
      FilterPullState.CLOSING -> 0
    }

    return circlePaint.apply {
      color = (circleColorAnimator?.animatedValue ?: circleBackgroundColor) as Int
      alpha = circleAlpha
    }
  }

  private fun getStrokePaint(): Paint {
    val strokeAlpha = max(0f, 1f - (circleToRectangleAnimator?.animatedFraction ?: 0f))

    return strokePaint.apply {
      color = (strokeColorAnimator?.animatedValue ?: strokeColor) as Int
      alpha = (strokeAlpha * 255).toInt()
    }
  }

  private data class Stroke(
    val triggerPoint: Float,
    @Px val width: Int,
    @Px val distanceFromBottomOfCircle: Int,
    val animationDuration: Duration
  ) {
    fun fillRect(rect: Rect, centerX: Float, circleBottom: Float, progress: Float) {
      rect.setEmpty()

      val width = progress * this.width
      if (width <= 0f) {
        return
      }

      rect.bottom = (circleBottom.toInt() - distanceFromBottomOfCircle)
      rect.top = rect.bottom - 2.dp
      rect.left = (centerX - (width / 2f)).toInt()
      rect.right = (centerX + (width / 2f)).toInt()
    }
  }
}
