/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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.android.systemui.controls.ui

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.service.controls.Control
import android.service.controls.templates.ControlTemplate
import android.service.controls.templates.RangeTemplate
import android.service.controls.templates.TemperatureControlTemplate
import android.service.controls.templates.ToggleRangeTemplate
import android.util.Log
import android.util.MathUtils
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.controls.ui.ControlViewHolder.Companion.MAX_LEVEL
import com.android.systemui.controls.ui.ControlViewHolder.Companion.MIN_LEVEL
import java.util.IllegalFormatException

/**
 * Supports [ToggleRangeTemplate] and [RangeTemplate], as well as when one of those templates is
 * defined as the subtemplate in [TemperatureControlTemplate].
 */
class ToggleRangeBehavior : Behavior {
    private var rangeAnimator: ValueAnimator? = null
    lateinit var clipLayer: Drawable
    lateinit var templateId: String
    lateinit var control: Control
    lateinit var cvh: ControlViewHolder
    lateinit var rangeTemplate: RangeTemplate
    lateinit var context: Context
    var currentStatusText: CharSequence = ""
    var currentRangeValue: String = ""
    var isChecked: Boolean = false
    var isToggleable: Boolean = false
    var colorOffset: Int = 0

    companion object {
        private const val DEFAULT_FORMAT = "%.1f"
    }

    override fun initialize(cvh: ControlViewHolder) {
        this.cvh = cvh
        context = cvh.context

        val gestureListener = ToggleRangeGestureListener(cvh.layout)
        val gestureDetector = GestureDetector(context, gestureListener)
        cvh.layout.setOnTouchListener { v: View, e: MotionEvent ->
            if (gestureDetector.onTouchEvent(e)) {
                // Don't return true to let the state list change to "pressed"
                return@setOnTouchListener false
            }

            if (e.getAction() == MotionEvent.ACTION_UP && gestureListener.isDragging) {
                v.getParent().requestDisallowInterceptTouchEvent(false)
                gestureListener.isDragging = false
                endUpdateRange()
                return@setOnTouchListener false
            }

            return@setOnTouchListener false
        }
    }

    private fun setup(template: ToggleRangeTemplate) {
        rangeTemplate = template.getRange()
        isToggleable = true
        isChecked = template.isChecked()
    }

    private fun setup(template: RangeTemplate) {
        rangeTemplate = template

        // only show disabled state when value is at the minimum
        isChecked = rangeTemplate.currentValue != rangeTemplate.minValue
    }

    private fun setupTemplate(template: ControlTemplate): Boolean {
        return when (template) {
            is ToggleRangeTemplate -> {
                setup(template)
                true
            }
            is RangeTemplate -> {
                setup(template)
                true
            }
            is TemperatureControlTemplate -> setupTemplate(template.getTemplate())
            else -> {
                Log.e(ControlsUiController.TAG, "Unsupported template type: $template")
                false
            }
        }
    }

    override fun bind(cws: ControlWithState, colorOffset: Int) {
        this.control = cws.control!!
        this.colorOffset = colorOffset

        currentStatusText = control.getStatusText()

        // ControlViewHolder sets a long click listener, but we want to handle touch in
        // here instead, otherwise we'll have state conflicts.
        cvh.layout.setOnLongClickListener(null)

        val ld = cvh.layout.getBackground() as LayerDrawable
        clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)

        val template = control.getControlTemplate()
        if (!setupTemplate(template)) return
        templateId = template.getTemplateId()

        updateRange(rangeToLevelValue(rangeTemplate.currentValue), isChecked,
            /* isDragging */ false)

        cvh.applyRenderInfo(isChecked, colorOffset)

        /*
         * This is custom widget behavior, so add a new accessibility delegate to
         * handle clicks and range events. Present as a seek bar control.
         */
        cvh.layout.setAccessibilityDelegate(object : View.AccessibilityDelegate() {
            override fun onInitializeAccessibilityNodeInfo(
                host: View,
                info: AccessibilityNodeInfo
            ) {
                super.onInitializeAccessibilityNodeInfo(host, info)

                val min = levelToRangeValue(MIN_LEVEL)
                val current = levelToRangeValue(clipLayer.getLevel())
                val max = levelToRangeValue(MAX_LEVEL)

                val step = rangeTemplate.getStepValue().toDouble()
                val type = if (step == Math.floor(step)) {
                    AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT
                } else {
                    AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT
                }

                if (isChecked) {
                    val rangeInfo = AccessibilityNodeInfo.RangeInfo.obtain(type, min, max, current)
                    info.setRangeInfo(rangeInfo)
                }
                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS)
            }

            override fun performAccessibilityAction(
                host: View,
                action: Int,
                arguments: Bundle?
            ): Boolean {
                val handled = when (action) {
                    AccessibilityNodeInfo.ACTION_CLICK -> {
                        if (!isToggleable) {
                            false
                        } else {
                            cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked)
                            true
                        }
                    }
                    AccessibilityNodeInfo.ACTION_LONG_CLICK -> {
                        cvh.controlActionCoordinator.longPress(cvh)
                        true
                    }
                    AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.getId() -> {
                        if (arguments == null || !arguments.containsKey(
                                AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) {
                            false
                        } else {
                            val value = arguments.getFloat(
                                AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)
                            val level = rangeToLevelValue(value)
                            updateRange(level, isChecked, /* isDragging */ true)
                            endUpdateRange()
                            true
                        }
                    }
                    else -> false
                }

                return handled || super.performAccessibilityAction(host, action, arguments)
            }

            override fun onRequestSendAccessibilityEvent(
                host: ViewGroup,
                child: View,
                event: AccessibilityEvent
            ): Boolean = true
        })
    }

    fun beginUpdateRange() {
        cvh.userInteractionInProgress = true
        cvh.setStatusTextSize(context.getResources()
                .getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
    }

    fun updateRange(level: Int, checked: Boolean, isDragging: Boolean) {
        val newLevel = Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, level))

        // If the current level is at the minimum and the user is dragging, set the control to
        // the enabled state to indicate their intention to enable the device. This will update
        // control colors to support dragging.
        if (clipLayer.level == MIN_LEVEL && newLevel > MIN_LEVEL) {
            cvh.applyRenderInfo(checked, colorOffset, false /* animated */)
        }

        rangeAnimator?.cancel()
        if (isDragging) {
            val isEdge = newLevel == MIN_LEVEL || newLevel == MAX_LEVEL
            if (clipLayer.level != newLevel) {
                cvh.controlActionCoordinator.drag(isEdge)
                clipLayer.level = newLevel
            }
        } else if (newLevel != clipLayer.level) {
            rangeAnimator = ValueAnimator.ofInt(cvh.clipLayer.level, newLevel).apply {
                addUpdateListener {
                    cvh.clipLayer.level = it.animatedValue as Int
                }
                addListener(object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator?) {
                        rangeAnimator = null
                    }
                })
                duration = ControlViewHolder.STATE_ANIMATION_DURATION
                interpolator = Interpolators.CONTROL_STATE
                start()
            }
        }

        if (checked) {
            val newValue = levelToRangeValue(newLevel)
            currentRangeValue = format(rangeTemplate.getFormatString().toString(),
                    DEFAULT_FORMAT, newValue)
            if (isDragging) {
                cvh.setStatusText(currentRangeValue, /* immediately */ true)
            } else {
                cvh.setStatusText("$currentStatusText $currentRangeValue")
            }
        } else {
            cvh.setStatusText(currentStatusText)
        }
    }

    private fun format(primaryFormat: String, backupFormat: String, value: Float): String {
        return try {
            String.format(primaryFormat, findNearestStep(value))
        } catch (e: IllegalFormatException) {
            Log.w(ControlsUiController.TAG, "Illegal format in range template", e)
            if (backupFormat == "") {
                ""
            } else {
                format(backupFormat, "", value)
            }
        }
    }

    private fun levelToRangeValue(i: Int): Float {
        return MathUtils.constrainedMap(rangeTemplate.minValue, rangeTemplate.maxValue,
                MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(), i.toFloat())
    }

    private fun rangeToLevelValue(i: Float): Int {
        return MathUtils.constrainedMap(MIN_LEVEL.toFloat(), MAX_LEVEL.toFloat(),
                rangeTemplate.minValue, rangeTemplate.maxValue, i).toInt()
    }

    fun endUpdateRange() {
        cvh.setStatusTextSize(context.getResources()
                .getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
        cvh.setStatusText("$currentStatusText $currentRangeValue", /* immediately */ true)
        cvh.controlActionCoordinator.setValue(cvh, rangeTemplate.getTemplateId(),
            findNearestStep(levelToRangeValue(clipLayer.getLevel())))
        cvh.userInteractionInProgress = false
    }

    fun findNearestStep(value: Float): Float {
        var minDiff = Float.MAX_VALUE

        var f = rangeTemplate.getMinValue()
        while (f <= rangeTemplate.getMaxValue()) {
            val currentDiff = Math.abs(value - f)
            if (currentDiff < minDiff) {
                minDiff = currentDiff
            } else {
                return f - rangeTemplate.getStepValue()
            }

            f += rangeTemplate.getStepValue()
        }

        return rangeTemplate.getMaxValue()
    }

    inner class ToggleRangeGestureListener(
        val v: View
    ) : SimpleOnGestureListener() {
        var isDragging: Boolean = false

        override fun onDown(e: MotionEvent): Boolean {
            return true
        }

        override fun onLongPress(e: MotionEvent) {
            if (isDragging) {
                return
            }
            cvh.controlActionCoordinator.longPress(cvh)
        }

        override fun onScroll(
            e1: MotionEvent,
            e2: MotionEvent,
            xDiff: Float,
            yDiff: Float
        ): Boolean {
            if (!isDragging) {
                v.getParent().requestDisallowInterceptTouchEvent(true)
                beginUpdateRange()
                isDragging = true
            }

            val ratioDiff = -xDiff / v.width
            val changeAmount = ((MAX_LEVEL - MIN_LEVEL) * ratioDiff).toInt()
            updateRange(clipLayer.level + changeAmount, checked = true, isDragging = true)
            return true
        }

        override fun onSingleTapUp(e: MotionEvent): Boolean {
            if (!isToggleable) return false
            cvh.controlActionCoordinator.toggle(cvh, templateId, isChecked)
            return true
        }
    }
}
