import * as React from "react"
import {Dimensions} from "react-native"
import {Gesture} from "react-native-gesture-handler"
import {
    runOnJS,
    type SharedValue,
    useAnimatedReaction,
    useSharedValue,
} from "react-native-reanimated"

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

const LOCK_ID_NAMESPACE = "useBottomSheetDragGesture"

// Gesture velocity is calculated between gesture updates.
// If the user does not move their finger, the gesture doesn't see update
// and so the velocity is miss-leading (i.e. not 0)
// We detect this by checking the time between the last gesture update.
const SUPPRESS_VELOCITY_TIME_MS = 150

export interface UseBottomSheetDragGestureArgs {
    id: string
    disabled?: boolean
    clamped?: boolean
    frozenSharedValue?: SharedValue<boolean>
}

export function useBottomSheetDragGesture({
    id,
    disabled = false,
    clamped = false,
    frozenSharedValue,
}: UseBottomSheetDragGestureArgs) {
    const {
        currentPositionSharedValue,
        currentPositionLastVelocitySharedValue,
        wrapperHeightSharedValue,
        normalizedSnapPointsSharedValue,
    } = useBottomSheetToolboxInternalContext()

    const {lock, release, setPosition, setVelocity, canLockSharedValue, hasLockSharedValue} =
        useBottomSheetPositionLock(
            `${LOCK_ID_NAMESPACE}:${id}`,
            BottomSheetPositionBuiltInLockPriority.GESTURE
        )

    const lastLockRef = React.useRef<string>(id)
    React.useEffect(() => {
        if (id !== lastLockRef.current) throw new Error("Lock id can't be changed.")
    }, [id])

    const startPositionSharedValue = useSharedValue(0)
    const lastUpdateTime = useSharedValue(0)

    const [enabled, setEnabled] = React.useState(false)
    useAnimatedReaction(
        () => canLockSharedValue.get() || hasLockSharedValue.get(),
        (lockable, lastLockable) => {
            if (lockable === lastLockable) return
            runOnJS(setEnabled)(lockable)
        },
        [canLockSharedValue, hasLockSharedValue.get]
    )

    const [windowHeight, setWindowHeight] = React.useState(Dimensions.get("window").height)
    React.useEffect(() => {
        const handle = Dimensions.addEventListener("change", ({window}) => {
            setWindowHeight(window.height)
        })

        return () => handle.remove()
    }, [])

    const frozenOriginOffsetYSharedValue = useSharedValue(0)

    return Gesture.Pan()
        .onBegin(() => {
            if (!canLockSharedValue.get()) return
            startPositionSharedValue.set(currentPositionSharedValue.get())
            frozenOriginOffsetYSharedValue.set(0)
        })
        .onUpdate((evt) => {
            const normalizedSnapPoints = normalizedSnapPointsSharedValue.get()
            const wrapperHeight = wrapperHeightSharedValue.get()

            const lockable = canLockSharedValue.get() || hasLockSharedValue.get()
            if (!lockable || !normalizedSnapPoints || !wrapperHeight) return

            // We want to lock onUpdate, rather than onBegin, so nested
            // touches do no unnecessarily lock the position.
            if (!hasLockSharedValue.get()) {
                lock()
            }

            if (frozenSharedValue?.get()) {
                frozenOriginOffsetYSharedValue.set(evt.translationY)
                return
            }

            setVelocity(evt.velocityY)

            const rawPosition =
                startPositionSharedValue.get() -
                evt.translationY +
                frozenOriginOffsetYSharedValue.get()

            const maxSnapPoint = Math.max(...normalizedSnapPoints)

            const overshoot = Math.max(0, rawPosition - maxSnapPoint)
            if (clamped) {
                const clampedPosition = Math.max(Math.min(rawPosition, maxSnapPoint), 0)
                setPosition(clampedPosition)
            } else if (overshoot > 0) {
                const topInset = Math.max(1, windowHeight - wrapperHeight)
                const frictionRatio = friction(overshoot / topInset)

                const frictionPosition = maxSnapPoint + topInset * frictionRatio
                const finalPosition = Math.min(frictionPosition, wrapperHeight + topInset)

                setPosition(finalPosition)
            } else {
                setPosition(rawPosition)
            }
            lastUpdateTime.set(Date.now())
        })
        .onFinalize((evt) => {
            if (!hasLockSharedValue.get()) return

            if (frozenSharedValue?.get()) {
                setVelocity(0)
            } else {
                const currentTime = Date.now()
                const velocity = -evt.velocityY
                const timeSinceLastUpdate = currentTime - lastUpdateTime.get()

                currentPositionLastVelocitySharedValue.set(
                    timeSinceLastUpdate > SUPPRESS_VELOCITY_TIME_MS ? 0 : velocity
                )

                setVelocity(velocity)
            }

            release()
        })
        .enabled(enabled && !disabled)
}
