/* 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/. */

/*
 * Copyright (C) 2015 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 org.mozilla.fenix.quickactionsheet

import android.animation.ValueAnimator
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams
import androidx.core.math.MathUtils
import androidx.core.view.ViewCompat
import androidx.customview.widget.ViewDragHelper
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import mozilla.components.browser.toolbar.BrowserToolbar
import org.mozilla.fenix.R
import java.lang.ref.WeakReference
import kotlin.math.abs
import kotlin.math.max

/**
 * An interaction behavior plugin for a child view of [CoordinatorLayout] to make it work as a
 * bottom sheet. This custom behavior is for non-modal bottom sheets that should not block accessibility
 * access to the rest of the screen controls.
 */
@Suppress("TooManyFunctions", "ComplexMethod", "LargeClass")
open class QuickActionSheetBehavior<V : View>(context: Context, attrs: AttributeSet) :
    CoordinatorLayout.Behavior<V>(context, attrs) {

    /**
     * Save flags to be preserved in bottomsheet on configuration change.
     *
     * @param flags bitwise int of [.SAVE_PEEK_HEIGHT], [.SAVE_FIT_TO_CONTENTS],
     * [.SAVE_HIDEABLE], [.SAVE_SKIP_COLLAPSED], [.SAVE_ALL] and
     * [.SAVE_NONE].
     * @see .getSaveFlags
     * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_saveFlags
     */
    var saveFlags = SAVE_NONE

    private var fitToContents = true

    private val maximumVelocity: Float

    /** Peek height set by the user.  */
    private var peekHeight: Int = 0

    /** Whether or not to use automatic peek height.  */
    private var peekHeightAuto: Boolean = false

    /** Minimum peek height permitted.  */
    @get:VisibleForTesting
    internal var peekHeightMin: Int = 0
        private set

    /** True if Behavior has a non-null value for the @shapeAppearance attribute  */
//    private val shapeThemingEnabled: Boolean

    private var materialShapeDrawable: MaterialShapeDrawable? = null

    /** Default Shape Appearance to be used in bottomsheet  */
    private var shapeAppearanceModelDefault: ShapeAppearanceModel? = null

    private var interpolatorAnimator: ValueAnimator? = null

    internal var expandedOffset: Int = 0

    internal var fitToContentsOffset: Int = 0

    internal var halfExpandedOffset: Int = 0

    internal var halfExpandedRatio = HALF_EXPANDED_RATIO_DEFAULT

    internal var collapsedOffset: Int = 0

    internal var elevation = -1f

    internal var hideable: Boolean = false

    /**
     * Sets whether this bottom sheet should skip the collapsed state when it is being hidden after it
     * is expanded once. Setting this to true has no effect unless the sheet is hideable.
     *
     * @param skipCollapsed True if the bottom sheet should skip the collapsed state.
     * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_skipCollapsed
     */
    var skipCollapsed: Boolean = false

    @State
    var state
        get() = internalState
        set(value) {
            @State val previousState = this.internalState
            if (value == this.internalState) {
                return
            }
            if (viewRef == null) {
                // The view is not laid out yet; modify mState and let onLayoutChild handle it later
                @Suppress("ComplexCondition")
                if ((value == STATE_COLLAPSED || value == STATE_EXPANDED ||
                            value == STATE_HALF_EXPANDED || (hideable && value == STATE_HIDDEN))
                ) {
                    this.internalState = value
                }
                return
            }
            startSettlingAnimationPendingLayout(value)
            updateDrawableOnStateChange(value, previousState)
        }

    @State
    internal var internalState = STATE_COLLAPSED

    internal var viewDragHelper: ViewDragHelper? = null

    private var ignoreEvents: Boolean = false

    private var lastNestedScrollDy: Int = 0

    private var nestedScrolled: Boolean = false

    internal var parentWidth: Int = 0
    internal var parentHeight: Int = 0

    internal var viewRef: WeakReference<V>? = null

    internal var nestedScrollingChildRef: WeakReference<View>? = null

    private var callback: QuickActionSheetCallback? = null

    private var velocityTracker: VelocityTracker? = null

    internal var activePointerId: Int = 0

    private var initialY: Int = 0

    internal var touchingScrollingChild: Boolean = false

    /**
     * Sets whether the height of the expanded sheet is determined by the height of its contents, or
     * if it is expanded in two stages (half the height of the parent container, full height of parent
     * container). Default value is true.
     *
     * @param fitToContents whether or not to fit the expanded sheet to its contents.
     */
    // If sheet is already laid out, recalculate the collapsed offset based on new setting.
    // Otherwise, let onLayoutChild handle this later.
    // Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents.
    var isFitToContents: Boolean
        get() = fitToContents
        set(fitToContents) {
            if (this.fitToContents == fitToContents) {
                return
            }
            this.fitToContents = fitToContents
            if (viewRef != null) {
                calculateCollapsedOffset()
            }
            setStateInternal(
                if (this.fitToContents && internalState == STATE_HALF_EXPANDED)
                    STATE_EXPANDED else internalState
            )
        }

    /**
     * Sets whether this bottom sheet can hide when it is swiped down.
     *
     * @param hideable `true` to make this bottom sheet hideable.
     * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_hideable
     */
    // Lift up to collapsed state
    var isHideable: Boolean
        get() = hideable
        set(hideable) {
            if (this.hideable != hideable) {
                this.hideable = hideable
                if (!hideable && internalState == STATE_HIDDEN) {
                    state = STATE_COLLAPSED
                }
            }
        }

    private val yVelocity: Float
        get() {
            if (velocityTracker == null) {
                return 0f
            }
            velocityTracker!!.computeCurrentVelocity(PIXELS_PER_SECOND_IN_MS, maximumVelocity)
            return velocityTracker!!.getYVelocity(activePointerId)
        }

    private val dragCallback = object : ViewDragHelper.Callback() {

        @Suppress("ReturnCount")
        override fun tryCaptureView(child: View, pointerId: Int): Boolean {
            if (internalState == STATE_DRAGGING) {
                return false
            }
            if (touchingScrollingChild) {
                return false
            }
            if (internalState == STATE_EXPANDED && activePointerId == pointerId) {
                val scroll = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null
                if (scroll != null && scroll.canScrollVertically(-1)) {
                    // Let the content scroll up
                    return false
                }
            }
            return viewRef != null && viewRef!!.get() === child
        }

        override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
            dispatchOnSlide(top)
        }

        override fun onViewDragStateChanged(state: Int) {
            if (state == ViewDragHelper.STATE_DRAGGING) {
                setStateInternal(STATE_DRAGGING)
            }
        }

        @Suppress("ComplexCondition", "LongMethod")
        override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
            val top: Int
            @State val targetState: Int
            if (yvel < 0) { // Moving up
                if (fitToContents) {
                    top = fitToContentsOffset
                    targetState = STATE_EXPANDED
                } else {
                    val currentTop = releasedChild.top
                    if (currentTop > halfExpandedOffset) {
                        top = halfExpandedOffset
                        targetState = STATE_HALF_EXPANDED
                    } else {
                        top = expandedOffset
                        targetState = STATE_EXPANDED
                    }
                }
            } else if ((hideable && shouldHide(
                    releasedChild,
                    yvel
                ) && (releasedChild.top > collapsedOffset || abs(xvel) < abs(yvel)))
            ) {
                // Hide if we shouldn't collapse and the view was either released low or it was a
                // vertical swipe.
                top = parentHeight
                targetState = STATE_HIDDEN
            } else if (yvel == 0f || abs(xvel) > abs(yvel)) {
                // If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity
                // being greater than the Y velocity, settle to the nearest correct height.
                val currentTop = releasedChild.top
                if (fitToContents) {
                    if ((abs(currentTop - fitToContentsOffset) < abs(currentTop - collapsedOffset))) {
                        top = fitToContentsOffset
                        targetState = STATE_EXPANDED
                    } else {
                        top = collapsedOffset
                        targetState = STATE_COLLAPSED
                    }
                } else {
                    if (currentTop < halfExpandedOffset) {
                        if (currentTop < abs(currentTop - collapsedOffset)) {
                            top = expandedOffset
                            targetState = STATE_EXPANDED
                        } else {
                            top = halfExpandedOffset
                            targetState = STATE_HALF_EXPANDED
                        }
                    } else {
                        if ((abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset))) {
                            top = halfExpandedOffset
                            targetState = STATE_HALF_EXPANDED
                        } else {
                            top = collapsedOffset
                            targetState = STATE_COLLAPSED
                        }
                    }
                }
            } else { // Moving Down
                if (fitToContents) {
                    top = collapsedOffset
                    targetState = STATE_COLLAPSED
                } else {
                    // Settle to the nearest correct height.
                    val currentTop = releasedChild.top
                    if ((abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset))) {
                        top = halfExpandedOffset
                        targetState = STATE_HALF_EXPANDED
                    } else {
                        top = collapsedOffset
                        targetState = STATE_COLLAPSED
                    }
                }
            }
            if (viewDragHelper!!.settleCapturedViewAt(releasedChild.left, top)) {
                setStateInternal(STATE_SETTLING)
                if (targetState == STATE_EXPANDED && interpolatorAnimator != null) {
                    interpolatorAnimator!!.reverse()
                }
                ViewCompat.postOnAnimation(
                    releasedChild, SettleRunnable(releasedChild, targetState)
                )
            } else {
                if (targetState == STATE_EXPANDED && interpolatorAnimator != null) {
                    interpolatorAnimator!!.reverse()
                }
                setStateInternal(targetState)
            }
        }

        override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
            return MathUtils.clamp(
                top, getExpandedOffset(), if (hideable) parentHeight else collapsedOffset
            )
        }

        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
            return child.left
        }

        override fun getViewVerticalDragRange(child: View): Int {
            return if (hideable) {
                parentHeight
            } else {
                collapsedOffset
            }
        }
    }

    /** Callback for monitoring events about bottom sheets.  */
    interface QuickActionSheetCallback {

        /**
         * Called when the bottom sheet changes its state.
         *
         * @param bottomSheet The bottom sheet view.
         * @param newState The new state. This will be one of [.STATE_DRAGGING], [     ][.STATE_SETTLING],
         * [.STATE_EXPANDED], [.STATE_COLLAPSED], [     ][.STATE_HIDDEN], or [.STATE_HALF_EXPANDED].
         */
        fun onStateChanged(bottomSheet: View, @State newState: Int)

        /**
         * Called when the bottom sheet is being dragged.
         *
         * @param bottomSheet The bottom sheet view.
         * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset increases
         * as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and
         * expanded states and from -1 to 0 it is between hidden and collapsed states.
         */
        fun onSlide(bottomSheet: View, slideOffset: Float)
    }

    /** @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @IntDef(STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN, STATE_HALF_EXPANDED)
    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
    annotation class State

    init {
        val a = context.obtainStyledAttributes(attrs, R.styleable.QuickActionSheetBehavior_Layout)
//        this.shapeThemingEnabled = a.hasValue(R.styleable.QuickActionSheetBehavior_Layout_shapeAppearance)
//        val hasBackgroundTint = a.hasValue(R.styleable.QuickActionSheetBehavior_Layout_backgroundTint)
//        if (hasBackgroundTint) {
//            val bottomSheetColor = MaterialResources.getColorStateList(
//                context, a, R.styleable.QuickActionSheetBehavior_Layout_backgroundTint
//            )
//            createMaterialShapeDrawable(context, attrs, hasBackgroundTint, bottomSheetColor)
//        } else {
//        createMaterialShapeDrawable(context, attrs, hasBackgroundTint)
//        }
        createShapeValueAnimator()
        this.elevation = a.getDimension(R.styleable.QuickActionSheetBehavior_Layout_android_elevation, -1f)
        val value = a.peekValue(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_peekHeight)
        if (value != null && value.data == PEEK_HEIGHT_AUTO) {
            setPeekHeight(value.data)
        } else {
            setPeekHeight(
                a.getDimensionPixelSize(
                    R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_peekHeight, PEEK_HEIGHT_AUTO
                )
            )
        }
        isHideable = a.getBoolean(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_hideable, false)
        isFitToContents = a.getBoolean(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_fitToContents, true)
        skipCollapsed = a.getBoolean(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_skipCollapsed, false)
        saveFlags = a.getInt(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_saveFlags, SAVE_NONE)
        setHalfExpandedRatio(
            a.getFloat(
                R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_halfExpandedRatio,
                HALF_EXPANDED_RATIO_DEFAULT
            )
        )
        setExpandedOffset(a.getInt(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_expandedOffset, 0))
        a.recycle()
        val configuration = ViewConfiguration.get(context)
        maximumVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
    }

    override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable? {
        return super.onSaveInstanceState(parent, child)?.let { SavedState(it, this) }
    }

    override fun onRestoreInstanceState(parent: CoordinatorLayout, child: V, state: Parcelable) {
        val ss = state as SavedState
        super.onRestoreInstanceState(parent, child, ss.superState!!)
        // Restore Optional State values designated by saveFlags
        restoreOptionalState(ss)
        // Intermediate states are restored as collapsed state
        if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) {
            this.internalState = STATE_COLLAPSED
        } else {
            this.internalState = ss.state
        }
    }

    override fun onAttachedToLayoutParams(layoutParams: LayoutParams) {
        super.onAttachedToLayoutParams(layoutParams)
        // These may already be null, but just be safe, explicitly assign them. This lets us know the
        // first time we layout with this behavior by checking (viewRef == null).
        viewRef = null
        viewDragHelper = null
    }

    override fun onDetachedFromLayoutParams() {
        super.onDetachedFromLayoutParams()
        // Release references so we don't run unnecessary codepaths while not attached to a view.
        viewRef = null
        viewDragHelper = null
    }

    override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
        if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) {
            child.fitsSystemWindows = true
        }
        // Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
        // default to android:background declared in styles or layout.
//        if (shapeThemingEnabled && materialShapeDrawable != null) {
//            ViewCompat.setBackground(child, materialShapeDrawable)
//        }
        // Set elevation on MaterialShapeDrawable
        if (materialShapeDrawable != null) {
            // Use elevation attr if set on bottomsheet; otherwise, use elevation of child view.
            materialShapeDrawable!!.elevation = if (elevation == -1f) ViewCompat.getElevation(child) else elevation
        }

        if (viewRef == null) {
            // First layout with this behavior.
            peekHeightMin = parent.resources.getDimensionPixelSize(R.dimen.design_quick_action_sheet_peek_height_min)
            viewRef = WeakReference(child)
        }
        if (viewDragHelper == null) {
            viewDragHelper = ViewDragHelper.create(parent, dragCallback)
        }

        val savedTop = child.top
        // First let the parent lay it out
        parent.onLayoutChild(child, layoutDirection)
        // Offset the bottom sheet
        parentWidth = parent.width
        parentHeight = parent.height
        fitToContentsOffset = max(0, parentHeight - child.height)
        calculateHalfExpandedOffset()
        calculateCollapsedOffset()

        if (internalState == STATE_EXPANDED) {
            ViewCompat.offsetTopAndBottom(child, getExpandedOffset())
        } else if (internalState == STATE_HALF_EXPANDED) {
            ViewCompat.offsetTopAndBottom(child, halfExpandedOffset)
        } else if (hideable && internalState == STATE_HIDDEN) {
            ViewCompat.offsetTopAndBottom(child, parentHeight)
        } else if (internalState == STATE_COLLAPSED) {
            ViewCompat.offsetTopAndBottom(child, collapsedOffset)
        } else if (internalState == STATE_DRAGGING || internalState == STATE_SETTLING) {
            ViewCompat.offsetTopAndBottom(child, savedTop - child.top)
        }

        nestedScrollingChildRef = WeakReference(findScrollingChild(child)!!)
        return true
    }

    @Suppress("ReturnCount")
    override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean {
        if (!child.isShown) {
            ignoreEvents = true
            return false
        }
        val action = event.actionMasked
        // Record the velocity
        if (action == MotionEvent.ACTION_DOWN) {
            reset()
        }
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain()
        }
        velocityTracker!!.addMovement(event)
        when (action) {
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                touchingScrollingChild = false
                activePointerId = MotionEvent.INVALID_POINTER_ID
                // Reset the ignore flag
                if (ignoreEvents) {
                    ignoreEvents = false
                    return false
                }
            }
            MotionEvent.ACTION_DOWN -> {
                val initialX = event.x.toInt()
                initialY = event.y.toInt()
                // Only intercept nested scrolling events here if the view not being moved by the
                // ViewDragHelper.
                if (internalState != STATE_SETTLING) {
                    val scroll = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null
                    if (scroll != null && parent.isPointInChildBounds(scroll, initialX, initialY)) {
                        activePointerId = event.getPointerId(event.actionIndex)
                        touchingScrollingChild = true
                    }
                }
                ignoreEvents = (activePointerId == MotionEvent.INVALID_POINTER_ID && !parent.isPointInChildBounds(
                    child,
                    initialX,
                    initialY
                ))
            }
        } // fall out
        if ((!ignoreEvents && viewDragHelper != null && viewDragHelper!!.shouldInterceptTouchEvent(event))
        ) {
            return true
        }
        // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
        // it is not the top most view of its parent. This is not necessary when the touch event is
        // happening over the scrolling content as nested scrolling logic handles that case.
        val scroll = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null
        return (action == MotionEvent.ACTION_MOVE && scroll != null &&
                !ignoreEvents && internalState != STATE_DRAGGING && !parent.isPointInChildBounds(
            scroll,
            event.x.toInt(),
            event.y.toInt()
        ) && viewDragHelper != null && abs(initialY - event.y) > viewDragHelper!!.touchSlop)
    }

    @Suppress("CollapsibleIfStatements")
    override fun onTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean {
        val action = event.actionMasked
        when {
            !child.isShown -> return false
            internalState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN -> return true
        }
        viewDragHelper?.processTouchEvent(event)

        // Record the velocity
        if (action == MotionEvent.ACTION_DOWN) {
            reset()
        }
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain()
        }
        velocityTracker!!.addMovement(event)
        // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
        // to capture the bottom sheet in case it is not captured and the touch slop is passed.
        if (action == MotionEvent.ACTION_MOVE && !ignoreEvents) {
            if (abs(initialY - event.y) > viewDragHelper!!.touchSlop) {
                viewDragHelper!!.captureChildView(child, event.getPointerId(event.actionIndex))
            }
        }
        return !ignoreEvents
    }

    override fun onStartNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        directTargetChild: View,
        target: View,
        axes: Int,
        type: Int
    ): Boolean {
        lastNestedScrollDy = 0
        nestedScrolled = false
        return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0
    }

    override fun onNestedPreScroll(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        target: View,
        dx: Int,
        dy: Int,
        consumed: IntArray,
        type: Int
    ) {
        if (type == ViewCompat.TYPE_NON_TOUCH) {
            // Ignore fling here. The ViewDragHelper handles it.
            return
        }
        val scrollingChild = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null
        if (target !== scrollingChild) {
            return
        }
        val currentTop = child.top
        val newTop = currentTop - dy
        if (dy > 0) { // Upward
            if (newTop < getExpandedOffset()) {
                consumed[1] = currentTop - getExpandedOffset()
                ViewCompat.offsetTopAndBottom(child, -consumed[1])
                setStateInternal(STATE_EXPANDED)
            } else {
                consumed[1] = dy
                ViewCompat.offsetTopAndBottom(child, -dy)
                setStateInternal(STATE_DRAGGING)
            }
        } else if (dy < 0) { // Downward
            if (!target.canScrollVertically(-1)) {
                if (newTop <= collapsedOffset || hideable) {
                    consumed[1] = dy
                    ViewCompat.offsetTopAndBottom(child, -dy)
                    setStateInternal(STATE_DRAGGING)
                } else {
                    consumed[1] = currentTop - collapsedOffset
                    ViewCompat.offsetTopAndBottom(child, -consumed[1])
                    setStateInternal(STATE_COLLAPSED)
                }
            }
        }
        dispatchOnSlide(child.top)
        lastNestedScrollDy = dy
        nestedScrolled = true
    }

    override fun onStopNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        target: View,
        type: Int
    ) {
        if (child.top == getExpandedOffset()) {
            setStateInternal(STATE_EXPANDED)
            return
        }
        if ((nestedScrollingChildRef == null || target !== nestedScrollingChildRef!!.get() || !nestedScrolled)
        ) {
            return
        }
        val top: Int
        val targetState: Int
        if (lastNestedScrollDy > 0) {
            top = getExpandedOffset()
            targetState = STATE_EXPANDED
        } else if (hideable && shouldHide(child, yVelocity)) {
            top = parentHeight
            targetState = STATE_HIDDEN
        } else if (lastNestedScrollDy == 0) {
            val currentTop = child.top
            if (fitToContents) {
                if (abs(currentTop - fitToContentsOffset) < abs(currentTop - collapsedOffset)) {
                    top = fitToContentsOffset
                    targetState = STATE_EXPANDED
                } else {
                    top = collapsedOffset
                    targetState = STATE_COLLAPSED
                }
            } else {
                if (currentTop < halfExpandedOffset) {
                    if (currentTop < abs(currentTop - collapsedOffset)) {
                        top = expandedOffset
                        targetState = STATE_EXPANDED
                    } else {
                        top = halfExpandedOffset
                        targetState = STATE_HALF_EXPANDED
                    }
                } else {
                    if (abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset)) {
                        top = halfExpandedOffset
                        targetState = STATE_HALF_EXPANDED
                    } else {
                        top = collapsedOffset
                        targetState = STATE_COLLAPSED
                    }
                }
            }
        } else {
            if (fitToContents) {
                top = collapsedOffset
                targetState = STATE_COLLAPSED
            } else {
                // Settle to nearest height.
                val currentTop = child.top
                if (abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset)) {
                    top = halfExpandedOffset
                    targetState = STATE_HALF_EXPANDED
                } else {
                    top = collapsedOffset
                    targetState = STATE_COLLAPSED
                }
            }
        }
        if (viewDragHelper!!.smoothSlideViewTo(child, child.left, top)) {
            setStateInternal(STATE_SETTLING)
            ViewCompat.postOnAnimation(child, SettleRunnable(child, targetState))
        } else {
            setStateInternal(targetState)
        }
        nestedScrolled = false
    }

    override fun onNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray
    ) {
        // Overridden to prevent the default consumption of the entire scroll distance.
    }

    override fun onNestedPreFling(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        target: View,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        return if (nestedScrollingChildRef != null) {
            (target === nestedScrollingChildRef!!.get() && ((internalState != STATE_EXPANDED || super.onNestedPreFling(
                coordinatorLayout,
                child,
                target,
                velocityX,
                velocityY
            ))))
        } else {
            false
        }
    }

    /**
     * Sets the height of the bottom sheet when it is collapsed while optionally animating between the
     * old height and the new height.
     *
     * @param peekHeight The height of the collapsed bottom sheet in pixels,
     * or [     ][.PEEK_HEIGHT_AUTO] to configure the sheet to peek automatically at 16:9 ratio keyline.
     * @param animate Whether to animate between the old height and the new height.
     * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_peekHeight
     */
    @Suppress("NestedBlockDepth")
    @JvmOverloads
    fun setPeekHeight(peekHeight: Int, animate: Boolean = false) {
        var layout = false
        if (peekHeight == PEEK_HEIGHT_AUTO) {
            if (!peekHeightAuto) {
                peekHeightAuto = true
                layout = true
            }
        } else if (peekHeightAuto || this.peekHeight != peekHeight) {
            peekHeightAuto = false
            this.peekHeight = max(0, peekHeight)
            layout = true
        }
        // If sheet is already laid out, recalculate the collapsed offset based on new setting.
        // Otherwise, let onLayoutChild handle this later.
        if (layout && viewRef != null) {
            calculateCollapsedOffset()
            if (internalState == STATE_COLLAPSED) {
                val view = viewRef!!.get()
                if (view != null) {
                    if (animate) {
                        startSettlingAnimationPendingLayout(internalState)
                    } else {
                        view.requestLayout()
                    }
                }
            }
        }
    }

    /**
     * Gets the height of the bottom sheet when it is collapsed.
     *
     * @return The height of the collapsed bottom sheet in pixels, or [.PEEK_HEIGHT_AUTO] if the
     * sheet is configured to peek automatically at 16:9 ratio keyline
     * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_peekHeight
     */
    @Suppress("unused")
    fun getPeekHeight(): Int {
        return if (peekHeightAuto) PEEK_HEIGHT_AUTO else peekHeight
    }

    /**
     * Determines the height of the QuickActionSheet in the [.STATE_HALF_EXPANDED] state. The
     * material guidelines recommended a value of 0.5, which results in the sheet filling half of the
     * parent. The height of the QuickActionSheet will be smaller as this ratio is decreased and taller as
     * it is increased. The default value is 0.5.
     *
     * @param ratio a float between 0 and 1, representing the [.STATE_HALF_EXPANDED] ratio.
     * @attr com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_halfExpandedRatio
     */
    fun setHalfExpandedRatio(ratio: Float) {

        if ((ratio <= 0) || (ratio >= 1)) {
            throw IllegalArgumentException("ratio must be a float value between 0 and 1")
        }
        this.halfExpandedRatio = ratio
    }

    /**
     * Determines the top offset of the QuickActionSheet in the [.STATE_EXPANDED] state when
     * fitsToContent is false. The default value is 0, which results in the sheet matching the
     * parent's top.
     *
     * @param offset an integer value greater than equal to 0, representing the [     ][.STATE_EXPANDED] offset.
     * Value must not exceed the offset in the half expanded state.
     * @attr com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_expandedOffset
     */
    fun setExpandedOffset(offset: Int) {
        if (offset < 0) {
            throw IllegalArgumentException("offset must be greater than or equal to 0")
        }
        this.expandedOffset = offset
    }

    /**
     * Gets the ratio for the height of the QuickActionSheet in the [.STATE_HALF_EXPANDED] state.
     *
     * @attr com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_halfExpandedRatio
     */
    @Suppress("unused")
    fun getHalfExpandedRatio(): Float {
        return halfExpandedRatio
    }

    /**
     * Sets a callback to be notified of bottom sheet events.
     *
     * @param callback The callback to notify when bottom sheet events occur.
     */
    fun setQuickActionSheetCallback(callback: QuickActionSheetCallback) {
        this.callback = callback
    }

    private fun startSettlingAnimationPendingLayout(@State state: Int) {
        val child = viewRef?.get() ?: return
        // Start the animation; wait until a pending layout if there is one.
        val parent = child.parent
        if (parent != null && parent.isLayoutRequested && ViewCompat.isAttachedToWindow(child)) {
            val finalState = state
            child.post { startSettlingAnimation(child, finalState) }
        } else {
            startSettlingAnimation(child, state)
        }
    }

    internal fun setStateInternal(@State state: Int) {
        val previousState = this.internalState

        if (this.internalState == state) {
            return
        }
        this.internalState = state

        if (viewRef == null) {
            return
        }

        val bottomSheet = viewRef!!.get() ?: return

        ViewCompat.setImportantForAccessibility(
            bottomSheet, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
        )
        bottomSheet.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)

        updateDrawableOnStateChange(state, previousState)
        if (callback != null) {
            callback!!.onStateChanged(bottomSheet, state)
        }
    }

    private fun updateDrawableOnStateChange(@State state: Int, @State previousState: Int) {
        if (materialShapeDrawable != null) {
            val isOpening =
                state == STATE_EXPANDED && (previousState == STATE_HIDDEN || previousState == STATE_COLLAPSED)
            // If the QuickActionSheetBehavior's state is set directly to STATE_EXPANDED from
            // STATE_HIDDEN or STATE_COLLAPSED, bypassing  STATE_DRAGGING, the corner transition animation
            // will not be triggered automatically, so we will trigger it here.
            if ((isOpening && interpolatorAnimator != null && interpolatorAnimator!!.animatedFraction == 1f)
            ) {
                interpolatorAnimator!!.reverse()
            }
            if ((state == STATE_DRAGGING && previousState == STATE_EXPANDED && interpolatorAnimator != null)
            ) {
                interpolatorAnimator!!.start()
            }
        }
    }

    private fun calculateCollapsedOffset() {
        val peek: Int = if (peekHeightAuto) {
            max(peekHeightMin, parentHeight - parentWidth * AUTO_ASPECT_RATIO_SHORT / AUTO_ASPECT_RATIO_LONG)
        } else {
            peekHeight
        }

        collapsedOffset = if (fitToContents) {
            max(parentHeight - peek, fitToContentsOffset)
        } else {
            parentHeight - peek
        }
    }

    private fun calculateHalfExpandedOffset() {
        this.halfExpandedOffset = (parentHeight * (1 - halfExpandedRatio)).toInt()
    }

    private fun reset() {
        activePointerId = ViewDragHelper.INVALID_POINTER
        if (velocityTracker != null) {
            velocityTracker!!.recycle()
            velocityTracker = null
        }
    }

    private fun restoreOptionalState(ss: SavedState) {
        if (this.saveFlags == SAVE_NONE) {
            return
        }
        if (this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_PEEK_HEIGHT) == SAVE_PEEK_HEIGHT) {
            this.peekHeight = ss.peekHeight
        }
        if ((this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_FIT_TO_CONTENTS) == SAVE_FIT_TO_CONTENTS)) {
            this.fitToContents = ss.fitToContents
        }
        if (this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_HIDEABLE) == SAVE_HIDEABLE) {
            this.hideable = ss.hideable
        }
        if ((this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_SKIP_COLLAPSED) == SAVE_SKIP_COLLAPSED)) {
            this.skipCollapsed = ss.skipCollapsed
        }
    }

    internal fun shouldHide(child: View, yvel: Float): Boolean {
        if (skipCollapsed) {
            return true
        }
        if (child.top < collapsedOffset) {
            // It should not hide, but collapse.
            return false
        }
        val newTop = child.top + yvel * HIDE_FRICTION
        return abs(newTop - collapsedOffset) / peekHeight.toFloat() > HIDE_THRESHOLD
    }

    @VisibleForTesting
    internal fun findScrollingChild(view: View): View? {
        if (ViewCompat.isNestedScrollingEnabled(view)) {
            return view
        }
        if (view is ViewGroup) {
            var i = 0
            val count = view.childCount
            while (i < count) {
                val scrollingChild = findScrollingChild(view.getChildAt(i))
                if (scrollingChild != null) {
                    return scrollingChild
                }
                i++
            }
        }
        return null
    }

//    private fun createMaterialShapeDrawable(
//        context: Context, attrs: AttributeSet, hasBackgroundTint: Boolean
//    ) {
//        this.createMaterialShapeDrawable(context, attrs, hasBackgroundTint, null)
//    }

//    private fun createMaterialShapeDrawable(
//        context: Context,
//        attrs: AttributeSet,
//        hasBackgroundTint: Boolean,
//        bottomSheetColor: ColorStateList?
//    ) {
//        if (this.shapeThemingEnabled) {
//            this.shapeAppearanceModelDefault =
//                ShapeAppearanceModel(context, attrs, R.attr.bottomSheetStyle, DEF_STYLE_RES)
//
//            this.materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModelDefault)
//            this.materialShapeDrawable!!.initializeElevationOverlay(context)
//
//            if (hasBackgroundTint && bottomSheetColor != null) {
//                materialShapeDrawable!!.fillColor = bottomSheetColor
//            } else {
//                // If the tint isn't set, use the theme default background color.
//                val defaultColor = TypedValue()
//                context.theme.resolveAttribute(android.R.attr.colorBackground, defaultColor, true)
//                materialShapeDrawable!!.setTint(defaultColor.data)
//            }
//        }
//    }

    private fun createShapeValueAnimator() {
        interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f)
        interpolatorAnimator!!.duration = CORNER_ANIMATION_DURATION.toLong()
        interpolatorAnimator!!.addUpdateListener { animation ->
            val value = animation.animatedValue as Float
            if (materialShapeDrawable != null) {
                materialShapeDrawable!!.interpolation = value
            }
        }
    }

    private fun getExpandedOffset(): Int {
        return if (fitToContents) fitToContentsOffset else expandedOffset
    }

    internal fun startSettlingAnimation(child: View?, state: Int) {
        var localState = state
        var top: Int
        if (localState == STATE_COLLAPSED) {
            top = collapsedOffset
        } else if (localState == STATE_HALF_EXPANDED) {
            top = halfExpandedOffset
            if (fitToContents && top <= fitToContentsOffset) {
                // Skip to the expanded state if we would scroll past the height of the contents.
                localState = STATE_EXPANDED
                top = fitToContentsOffset
            }
        } else if (localState == STATE_EXPANDED) {
            top = getExpandedOffset()
        } else if (hideable && localState == STATE_HIDDEN) {
            top = parentHeight
        } else {
            throw IllegalArgumentException("Illegal state argument: $state")
        }
        if (viewDragHelper!!.smoothSlideViewTo(child!!, child.left, top)) {
            setStateInternal(STATE_SETTLING)
            ViewCompat.postOnAnimation(child, SettleRunnable(child, localState))
        } else {
            setStateInternal(localState)
        }
    }

    internal fun dispatchOnSlide(top: Int) {
        val bottomSheet = viewRef!!.get()
        if (bottomSheet != null && callback != null) {
            if (top > collapsedOffset) {
                callback!!.onSlide(
                    bottomSheet, (collapsedOffset - top).toFloat() / (parentHeight - collapsedOffset)
                )
            } else {
                callback!!.onSlide(
                    bottomSheet, (collapsedOffset - top).toFloat() / (collapsedOffset - getExpandedOffset())
                )
            }
        }
    }

    /**
     * Disables the shaped corner [ShapeAppearanceModel] interpolation transition animations.
     * Will have no effect unless the sheet utilizes a [MaterialShapeDrawable] with set shape
     * theming properties. Only For use in UI testing.
     */
    @Suppress("unused")
    @VisibleForTesting
    fun disableShapeAnimations() {
        // Sets the shape value animator to null, prevents animations from occuring during testing.
        interpolatorAnimator = null
    }

    private inner class SettleRunnable internal constructor(
        private val view: View,
        @param:State @field:State private val targetState: Int
    ) :
        Runnable {

        override fun run() {
            if (viewDragHelper != null && viewDragHelper!!.continueSettling(true)) {
                ViewCompat.postOnAnimation(view, this)
            } else {
                if (internalState == STATE_SETTLING) {
                    setStateInternal(targetState)
                }
            }
        }
    }

    /** State persisted across instances  */
    protected class SavedState : QuickActionSavedState {
        @State
        internal var state: Int = STATE_COLLAPSED
        internal var peekHeight: Int = 0
        internal var fitToContents: Boolean = false
        internal var hideable: Boolean = false
        internal var skipCollapsed: Boolean = false

        @JvmOverloads
        constructor(source: Parcel, loader: ClassLoader? = null) : super(source, loader) {

            state = source.readInt()
            peekHeight = source.readInt()
            fitToContents = source.readInt() == 1
            hideable = source.readInt() == 1
            skipCollapsed = source.readInt() == 1
        }

        constructor(superState: Parcelable, behavior: QuickActionSheetBehavior<*>) : super(superState) {
            this.state = behavior.internalState
            this.peekHeight = behavior.peekHeight
            this.fitToContents = behavior.fitToContents
            this.hideable = behavior.hideable
            this.skipCollapsed = behavior.skipCollapsed
        }

        override fun writeToParcel(dest: Parcel, flags: Int) {
            super.writeToParcel(dest, flags)
            dest.writeInt(state)
            dest.writeInt(peekHeight)
            dest.writeInt(if (fitToContents) 1 else 0)
            dest.writeInt(if (hideable) 1 else 0)
            dest.writeInt(if (skipCollapsed) 1 else 0)
        }

        override fun describeContents(): Int {
            return 0
        }

        companion object CREATOR : Parcelable.Creator<SavedState> {
            override fun createFromParcel(parcel: Parcel): SavedState {
                return SavedState(parcel)
            }

            override fun newArray(size: Int): Array<SavedState?> {
                return newArray(size)
            }
        }
    }

    override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
        if (dependency is BrowserToolbar) {
            return true
        }

        return super.layoutDependsOn(parent, child, dependency)
    }

    override fun onDependentViewChanged(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
        return if (dependency is BrowserToolbar) {
            repositionQuickActionSheet(child, dependency)
            true
        } else {
            false
        }
    }

    private fun repositionQuickActionSheet(quickActionSheetContainer: V, toolbar: BrowserToolbar) {
        if (toolbar.translationY >= toolbar.height.toFloat() - POSITION_SNAP_BUFFER) {
            internalState = STATE_HIDDEN
        } else if (internalState == STATE_HIDDEN || internalState == STATE_SETTLING) {
            internalState = STATE_COLLAPSED
        }
        quickActionSheetContainer.translationY = toolbar.translationY + toolbar.height * -1.0f
    }

    companion object {

        /** The bottom sheet is dragging.  */
        const val STATE_DRAGGING = 1

        /** The bottom sheet is settling.  */
        const val STATE_SETTLING = 2

        /** The bottom sheet is expanded.  */
        const val STATE_EXPANDED = 3

        /** The bottom sheet is collapsed.  */
        const val STATE_COLLAPSED = 4

        /** The bottom sheet is hidden.  */
        const val STATE_HIDDEN = 5

        /** The bottom sheet is half-expanded (used when mFitToContents is false).  */
        const val STATE_HALF_EXPANDED = 6

        /**
         * Peek at the 16:9 ratio keyline of its parent.
         *
         *
         * This can be used as a parameter for [.setPeekHeight]. [.getPeekHeight]
         * will return this when the value is set.
         */
        const val PEEK_HEIGHT_AUTO = -1

        /**
         * This flag will preserve the peekHeight int value on configuration change.
         */
        const val SAVE_PEEK_HEIGHT = 0x1

        /**
         * This flag will preserve the fitToContents boolean value on configuration change.
         */
        const val SAVE_FIT_TO_CONTENTS = 0x2

        /**
         * This flag will preserve the hideable boolean value on configuration change.
         */
        const val SAVE_HIDEABLE = 0x4

        /**
         * This flag will preserve the skipCollapsed boolean value on configuration change.
         */
        const val SAVE_SKIP_COLLAPSED = 0x8

        /**
         * This flag will preserve all aforementioned values on configuration change.
         */
        const val SAVE_ALL = -1

        /**
         * This flag will not preserve the aforementioned values set at runtime if the view is
         * destroyed and recreated. The only value preserved will be the positional state,
         * e.g. collapsed, hidden, expanded, etc. This is the default behavior.
         */
        const val SAVE_NONE = 0

        private const val HIDE_THRESHOLD = 0.5f

        private const val HIDE_FRICTION = 0.1f

        private const val CORNER_ANIMATION_DURATION = 500

        private const val PIXELS_PER_SECOND_IN_MS = 1000

        private const val HALF_EXPANDED_RATIO_DEFAULT = 0.5f

        private const val AUTO_ASPECT_RATIO_SHORT = 9

        private const val AUTO_ASPECT_RATIO_LONG = 16

        private const val DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal

        /**
         * A utility function to get the [QuickActionSheetBehavior] associated with the `view`.
         *
         * @param view The [View] with [QuickActionSheetBehavior].
         * @return The [QuickActionSheetBehavior] associated with the `view`.
         */
        fun <V : View> from(view: V): QuickActionSheetBehavior<V> {
            val params = view.layoutParams as? LayoutParams
                ?: throw IllegalArgumentException("The view is not a child of CoordinatorLayout")
            val behavior = params.behavior as? QuickActionSheetBehavior<*>
                ?: throw IllegalArgumentException("The view is not associated with QuickActionSheetBehavior")
            @Suppress("UNCHECKED_CAST")
            return behavior as QuickActionSheetBehavior<V>
        }
    }
}

/**
 * A [Parcelable] implementation that should be used by inheritance
 * hierarchies to ensure the state of all classes along the chain is saved.
 */
abstract class QuickActionSavedState : Parcelable {

    var superState: Parcelable? = null

    /**
     * Constructor called by derived classes when creating their SavedState objects
     *
     * @param superState The state of the superclass of this view
     */
    protected constructor(superState: Parcelable? = null) {
        this.superState = if (superState !== EMPTY_STATE) superState else null
    }

    /**
     * Constructor used when reading from a parcel. Reads the state of the superclass.
     *
     * @param source parcel to read from
     * @param loader ClassLoader to use for reading
     */
    @JvmOverloads
    protected constructor(source: Parcel, loader: ClassLoader? = null) {
        val superState = source.readParcelable<Parcelable>(loader)
        this.superState = superState ?: EMPTY_STATE
    }

    override fun describeContents(): Int {
        return 0
    }

    override fun writeToParcel(dest: Parcel, flags: Int) {
        dest.writeParcelable(superState, flags)
    }

    companion object {
        val EMPTY_STATE: QuickActionSavedState = object : QuickActionSavedState() {}

        @Suppress("unused")
        val CREATOR: Parcelable.Creator<QuickActionSavedState> =
            object : Parcelable.ClassLoaderCreator<QuickActionSavedState> {
                override fun createFromParcel(`in`: Parcel, loader: ClassLoader?): QuickActionSavedState {
                    val superState = `in`.readParcelable<Parcelable>(loader)
                    if (superState != null) {
                        throw IllegalStateException("superState must be null")
                    }
                    return EMPTY_STATE
                }

                override fun createFromParcel(`in`: Parcel): QuickActionSavedState {
                    return createFromParcel(`in`, null)
                }

                override fun newArray(size: Int): Array<QuickActionSavedState> {
                    return newArray(size)
                }
            }
    }
}
