import React from "react"
import {PixelRatio} from "react-native"
import {
    cancelAnimation,
    runOnUI,
    type SpringAnimation,
    useDerivedValue,
    type WithSpringConfig,
    withSpring,
} from "react-native-reanimated"

import {useBottomSheetToolboxInternalContext} from "../context"
import {DEFAULT_SHEET_SNAP_POINT_SPRING_CONFIG} from "../utils/default-sheet-snap-point-spring-config"
import {useWorkletUiCallback} from "./use-worklet-ui-callback"

const PIXEL_RATIO = PixelRatio.get()
const MIN_PERCEIVED_JUMP_DISTANCE = 10

/**
 * A priority lock for controlling the position of the bottom-sheet.
 * This allows our bottom-sheet's position to be modified by multiple sources.
 */
export function useBottomSheetPositionLock(id: string, priority: number) {
    const {
        isReadySharedValue,
        currentPositionSharedValue,
        currentPositionDestinationSharedValue,
        currentPositionLastVelocitySharedValue,
        currentPositionLockSharedValue,
    } = useBottomSheetToolboxInternalContext()

    const canLockSharedValue = useDerivedValue(() => {
        const isReady = isReadySharedValue.get()
        const currentLock = currentPositionLockSharedValue.get()

        return isReady && (currentLock === null || currentLock.priority < priority)
    }, [isReadySharedValue, currentPositionLockSharedValue, priority])

    const hasLockSharedValue = useDerivedValue(
        () => id === currentPositionLockSharedValue.get()?.id,
        [currentPositionLockSharedValue, id]
    )

    const lock = useWorkletUiCallback(() => {
        "worklet"

        const currentLock = currentPositionLockSharedValue.get()

        if (currentLock !== null) {
            if (priority < currentLock.priority) {
                // Lock checks should happen separately to lock requests.
                // So this should never happen. However, there are super
                // rare race conditions where this can happen in SharedValue
                // world.
                console.warn(`Unable to lock '${id}' as '${currentLock.id}' is already engaged.`)

                return
            }

            if (id !== currentLock.id) {
                cancelAnimation(currentPositionSharedValue)
            }
        }

        const newLock = {id, priority}
        currentPositionLockSharedValue.set(newLock)
    }, [currentPositionLockSharedValue, currentPositionSharedValue, id, priority])

    const release = useWorkletUiCallback(() => {
        "worklet"

        const currentLock = currentPositionLockSharedValue.get()
        if (currentLock?.id !== id) return

        currentPositionLockSharedValue.set(null)
    }, [currentPositionLockSharedValue, id])

    // Release lock when we unmount

    const unmountFn = () => {
        runOnUI(release)()
    }
    const onUnmountRef = React.useRef(unmountFn)
    onUnmountRef.current = unmountFn
    React.useLayoutEffect(() => () => onUnmountRef.current(), [])

    const setPositionInternal = useWorkletUiCallback(
        (v: number) => {
            "worklet"

            const currentLock = currentPositionLockSharedValue.get()
            if (currentLock !== null && currentLock.id !== id) {
                console.warn(
                    `Cannot setPosition with '${id}' as '${currentLock.id}' is already engaged.`
                )

                return
            }

            currentPositionSharedValue.set(v)

            if (typeof v === "number") {
                currentPositionDestinationSharedValue.set(v)
            }
        },
        [
            currentPositionLockSharedValue,
            id,
            currentPositionSharedValue,
            currentPositionDestinationSharedValue.set,
        ]
    )

    const setPositionAnimate = useWorkletUiCallback(
        (
            v: number,
            animationConfig?: WithSpringConfig,
            callback?: (finished?: boolean) => void
        ) => {
            "worklet"

            const animConfigWithDefault = animationConfig || DEFAULT_SHEET_SNAP_POINT_SPRING_CONFIG

            const springAnim = withSpring(
                v,
                animConfigWithDefault,
                callback
            ) as unknown as SpringAnimation

            const oldOnStart = springAnim.onStart

            /**
             * This is a fun bit of code.
             *
             * Sometimes the destination value we're animating to is itself being
             * animated. Reanimated does a good job of handling in-flight changes to
             * animation configs.
             *
             * However, we want changes to the destination value to supersede the
             * sheet's own physical behavior (i.e. its spring config).
             *
             * Another way to think of this is we want the sheet to appear as if its
             * easing consistently despite changes to its destination position.
             *
             * A practical example: if the sheet is opening simultaneously with the
             * keyboard, we want the sheet's travel to compensate for the keyboard's
             * height so it remains visible at all times.
             *
             * To achieve this, we use a rough heuristic to determine if the
             * destination delta can be stepped without the user noticing. If so,
             * we perform the step.
             */
            springAnim.onStart = (nextAnimation, current, timestamp, previousAnimation) => {
                oldOnStart(nextAnimation, current, timestamp, previousAnimation)

                const tickDelta =
                    // biome-ignore lint/complexity/useLiteralKeys: Property 'lastTickTime' comes from an index signature, so it must be accessed with ['lastTickTime'].
                    Date.now() - (previousAnimation ? previousAnimation["lastTickTime"] : 0)

                const goodFrameRate = 1000 / 60
                const deviatedGoodFrameRate = goodFrameRate * 1.25
                const destinationAnimating = tickDelta <= deviatedGoodFrameRate

                if (destinationAnimating) {
                    const destinationDelta =
                        (nextAnimation.toValue as number) -
                        currentPositionDestinationSharedValue.get()

                    const physicalDistance = Math.abs(destinationDelta * PIXEL_RATIO)
                    const minPerceivedJumpDistance = MIN_PERCEIVED_JUMP_DISTANCE * PIXEL_RATIO
                    if (physicalDistance < minPerceivedJumpDistance) {
                        ;(nextAnimation.current as number) += destinationDelta
                    }
                }

                currentPositionDestinationSharedValue.set(nextAnimation.toValue as number)
                // biome-ignore lint/complexity/useLiteralKeys: Property 'lastTickTime' comes from an index signature, so it must be accessed with ['lastTickTime'].
                nextAnimation["lastTickTime"] = Date.now()
            }

            setPositionInternal(springAnim as unknown as number)
        },
        [
            setPositionInternal,
            currentPositionDestinationSharedValue.get,
            currentPositionDestinationSharedValue.set,
        ]
    )

    const setPosition = useWorkletUiCallback(
        (v: number) => {
            "worklet"

            setPositionAnimate(v, {duration: 0})
        },
        [setPositionAnimate]
    )

    const setVelocity = useWorkletUiCallback(
        (v: number) => {
            "worklet"

            const currentLock = currentPositionLockSharedValue.get()
            if (currentLock !== null && currentLock.id !== id) {
                console.warn(
                    `Cannot setVelocity with '${id}' as '${currentLock.id}' is already engaged.`
                )

                return
            }

            currentPositionLastVelocitySharedValue.set(v)
        },
        [currentPositionLockSharedValue, id, currentPositionLastVelocitySharedValue]
    )

    return {
        canLockSharedValue,
        hasLockSharedValue,
        lock,
        release,
        setPosition,
        setPositionAnimate,
        setVelocity,
    }
}
