import { inRect } from "@common/geometry";
import { SeededRandom } from "@common/random";

export namespace BSP {
    export interface BSPOptions {
        depth?: number;
        minWidth?: number;
        minHeight?: number;
        seed?: string | number;
        random?: SeededRandom;
    }
    interface Node {
        id: string;
        x: number;
        y: number;
        width: number;
        height: number;
        depth: number;
        children: Node[];
    }
    type Carver = (x: number, y: number, node?: Node) => void;
    export function generateLevel(
        width: number,
        height: number,
        carver: Carver,
        options?: BSPOptions,
    ) {
        const minWidth = options?.minWidth ?? 4;
        const minHeight = options?.minHeight ?? 4;
        const depth = options?.depth ?? 4;
        const random = options?.random ?? new SeededRandom(options?.seed);

        const root: Node = {
            id: '/',
            x: 0,
            y: 0,
            width: width,
            height: height,
            depth: 0,
            children: []
        };

        const carved = new Set<string>();

        const dedupCarver = (x: number, y: number, node?: Node) => {
            if (carved.has(`${x},${y}`)) return;
            carved.add(`${x},${y}`);
            carver(x, y, node);
        }

        const stack = [root];

        while (stack.length > 0) {
            const node = stack.pop()!;

            if (node.depth >= depth) continue;

            const splitHorizontally = node.width > node.height;

            if (splitHorizontally && node.width > minWidth * 2) {
                const splitX = random.nextInt(minWidth, node.width - minWidth);

                const nodeA: Node = {
                    id: node.id + '/A',
                    x: node.x,
                    y: node.y,
                    width: splitX,
                    height: node.height,
                    depth: node.depth + 1,
                    children: []
                };
                const nodeB: Node = {
                    id: node.id + '/B',
                    x: node.x + splitX,
                    y: node.y,
                    width: node.width - splitX,
                    height: node.height,
                    depth: node.depth + 1,
                    children: []
                };
                node.children = [nodeA, nodeB];
                stack.push(nodeA, nodeB);
            } else if (!splitHorizontally && node.height > minHeight * 2) {
                const splitY = random.nextInt(minHeight, node.height - minHeight);

                const nodeA: Node = {
                    id: node.id + '/A',
                    x: node.x,
                    y: node.y,
                    width: node.width,
                    height: splitY,
                    depth: node.depth + 1,
                    children: []
                };
                const nodeB: Node = {
                    id: node.id + '/B',
                    x: node.x,
                    y: node.y + splitY,
                    width: node.width,
                    height: node.height - splitY,
                    depth: node.depth + 1,
                    children: []
                };
                node.children = [nodeA, nodeB];
                stack.push(nodeA, nodeB);
            } else {
                // Leaf node
                node.children = [];
            }
        }

        // Carve rooms

        stack.push(root);

        while (stack.length > 0) {
            const node = stack.pop()!;
            if (node.children.length === 0) {
                const width = random.nextInt(minWidth, node.width) - 2;
                const height = random.nextInt(minHeight, node.height) - 2;
                const x = random.nextInt(node.x + 1, node.x + node.width - width - 1);
                const y = random.nextInt(node.y + 1, node.y + node.height - height - 1);

                node.x = x;
                node.y = y;
                node.width = width;
                node.height = height;

                carveRoom(x, y, width, height, node, dedupCarver);
            } else {
                stack.push(...node.children);
            }
        }

        stack.push(root);

        // Carve corridors
        while (stack.length > 0) {
            const node = stack.pop()!;

            if (node.children.length === 0) continue;

            const [nodeA, nodeB] = node.children;

            const leafA = findLeaf(nodeA, nodeB);
            const leafB = findLeaf(nodeB, nodeA);

            carveCorridor(leafA, leafB, dedupCarver, random);
            stack.push(...node.children);
        }
    }

    function carveRoom(x: number, y: number, width: number, height: number, node: Node, carver: Carver) {
        for (let i = x; i < x + width; i++) {
            for (let j = y; j < y + height; j++) {
                carver(i, j, node);
            }
        }
    }

    function carveCorridor(nodeA: Node, nodeB: Node, carver: Carver, random: SeededRandom) {
        let ax = random.nextInt(nodeA.x, nodeA.x + nodeA.width);
        let ay = random.nextInt(nodeA.y, nodeA.y + nodeA.height);

        const bx = random.nextInt(nodeB.x, nodeB.x + nodeB.width);
        const by = random.nextInt(nodeB.y, nodeB.y + nodeB.height);

        const dx = Math.sign(bx - ax);
        const dy = Math.sign(by - ay);

        for (; ax !== bx; ax += dx) {
            if (inRect(ax, ay, nodeA)) {
                continue;
            }
            if (inRect(ax, ay, nodeB)) {
                break;
            }
            carver(ax, ay);
        }
        for (; ay != by; ay += dy) {
            if (inRect(ax, ay, nodeA)) {
                continue;
            }
            if (inRect(ax, ay, nodeB)) {
                break;
            }
            carver(ax, ay);
        }
    }

    const center = (node: Node) => ({
        x: node.x + node.width / 2,
        y: node.y + node.height / 2
    });

    function findLeaf(node: Node, closestNode: Node): Node {
        if (node.children.length === 0) {
            return node;
        }

        let minDist = Infinity;
        let closest = null;
        const closestCenter = center(closestNode);

        for (const child of node.children.map(child => findLeaf(child, closestNode))) {
            const childCenter = center(child);
            const dist = Math.abs(childCenter.x - closestCenter.x) + Math.abs(childCenter.y - closestCenter.y);
            if (dist < minDist) {
                minDist = dist;
                closest = child;
            }
        }

        return closest ?? node;
    }
}