/*
 * 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.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.ColorUtils
import androidx.core.view.children
import androidx.core.widget.addTextChangedListener
import com.jrummyapps.android.colorpicker.databinding.CpvDialogColorPickerBinding
import com.jrummyapps.android.colorpicker.databinding.CpvDialogPresetsBinding
import jp.juggler.util.log.LogCategory
import jp.juggler.util.systemService
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.gone
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.Locale
import kotlin.coroutines.resumeWithException
import kotlin.math.roundToInt

private val log = LogCategory("ColorPickerDialog")

enum class ColorPickerDialogType { Custom, Presets, }

internal const val ALPHA_THRESHOLD = 165

/**
 * Material design colors used as the default color presets
 */
val MATERIAL_COLORS = intArrayOf(
    -0xbbcca,  // RED 500
    -0x16e19d,  // PINK 500
    -0xd36d,  // LIGHT PINK 500
    -0x63d850,  // PURPLE 500
    -0x98c549,  // DEEP PURPLE 500
    -0xc0ae4b,  // INDIGO 500
    -0xde690d,  // BLUE 500
    -0xfc560c,  // LIGHT BLUE 500
    -0xff432c,  // CYAN 500
    -0xff6978,  // TEAL 500
    -0xb350b0,  // GREEN 500
    -0x743cb6,  // LIGHT GREEN 500
    -0x3223c7,  // LIME 500
    -0x14c5,  // YELLOW 500
    -0x3ef9,  // AMBER 500
    -0x6800,  // ORANGE 500
    -0x86aab8,  // BROWN 500
    -0x9f8275,  // BLUE GREY 500
    -0x616162
)

internal fun unshiftIfNotExists(array: IntArray, value: Int): IntArray {
    if (array.any { it == value }) return array
    val newArray = IntArray(array.size + 1)
    newArray[0] = value
    System.arraycopy(array, 0, newArray, 1, newArray.size - 1)
    return newArray
}

internal fun pushIfNotExists(array: IntArray, value: Int): IntArray {
    if (array.any { it == value }) {
        return array
    }
    val newArray = IntArray(array.size + 1)
    newArray[newArray.size - 1] = value
    System.arraycopy(array, 0, newArray, 0, newArray.size - 1)
    return newArray
}

internal fun shadeColor(@ColorInt color: Int, percent: Double): Int {
    val hex = "#%06X".format(color and 0xFFFFFF)
    val f = hex.substring(1).toLong(16)
    val t = (if (percent < 0) 0 else 255).toDouble()
    val p = if (percent < 0) percent * -1 else percent
    val cR = f shr 16
    val cG = f shr 8 and 0x00FF
    val cB = f and 0x0000FF
    return Color.argb(
        Color.alpha(color),
        ((t - cR) * p).roundToInt() + cR.toInt(),
        ((t - cG) * p).roundToInt() + cG.toInt(),
        ((t - cB) * p).roundToInt() + cB.toInt(),
    )
}

internal fun getColorShades(@ColorInt color: Int) = intArrayOf(
    shadeColor(color, 0.9),
    shadeColor(color, 0.7),
    shadeColor(color, 0.5),
    shadeColor(color, 0.333),
    shadeColor(color, 0.166),
    shadeColor(color, -0.125),
    shadeColor(color, -0.25),
    shadeColor(color, -0.375),
    shadeColor(color, -0.5),
    shadeColor(color, -0.675),
    shadeColor(color, -0.7),
    shadeColor(color, -0.775),
)

internal fun View.showSoftInput(show: Boolean) {
    val imm: InputMethodManager = systemService(context) ?: return
    if (show) {
        imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
    } else {
        imm.hideSoftInputFromWindow(this.windowToken, 0)
    }
}

fun Context.getTextColorPrimary() = try {
    val value = TypedValue()
    val typedArray = obtainStyledAttributes(
        value.data,
        intArrayOf(android.R.attr.textColorPrimary)
    )
    try {
        typedArray.getColor(0, Color.BLACK)
    } finally {
        typedArray.recycle()
    }
} catch (_: Throwable) {
    null
}

internal fun loadPresets(from: IntArray, newColor: Int): IntArray {
    var presets = from.copyOf()

    val isMaterialColors = presets.contentEquals(MATERIAL_COLORS)

    val alpha = Color.alpha(newColor)

    // don't update the original array when modifying alpha
    if (alpha != 255) {
        // add alpha to the presets
        for (i in presets.indices) {
            val color = presets[i]
            val red = Color.red(color)
            val green = Color.green(color)
            val blue = Color.blue(color)
            presets[i] = Color.argb(alpha, red, green, blue)
        }
    }
    presets = unshiftIfNotExists(presets, newColor)
    if (isMaterialColors && presets.size == 19) {
        // Add black to have a total of 20 colors if the current color is in the material color palette
        presets = pushIfNotExists(
            presets,
            Color.argb(alpha, 0, 0, 0)
        )
    }
    return presets
}

@SuppressLint("ClickableViewAccessibility")
suspend fun Activity.dialogColorPicker(
    // the original color.
    @ColorInt
    colorInitial: Int?,

    // the alpha slider
    // true to show the alpha slider.
    // Currently only supported with the ColorPickerView.
    alphaEnabled: Boolean,
): Int = suspendCancellableCoroutine { cont ->
    // dialog title string resource id.
    @StringRes
    val dialogTitle = R.string.cpv_default_title

    val initialDialogType = ColorPickerDialogType.Custom

    // the colors used for the presets.
    val initialPresets: IntArray = MATERIAL_COLORS

    // true if showing a neutral button to switch preset/custom.
    val dialogTypeSwitcher = true

    // Show/Hide the color shades in the presets picker
    // false to hide the color shades.
    val useColorShade = true

    // the shape of the color panel view.
    // Either [ColorShape.CIRCLE] or [ColorShape.SQUARE].
    val panelShape = ColorShape.Circle

    val activity = this
    val rootView = FrameLayout(activity)

    var currentColor = colorInitial ?: Color.BLACK
    var dialogType = initialDialogType
    var presets = initialPresets

    var dialog: AlertDialog? = null
    var colorPaletteAdapter: ColorPaletteAdapter? = null
    var shadesLayout: ViewGroup? = null
    var tvPercent: TextView? = null

    fun dismiss() = dialog?.dismissSafe()

    fun complete() {
        if (cont.isActive) cont.resume(currentColor) {}
        dismiss()
    }

    fun addPickerView() {
        CpvDialogColorPickerBinding.inflate(layoutInflater, rootView, true).apply {
            // ColorPickerDialog.colorPicker = contentView.findViewById(R.id.cpv_color_picker_view)
            // ColorPickerDialog.newColorPanel = contentView.findViewById(R.id.cpv_color_panel_new)
            // ColorPickerDialog.hexEditText = contentView.findViewById(R.id.cpv_hex)
            // val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old)
            // val arrowRight = contentView.findViewById<ImageView>(R.id.cpv_arrow_right)
            activity.getTextColorPrimary()
                ?.let { cpvArrowRight.setColorFilter(it) }

            var fromEditText = false
            fun setHex(color: Int, fromColorPicker: Boolean = false) {
                if (fromColorPicker) {
                    if (fromEditText) {
                        fromEditText = false
                        return
                    }
                    if (cpvHex.hasFocus()) {
                        cpvHex.showSoftInput(false)
                        cpvHex.clearFocus()
                    }
                }
                val hexText = when {
                    alphaEnabled -> "%08X".format(color)
                    else -> "%06X".format(color and 0xFFFFFF)
                }
                cpvHex.setText(hexText)
            }

            setHex(currentColor)
            cpvColorPanelOld.color = currentColor
            cpvColorPickerView.apply {
                setAlphaSliderVisible(alphaEnabled)
                setColor(currentColor, true)
                onColorChangedListener = ColorPickerView.OnColorChangedListener { newColor ->
                    currentColor = newColor
                    cpvColorPanelNew.color = newColor
                    setHex(newColor, fromColorPicker = true)
                }
            }
            cpvColorPanelNew.apply {
                this.color = currentColor
                setOnClickListener {
                    if (currentColor == this.color) complete()
                }
            }
            cpvHex.apply {
                addTextChangedListener { editable ->
                    if (cpvHex.isFocused) {
                        try {
                            currentColor = editable.toString().parseColor()
                            if (currentColor != cpvColorPickerView.color) {
                                fromEditText = true
                                cpvColorPickerView.setColor(currentColor, true)
                            }
                        } catch (_: NumberFormatException) {
                        } catch (ex: Throwable) {
                            log.e(ex, "parseColorString failed.")
                        }
                    }
                }
                setOnFocusChangeListener { _, hasFocus ->
                    if (hasFocus) showSoftInput(true)
                }
                if (!alphaEnabled) {
                    filters = arrayOf<InputFilter>(LengthFilter(6))
                }
            }
            root.setOnTouchListener { v, _ ->
                cpvHex.run {
                    when {
                        hasFocus() && v !== this -> {
                            clearFocus()
                            showSoftInput(false)
                            clearFocus()
                            true
                        }

                        else -> false
                    }
                }
            }
        }
    }

    fun createColorShades() {
        val colorShades = getColorShades(currentColor)
        shadesLayout?.takeIf { it.childCount > 0 }?.run {
            children.forEachIndexed { i, child ->
                val layout = child as FrameLayout
                val cpv: ColorPanelView = layout.findViewById(R.id.cpv_color_panel_view)
                val iv = layout.findViewById<ImageView>(R.id.cpv_color_image_view)
                cpv.color = colorShades[i]
                cpv.tag = false
                iv.setImageDrawable(null)
            }
            return
        }
        val horizontalPadding =
            activity.resources.getDimensionPixelSize(R.dimen.cpv_item_horizontal_padding)
        for (colorShade in colorShades) {
            var layoutResId: Int
            layoutResId = when (panelShape) {
                ColorShape.Square -> R.layout.cpv_color_item_square
                ColorShape.Circle -> R.layout.cpv_color_item_circle
            }
            val view = View.inflate(activity, layoutResId, null)
            val colorPanelView: ColorPanelView = view.findViewById(R.id.cpv_color_panel_view)
            val params = colorPanelView
                .layoutParams as MarginLayoutParams
            params.rightMargin = horizontalPadding
            params.leftMargin = params.rightMargin
            colorPanelView.layoutParams = params
            colorPanelView.color = colorShade
            shadesLayout?.addView(view)
            colorPanelView.post {
                // The color is black when rotating the dialog. This is a dirty fix. WTF!?
                colorPanelView.color = colorShade
            }
            colorPanelView.setOnClickListener { v: View ->
                when {
                    (v.tag as? Boolean) == true -> complete()
                    else -> {
                        currentColor = colorPanelView.color
                        colorPaletteAdapter?.selectNone()
                        shadesLayout?.children?.forEach { child ->
                            val layout = child as FrameLayout
                            val cpv: ColorPanelView = layout.findViewById(R.id.cpv_color_panel_view)
                            val iv = layout.findViewById<ImageView>(R.id.cpv_color_image_view)
                            iv.setImageResource(if (cpv === v) R.drawable.cpv_preset_checked else 0)
                            when {
                                cpv === v && ColorUtils.calculateLuminance(cpv.color) >= 0.65 ||
                                        Color.alpha(cpv.color) <= ALPHA_THRESHOLD -> {
                                    iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)
                                }

                                else -> iv.colorFilter = null
                            }
                            cpv.tag = cpv === v
                        }
                    }
                }
            }
            colorPanelView.setOnLongClickListener {
                colorPanelView.showHint()
                true
            }
        }
    }

    fun handleTransparencyChanged(transparency: Int) {
        val percentage = (transparency.toDouble() * 100 / 255).toInt()
        val alpha = 255 - transparency

        tvPercent?.text = String.format(Locale.ENGLISH, "%d%%", percentage)

        // update color:
        val red = Color.red(currentColor)
        val green = Color.green(currentColor)
        val blue = Color.blue(currentColor)
        currentColor = Color.argb(alpha, red, green, blue)

        // update items in GridView:
        colorPaletteAdapter?.apply {
            for (i in colors.indices) {
                val c = colors[i]
                colors[i] = Color.argb(
                    alpha,
                    Color.red(c),
                    Color.green(c),
                    Color.blue(c)
                )
            }
            notifyDataSetChanged()
        }

        // update shades:
        shadesLayout?.children?.forEach { child ->
            val layout = child as FrameLayout
            val cpv: ColorPanelView = layout.findViewById(R.id.cpv_color_panel_view)
            val iv = layout.findViewById<ImageView>(R.id.cpv_color_image_view)
            if (layout.tag == null) {
                // save the original border color
                layout.tag = cpv.borderColor
            }
            val c = Color.argb(
                alpha,
                Color.red(cpv.color),
                Color.green(cpv.color),
                Color.blue(cpv.color)
            )
            if (alpha <= ALPHA_THRESHOLD) {
                cpv.borderColor = c or -0x1000000
            } else {
                cpv.borderColor = layout.tag as Int
            }
            if (cpv.tag != null && cpv.tag as Boolean) {
                // The alpha changed on the selected shaded color. Update the checkmark color filter.
                if (alpha <= ALPHA_THRESHOLD) {
                    iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)
                } else {
                    if (ColorUtils.calculateLuminance(c) >= 0.65) {
                        iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)
                    } else {
                        iv.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
                    }
                }
            }
            cpv.color = c
        }
    }

    fun addPresetView() {
        CpvDialogPresetsBinding.inflate(layoutInflater, rootView, true).apply {
            shadesLayout = this.shadesLayout
            tvPercent = this.transparencyText
            // ColorPickerDialog.shadesLayout = contentView.findViewById(R.id.shades_layout)
            // ColorPickerDialog.transparencySeekBar = contentView.findViewById(R.id.transparency_seekbar)
            // ColorPickerDialog.transparencyPercText = contentView.findViewById(R.id.transparency_text)
            // val gridView = contentView.findViewById<GridView>(R.id.gridView)
            presets = loadPresets(presets, currentColor)
            if (useColorShade) {
                createColorShades()
            } else {
                shadesLayout?.gone()
                shadesDivider.gone()
            }
            ColorPaletteAdapter(
                presets,
                presets.indexOf(currentColor),
                panelShape,
                listener = {
                    when (it) {
                        currentColor -> {
                            if (cont.isActive) cont.resume(currentColor) {}
                            dismiss()
                        }

                        else -> {
                            currentColor = it
                            if (useColorShade) createColorShades()
                        }
                    }
                },
            ).also {
                gridView.adapter = it
                colorPaletteAdapter = it
            }

            when {
                alphaEnabled -> {
                    val transparency = 255 - Color.alpha(currentColor)
                    val percentage = (transparency.toDouble() * 100 / 255).toInt()
                    transparencyText.text = String.format(Locale.ENGLISH, "%d%%", percentage)
                    transparencySeekbar.apply {
                        max = 255
                        progress = transparency
                        this.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
                            override fun onStartTrackingTouch(seekBar: SeekBar) {}
                            override fun onStopTrackingTouch(seekBar: SeekBar) {}
                            override fun onProgressChanged(
                                seekBar: SeekBar,
                                progress: Int,
                                fromUser: Boolean,
                            ) {
                                handleTransparencyChanged(transparency = progress)
                            }
                        })
                    }
                }

                else -> {
                    transparencyLayout.gone()
                    transparencyTitle.gone()
                }
            }
        }
    }

    fun addViewByType() {
        when (dialogType) {
            ColorPickerDialogType.Custom -> addPickerView()
            ColorPickerDialogType.Presets -> addPresetView()
        }
    }
    addViewByType()

    dialog = AlertDialog.Builder(activity).apply {
        setView(rootView)
        if (dialogTitle != 0) {
            setTitle(dialogTitle)
        }
        setPositiveButton(R.string.cpv_select) { _, _ ->
            if (cont.isActive) cont.resume(currentColor) {}
        }
        val neutralButtonStringRes = when {
            !dialogTypeSwitcher -> 0
            else -> when (dialogType) {
                ColorPickerDialogType.Custom -> R.string.cpv_presets
                ColorPickerDialogType.Presets -> R.string.cpv_custom
            }
        }
        if (neutralButtonStringRes != 0) {
            setNeutralButton(neutralButtonStringRes, null)
            // ビルダーでボタンを指定するとダイアログを閉じるボタンになってしまうが、
            // このボタンではダイアログを閉じないので、リスナは後で設定する。
        }
    }.create()

    dialog.setOnDismissListener {
        log.i("onDismissListener. isActive=${cont.isActive}")
        if (cont.isActive) cont.resumeWithException(CancellationException())
    }
    cont.invokeOnCancellation { dismiss() }

    // http://stackoverflow.com/a/16972670/1048340
    dialog.window?.clearFlags(
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
    )
    dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
    dialog.show()

    // プリセット切り替えボタンのリスナでダイアログを閉じないようにするため、後から上書きする
    // dialog.getButton の呼び出しは show()より後に行う必要がある
    val neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
    log.i("neutralButton=$neutralButton")
    neutralButton?.setOnClickListener {
        log.i("(neutralButton?.setOnClickListener. isActive=${cont.isActive}")
        rootView.removeAllViews()
        dialogType = when (dialogType) {
            ColorPickerDialogType.Custom -> {
                neutralButton.setText(R.string.cpv_custom)
                ColorPickerDialogType.Presets
            }

            ColorPickerDialogType.Presets -> {
                neutralButton.setText(R.string.cpv_presets)
                ColorPickerDialogType.Custom
            }
        }
        addViewByType()
        log.i(")neutralButton?.setOnClickListener. isActive=${cont.isActive}")
    }
}
