import { Signal, useSignal, useSignalEffect } from "@preact/signals";
import { useLiveSignal } from "@preact/signals/utils";
import { ComponentChildren, Ref, RefObject } from "preact";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import { Task } from ".";
import { flushSync } from "preact/compat";


type UseDragFunction = (onStartDrag: (e: MouseEvent) => void, onUpdatePosition: (e: MouseEvent) => void) => [Signal<Boolean>, (e: MouseEvent) => void]

export function Window(props: Window.Props) {
    const useDrag = useCallback<UseDragFunction>((onStartDrag, onUpdatePosition) => {
        const dragTimeout: Ref<ReturnType<typeof setTimeout>> = useRef(null);
        const dragging = useSignal(false);

        const stopDrag = useCallback((e: MouseEvent) => {
            if (e.button === 0 && dragging.value && !dragTimeout.current) {
                dragTimeout.current = setTimeout(() => {
                    dragging.value = false
                    dragTimeout.current = null;
                    props.desktop.current?.removeEventListener("mousemove", onUpdatePosition)
                    props.desktop.current?.removeEventListener("mouseup", stopDrag)
                }, 50) // intrdouce a small delay where the user can still drag the window without actually pressing down lmb to prevent accidental drops
            }
        }, [onUpdatePosition]);

        const startDrag = useCallback((e: MouseEvent) => {
            if (e.button === 0 && !maximized.value.value) {
                if (dragTimeout.current !== null) {
                    clearTimeout(dragTimeout.current);
                    dragTimeout.current = null;
                } else {

                    onStartDrag(e);
                    dragging.value = true
                    props.desktop.current?.addEventListener("mousemove", onUpdatePosition)
                    props.desktop.current?.addEventListener("mouseleave", stopDrag, { once: true })
                    props.desktop.current?.addEventListener("mouseup", stopDrag, { once: true })
                }
            }
        }, [onUpdatePosition, onStartDrag, stopDrag]);

        return [dragging, startDrag]
    }, [props])

    const windowRef: Ref<HTMLDivElement> = useRef(null);

    const source = useLiveSignal(props.task.source);
    const title = useLiveSignal(props.task.title);
    const contentReady = useLiveSignal(props.task.contentReady);
    const sizingFinished = useLiveSignal(props.task.sizingFinished);
    const [titleReady, setTitleReady] = useState(false);
    const [titleRendered, setTitleRendered] = useState(false);

    const maximized = useLiveSignal(props.task.window.maximized);
    const minimized = useLiveSignal(props.task.window.minimized);
    const position = props.task.window.position;
    const z = useLiveSignal(props.task.window.z);

    const x = useSignal(position?.x ?? "50%");
    const y = useSignal(position?.y ?? "50%");

    const initialOffsetX = useRef(0);
    const initialOffsetY = useRef(0);

    const onWindowUpdatePosition = useCallback((e: MouseEvent) => {
        x.value = e.clientX - initialOffsetX.current
        y.value = e.clientY - initialOffsetY.current
    }, [])

    const onStartWindowMove = useCallback((e: MouseEvent) => {
        const BORDER_WIDTH = 1;
        // Naturally, I have to add the border width here, because offset* uses padding-edge for the calculations
        initialOffsetX.current = e.offsetX + BORDER_WIDTH + (e.target as HTMLDivElement).offsetLeft
        initialOffsetY.current = e.offsetY + BORDER_WIDTH + (e.target as HTMLDivElement).offsetTop
        document.getSelection()?.empty()
    }, [])

    const [dragging, startWindowMove] = useDrag(onStartWindowMove, onWindowUpdatePosition);

    const height = useSignal(props.task.window.height);
    const width = useSignal(props.task.window.width);

    const onStartResize = useCallback((e: MouseEvent) => {
        initialOffsetX.current = e.offsetX
        initialOffsetY.current = e.offsetY
    }, [])

    const onNorthResize = useCallback((e: MouseEvent) => {
        const oldY = y.value as number;
        y.value = e.clientY - initialOffsetY.current;
        height.value = (height.value as number) - (y.value - oldY)
    }, [])

    const [, startNorthResize] = useDrag(onStartResize, onNorthResize)


    const onEastResize = useCallback((e: MouseEvent) => {
        width.value = (e.clientX - (x.value as number) - initialOffsetX.current)
    }, [])

    const [, startEastResize] = useDrag(onStartResize, onEastResize)


    const onSouthResize = useCallback((e: MouseEvent) => {
        height.value = (e.clientY - (y.value as number) - initialOffsetY.current)
    }, [])

    const [, startSouthResize] = useDrag(onStartResize, onSouthResize)

    const onWestResize = useCallback((e: MouseEvent) => {
        const oldX = x.value as number;
        x.value = e.clientX - initialOffsetX.current;
        width.value = (width.value as number) - (x.value - oldX)
    }, [])

    const [, startWestResize] = useDrag(onStartResize, onWestResize)

    const startNEResize = useCallback((e: MouseEvent) => {
        startNorthResize(e)
        startEastResize(e)
    }, [])

    const startNWResize = useCallback((e: MouseEvent) => {
        startNorthResize(e)
        startWestResize(e)
    }, [])

    const startSEResize = useCallback((e: MouseEvent) => {
        startSouthResize(e)
        startEastResize(e)
    }, [])

    const startSWResize = useCallback((e: MouseEvent) => {
        startSouthResize(e)
        startWestResize(e)
    }, [])

    // tldr: wait until the title signal is ready, 
    // rerender so that the new title is rendered, calculate the position, 
    // and then rerender *again*, this time to reposition and show the window

    useSignalEffect(() => {
        if (title.value.value !== undefined && !titleReady) {
            setTitleReady(true)
        }
    })

    useLayoutEffect(() => {
        if (titleReady && contentReady.value.value) {
            setTitleRendered(true);
            // normalize position and dimensions from css values to pixels
            const { height: bounding_height, width: bounding_width } = windowRef.current!.getBoundingClientRect();
            height.value = bounding_height
            width.value = bounding_width
            
            if (position === undefined) {
                // center
                x.value = windowRef.current!.offsetLeft - (bounding_width / 2);
                y.value = windowRef.current!.offsetTop - (bounding_height / 2);
            } else {
                x.value = windowRef.current!.offsetLeft;
                y.value = windowRef.current!.offsetTop;
            }
            sizingFinished.value.value = true
        }
    }, [titleReady, contentReady.value.value])

    const handleMinimize = useCallback(() => {
        minimized.value.value = !minimized.value.value
    }, [])

    const handleMaximize = useCallback(() => {
        maximized.value.value = !maximized.value.value
    }, [])

    const onClose = useCallback(() => {
        props.onClose(props.task)
    }, [props])
    
    const onFocus = useCallback(() => {
        props.onFocus(props.task);
    }, [props])

    const onPointerDownButton = useCallback((e: MouseEvent) => {
        onFocus();
        e.stopPropagation();
    }, [])

    return <div
        class={`window ${(titleRendered && !props.task.window.minimized.value) ? "" : " hidden"}${dragging.value ? "dragging" : ""}${maximized.value.value ? " maximized" : ""}` }
        ref={windowRef}
        style={
            maximized.value.value
                ? { width: "100%", height: "100%", left: 0, top: 0, "z-index": z.value.value }
                : { width: width.value, height: height.value, left: x.value, top: y.value, "z-index": z.value.value }
        }
        onPointerDown={onFocus}
    >
        <div class="titlebar" onPointerDown={startWindowMove}>
            <img src={`/assets/${source.value.value.type}.png`} />
            <div class="title">{title.value.value ?? "[404 Title Not Found]"}</div>
            <div class="buttons" onPointerDown={onPointerDownButton}>
                <button onClick={handleMinimize}>
                    <img src="/assets/minimize.png" />
                </button>
                <button onClick={handleMaximize}>
                    <img src="/assets/maximize.png" />
                </button>
                <button onClick={onClose}>
                    <img src="/assets/close.png" />
                </button>
            </div>
        </div>
        <div class="content">
            {props.children}
        </div>
        <div class="corner n w" onPointerDown={startNWResize}></div>
        <div class="corner n e" onPointerDown={startNEResize}></div>
        <div class="corner s e" onPointerDown={startSEResize}></div>
        <div class="corner s w" onPointerDown={startSWResize}></div>
        <div class="side_v n" onPointerDown={startNorthResize}></div>
        <div class="side_h e" onPointerDown={startEastResize}></div>
        <div class="side_v s" onPointerDown={startSouthResize}></div>
        <div class="side_h w" onPointerDown={startWestResize}></div>
    </div>
}

export namespace Window {
    export type Props = {
        desktop: RefObject<HTMLDivElement>;
        task: Task;
        children: ComponentChildren;

        onClose: (task: Task) => void;
        onFocus: (task: Task) => void;
    }
}
