import {useAnimatedReaction, useSharedValue} from "react-native-reanimated"

import {useBottomSheetToolboxInternalContext} from "../context"
import {BottomSheetPositionBuiltInLockPriority, snapPoint} from "../utils"
import {useBottomSheetPositionLock} from "./use-bottom-sheet-position-lock"

const LOCK_ID = "useBottomSheetSnapWhenIdle"

/**
 * This hook snaps the bottom sheet to the nearest snap point when
 * nothing else is interacting with it. It takes into account the
 * last known velocity.
 *
 * A snap is triggered after either of the following two conditions:
 * - The position off any snap-point, and has been released.
 * - The underlying snap-points have changed.
 */
export function useBottomSheetSnapWhenIdle() {
    const {
        currentPositionSharedValue,
        currentPositionLastVelocitySharedValue,
        currentPositionDestinationSharedValue,
        normalizedSnapPointsSharedValue,
        isReadySharedValue,
        currentPositionLockSharedValue,
    } = useBottomSheetToolboxInternalContext()

    const {canLockSharedValue, hasLockSharedValue, lock, release, setPositionAnimate, setVelocity} =
        useBottomSheetPositionLock(LOCK_ID, BottomSheetPositionBuiltInLockPriority.SNAP_WHEN_IDLE)

    const lastSnapPointIndexSharedValue = useSharedValue<number>(-1)
    useAnimatedReaction(
        () => ({
            canLock: isReadySharedValue.get() && canLockSharedValue.get(),
            hasLock: hasLockSharedValue.get(),
            snapPoints: normalizedSnapPointsSharedValue.get(),
        }),
        ({canLock, hasLock, snapPoints}, lastResults) => {
            const canLockOrHasLock = canLock || hasLock
            if (!snapPoints || !canLockOrHasLock) {
                lastSnapPointIndexSharedValue.set(-1)

                return
            }

            const lastCanLockOrHasLock = lastResults?.canLock || lastResults?.hasLock
            const didLockStatusChange = lastCanLockOrHasLock !== canLockOrHasLock

            const lastSnapPoints = lastResults?.snapPoints || []
            const didSnapPointsChange =
                snapPoints === null
                    ? lastSnapPoints === null
                    : lastSnapPoints.length !== snapPoints.length ||
                      lastSnapPoints.some((v, index) => v !== snapPoints[index])

            // Nothing to do
            if (!didLockStatusChange && !didSnapPointsChange) return

            /**
             * If our snap-points change, then try to follow the snap-point we
             * were last at. This also supports the case where the snap-points
             * are updated to be equal, but then separate again. That case can
             * happen when the keyboard opens.
             */
            lastSnapPointIndexSharedValue.set(
                lastSnapPoints.indexOf(currentPositionDestinationSharedValue.get())
            )

            const currentPosition = currentPositionSharedValue.get()
            const point =
                lastSnapPointIndexSharedValue.get() === -1
                    ? snapPoint(
                          currentPosition,
                          currentPositionLastVelocitySharedValue.get(),
                          snapPoints
                      )
                    : (snapPoints[lastSnapPointIndexSharedValue.get()] ?? null)

            if (point === null || point === currentPosition) return

            if (!hasLock) {
                lock()
            }

            setPositionAnimate(point, undefined, (finished) => {
                const currentPositionLock = currentPositionLockSharedValue.get()
                if (finished || currentPositionLock?.id !== LOCK_ID) {
                    release()
                }
            })
            setVelocity(0)
        },
        [
            canLockSharedValue,
            hasLockSharedValue,
            normalizedSnapPointsSharedValue,
            isReadySharedValue.get,
        ]
    )
}
