/*
 * Copyright (C) 2017 JRummy Apps Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jrummyapps.android.colorpicker

import android.content.Context
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat

enum class ColorShape(val attrEnum: Int) {
    Square(0),
    Circle(1),
    ;

    companion object {
        fun fromInt(i: Int): ColorShape =
            entries.find { it.attrEnum == i } ?: Square
    }
}

/**
 * This class draws a panel which which will be filled with a color which can be set. It can be used to show the
 * currently selected color which you will get from the [ColorPickerView].
 */
class ColorPanelView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0,
) : View(context, attrs, defStyle) {

    companion object {
        private const val DEFAULT_BORDER_COLOR = -0x919192
    }

    /* The width in pixels of the border surrounding the color panel. */
    private val borderWidthPx = context.dpToPx(1f)

    private val borderPaint = Paint().apply {
        isAntiAlias = true
    }

    private val colorPaint = Paint().apply {
        isAntiAlias = true
    }

    private val alphaPaint = Paint().apply {
        val bitmap = (ContextCompat.getDrawable(context, R.drawable.cpv_alpha) as BitmapDrawable)
            .bitmap
        shader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
        isAntiAlias = true
    }

    private val originalPaint = Paint()

    private val centerRect = RectF()
    private var drawingRect = Rect()
    private var colorRect = Rect()

    private var alphaPattern = TilePatternDrawable(context.dpToPx(4f))

    private var showOldColor = false

    var borderColor = DEFAULT_BORDER_COLOR
        set(value) {
            if (field != value) {
                field = value
                invalidate()
            }
        }

    var color = Color.BLACK
        set(value) {
            if (field != value) {
                field = value
                invalidate()
            }
        }

    private var shape = ColorShape.Circle
        set(value) {
            if (field != value) {
                field = value
                invalidate()
            }
        }

    init {
        val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
        shape = ColorShape.fromInt(a.getInt(R.styleable.ColorPanelView_cpv_colorShape, -1))
        showOldColor = a.getBoolean(R.styleable.ColorPanelView_cpv_showOldColor, false)
        check(!(showOldColor && shape != ColorShape.Circle)) { "Color preview is only available in circle mode" }
        borderColor = a.getColor(R.styleable.ColorPanelView_cpv_borderColor, DEFAULT_BORDER_COLOR)
        a.recycle()

        if (borderColor == DEFAULT_BORDER_COLOR) {
            // If no specific border color has been set we take the default secondary text color as border/slider color.
            // Thus it will adopt to theme changes automatically.
            val value = TypedValue()
            val typedArray = context.obtainStyledAttributes(
                value.data,
                intArrayOf(android.R.attr.textColorSecondary)
            )
            borderColor = typedArray.getColor(0, borderColor)
            typedArray.recycle()
        }
    }

    public override fun onSaveInstanceState(): Parcelable {
        val state = Bundle()
        state.putParcelable("instanceState", super.onSaveInstanceState())
        state.putInt("color", color)
        return state
    }

    public override fun onRestoreInstanceState(state: Parcelable) {
        if (state is Bundle) {
            color = state.getInt("color")
            super.onRestoreInstanceState(state.getParcelableCompat("instanceState"))
        } else {
            super.onRestoreInstanceState(state)
        }
    }

    override fun onDraw(canvas: Canvas) {
        borderPaint.color = borderColor
        colorPaint.color = color
        if (shape == ColorShape.Square) {
            if (borderWidthPx > 0) {
                canvas.drawRect(drawingRect, borderPaint)
            }
            alphaPattern.draw(canvas)
            canvas.drawRect(colorRect, colorPaint)
        } else if (shape == ColorShape.Circle) {
            val outerRadius = measuredWidth / 2
            if (borderWidthPx > 0) {
                canvas.drawCircle(
                    measuredWidth / 2f,
                    measuredHeight / 2f,
                    outerRadius.toFloat(),
                    borderPaint
                )
            }
            if (Color.alpha(color) < 255) {
                canvas.drawCircle(
                    measuredWidth / 2f,
                    measuredHeight / 2f, (
                            outerRadius - borderWidthPx).toFloat(), alphaPaint
                )
            }
            if (showOldColor) {
                canvas.drawArc(centerRect, 90f, 180f, true, originalPaint)
                canvas.drawArc(centerRect, 270f, 180f, true, colorPaint)
            } else {
                canvas.drawCircle(
                    measuredWidth / 2f,
                    measuredHeight / 2f, (
                            outerRadius - borderWidthPx).toFloat(),
                    colorPaint
                )
            }
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        when (shape) {
            ColorShape.Square -> {
                val width = MeasureSpec.getSize(widthMeasureSpec)
                val height = MeasureSpec.getSize(heightMeasureSpec)
                setMeasuredDimension(width, height)
            }

            ColorShape.Circle -> {
                super.onMeasure(widthMeasureSpec, widthMeasureSpec)
                setMeasuredDimension(measuredWidth, measuredWidth)
            }
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        if (shape == ColorShape.Square || showOldColor) {
            drawingRect.set(
                paddingLeft,
                paddingTop,
                w - paddingRight,
                h - paddingBottom
            )
            if (showOldColor) {
                setUpCenterRect()
            } else {
                setUpColorRect()
            }
        }
    }

    private fun setUpCenterRect() {
        val dRect = drawingRect
        val left = dRect.left + borderWidthPx
        val top = dRect.top + borderWidthPx
        val bottom = dRect.bottom - borderWidthPx
        val right = dRect.right - borderWidthPx
        centerRect.set(
            left.toFloat(),
            top.toFloat(),
            right.toFloat(),
            bottom.toFloat()
        )
    }

    private fun setUpColorRect() {
        val left = drawingRect.left + borderWidthPx
        val top = drawingRect.top + borderWidthPx
        val bottom = drawingRect.bottom - borderWidthPx
        val right = drawingRect.right - borderWidthPx
        colorRect.set(left, top, right, bottom)
        alphaPattern.setBounds(left, top, right, bottom)
    }

    /**
     * Set the original color. This is only used for previewing colors.
     *
     * @param color
     * The original color
     */
    fun setOriginalColor(@ColorInt color: Int) {
        originalPaint.color = color
    }

    /**
     * Show a toast message with the hex color code below the view.
     */
    fun showHint() {
        val screenPos = IntArray(2)
        val displayFrame = Rect()
        getLocationOnScreen(screenPos)
        getWindowVisibleDisplayFrame(displayFrame)
        val context = context
        val width = width
        val height = height
        val midy = screenPos[1] + height / 2
        var referenceX = screenPos[0] + width / 2
        if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
            val screenWidth = context.resources.displayMetrics.widthPixels
            referenceX = screenWidth - referenceX // mirror
        }
        val hexText = when {
            Color.alpha(color) == 255 -> "%06X".format(color and 0xFFFFFF)
            else -> Integer.toHexString(color)
        }
        val hint = "#${hexText.uppercase()}"
        val cheatSheet = Toast.makeText(context, hint, Toast.LENGTH_SHORT)
        if (midy < displayFrame.height()) {
            // Show along the top; follow action buttons
            cheatSheet.setGravity(
                Gravity.TOP or GravityCompat.END, referenceX,
                screenPos[1] + height - displayFrame.top
            )
        } else {
            // Show along the bottom center
            cheatSheet.setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, height)
        }
        cheatSheet.show()
    }
}