import { useEffect, useMemo, useRef, useState } from 'react'

import './TimelineZoom.scss'

type TimelineZoomProps = {
    min: number
    max: number

    start: number
    end: number

    onChange: (range: {
        start: number
        end: number
    }) => void

    formatLabel?: (value: number) => string

    minWindow?: number
}

type DragType =
    | 'left'
    | 'right'
    | 'move'
    | null

const CHANGE_THROTTLE_MS = 90

export function TimelineZoom({
    min,
    max,
    start,
    end,
    onChange,
    formatLabel = (value) =>
        new Date(value).toLocaleString('ru-RU', {
            day: '2-digit',
            month: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
        }),
    minWindow = 5 * 60 * 1000,
}: TimelineZoomProps) {

    const STEP = 5 * 60 * 1000

    function snap(value: number) {
        return Math.round(value / STEP) * STEP
    }

    const trackRef = useRef<HTMLDivElement | null>(null)

    const dragTypeRef = useRef<DragType>(null)

    const startXRef = useRef(0)

    const initialRangeRef = useRef({
        start,
        end,
    })

    const [draftRange, setDraftRange] = useState({
        start,
        end,
    })

    const draftRangeRef = useRef(draftRange)

    const pendingRangeRef = useRef<{
        start: number
        end: number
    } | null>(null)

    const changeTimerRef = useRef<number | null>(null)

    const lastChangeAtRef = useRef(0)

    useEffect(() => {
        if (dragTypeRef.current) return

        const nextRange = {
            start,
            end,
        }

        draftRangeRef.current = nextRange
        setDraftRange(nextRange)
    }, [end, start])

    function setNextDraftRange(nextRange: {
        start: number
        end: number
    }) {
        if (
            draftRangeRef.current.start === nextRange.start &&
            draftRangeRef.current.end === nextRange.end
        ) {
            return
        }

        draftRangeRef.current = nextRange
        setDraftRange(nextRange)
    }

    function emitRange(nextRange: {
        start: number
        end: number
    }) {
        pendingRangeRef.current = nextRange

        const now = Date.now()
        const remaining = CHANGE_THROTTLE_MS - (now - lastChangeAtRef.current)

        if (remaining <= 0) {
            if (changeTimerRef.current !== null) {
                window.clearTimeout(changeTimerRef.current)
                changeTimerRef.current = null
            }

            pendingRangeRef.current = null
            lastChangeAtRef.current = now
            onChange(nextRange)
            return
        }

        if (changeTimerRef.current !== null) return

        changeTimerRef.current = window.setTimeout(() => {
            changeTimerRef.current = null

            const range = pendingRangeRef.current
            if (!range) return

            pendingRangeRef.current = null
            lastChangeAtRef.current = Date.now()
            onChange(range)
        }, remaining)
    }

    function changeDraftRange(nextRange: {
        start: number
        end: number
    }) {
        setNextDraftRange(nextRange)
        emitRange(nextRange)
    }

    const total = max - min

    const leftPercent = ((draftRange.start - min) / total) * 100
    const widthPercent = ((draftRange.end - draftRange.start) / total) * 100
    
    const leftLabel = useMemo(
        () => formatLabel(draftRange.start),
        [draftRange.start, formatLabel],
    )

    const rightLabel = useMemo(
        () => formatLabel(draftRange.end),
        [draftRange.end, formatLabel],
    )

    function beginDrag(
        type: DragType,
        clientX: number,
    ) {
        dragTypeRef.current = type

        startXRef.current = clientX

        initialRangeRef.current = {
            start,
            end,
        }

        setNextDraftRange({
            start,
            end,
        })

        pendingRangeRef.current = null
        lastChangeAtRef.current = 0

        if (changeTimerRef.current !== null) {
            window.clearTimeout(changeTimerRef.current)
            changeTimerRef.current = null
        }

        document.body.style.userSelect = 'none'
        document.body.style.cursor =
            type === 'move'
                ? 'grabbing'
                : 'ew-resize'
    }

    useEffect(() => {
        function handleMove(event: MouseEvent) {
            if (!dragTypeRef.current) return
            if (!trackRef.current) return

            const rect =
                trackRef.current.getBoundingClientRect()

            const deltaPx =
                event.clientX - startXRef.current

            const deltaPercent =
                deltaPx / rect.width

            const deltaValue =
                deltaPercent * total

            const initial =
                initialRangeRef.current

            if (dragTypeRef.current === 'move') {
                const windowSize =
                    initial.end - initial.start

                let nextStart =
                    snap(initial.start + deltaValue)

                let nextEnd =
                    snap(initial.end + deltaValue)

                if (nextStart < min) {
                    nextStart = min
                    nextEnd = min + windowSize
                }

                if (nextEnd > max) {
                    nextEnd = max
                    nextStart = max - windowSize
                }

                changeDraftRange({
                    start: nextStart,
                    end: nextEnd,
                })

                return
            }

            if (dragTypeRef.current === 'left') {
                let nextStart =
                    snap(
                        initial.start + deltaValue,
                    )

                nextStart = Math.max(
                    min,
                    Math.min(
                        nextStart,
                        initial.end - minWindow,
                    ),
                )

                changeDraftRange({
                    start: nextStart,
                    end: initial.end,
                })

                return
            }

            if (dragTypeRef.current === 'right') {
                let nextEnd = snap(
                    initial.end + deltaValue,
                )

                nextEnd = Math.min(
                    max,
                    Math.max(
                        nextEnd,
                        initial.start + minWindow,
                    ),
                )

                changeDraftRange({
                    start: initial.start,
                    end: nextEnd,
                })
            }
        }

        function handleUp() {
            if (dragTypeRef.current) {
                if (changeTimerRef.current !== null) {
                    window.clearTimeout(changeTimerRef.current)
                    changeTimerRef.current = null
                }

                pendingRangeRef.current = null
                lastChangeAtRef.current = Date.now()
                onChange(draftRangeRef.current)
            }

            dragTypeRef.current = null

            document.body.style.userSelect = ''
            document.body.style.cursor = ''
        }

        window.addEventListener(
            'mousemove',
            handleMove,
        )

        window.addEventListener(
            'mouseup',
            handleUp,
        )

        return () => {
            window.removeEventListener(
                'mousemove',
                handleMove,
            )

            window.removeEventListener(
                'mouseup',
                handleUp,
            )

            if (changeTimerRef.current !== null) {
                window.clearTimeout(changeTimerRef.current)
                changeTimerRef.current = null
            }
        }
    }, [
        max,
        min,
        minWindow,
        onChange,
        total,
    ])

    return (
        <div className="timeline-zoom">
            <div
                ref={trackRef}
                className="timeline-zoom__track"
            >
                <div
                    className={`timeline-zoom__selection`}
                    style={{
                        left: `${leftPercent}%`,
                        width: `${widthPercent}%`,
                    }}
                >
                    <button
                        type="button"
                        className="timeline-zoom__handle timeline-zoom__handle--left"
                        onMouseDown={(event) => {
                            beginDrag(
                                'left',
                                event.clientX,
                            )
                        }}
                    />

                    <button
                        type="button"
                        className="timeline-zoom__window"
                        onMouseDown={(event) => {
                            beginDrag(
                                'move',
                                event.clientX,
                            )
                        }}
                    />

                    <button
                        type="button"
                        className="timeline-zoom__handle timeline-zoom__handle--right"
                        onMouseDown={(event) => {
                            beginDrag(
                                'right',
                                event.clientX,
                            )
                        }}
                    />
                </div>

                <div
                    className={`timeline-zoom__label timeline-zoom__label--left  ${leftPercent > 5 ? 'out-place' : ''}`}
                    style={{
                        left: `${leftPercent}%`,
                    }}
                >
                    {leftLabel}
                </div>

                <div
                    className={`timeline-zoom__label timeline-zoom__label--right ${(leftPercent + widthPercent) < 94 ? 'out-place' : ''}`}
                    style={{
                        left: `${leftPercent + widthPercent}%`,
                    }}
                >
                    {rightLabel}
                </div>
            </div>
        </div>
    )
}
