/*
 * SPDX-License-Identifier: MPL-2.0
 * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
 */

package emu.skyline.input.onscreen

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.util.TypedValue
import androidx.core.content.ContextCompat
import emu.skyline.R
import emu.skyline.input.ButtonId
import kotlin.math.roundToInt

/**
 * Converts relative values, such as coordinates and boundaries, to their absolute counterparts, also handles layout modifications like scaling and custom positioning
 */
abstract class OnScreenButton(
    onScreenControllerView : OnScreenControllerView,
    final override val buttonId : ButtonId,
    private val defaultRelativeX : Float,
    private val defaultRelativeY : Float,
    private val defaultRelativeWidth : Float,
    private val defaultRelativeHeight : Float,
    drawableId : Int,
    private val defaultEnabled : Boolean
) : ConfigurableButton {
    companion object {
        /**
         * Aspect ratio the default values were based on
         */
        const val CONFIGURED_ASPECT_RATIO = 2074f / 874f

        private var disabledSelectionPaint : Paint? = null
    }

    init {
        if (disabledSelectionPaint == null) {
            disabledSelectionPaint = Paint().apply {
                color = onScreenControllerView.context.obtainStyledAttributes(intArrayOf(R.attr.colorTertiary)).use { it.getColor(0, Color.GREEN) }
                style = Paint.Style.STROKE
                strokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, onScreenControllerView.context.resources.displayMetrics)
            }
        }
    }

    final override val config = OnScreenConfigurationImpl(onScreenControllerView.context, buttonId, defaultRelativeX, defaultRelativeY, defaultEnabled)

    protected val drawable = ContextCompat.getDrawable(onScreenControllerView.context, drawableId)!!

    private val buttonSymbolPaint = Paint().apply {
        color = config.textColor
        typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
        isAntiAlias = true
    }
    private val textBoundsRect = Rect()

    var relativeX = config.relativeX
    var relativeY = config.relativeY
    private val relativeWidth get() = defaultRelativeWidth * config.scale
    private val relativeHeight get() = defaultRelativeHeight * config.scale

    /**
     * The width of the view this button is in, populated by the view during draw
     */
    var width = 0

    /**
     * The height of the view this button is in, populated by the view during draw
     */
    var height = 0

    /**
     * The height of the view this button is in, adjusted to an arbitrary aspect ratio used during configuration
     */
    protected val adjustedHeight get() = width / CONFIGURED_ASPECT_RATIO

    /**
     * The difference between the view height and the adjusted view height.
     * This is used to offset the buttons to the bottom of the screen when the view height is larger than [adjustedHeight]
     */
    protected val heightDiff get() = (height - adjustedHeight).coerceAtLeast(0f)

    protected val itemWidth get() = width * relativeWidth
    private val itemHeight get() = adjustedHeight * relativeHeight

    val currentX get() = width * relativeX
    val currentY get() = adjustedHeight * relativeY + heightDiff

    private val left get() = currentX - itemWidth / 2f
    private val top get() = currentY - itemHeight / 2f

    val currentBounds get() = Rect(left.roundToInt(), top.roundToInt(), (left + itemWidth).roundToInt(), (top + itemHeight).roundToInt())

    /**
     * Keeps track of finger when there are multiple touches
     */
    var touchPointerId = -1
    var partnerPointerId = -1

    var isPressed = false

    var hapticFeedback = false

    /**
     * The edit session information, populated by the view
     */
    protected val editInfo = onScreenControllerView.editInfo

    /**
     * The touch point when the edit session started
     */
    protected val editInitialTouchPoint = PointF()

    /**
     * The scale of the button when the edit session started
     */
    private var editInitialScale = 0f

    protected open fun renderCenteredText(canvas : Canvas, text : String, size : Float, x : Float, y : Float, alpha : Int) {
        buttonSymbolPaint.apply {
            textSize = size
            textAlign = Paint.Align.LEFT
            color = config.textColor
            this.alpha = alpha
            getTextBounds(text, 0, text.length, textBoundsRect)
        }
        canvas.drawText(text, x - textBoundsRect.width() / 2f - textBoundsRect.left, y + textBoundsRect.height() / 2f - textBoundsRect.bottom, buttonSymbolPaint)
    }

    private fun applyColorsToDrawable(drawable : Drawable) {
        when (drawable) {
            is GradientDrawable -> drawable.setColor(config.backgroundColor)
            is LayerDrawable -> for (i in 0 until drawable.numberOfLayers) applyColorsToDrawable(drawable.getDrawable(i))
            else -> drawable.setTint(config.backgroundColor)
        }
    }

    open fun render(canvas : Canvas) {
        var bounds : Rect? = null
        if (config.enabled) {
            bounds = currentBounds
            val alpha = if (isPressed) config.alpha / 3 else config.alpha
            drawable.apply {
                this.bounds = bounds
                this.alpha = alpha
                applyColorsToDrawable(this)
                draw(canvas)
            }

            renderCenteredText(canvas, buttonId.short!!, itemWidth.coerceAtMost(itemHeight) * 0.4f, bounds.centerX().toFloat(), bounds.centerY().toFloat(), alpha)
            }
        }

        // Draw a box around the button when in edit mode and it's either disabled or the alpha is too low (< 5%)
        if (editInfo.isEditing && (!config.enabled || config.alpha < 12))
            canvas.drawRect(bounds ?: currentBounds, disabledSelectionPaint ?: return)
    }

    abstract fun isTouched(x : Float, y : Float) : Boolean

    open fun onFingerDown(x : Float, y : Float) {
        isPressed = true
    }

    open fun onFingerUp(x : Float, y : Float) {
        isPressed = false
    }

    fun loadConfigValues() {
        relativeX = config.relativeX
        relativeY = config.relativeY
    }

    fun saveConfigValues() {
        config.relativeX = relativeX
        config.relativeY = relativeY
    }

    override fun startMove(x : Float, y : Float) {
        editInitialTouchPoint.set(x, y)
        editInitialScale = config.scale
    }

    override fun move(x : Float, y : Float) {
        var adjustedX = x
        var adjustedY = y

        if (editInfo.snapToGrid) {
            val centerX = width / 2f
            val centerY = height / 2f
            val gridSize = editInfo.gridSize
            // The coordinates of the first grid line for each axis, because the grid is centered and might not start at [0,0]
            val startX = centerX % gridSize
            val startY = centerY % gridSize

            /**
             * The offset to apply to a coordinate to snap it to the grid is the remainder of
             * the coordinate divided by the grid size.
             * Since the grid is centered on the screen and might not start at [0,0] we need to
             * subtract the grid start offset, otherwise we'd be calculating the offset for a grid that starts at [0,0].
             *
             * Example: Touch event X: 158 | Grid size: 50 | Grid start X: 40 -> Grid lines at 40, 90, 140, 190, ...
             * Snap offset: 158 - 40 = 118 -> 118 % 50 = 18
             * Apply offset to X: 158 - 18 = 140 which is a grid line
             *
             * If we didn't subtract the grid start offset:
             * Snap offset: 158 % 50 = 8
             * Apply offset to X: 158 - 8 = 150 which is not a grid line
             */
            val snapOffsetX = (x - startX) % gridSize
            val snapOffsetY = (y - startY) % gridSize

            adjustedX = x - snapOffsetX
            adjustedY = y - snapOffsetY
        }

        relativeX = adjustedX / width
        relativeY = (adjustedY - heightDiff) / adjustedHeight
    }

    override fun endMove() {
        saveConfigValues()
    }

    /**
     * Returns the amount to move the button when the user presses an arrow key
     * @param gridOffset The offset to apply to the grid size so that the button always ends up on a grid line
     */
    private fun getMoveAmount(gridOffset : Int) : Int {
        return if (editInfo.snapToGrid)
            editInfo.gridSize + gridOffset
        else
            OnScreenEditInfo.ArrowKeyMoveAmount
    }

    override fun moveUp() {
        move(currentX, currentY - getMoveAmount(-1))
        saveConfigValues()
    }

    override fun moveDown() {
        move(currentX, currentY + getMoveAmount(+1))
        saveConfigValues()
    }

    override fun moveLeft() {
        move(currentX - getMoveAmount(-1), currentY)
        saveConfigValues()
    }

    override fun moveRight() {
        move(currentX + getMoveAmount(+1), currentY)
        saveConfigValues()
    }

    override fun resetConfig() {
        config.enabled = defaultEnabled
        config.alpha = OnScreenConfiguration.DefaultAlpha
        config.textColor = OnScreenConfiguration.DefaultTextColor
        config.backgroundColor = OnScreenConfiguration.DefaultBackgroundColor
        config.scale = OnScreenConfiguration.DefaultScale
        config.relativeX = defaultRelativeX
        config.relativeY = defaultRelativeY

        relativeX = defaultRelativeX
        relativeY = defaultRelativeY
    }
}
