import { useCallback, useEffect, useRef, useState } from "react";

export type VncConnectionState =
    | "IDLE"
    | "CONNECTING_WS"
    | "CONNECTING_VNC"
    | "CONNECTED"
    | "FIRST_FRAME"
    | "DISCONNECTED"
    | "ERROR";

export type UseVncConsoleOptions = {
    websocketUrl?: string;
    defaultHost?: string;
    defaultPort?: number;
};

export type ConnectVncInput = {
    host: string;
    port: number;
    password: string;
};

const DEFAULT_WEBSOCKET_URL = "ws://localhost:18450/ws/vnc";
const DEFAULT_HOST = "198.19.0.176";
const DEFAULT_PORT = 5900;

export function useVncConsole(options: UseVncConsoleOptions = {}) {
    const websocketUrl = options.websocketUrl ?? DEFAULT_WEBSOCKET_URL;

    const canvasRef = useRef<HTMLCanvasElement | null>(null);
    const wsRef = useRef<WebSocket | null>(null);
    const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
    const rgbaRef = useRef<Uint8ClampedArray | null>(null);
    const framebufferRef = useRef({
        width: 0,
        height: 0,
    });

    const [state, setState] = useState<VncConnectionState>("IDLE");
    const [error, setError] = useState<string | null>(null);
    const [host, setHost] = useState(options.defaultHost ?? DEFAULT_HOST);
    const [port, setPort] = useState(options.defaultPort ?? DEFAULT_PORT);
    const [password, setPassword] = useState("");
    const [frameSize, setFrameSize] = useState({ width: 0, height: 0 });
    const [lastFrameAt, setLastFrameAt] = useState<string | null>(null);

    const closeSocket = useCallback(() => {
        const ws = wsRef.current;

        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: "disconnect" }));
            ws.close();
        } else if (ws) {
            ws.close();
        }

        wsRef.current = null;
    }, []);

    const clearFrame = useCallback(() => {
        const canvas = canvasRef.current;
        const ctx = ctxRef.current;

        if (canvas && ctx) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        }

        rgbaRef.current = null;
        framebufferRef.current = {
            width: 0,
            height: 0,
        };

        setFrameSize({ width: 0, height: 0 });
        setLastFrameAt(null);
    }, []);

    const drawFrame = useCallback((buffer: ArrayBuffer) => {
        const canvas = canvasRef.current;

        if (!canvas) {
            return;
        }

        const view = new DataView(buffer);
        const width = view.getInt32(0);
        const height = view.getInt32(4);
        const pixels = new Uint8ClampedArray(buffer, 8);

        framebufferRef.current = {
            width,
            height,
        };

        if (!ctxRef.current) {
            ctxRef.current = canvas.getContext("2d", {
                alpha: false,
                desynchronized: true,
            });
        }

        const ctx = ctxRef.current;

        if (!ctx) {
            return;
        }

        if (canvas.width !== width || canvas.height !== height) {
            canvas.width = width;
            canvas.height = height;
            rgbaRef.current = new Uint8ClampedArray(width * height * 4);
            setFrameSize({ width, height });
        }

        if (!rgbaRef.current || rgbaRef.current.length !== width * height * 4) {
            rgbaRef.current = new Uint8ClampedArray(width * height * 4);
        }

        const rgba = rgbaRef.current;

        for (let index = 0; index < pixels.length; index += 4) {
            rgba[index] = pixels[index + 2];
            rgba[index + 1] = pixels[index + 1];
            rgba[index + 2] = pixels[index];
            rgba[index + 3] = 255;
        }
        const imageData = ctx.createImageData(width, height);
        imageData.data.set(rgba);

        ctx.putImageData(imageData, 0, 0);
        setLastFrameAt(new Date().toLocaleTimeString());
    }, []);

    const connect = useCallback(
        async (input?: Partial<ConnectVncInput>) => {
            const nextHost = input?.host ?? host;
            const nextPort = input?.port ?? port;
            const nextPassword = input?.password ?? password;

            if (!nextHost.trim()) {
                setError("Host VNC em falta.");
                setState("ERROR");
                return;
            }

            setError(null);
            setState("CONNECTING_WS");

            closeSocket();

            const socket = new WebSocket(websocketUrl);
            wsRef.current = socket;
            socket.binaryType = "arraybuffer";

            socket.onopen = () => {
                setState("CONNECTING_VNC");

                socket.send(
                    JSON.stringify({
                        type: "connect",
                        host: nextHost,
                        port: nextPort,
                        password: nextPassword,
                    }),
                );
            };

            socket.onmessage = (event) => {
                if (typeof event.data === "string") {
                    const message = JSON.parse(event.data) as {
                        type?: string;
                        state?: string;
                        message?: string;
                    };

                    if (message.type === "state") {
                        if (message.state === "CONNECTING") {
                            setState("CONNECTING_VNC");
                            return;
                        }

                        if (message.state === "CONNECTED") {
                            setState("CONNECTED");
                            return;
                        }

                        if (message.state === "FIRST_FRAME") {
                            setState("FIRST_FRAME");
                            return;
                        }

                        if (message.state === "DISCONNECTED") {
                            setState("DISCONNECTED");
                            return;
                        }
                    }

                    if (message.type === "error") {
                        setError(message.message ?? "Erro VNC desconhecido.");
                        setState("ERROR");
                    }

                    return;
                }

                drawFrame(event.data);
            };

            socket.onerror = () => {
                setError("Erro na ligação WebSocket da consola VNC.");
                setState("ERROR");
            };

            socket.onclose = () => {
                wsRef.current = null;
                clearFrame();

                setState((current) =>
                    current === "ERROR" ? "ERROR" : "DISCONNECTED",
                );
            };
        },
        [closeSocket, drawFrame, host, password, port, websocketUrl],
    );

    const disconnect = useCallback(() => {
        closeSocket();
        clearFrame();
        setState("DISCONNECTED");
    }, [clearFrame, closeSocket]);

    const sendClick = useCallback((clientX: number, clientY: number) => {
        const canvas = canvasRef.current;
        const ws = wsRef.current;
        const framebuffer = framebufferRef.current;

        if (!canvas || !ws || ws.readyState !== WebSocket.OPEN) {
            return;
        }

        if (!framebuffer.width || !framebuffer.height) {
            return;
        }

        const rect = canvas.getBoundingClientRect();

        const relativeX = (clientX - rect.left) / rect.width;
        const relativeY = (clientY - rect.top) / rect.height;

        const x = Math.max(
            0,
            Math.min(framebuffer.width - 1, Math.round(relativeX * framebuffer.width)),
        );

        const y = Math.max(
            0,
            Math.min(framebuffer.height - 1, Math.round(relativeY * framebuffer.height)),
        );

        ws.send(
            JSON.stringify({
                type: "click",
                x,
                y,
            }),
        );
    }, []);

    const handleCanvasPointerDown = useCallback(
        (event: React.PointerEvent) => {
            event.preventDefault();
            sendClick(event.clientX, event.clientY);
        },
        [sendClick],
    );

    useEffect(() => {
        return () => {
            closeSocket();
        };
    }, [closeSocket]);

    return {
        canvasRef,
        state,
        error,
        host,
        setHost,
        port,
        setPort,
        password,
        setPassword,
        frameSize,
        lastFrameAt,
        connect,
        disconnect,
        handleCanvasPointerDown,
        connected: state === "CONNECTED" || state === "FIRST_FRAME",
        connecting: state === "CONNECTING_WS" || state === "CONNECTING_VNC",
    };
}
