import { Inventory } from "@common/rpg/components/inventory";
import { getPosition, Position } from "@common/rpg/components/position";
import { Hidden, Sprite } from "@common/rpg/components/sprite";
import { Component, type Entity, type World } from "@common/rpg/core/world";
import { component } from "@common/rpg/utils/decorators";
import { randInt } from "@common/utils";
import { Direction, DIRECTION_OFFSETS, getOppositeDirection, MAP_ROOM_CHARS, ROOM_AREA_HEIGHT, ROOM_AREA_WIDTH, ROOM_AREA_X, ROOM_AREA_Y } from "./const";
import { DOOR_SPRITE, ITEM_SPRITE } from "./images";
import { spawnItem } from "./item";
import { Resources } from "@common/rpg/utils/resources";
import { TextRegion } from "@common/display/text";

type IDoors = [string?, string?, string?, string?, string?, string?];

@component
export class Room extends Component<{ doors: IDoors }> {
    get x() {
        const pos = this.entity.get(Position);
        return pos ? pos.state.x : 0;
    }
    get y() {
        const pos = this.entity.get(Position);
        return pos ? pos.state.y : 0;
    }
    get width() {
        const pos = this.entity.get(Position, 'size');
        return pos ? pos.state.x : 0;
    }
    get height() {
        const pos = this.entity.get(Position, 'size');
        return pos ? pos.state.y : 0;
    }
    get worldX() {
        const pos = this.entity.get(Position, 'world');
        return pos ? pos.state.x : 0;
    }
    get worldY() {
        const pos = this.entity.get(Position, 'world');
        return pos ? pos.state.y : 0;
    }
    get worldZ() {
        const pos = this.entity.get(Position, 'world');
        return pos ? pos.state.z : 0;
    }
    get doors() {
        return this.state.doors;
    }
    getActivatedDoor(px: number, py: number): Door | undefined {
        const id = this.state.doors.find((doorId) => {
            if (!doorId) return false;

            const door = this.entity.world.getEntity(doorId);
            const doorPos = door?.get(Position);
            if (!doorPos) return false;

            const offset = door?.get(Position, 'offset');
            const { x: dx = 0, y: dy = 0 } = offset?.state ?? {}

            const { x, y } = doorPos.state;
            return px === x - dx && py === y - dy;
        });

        if (!id) return undefined;

        return this.entity.world.getEntity(id)?.get(Door);
    }

    getDoor(direction: Direction): Door | undefined {
        const id = this.doors[direction];
        if (!id) return undefined;
        return this.entity.world.getEntity(id)?.get(Door);
    }

    onEnter() {
        this.doors.forEach(doorId => doorId && this.entity.world.getEntity(doorId)?.remove(Hidden));
    }

    onLeave() {
        this.doors.forEach(doorId => doorId && this.entity.world.getEntity(doorId)?.add(new Hidden()));
    }
}

@component
export class Door extends Component<{ direction: Direction }> {
    constructor(direction: Direction) {
        super({ direction });
    }
    get x() {
        const pos = this.entity.get(Position);
        return pos ? pos.state.x : 0;
    }
    get y() {
        const pos = this.entity.get(Position);
        return pos ? pos.state.y : 0;
    }
    get direction() {
        return this.state.direction;
    }
    get worldX() {
        const pos = this.entity.get(Position, 'world');
        return pos ? pos.state.x : 0;
    }
    get worldY() {
        const pos = this.entity.get(Position, 'world');
        return pos ? pos.state.y : 0;
    }
    get worldZ() {
        const pos = this.entity.get(Position, 'world');
        return pos ? pos.state.z : 0;
    }
}

interface IRoomBlank {
    x: number;
    y: number;
    width: number;
    height: number;
    worldX: number;
    worldY: number;
    worldZ: number;
    doors: IDoors;
};

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

const createDoor = (world: World, x: number, y: number, direction: Direction, worldX: number, worldY: number, worldZ: number): string => {
    const door = world.createEntity('door_*');
    door.add(new Position(x, y));
    door.add(new Position(worldX, worldY, worldZ), 'world');
    door.add(new Door(direction));
    const resourceId = DOOR_SPRITE[direction];
    door.add(new Sprite(resourceId));
    const spr = Resources.get(TextRegion, resourceId);
    if (spr) {
        door.add(new Position((-spr.width / 2) | 0, (-spr.height / 2) | 0), 'offset');
    }
    return door.id;
}

const generateDoors = (world: World, { x, y, width, height, worldX, worldY, worldZ }: IRoomBlank): IDoors => {
    const doors: IDoors = [];
    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 = roomId(doorWorldX, doorWorldY, worldZ);
        const doorRoom = world.getEntity(doorId);
        if (doorRoom && !doorRoom.get(Room)?.state.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] = createDoor(world, 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] = createDoor(world, 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] = createDoor(world, upDoorX, upDoorY, Direction.UP, 0, 0, worldZ + upZoff);
        }
    }

    return doors;
};

const generatedItems = new Set<number>();

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

    const hasObjectAt = (x: number, y: number) =>
        doors
            .filter(x => x != null)
            .map((id) => getPosition(world.getEntity(id)))
            .some((d) => d?.x === x && d.y === y)
        || items.map((i) => getPosition(i))
            .some((i) => i?.x === x && i.y === y);

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

    const id = randInt(0, ITEM_SPRITE.length);

    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 = spawnItem(world, id, 1, newItemX, newItemY);

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

    return items;
};

export function getRoom(world: World, worldX: number, worldY: number, worldZ: number): Entity {
    const id = roomId(worldX, worldY, worldZ);
    const existingRoom = world.getEntity(id);
    if (existingRoom) {
        return existingRoom;
    }
    const room = world.createEntity(id);

    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);

    room.add(new Position(x, y));
    room.add(new Position(width, height), 'size');
    room.add(new Position(worldX, worldY, worldZ), 'world');

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

    const doors = generateDoors(world, roomBlank);
    room.add(new Room({ doors }));
    const inv = room.add(new Inventory());

    const items = generateItems(world, roomBlank);

    items.forEach((item) => inv.add(item));

    return room;
}

export const getAllRooms = (world: World) => Array.from(world.query(Room)).map(x => x[0]);
export const getRoomsCount = (world: World) => Array.from(world.query(Room)).length;
export const getPossibleRoomsCount = (world: World) => {
    const rooms = getAllRooms(world);
    const roomsSet = new Set(rooms.map(x => x.id));
    for (const room of rooms) {
        const roomComp = room.get(Room);
        roomComp?.state.doors.forEach((d) => {
            if (!d) return;
            const worldPos = getPosition(world.getEntity(d) ,'world');
            if (worldPos) {
                const id = roomId(worldPos.x, worldPos.y, worldPos.z);
                roomsSet.add(id);
            }
        });
    }

    return roomsSet.size;
};

export const getRoomsForLayer = (world: World, z: number): Entity[] =>
    getAllRooms(world).filter(r => r.get(Position, 'world')?.state.z === z);

export function getMapRoomChar(room: Entity): string {
    const roomComp = room.get(Room);
    if (!roomComp) return ' ';

    const doors = roomComp.state.doors;
    let doorsMask = 0;
    for (let i = 0; i < 4; i++) {
        const mask = 1 << i;
        if (doors[i]) {
            doorsMask |= mask;
        }
    }

    return MAP_ROOM_CHARS[doorsMask];
}
