import { randInt } from "@common/utils";
import { Color, Direction, DIRECTION_OFFSETS, getOppositeDirection, ROOM_AREA_HEIGHT, ROOM_AREA_WIDTH, ROOM_AREA_X, ROOM_AREA_Y } from "./const";
import { drawBox } from "./display";
import { Drawable } from "./drawable";
import { Figure } from "./figure";
import { DOOR_SPRITE } from "./images";
import { getItemsCount, Item } from "./item";

type IDoors = [Door | null, Door | null, Door | null, Door | null, Door | null, Door | null];

export class Door extends Figure {
    constructor(
        x: number,
        y: number,
        direction: Direction,
        public worldX: number,
        public worldY: number,
        public worldZ: number,
    ) {
        super(x, y, DOOR_SPRITE);
        this.frame = direction;
    }

    isActivated(x: number, y: number): boolean {
        const cx = Math.floor(this.x + this.width / 2);
        const cy = Math.floor(this.y + this.height / 2);

        return (cx === x && cy === y);
    }

    get direction(): Direction {
        return this.frame as Direction;
    }
}

export class Room extends Drawable {
    constructor(
        public readonly x: number,
        public readonly y: number,
        public readonly width: number,
        public readonly height: number,
        public readonly worldX: number,
        public readonly worldY: number,
        public readonly worldZ: number,
        public readonly doors: IDoors = [null, null, null, null, null, null],
        public readonly items: Item[] = [],
    ) {
        super();
    }

    override doDraw() {
        drawBox(ROOM_AREA_X, ROOM_AREA_Y, ROOM_AREA_WIDTH - 2, ROOM_AREA_HEIGHT - 2, { fill: [' '] });
        drawBox(this.x, this.y, this.width, this.height, { fill: ['.', Color.GRAY] });

        this.doors.forEach((door) => {
            if (door) {
                door.doDraw();
            }
        });

        this.items.forEach((item) => item.doDraw());
    }

    getActivatedDoor(x: number, y: number): Door | null {
        return this.doors.find((door) => door?.isActivated(x, y)) ?? null;
    }

    pickItem(x: number, y: number): Item | null {
        const itemIndex = this.items.findIndex((item) => item.x === x && item.y === y);
        if (itemIndex >= 0) {
            const [item] = this.items.splice(itemIndex, 1);
            this.invalidate();
            return item;
        }
        return null;
    }
}

type IRoomBlank = {
    -readonly [P in keyof Room]?: Room[P]
};

const rooms = new Map<string, Room>();

const generateRoomId = (worldX: number, worldY: number, worldZ: number) => `${worldX},${worldY},${worldZ}`;

const generateDoors = ({ x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
    const doors: IDoors = [null, null, null, null, null, null];
    if (typeof worldX === 'undefined' || typeof worldY === 'undefined' || typeof worldZ === 'undefined') {
        console.error('World coordinates not defined for doors generation');
        return doors;
    }
    if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
        console.error('Screen coordinates not defined for doors generation');
        return doors;
    }

    for (let i = 0; i < 4; i++) {
        const [xoff, yoff] = DIRECTION_OFFSETS[i];
        const doorWorldX = worldX + xoff;
        const doorWorldY = worldY + yoff;
        const doorId = generateRoomId(doorWorldX, doorWorldY, worldZ);
        const doorRoom = rooms.get(doorId);
        if (doorRoom && !doorRoom.doors[getOppositeDirection(i)]) {
            continue;
        }

        if ((worldX === 0 && worldY === 0) || doorRoom || Math.random() < 0.3) {
            const doorX = randInt(x + 1, x + width - 2);
            const doorY = randInt(y + 1, y + height - 2);
            let dx = 0;
            let dy = 0;
            switch (i) {
                case Direction.NORTH: dx = doorX; dy = y; break;
                case Direction.SOUTH: dx = doorX; dy = y + height + 1; break;
                case Direction.EAST: dx = x + width + 1; dy = doorY; break;
                case Direction.WEST: dx = x; dy = doorY; break;
            }
            doors[i] = new Door(dx, dy, i as Direction, doorWorldX, doorWorldY, worldZ);
        }
    }

    if (worldX === 0 && worldY === 0) {
        const doorX = randInt(x + 1, x + width - 2);
        const doorY = randInt(y + 1, y + height - 2);
        const [, , zoff] = DIRECTION_OFFSETS[Direction.DOWN];
        doors[Direction.DOWN] = new Door(doorX, doorY, Direction.DOWN, 0, 0, worldZ + zoff);

        if (worldZ !== 0) {
            let upDoorX: number;
            let upDoorY: number;
            do {
                upDoorX = randInt(x + 1, x + width - 2);
                upDoorY = randInt(y + 1, y + height - 2);
            } while (doorX === upDoorX && doorY === upDoorY);
            const [, , upZoff] = DIRECTION_OFFSETS[Direction.UP];
            doors[Direction.UP] = new Door(upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
        }
    }

    return doors;
};

const generatedItems = new Set<number>();

const generateItems = ({ x, y, width, height, doors }: IRoomBlank): Item[] => {
    const items: Item[] = [];
    if (typeof x === 'undefined' || typeof y === 'undefined' || typeof width === 'undefined' || typeof height === 'undefined') {
        console.error('Screen coordinates not defined for items generation');
        return items;
    }
    if (typeof doors === 'undefined') {
        console.error('Doors not defined for items generation');
        return items;
    }

    const hasObjectAt = (x: number, y: number) =>
        doors.some((d) => d?.x === x && d?.y === y) ||
        items.some((i) => i.x === x && i.y === y);

    const oneDoor = doors.filter((d) => Boolean(d)).length === 1;

    const id = randInt(0, getItemsCount());

    if (Math.random() < 0.4 && oneDoor && !generatedItems.has(id)) {
        let newItemX: number;
        let newItemY: number;

        do {
            newItemX = randInt(x + 1, x + width - 2);
            newItemY = randInt(y + 1, y + height - 2);
        } while (hasObjectAt(newItemX, newItemY));
        const item = new Item(id, 1, newItemX, newItemY);

        items.push(item);
        generatedItems.add(id);
    }

    return items;
};

export function getRoom(worldX: number, worldY: number, worldZ: number) {
    const id = generateRoomId(worldX, worldY, worldZ);
    const existingRoom = rooms.get(id);
    if (existingRoom) {
        return existingRoom;
    }
    const width = randInt(5, ROOM_AREA_WIDTH - 5);
    const height = randInt(5, ROOM_AREA_HEIGHT - 5);

    const x = Math.max(ROOM_AREA_X + randInt(2, ROOM_AREA_WIDTH - width - 2), ROOM_AREA_X + 2);
    const y = Math.max(ROOM_AREA_Y + randInt(2, ROOM_AREA_HEIGHT - height - 2), ROOM_AREA_Y + 2);

    const roomBlank: IRoomBlank = { x, y, width, height, worldX, worldY, worldZ };

    roomBlank.doors = generateDoors(roomBlank);
    roomBlank.items = generateItems(roomBlank);

    const room = new Room(x, y, width, height, worldX, worldY, worldZ, roomBlank.doors, roomBlank.items);

    rooms.set(id, room);
    return room;
}

export const getRoomsCount = () => rooms.size;
export const getPossibleRoomsCount = () => {
    const roomsSet = new Set(rooms.keys());
    for (const room of rooms.values()) {
        room.doors.forEach((d) => {
            if (d) {
                const id = generateRoomId(d.worldX, d.worldY, d.worldZ);
                roomsSet.add(id);
            }
        });
    }

    return roomsSet.size;
};

export const getRoomsForLayer = (z: number): Room[] => Array.from(rooms.values()).filter((r) => r.worldZ === z);
