/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.browser

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.Activity
import android.graphics.PointF
import android.graphics.Rect
import android.util.TypedValue
import android.view.View
import android.view.ViewConfiguration
import androidx.annotation.Dimension
import androidx.annotation.Dimension.DP
import androidx.core.graphics.contains
import androidx.core.graphics.toPoint
import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FlingAnimation
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.android.util.dpToPx
import mozilla.components.support.ktx.android.view.getRectWithViewLocation
import org.mozilla.fenix.ext.getWindowInsets
import org.mozilla.fenix.ext.isKeyboardVisible
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

/**
 * Handles intercepting touch events on the toolbar for swipe gestures and executes the
 * necessary animations.
 */
class ToolbarGestureHandler(
    private val activity: Activity,
    private val contentLayout: View,
    private val tabPreview: TabPreview,
    private val toolbarLayout: View,
    private val sessionManager: SessionManager
) : SwipeGestureListener {

    private enum class GestureDirection {
        LEFT_TO_RIGHT, RIGHT_TO_LEFT
    }

    private sealed class Destination {
        data class Tab(val session: Session) : Destination()
        object None : Destination()
    }

    private val windowWidth: Int
        get() = activity.resources.displayMetrics.widthPixels

    private val previewOffset = PREVIEW_OFFSET.dpToPx(activity.resources.displayMetrics)

    private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop
    private val minimumFlingVelocity = ViewConfiguration.get(activity).scaledMinimumFlingVelocity
    private val defaultVelocity = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        MINIMUM_ANIMATION_VELOCITY,
        activity.resources.displayMetrics
    )

    private var gestureDirection = GestureDirection.LEFT_TO_RIGHT

    override fun onSwipeStarted(start: PointF, next: PointF): Boolean {
        val dx = next.x - start.x
        val dy = next.y - start.y
        gestureDirection = if (dx < 0) {
            GestureDirection.RIGHT_TO_LEFT
        } else {
            GestureDirection.LEFT_TO_RIGHT
        }

        return if (
            !activity.window.decorView.isKeyboardVisible() &&
            start.isInToolbar() &&
            abs(dx) > touchSlop &&
            abs(dy) < abs(dx)
        ) {
            preparePreview(getDestination())
            true
        } else {
            false
        }
    }

    override fun onSwipeUpdate(distanceX: Float, distanceY: Float) {
        when (getDestination()) {
            is Destination.Tab -> {
                // Restrict the range of motion for the views so you can't start a swipe in one direction
                // then move your finger far enough in the other direction and make the content visually
                // start sliding off screen the other way.
                tabPreview.translationX = when (gestureDirection) {
                    GestureDirection.RIGHT_TO_LEFT -> min(
                        windowWidth.toFloat() + previewOffset,
                        tabPreview.translationX - distanceX
                    )
                    GestureDirection.LEFT_TO_RIGHT -> max(
                        -windowWidth.toFloat() - previewOffset,
                        tabPreview.translationX - distanceX
                    )
                }
                contentLayout.translationX = when (gestureDirection) {
                    GestureDirection.RIGHT_TO_LEFT -> min(
                        0f,
                        contentLayout.translationX - distanceX
                    )
                    GestureDirection.LEFT_TO_RIGHT -> max(
                        0f,
                        contentLayout.translationX - distanceX
                    )
                }
            }
            is Destination.None -> {
                // If there is no "next" tab to swipe to in the gesture direction, only do a
                // partial animation to show that we are at the end of the tab list
                val maxContentHidden = contentLayout.width * OVERSCROLL_HIDE_PERCENT
                contentLayout.translationX = when (gestureDirection) {
                    GestureDirection.RIGHT_TO_LEFT -> max(
                        -maxContentHidden.toFloat(),
                        contentLayout.translationX - distanceX
                    ).coerceAtMost(0f)
                    GestureDirection.LEFT_TO_RIGHT -> min(
                        maxContentHidden.toFloat(),
                        contentLayout.translationX - distanceX
                    ).coerceAtLeast(0f)
                }
            }
        }
    }

    override fun onSwipeFinished(
        velocityX: Float,
        velocityY: Float
    ) {
        val destination = getDestination()
        if (destination is Destination.Tab && isGestureComplete(velocityX)) {
            animateToNextTab(velocityX, destination.session)
        } else {
            animateCanceledGesture(velocityX)
        }
    }

    private fun createFlingAnimation(
        view: View,
        minValue: Float,
        maxValue: Float,
        startVelocity: Float
    ): FlingAnimation =
        FlingAnimation(view, DynamicAnimation.TRANSLATION_X).apply {
            setMinValue(minValue)
            setMaxValue(maxValue)
            setStartVelocity(startVelocity)
            friction = ViewConfiguration.getScrollFriction()
        }

    private fun getDestination(): Destination {
        val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
        val currentSession = sessionManager.selectedSession ?: return Destination.None
        val currentIndex = sessionManager.sessionsOfType(currentSession.private).indexOfFirst {
            it.id == currentSession.id
        }

        return if (currentIndex == -1) {
            Destination.None
        } else {
            val sessions = sessionManager.sessionsOfType(currentSession.private)
            val index = when (gestureDirection) {
                GestureDirection.RIGHT_TO_LEFT -> if (isLtr) {
                    currentIndex - 1
                } else {
                    currentIndex + 1
                }
                GestureDirection.LEFT_TO_RIGHT -> if (isLtr) {
                    currentIndex + 1
                } else {
                    currentIndex - 1
                }
            }

            if (index < sessions.count() && index >= 0) {
                Destination.Tab(sessions.elementAt(index))
            } else {
                Destination.None
            }
        }
    }

    private fun preparePreview(destination: Destination) {
        val thumbnailId = when (destination) {
            is Destination.Tab -> destination.session.id
            is Destination.None -> return
        }

        tabPreview.loadPreviewThumbnail(thumbnailId)
        tabPreview.alpha = 1f
        tabPreview.translationX = when (gestureDirection) {
            GestureDirection.RIGHT_TO_LEFT -> windowWidth.toFloat() + previewOffset
            GestureDirection.LEFT_TO_RIGHT -> -windowWidth.toFloat() - previewOffset
        }
        tabPreview.isVisible = true
    }

    /**
     * Checks if the gesture is complete based on the position of tab preview and the velocity of
     * the gesture. A completed gesture means the user has indicated they want to swipe to the next
     * tab. The gesture is considered complete if one of the following is true:
     *
     * 1. The user initiated a fling in the same direction as the initial movement
     * 2. There is no fling initiated, but the percentage of the tab preview shown is at least
     * [GESTURE_FINISH_PERCENT]
     *
     * If the user initiated a fling in the opposite direction of the initial movement, the
     * gesture is always considered incomplete.
     */
    private fun isGestureComplete(velocityX: Float): Boolean {
        val previewWidth = tabPreview.getRectWithViewLocation().visibleWidth.toDouble()
        val velocityMatchesDirection = when (gestureDirection) {
            GestureDirection.RIGHT_TO_LEFT -> velocityX <= 0
            GestureDirection.LEFT_TO_RIGHT -> velocityX >= 0
        }
        val reverseFling =
            abs(velocityX) >= minimumFlingVelocity && !velocityMatchesDirection

        return !reverseFling && (previewWidth / windowWidth >= GESTURE_FINISH_PERCENT ||
            abs(velocityX) >= minimumFlingVelocity)
    }

    private fun getVelocityFromFling(velocityX: Float): Float {
        return max(abs(velocityX), defaultVelocity)
    }

    private fun animateToNextTab(velocityX: Float, session: Session) {
        val browserFinalXCoordinate: Float = when (gestureDirection) {
            GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset
            GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset
        }
        val animationVelocity = when (gestureDirection) {
            GestureDirection.RIGHT_TO_LEFT -> -getVelocityFromFling(velocityX)
            GestureDirection.LEFT_TO_RIGHT -> getVelocityFromFling(velocityX)
        }

        // Finish animating the contentLayout off screen and tabPreview on screen
        createFlingAnimation(
            view = contentLayout,
            minValue = min(0f, browserFinalXCoordinate),
            maxValue = max(0f, browserFinalXCoordinate),
            startVelocity = animationVelocity
        ).addUpdateListener { _, value, _ ->
            tabPreview.translationX = when (gestureDirection) {
                GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
                GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
            }
        }.addEndListener { _, _, _, _ ->
            contentLayout.translationX = 0f
            sessionManager.select(session)

            // Fade out the tab preview to prevent flickering
            val shortAnimationDuration =
                activity.resources.getInteger(android.R.integer.config_shortAnimTime)
            tabPreview.animate()
                .alpha(0f)
                .setDuration(shortAnimationDuration.toLong())
                .setListener(object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator?) {
                        tabPreview.isVisible = false
                    }
                })
        }.start()
    }

    private fun animateCanceledGesture(gestureVelocity: Float) {
        val velocity = if (getDestination() is Destination.None) {
            defaultVelocity
        } else {
            getVelocityFromFling(gestureVelocity)
        }.let { v ->
            when (gestureDirection) {
                GestureDirection.RIGHT_TO_LEFT -> v
                GestureDirection.LEFT_TO_RIGHT -> -v
            }
        }

        createFlingAnimation(
            view = contentLayout,
            minValue = min(0f, contentLayout.translationX),
            maxValue = max(0f, contentLayout.translationX),
            startVelocity = velocity
        ).addUpdateListener { _, value, _ ->
            tabPreview.translationX = when (gestureDirection) {
                GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset
                GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset
            }
        }.addEndListener { _, _, _, _ ->
            tabPreview.isVisible = false
        }.start()
    }

    private fun PointF.isInToolbar(): Boolean {
        val toolbarLocation = toolbarLayout.getRectWithViewLocation()
        // In Android 10, the system gesture touch area overlaps the bottom of the toolbar, so
        // lets make our swipe area taller by that amount
        activity.window.decorView.getWindowInsets()?.let { insets ->
            if (activity.settings().shouldUseBottomToolbar) {
                toolbarLocation.top -= (insets.mandatorySystemGestureInsets.bottom - insets.stableInsetBottom)
            }
        }
        return toolbarLocation.contains(toPoint())
    }

    private val Rect.visibleWidth: Int
        get() = if (left < 0) {
            right
        } else {
            windowWidth - left
        }

    companion object {
        /**
         * The percentage of the tab preview that needs to be visible to consider the
         * tab switching gesture complete.
         */
        private const val GESTURE_FINISH_PERCENT = 0.25

        /**
         * The percentage of the content view that can be hidden by the tab switching gesture if
         * there is not tab available to switch to
         */
        private const val OVERSCROLL_HIDE_PERCENT = 0.20

        /**
         * The speed of the fling animation (in dp per second).
         */
        @Dimension(unit = DP)
        private const val MINIMUM_ANIMATION_VELOCITY = 1500f

        /**
         * The size of the gap between the tab preview and content layout.
         */
        @Dimension(unit = DP)
        private const val PREVIEW_OFFSET = 48
    }
}
