/* eslint-disable react/no-array-index-key */
/* eslint-disable react/require-default-props */
import * as React from 'react';
import { useEffect, useState, useRef } from 'react';
import './ProgressBar.scss';
import { MediaSegment } from './RecordingView';

interface ProgressBarProps {
    videos: MediaSegment[];
    setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>;
    orderVideos: boolean;
    progress: number;
    recording: boolean;
    doUndo: boolean;
    setCanUndo?: React.Dispatch<React.SetStateAction<boolean>>;
}

interface SegmentBox {
    endTime: number;
    startTime: number;
    order: number;
}
interface CurrentHover {
    index: number;
    minX: number;
    maxX: number;
}

export function ProgressBar(props: ProgressBarProps) {
    const progressBarRef = useRef<HTMLDivElement | null>(null);

    // the actual list of JSX elements rendered as segments
    const [segments, setSegments] = useState<JSX.Element[]>([]);
    // array for the order of video segments
    const [ordered, setOrdered] = useState<SegmentBox[]>([]);

    const [undoStack, setUndoStack] = useState<SegmentBox[]>([]);

    // -1 if no segment is currently being dragged around; else, it is the id of that segment over
    // NOTE: the id of a segment is its index in the ordered array
    const [dragged, setDragged] = useState<number>(-1);

    // length of the time removed from the video, in seconds*100
    const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);

    // this holds the index of the videoc segment to be removed
    const [removed, setRemoved] = useState<number>(-1);

    // update the canUndo props based on undo stack
    useEffect(() => props.setCanUndo?.(undoStack.length > 0), [undoStack.length]);

    const handleUndo = () => {
        // get the last element from the undo if it exists
        if (undoStack.length === 0) return;
        // get and remove the last element from the undo stack
        const last = undoStack.lastElement();
        setUndoStack(prevUndo => prevUndo.slice(0, -1));

        // update the removed time and place element back into ordered
        setTotalRemovedTime(prevRemoved => prevRemoved - (last.endTime - last.startTime));
        setOrdered(prevOrdered => [...prevOrdered, last]);
    };
    // useEffect for undo - brings back the most recently deleted segment
    useEffect(() => handleUndo(), [props.doUndo]);

    // useEffect for recording changes - changes style to disabled and adds the "expanding-segment"
    useEffect(() => {
        // get segments segment's html using it's id -> make them appeared disabled (or enabled)
        segments.forEach(seg => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording));
        progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording);

        if (props.recording)
            setSegments(prevSegments => [
                ...prevSegments,
                <div key="segment-expanding" id="segment-expanding" className="segment segment-expanding blink" style={{ width: 'fit-content' }}>
                    {props.videos.length + 1}
                </div>,
            ]);
    }, [props.recording]);

    // useEffect that updates the segmentsJSX, which is rendered
    // only updated when ordered is updated or if the user is dragging around a segment
    useEffect(() => {
        const totalTime = props.progress * 1000 - totalRemovedTime;
        const segmentsJSX = ordered.map((seg, i) => (
            <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>
                {seg.order + 1}
            </div>
        ));

        setSegments(segmentsJSX);
    }, [dragged, ordered]);

    // useEffect for dragged - update the cursor to be grabbing while grabbing
    useEffect(() => {
        progressBarRef.current?.classList.toggle('progressbar-dragging', dragged !== -1);
    }, [dragged]);

    // to imporve performance, only want to update the CSS width, not re-render the whole JSXList
    useEffect(() => {
        if (!props.recording) return;
        const totalTime = props.progress * 1000 - totalRemovedTime;
        let remainingTime = totalTime;
        segments.forEach((seg, i) => {
            // for the last segment, we need to set that directly
            if (i === segments.length - 1) return;
            // update remaining time
            remainingTime -= ordered[i].endTime - ordered[i].startTime;

            // update the width for this segment
            const htmlId = seg.props.id;
            const segmentHtml = document.getElementById(htmlId);
            if (segmentHtml) segmentHtml.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`;
        });

        // update the width of the expanding segment using the remaining time
        const segExapandHtml = document.getElementById('segment-expanding');
        if (segExapandHtml) segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`;
    }, [props.progress]);

    // useEffect for props.videos - update the ordered array when a new video is added
    useEffect(() => {
        // this useEffect fired when the videos are being rearragned to the order
        // in this case, do nothing.
        if (props.orderVideos) return;

        const order = props.videos.length - 1;
        // in this case, a new video is added -> push it onto ordered
        if (order >= ordered.length) {
            const { endTime, startTime } = props.videos.lastElement();
            setOrdered(prevOrdered => [...prevOrdered, { endTime, startTime, order }]);
        }

        // in this case, a video is removed
        else if (order < ordered.length) {
            console.warn('warning: video removed from parent');
        }
    }, [props.videos]);

    // useEffect for props.orderVideos - matched the order array with the videos array before the export
    useEffect(() => props.setVideos(vids => ordered.map(seg => vids[seg.order])), [props.orderVideos]);

    // useEffect for removed - handles logic for removing a segment
    useEffect(() => {
        if (removed === -1) return;
        // update total removed time
        setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));

        // put the element on the undo stack
        setUndoStack(prevUndo => [...prevUndo, ordered[removed]]);
        // remove the segment from the array
        setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed));
        // reset to default/nullish state
        setRemoved(-1);
    }, [removed]);

    // returns the new currentHover based on the new index
    const updateCurrentHover = (segId: number): CurrentHover | null => {
        // get the segId of the segment that will become the new bounding area
        const rect = progressBarRef.current?.children[segId].getBoundingClientRect();
        if (rect == null) return null;
        return {
            index: segId,
            minX: rect.x,
            maxX: rect.x + rect.width,
        };
    };

    const swapSegments = (oldIndex: number, newIndex: number) => {
        if (newIndex == null) return;
        setOrdered(prevOrdered => {
            const temp = { ...prevOrdered[oldIndex] };
            prevOrdered[oldIndex] = prevOrdered[newIndex];
            prevOrdered[newIndex] = temp;
            return prevOrdered;
        });
        // update visually where the segment is hovering over
        setDragged(newIndex);
    };

    // functions for the floating segment that tracks the cursor while grabbing it
    const initDetachSegment = (dot: HTMLDivElement, rect: DOMRect) => {
        dot.classList.add('segment-selected');
        dot.style.transitionDuration = '0s';
        dot.style.position = 'absolute';
        dot.style.zIndex = '999';
        dot.style.width = `${rect.width}px`;
        dot.style.height = `${rect.height}px`;
        dot.style.left = `${rect.x}px`;
        dot.style.top = `${rect.y}px`;
        dot.draggable = false;
        document.body.append(dot);
    };
    const followCursor = (event: PointerEvent, dot: HTMLDivElement): void => {
        // event.stopPropagation()
        const { width, height } = dot.getBoundingClientRect();
        dot.style.left = `${event.clientX - width / 2}px`;
        dot.style.top = `${event.clientY - height / 2}px`;
    };

    // pointerdown event for the progress bar
    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
        // don't move the videobox element
        e.stopPropagation();

        // if recording, do nothing
        if (props.recording) return;

        // get the segment the user clicked on to be dragged
        const clickedSegment = e.target as HTMLDivElement & EventTarget;

        // get the profess bar ro add event listeners
        // don't do anything if null
        const progressBar = progressBarRef.current;
        if (progressBar == null || clickedSegment.id === progressBar.id) return;

        // if holding shift key, let's remove that segment
        if (e.shiftKey) {
            const segId = parseInt(clickedSegment.id.split('-')[1]);
            setRemoved(segId);
            return;
        }

        // if holding ctrl key and click, let's undo that segment #hiddenfeature lol
        if (e.ctrlKey) {
            handleUndo();
            return;
        }

        // if we're here, the user is dragging a segment around
        // let the progress bar capture all the pointer events until the user releases (pointerUp)
        const ptrId = e.pointerId;
        progressBar.setPointerCapture(ptrId);

        const rect = clickedSegment.getBoundingClientRect();
        // id for segment is like 'segment-1' or 'segment-10',
        // so this works to get the id
        const segId = parseInt(clickedSegment.id.split('-')[1]);
        // set the selected segment to be the one dragged
        setDragged(segId);

        // this is the logic for storing the lower X bound and upper X bound to know
        // whether a swap is needed between two segments
        let currentHover: CurrentHover = {
            index: segId,
            minX: rect.x,
            maxX: rect.x + rect.width,
        };

        // create the floating segment that tracks the cursor
        const detchedSegment = document.createElement('div');
        initDetachSegment(detchedSegment, rect);

        const updateSegmentOrder = (event: PointerEvent): void => {
            event.stopPropagation();
            event.preventDefault();

            // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged
            if (!progressBar.hasPointerCapture(ptrId)) {
                // eslint-disable-next-line no-use-before-define
                placeSegmentandCleanup();
                return;
            }

            followCursor(event, detchedSegment);

            const curX = event.clientX;
            // handle the left bound
            if (curX < currentHover.minX && currentHover.index > 0) {
                swapSegments(currentHover.index, currentHover.index - 1);
                currentHover = updateCurrentHover(currentHover.index - 1) ?? currentHover;
            }
            // handle the right bound
            else if (curX > currentHover.maxX && currentHover.index < segments.length - 1) {
                swapSegments(currentHover.index, currentHover.index + 1);
                currentHover = updateCurrentHover(currentHover.index + 1) ?? currentHover;
            }
        };

        // handles when the user is done dragging the segment (pointerUp)
        const placeSegmentandCleanup = (event?: PointerEvent): void => {
            event?.stopPropagation();
            event?.preventDefault();
            // if they put the segment outside of the bounds, remove it
            if (event && (event.clientX < 0 || event.clientX > document.body.clientWidth || event.clientY < 0 || event.clientY > document.body.clientHeight)) setRemoved(currentHover.index);

            // remove the update event listener for pointermove
            progressBar.removeEventListener('pointermove', updateSegmentOrder);
            // remove the floating segment from the DOM
            detchedSegment.remove();
            // dragged is -1 is equiv to nothing being dragged, so the normal state
            // so this will place the segment in it's location and update the segment bar
            setDragged(-1);
        };

        // event listeners that allow the user to drag and release the floating segment
        progressBar.addEventListener('pointermove', updateSegmentOrder);
        progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true });
    };

    return (
        <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
            {segments}
        </div>
    );
}
