/home/smartbloks/.trash/extendify/src/Assist/components/GuidedTour.js
import { Button, Spinner } from '@wordpress/components'
import {
    useRef,
    useCallback,
    useEffect,
    useLayoutEffect,
    useState,
    useMemo,
} from '@wordpress/element'
import { sprintf, __ } from '@wordpress/i18n'
import { Icon, close } from '@wordpress/icons'
import { Dialog } from '@headlessui/react'
import { motion, AnimatePresence } from 'framer-motion'
import { useDesignColors } from '@assist/hooks/useDesignColors'
import { router } from '@assist/hooks/useRouter'
import { useGlobalSyncStore } from '@assist/state/GlobalSync'
import { useTasksStore } from '@assist/state/Tasks'
import { useTourStore } from '@assist/state/Tours'
import availableTours from '@assist/tours/tours.js'

const getBoundingClientRect = (element) => {
    const { top, right, bottom, left, width, height, x, y } =
        element.getBoundingClientRect()
    return { top, right, bottom, left, width, height, x, y }
}

export const GuidedTour = () => {
    const tourBoxRef = useRef()
    const {
        currentTour,
        currentStep,
        startTour,
        closeCurrentTour,
        getStepData,
        onTourPage,
    } = useTourStore()
    const { settings } = currentTour || {}
    const { image, title, text, attachTo, events, options } =
        getStepData(currentStep)

    const { queueTourForRedirect, queuedTour, clearQueuedTour } =
        useGlobalSyncStore()
    const { element, frame, offset, position, hook, boxPadding } =
        attachTo || {}

    const elementSelector = useMemo(
        () => (typeof element === 'function' ? element() : element),
        [element],
    )

    const frameSelector = useMemo(
        () => (typeof frame === 'function' ? frame() : frame),
        [frame],
    )

    const offsetNormalized = useMemo(
        () => (typeof offset === 'function' ? offset() : offset),
        [offset],
    )
    const hookNormalized = useMemo(
        () => (typeof hook === 'function' ? hook() : hook),
        [hook],
    )

    const [targetedElement, setTargetedElement] = useState(null)

    const initialFocus = useRef()
    const [redirecting, setRedirecting] = useState(false)
    const [visible, setVisible] = useState(false)

    const [overlayRect, setOverlayRect] = useState(null)

    const [placement, setPlacement] = useState({
        x: undefined,
        y: undefined,
        ...offsetNormalized,
    })
    const setTourBox = useCallback(
        (x, y) => {
            // x is 20 on mobile, so exclude the offset here
            setPlacement(x === 20 ? { x, y } : { x, y, ...offsetNormalized })
        },
        [offsetNormalized],
    )
    const getOffset = useCallback(() => {
        const hooks = hookNormalized?.split(' ') || []
        return {
            x: hooks.includes('right') ? tourBoxRef.current?.offsetWidth : 0,
            y: hooks.includes('bottom') ? tourBoxRef.current?.offsetHeight : 0,
        }
    }, [hookNormalized])

    const startOrRecalc = useCallback(() => {
        if (!targetedElement) return

        const frame = frameSelector
            ? document.querySelector(frameSelector)?.contentDocument ?? document
            : document

        const rect = getBoundingClientRect(
            frame.querySelector(elementSelector) ?? targetedElement,
        )

        // Adjust the frame position if we're in an iframe
        if (frame !== document) {
            const frameRect = getBoundingClientRect(
                frame.defaultView.frameElement,
            )
            rect.x += frameRect.x
            rect.left += frameRect.x
            rect.right += frameRect.x
            rect.y += frameRect.y
            rect.top += frameRect.y
            rect.bottom += frameRect.y
        }

        if (window.innerWidth <= 960) {
            closeCurrentTour('closed-resize')
            return
        }
        if (position?.x === undefined) {
            setTourBox(undefined, undefined)
            setOverlayRect(null)
            setVisible(false)
            return
        }
        const x = rect?.[position.x] - getOffset().x
        const y = rect?.[position.y] - getOffset().y
        const box = tourBoxRef.current
        // make sure it doesn't go off-screen
        setTourBox(
            Math.min(x, window.innerWidth - (box?.offsetWidth ?? 0) - 20),
            Math.min(y, window.innerHeight - (box?.offsetHeight ?? 0) - 20),
        )
        setOverlayRect(rect)
    }, [
        targetedElement,
        position,
        getOffset,
        setTourBox,
        frameSelector,
        elementSelector,
        closeCurrentTour,
    ])

    // Pre-launch check whether to redirect
    useLayoutEffect(() => {
        // if the tour has a start from url, redirect there
        if (!settings?.startFrom) return
        if (onTourPage()) return
        setRedirecting(true)
        queueTourForRedirect(currentTour.id)
        closeCurrentTour('redirected')
        window.location.assign(settings?.startFrom[0])
        if (
            window.location.href.split('#')[0] ===
            settings.startFrom[0].split('#')[0]
        ) {
            // Reload if hash is the only difference
            window.location.reload()
        }
    }, [
        settings?.startFrom,
        currentTour,
        queueTourForRedirect,
        closeCurrentTour,
        onTourPage,
    ])

    // Possibly start the tour, or wait for the load event
    useLayoutEffect(() => {
        if (redirecting) return
        const tour = queuedTour
        let rafId = 0
        if (!tour || !availableTours[tour]) return clearQueuedTour()
        const handle = () => {
            requestAnimationFrame(() => {
                startTour(availableTours[tour])
            })
            clearQueuedTour()
        }

        addEventListener('load', handle)
        if (document.readyState === 'complete') {
            // Page is already loaded, so we can start the tour immediately
            rafId = requestAnimationFrame(handle)
        }
        return () => {
            cancelAnimationFrame(rafId)
            removeEventListener('load', handle)
        }
    }, [startTour, queuedTour, clearQueuedTour, redirecting])

    useEffect(() => {
        // Find and set the element we are attaching to
        const frame = frameSelector
            ? document.querySelector(frameSelector)?.contentDocument ?? document
            : document
        const element = frame.querySelector(elementSelector)

        if (!element) return
        setTargetedElement(element)
        return () => setTargetedElement(null)
    }, [frameSelector, elementSelector])

    // Start building the tour step
    useLayoutEffect(() => {
        if (!targetedElement || redirecting) return
        setVisible(true)
        startOrRecalc()
        addEventListener('resize', startOrRecalc)
        targetedElement.style.pointerEvents = 'none'
        return () => {
            removeEventListener('resize', startOrRecalc)
            targetedElement.style.pointerEvents = 'auto'
        }
    }, [redirecting, targetedElement, startOrRecalc])

    // Handle the attach and detach events
    useEffect(() => {
        if (currentStep === undefined || !targetedElement) return
        events?.onAttach?.(targetedElement)
        let inner = 0
        const id = requestAnimationFrame(() => {
            inner = requestAnimationFrame(startOrRecalc)
        })
        return () => {
            events?.onDetach?.(targetedElement)
            cancelAnimationFrame(id)
            cancelAnimationFrame(inner)
        }
    }, [currentStep, events, targetedElement, startOrRecalc])

    useLayoutEffect(() => {
        if (!settings?.allowOverflow) return
        document.documentElement.classList.add('ext-force-overflow-auto')
        return () => {
            document.documentElement.classList.remove('ext-force-overflow-auto')
        }
    }, [settings])

    useEffect(() => {
        // This closes the tour if the user switches tabs
        // (likely by pressing the browser back button)
        const stopTheTour = () => closeCurrentTour('assist-route-change')
        router.onRouteChange(stopTheTour)
        return () => router.removeOnRouteChange(stopTheTour)
    }, [closeCurrentTour])

    if (!visible) return null

    const rectWithPadding = addPaddingToRect(overlayRect, boxPadding)
    return (
        <>
            <AnimatePresence>
                {Boolean(currentTour) && (
                    <Dialog
                        as={motion.div}
                        static
                        initialFocus={initialFocus}
                        className="extendify-assist"
                        open={Boolean(currentTour)}
                        onClose={() => undefined}>
                        <div className="relative z-max">
                            <motion.div
                                ref={tourBoxRef}
                                animate={{ opacity: 1, ...placement }}
                                initial={{ opacity: 0, ...placement }}
                                // TODO: fire another event after animation completes?
                                onAnimationComplete={() => {
                                    startOrRecalc()
                                }}
                                transition={{
                                    duration: 0.5,
                                    ease: 'easeInOut',
                                }}
                                className="fixed top-0 left-0 shadow-2xl sm:overflow-hidden bg-transparent flex flex-col max-w-xs z-20"
                                style={{
                                    minWidth: settings?.minBoxWidth ?? '325px',
                                }}>
                                <button
                                    data-test="close-tour"
                                    className="absolute bg-white cursor-pointer flex ring-gray-200 ring-1 focus:ring-wp focus:ring-design-main focus:shadow-none h-6 items-center justify-center leading-none m-2 outline-none p-0 right-0 rounded-full top-0 w-6 border-0 z-20"
                                    onClick={() =>
                                        closeCurrentTour('closed-manually')
                                    }
                                    aria-label={__('Close Modal', 'extendify')}>
                                    <Icon icon={close} className="w-4 h-4" />
                                </button>
                                <Dialog.Title className="sr-only">
                                    {currentTour?.title ??
                                        __('Tour', 'extendify')}
                                </Dialog.Title>
                                {image && (
                                    <div
                                        className="w-full p-6"
                                        style={{
                                            minHeight: 150,
                                            background:
                                                'linear-gradient(58.72deg, #485563 7.71%, #29323C 92.87%)',
                                        }}>
                                        <img
                                            src={image}
                                            className="w-full block"
                                            alt={title}
                                        />
                                    </div>
                                )}
                                <div className="m-0 p-6 pt-0 text-left relative bg-white">
                                    {title && (
                                        <h2 className="text-xl font-medium mb-2">
                                            {title}
                                        </h2>
                                    )}
                                    {text && <p className="mb-6">{text}</p>}
                                    <BottomNav initialFocus={initialFocus} />
                                </div>
                            </motion.div>
                        </div>
                    </Dialog>
                )}
            </AnimatePresence>
            {options?.blockPointerEvents && (
                <div aria-hidden={true} className="fixed inset-0 z-max-1" />
            )}
            <AnimatePresence>
                {Boolean(currentTour) && overlayRect?.left !== undefined && (
                    <>
                        <motion.div
                            initial={{
                                opacity: 0,
                                clipPath:
                                    'polygon(0px 0px, 100% 0px, 100% 100%, 0px 100%, 0 0)',
                            }}
                            animate={{
                                opacity: 1,
                                clipPath: `polygon(0px 0px, 100% 0px, 100% 100%, 0px 100%, 0 0, ${rectWithPadding.left}px 0, ${rectWithPadding.left}px ${rectWithPadding?.bottom}px, ${rectWithPadding?.right}px ${rectWithPadding.bottom}px, ${rectWithPadding.right}px ${rectWithPadding.top}px, ${rectWithPadding.left}px ${rectWithPadding.top}px)`,
                            }}
                            transition={{ duration: 0.5, ease: 'easeInOut' }}
                            className="hidden lg:block fixed inset-0 bg-black bg-opacity-70 z-max-1"
                            aria-hidden="true"
                        />
                        <motion.div
                            initial={{
                                opacity: 0,
                                ...(rectWithPadding ?? {}),
                            }}
                            animate={{
                                opacity: 1,
                                ...(rectWithPadding ?? {}),
                            }}
                            transition={{ duration: 0.5, ease: 'easeInOut' }}
                            className="hidden lg:block fixed inset-0 border-2 border-design-main z-high"
                            aria-hidden="true"
                        />
                    </>
                )}
            </AnimatePresence>
        </>
    )
}

const BottomNav = ({ initialFocus }) => {
    const {
        goToStep,
        completeCurrentTour,
        currentStep,
        preparingStep,
        getStepData,
        hasNextStep,
        nextStep,
        hasPreviousStep,
        prevStep,
        currentTour,
    } = useTourStore()
    const { options = {} } = getStepData(currentStep)
    const { hideBackButton = false } = options
    const { id, steps, settings } = currentTour || {}
    const { mainColor } = useDesignColors()
    const { completeTask } = useTasksStore()

    return (
        <div
            id="extendify-tour-navigation"
            className="flex justify-between items-center w-full">
            <div className="flex-1 flex justify-start">
                <AnimatePresence>
                    {hasPreviousStep() && !hideBackButton && (
                        <motion.div
                            initial={{ opacity: 0 }}
                            animate={{ opacity: 1 }}>
                            <button
                                className="flex gap-2 p-0 h-8 rounded-sm items-center justify-center bg-transparent hover:bg-transparent focus:outline-none ring-design-main focus:ring-wp focus:ring-offset-1 focus:ring-offset-white text-gray-900 disabled:opacity-60"
                                onClick={prevStep}
                                disabled={preparingStep > -1}>
                                {preparingStep < currentStep && (
                                    <Spinner
                                        style={{
                                            color: mainColor,
                                            margin: 0,
                                            height: '1em',
                                        }}
                                    />
                                )}
                                {__('Back', 'extendify')}
                            </button>
                        </motion.div>
                    )}
                </AnimatePresence>
            </div>

            {steps?.length > 2 && !settings?.hideDotsNav ? (
                <nav
                    role="navigation"
                    aria-label={__('Tour Steps', 'extendify')}
                    className="flex-1 flex items-center justify-center gap-1 transform -translate-x-3">
                    {steps.map((_step, index) => (
                        <div key={index}>
                            <button
                                style={{
                                    backgroundColor:
                                        index === currentStep
                                            ? mainColor
                                            : undefined,
                                }}
                                aria-label={sprintf(
                                    __('%s of %s', 'extendify'),
                                    index + 1,
                                    steps.length,
                                )}
                                aria-current={index === currentStep}
                                className={`focus:ring-wp focus:outline-none ring-offset-1 ring-offset-white focus:ring-design-main block cursor-pointer w-2.5 h-2.5 m-0 p-0 rounded-full ${
                                    index === currentStep
                                        ? 'bg-design-main'
                                        : 'bg-gray-300'
                                }`}
                                onClick={() => goToStep(index)}
                                disabled={preparingStep > -1}
                            />
                        </div>
                    ))}
                </nav>
            ) : null}

            <div className="flex-1 flex justify-end">
                {hasNextStep() ? (
                    <Button
                        ref={initialFocus}
                        id="assist-tour-next-button"
                        data-test="assist-tour-next-button"
                        onClick={nextStep}
                        disabled={preparingStep > -1}
                        style={{
                            backgroundColor: mainColor,
                        }}
                        className="flex gap-2 text-design-text focus:text-design-text disabled:opacity-60"
                        variant="primary">
                        {preparingStep > currentStep && (
                            <Spinner
                                style={{
                                    color: mainColor,
                                    margin: 0,
                                    height: '1em',
                                }}
                            />
                        )}
                        {__('Next', 'extendify')}
                    </Button>
                ) : (
                    <Button
                        id="assist-tour-next-button"
                        data-test="assist-tour-next-button"
                        onClick={() => {
                            completeTask(id)
                            completeCurrentTour()
                        }}
                        style={{
                            backgroundColor: mainColor,
                        }}
                        variant="primary">
                        {__('Done', 'extendify')}
                    </Button>
                )}
            </div>
        </div>
    )
}

const addPaddingToRect = (rect, padding) => ({
    top: rect.top - (padding?.top ?? 0),
    left: rect.left - (padding?.left ?? 0),
    right: rect.right + (padding?.right ?? 0),
    bottom: rect.bottom + (padding?.bottom ?? 0),
    width: rect.width + (padding?.left ?? 0) + (padding?.right ?? 0),
    height: rect.height + (padding?.top ?? 0) + (padding?.bottom ?? 0),
    x: rect.x - (padding?.left ?? 0),
    y: rect.y - (padding?.top ?? 0),
})