import { enumerate, randInt, range, shuffle, zip } from "@common/utils";
import Entity from "./entity";
import Tile, { TileType } from "./tile";
import Pathfinding from "./pathfinding";
import Item, { ItemType } from "./item";
import Player, { Players } from "./player";
import Spinner, { SpinnerAction } from "./spinner";
import Character from "./character";

enum GameState {
    NORMAL,
    FIGHT,
}

export default class TileMap extends Entity {
    private tiles: Tile[] = [];
    public startTile: Tile;

    public readonly players: Player[];
    private currentPlayerIdx = 0;
    private state = GameState.NORMAL;
    private availableTiles: Tile[] = [];

    public keysFound = false;
    public fuelFound = false;
    public bossKilled = false;
    public readonly foundPlayers: Set<Player>;

    constructor(
        position: [number, number],
        private mapSize: number,
        private tileSize: number,
        private spinner: Spinner,
        numPlayers: number = 2,
    ) {
        super(position, [mapSize * tileSize, mapSize * tileSize]);
        this.players = shuffle(Object.values(Players)).slice(0, numPlayers);
        this.foundPlayers = new Set(this.players);
        this.startTile = this.createMap();
        this.findAvailableTiles();
    }

    public createMap() {
        const map = range(this.mapSize)
            .map(x =>
                range(this.mapSize)
                    .map(y =>
                        new Tile([x * this.tileSize, y * this.tileSize], this.tileSize)
                    )
            );

        const startTile = new Tile([0, (this.mapSize - 2) * this.tileSize], this.tileSize * 2, TileType.START);

        delete map[0][this.mapSize - 2];
        delete map[1][this.mapSize - 2];
        delete map[1][this.mapSize - 1];

        map[0][this.mapSize - 1] = startTile;

        const verticalWalls: [number, number][] = [
            [1, 3], [3, 3], [6, 3], [10, 3],
            [3, 4],
            [1, 5], [6, 5], [10, 5],
            [1, 6], [6, 6], [10, 6],
            [1, 8], [6, 8], [10, 8],
            [1, 9], [6, 9], [10, 9],
        ];
        const horizontalWalls: [number, number][] = [
            [2, 2], [2, 5], [2, 9],
            [3, 2], [3, 5], [3, 9],
            [4, 2], [5, 5],
            [6, 2], [6, 5], [6, 9],
            [7, 2], [7, 5], [7, 9],
            [8, 2], [8, 5],
            [9, 2], [9, 9],
            [10, 2], [10, 5], [10, 9],
        ];

        for (let x = 0; x < map.length; x++) {
            const column = map[x];
            if (!column) continue;
            for (let y = 0; y < column.length; y++) {
                const tile = column[y];
                if (!tile) continue;
                tile.connections = [
                    verticalWalls.find(w => w[0] === x - 1 && w[1] === y)
                        ? undefined
                        : map[x - 1]?.[y],

                    verticalWalls.find(w => w[0] === x && w[1] === y)
                        ? undefined
                        : map[x + 1]?.[y],

                    horizontalWalls.find(w => w[0] === x && w[1] === y - 1)
                        ? undefined
                        : map[x]?.[y - 1],

                    horizontalWalls.find(w => w[0] === x && w[1] === y)
                        ? undefined
                        : map[x]?.[y + 1],
                ].filter(t => t != null);
            }
        }

        startTile.connections = [
            map[0][this.mapSize - 3],
            map[1][this.mapSize - 3],
            map[2][this.mapSize - 2],
            map[2][this.mapSize - 1],
        ].filter(t => t);

        startTile.connections.forEach(t => t.connections.push(startTile));

        const endTiles = [[8, 1], [9, 1], [10, 1]];
        for (const [x, y] of endTiles) {
            map[x][y].type = TileType.END;
        }

        const doors = [
            [2, 4, 1, 4],
            [2, 7, 1, 7],
            [3, 5, 4, 5],
            [4, 6, 4, 5],
            [4, 10, 4, 9],
            [5, 3, 5, 2],
            [5, 10, 5, 9],
            [6, 4, 7, 4],
            [6, 7, 7, 7],
            [8, 10, 8, 9],
            [9, 6, 9, 5],
            [10, 4, 11, 4],
            [10, 7, 11, 7],
        ];

        for (const [doorX, doorY, wayX, wayY] of doors) {
            map[doorX][doorY].type = TileType.DOOR;
            map[wayX][wayY].type = TileType.DOORWAY;
        }

        this.tiles = map.flat();
        this.startTile = startTile;

        this.fillItems();
        this.players.forEach(c => c.tile = startTile);

        return startTile;
    }

    public fillItems() {
        const itemsMap = new Map([
            [ItemType.ENEMY_BOSS, 1],
            [ItemType.ENEMY_DOG, 5],
            [ItemType.ENEMY_SPIDER, 5],
            [ItemType.ENEMY_ZOMBIE, 17],

            [ItemType.ITEM_FUEL, 1],
            [ItemType.ITEM_HEALING_KIT, 6],
            [ItemType.ITEM_KEYS, 1],
            [ItemType.ITEM_PLANKS, 8],

            [ItemType.WEAPON_ASSAULT_RIFLE, 1],
            [ItemType.WEAPON_AXE, 1],
            [ItemType.WEAPON_CROSSBOW, 1],
            [ItemType.WEAPON_GRENADE, 4],
            [ItemType.WEAPON_KNIFE, 1],
            [ItemType.WEAPON_PISTOL, 1],
            [ItemType.WEAPON_ROCKET_LAUNCHER, 1],
            [ItemType.WEAPON_SHOTGUN, 1],
        ]);

        const items: Item[] = [];

        for (const [type, amount] of itemsMap.entries()) {
            for (const _ in range(amount)) {
                const Constructor = [
                    ItemType.ENEMY_BOSS,
                    ItemType.ENEMY_CROW,
                    ItemType.ENEMY_DOG,
                    ItemType.ENEMY_SPIDER,
                    ItemType.ENEMY_ZOMBIE,
                ].includes(type) ? Character : Item;

                const item = new Constructor(type);
                items.push(item);
            }
        }

        for (const char of Object.values(Players)) {
            if (this.players.includes(char)) continue;
            items.push(char);
        }

        const endTiles = this.tiles.filter(t => t.type === TileType.END);
        const doorTiles = this.tiles.filter(t => t.type === TileType.DOOR || t.type === TileType.DOORWAY);
        const normalTiles = this.tiles.filter(t =>
            t.type === TileType.NORMAL
            && !this.startTile.connections.includes(t)
        );

        const fillableTiles = [
            ...endTiles,
            ...this.startTile.connections,
            ...doorTiles,
            ...shuffle(normalTiles),
        ];

        for (const [tile, item] of zip(fillableTiles, shuffle(items))) {
            tile.addItem(item);
            if (item instanceof Character) {
                item.tile = tile;
            }
        }

        for (const tile of this.tiles) {
            if (!tile.hasAnyItems) {
                tile.isOpen = true;
            }
        }
    }

    get player() {
        return this.players[this.currentPlayerIdx];
    }

    get activeTiles() {
        return this.tiles.filter(tile => (
            tile.hasAnyItems
            || tile.type === TileType.LOCKED_DOOR
            || tile.hovered
            || (this.state === GameState.NORMAL && this.availableTiles.includes(tile))
        ));
    }

    private get enemies() {
        return this.tiles.filter(
            (tile) =>
                tile.isOpen
                && tile.enemy
                && tile.hasSingleItem
        )
    }

    public canTransferTo(player: Player, target: Player): boolean {
        if (target === player) return false;

        if (!player.hasAnyItems) return false;

        if (player.tile === target.tile) {
            return true;
        }
        for (const tile of player.tile.connections) {
            if (target.tile === tile) {
                return true;
            }
        }

        return false;
    }

    public canHeal(healer: Player, patient: Player): boolean {
        if (!healer.healingKit) return false;

        return healer === patient || this.canTransferTo(healer, patient);
    }

    public override handleMouseMove(x: number, y: number): void {
        this.activeTiles.forEach(tile => tile.handleMouseMove(x - this.left, y - this.top));
    }

    public override handleClick(x: number, y: number): void {
        if (this.state !== GameState.NORMAL) {
            return;
        }
        for (const tile of this.availableTiles) {
            if (tile.isPointInBounds(x - this.left, y - this.top)) {
                const path = Pathfinding.findPath(this.player.tile, tile);
                if (path.length > 1) {
                    this.spinner.stop();
                    this.player.lastDoor = path.find(t => t.type === TileType.DOOR);
                    this.player.moveTo(tile, path);
                    tile.open();
                    for (const item of tile) {
                        if (item.isPickable) {
                            if (!tile.enemy) {
                                tile.removeItem(item);
                                this.player.addItem(item);
                            }
                        } else if (item instanceof Player) {
                            tile.removeItem(item);
                            this.players.push(item);
                            this.foundPlayers.add(item);
                        } else if (item.isBoss && this.player.removeItem(this.player.rocketLauncher)) {
                            tile.killEnemy();
                            this.bossKilled = true;
                        } else if (item.isEnemy) {
                            this.state = GameState.FIGHT;
                            break;
                        } else {
                            alert(`Unknown item found: ${item}`);
                        }
                    }
                    if (tile.type === TileType.END) {
                        if (this.player.removeItem(this.player.fuel)) {
                            this.fuelFound = true;
                        }
                        if (this.player.removeItem(this.player.keys)) {
                            this.keysFound = true;
                        }
                    }
                    if (this.state === GameState.NORMAL) {
                        this.nextPlayer();
                    }
                }

                this.findAvailableTiles();
                break;
            }
        }
    }

    public handleSpin(action: SpinnerAction) {
        switch (this.state) {
            case GameState.NORMAL:
                this.findAvailableTiles(action);
                break;
            case GameState.FIGHT:
                const success = this.player.handleSpin(action);
                if (success) {
                    switch (action) {
                        case SpinnerAction.BITE:
                            if (this.player.isDead) {
                                this.player.removeAllItems().forEach(i => this.player.tile.addItem(i));
                                this.players.splice(this.currentPlayerIdx, 1);
                                this.currentPlayerIdx = this.currentPlayerIdx % this.players.length;
                                this.setNormalState();
                            } else if (!this.player.grenade) {
                                this.spinner.spin();
                            }
                            break;
                        case SpinnerAction.MELEE:
                        case SpinnerAction.SHOOT:
                            this.killEnemy();
                            break;
                        case SpinnerAction.RUN:
                            this.setNormalState();
                            this.spinner.spin();
                            break;
                    }
                } else if (!this.player.grenade) {
                    this.spinner.spin();
                }
                break;
        }
    }

    public handleItemUse(player: Player, item: Item) {
        let success = player.hasItem(item);
        if (success) {
            if (item.type === ItemType.WEAPON_GRENADE && this.state === GameState.FIGHT) {
                this.killEnemy();
            } else if (item.type === ItemType.ITEM_PLANKS && player.lastDoor && !player.lastDoor.enemy) {
                player.lastDoor.type = TileType.LOCKED_DOOR;
            } else {
                success = false;
                console.warn(`Dont know how to use ${item}`);
            }
        }
        if (success) {
            player.removeItem(item);
        }
    }

    private findAvailableTiles(moveDistance: number = 1) {
        const playerTiles = new Set(this.players.map(c => c.tile));
        this.availableTiles = Pathfinding.findPossibleMoves(this.player.tile, moveDistance + this.player.moveBonus)
            .filter(t => !playerTiles.has(t));
    }

    private setNormalState() {
        this.state = GameState.NORMAL;
        this.findAvailableTiles();
    }

    private nextPlayer() {
        this.spinner.stop();
        let overflow = false;
        do {
            let nextIdx = this.currentPlayerIdx + 1;
            if (nextIdx >= this.players.length) {
                if (overflow) {
                    this.currentPlayerIdx = 0;
                    this.player.active = true;
                    break;
                } else {
                    overflow = true;
                    nextIdx = 0;
                }
            }
            this.currentPlayerIdx = nextIdx;
        } while (!this.player.active);

        const numFoundPlayers = this.foundPlayers.size;
        const enemiesActive = numFoundPlayers === Object.keys(Players).length 
            || this.players.length < numFoundPlayers;

        if (this.win) {
            const endTile = this.tiles.findLast(t => t.type === TileType.END);
            if (endTile) {
                for (const player of this.players) {
                    const path = Pathfinding.findPath(player.tile, endTile);
                    player.moveTo(endTile, path);
                }
            }
            setTimeout(alert, 2000, "🎉🎉🎉 ПОБЕДА! 🚗 🎉🎉🎉");
        } else if (overflow && enemiesActive) {
            const enemies = this.enemies;

            loop:
            for (const enemyTile of shuffle(enemies)) {
                const moves = Pathfinding.findPossibleMoves(enemyTile, 5);

                for (const move of shuffle(moves)) {
                    if (move.hasAnyItems) continue;

                    for (const [playerIndex, player] of enumerate(this.players)) {
                        if (player.tile === move) {
                            const enemy = enemyTile.enemy!;
                            const allowedSteps = randInt(1, 5) + enemy.moveBonus;

                            console.log({ enemy, allowedSteps });
                            if (allowedSteps <= 0) continue;

                            const path = Pathfinding.findPath(enemyTile, player.tile, true);

                            const targetTile = path[allowedSteps] ?? player.tile;

                            enemyTile.removeItem(enemy);
                            targetTile.addItem(enemy);
                            enemy.moveTo(targetTile, path.slice(0, allowedSteps + 1));

                            if (targetTile === player.tile) {
                                this.state = GameState.FIGHT;
                                this.currentPlayerIdx = playerIndex;
                            }

                            break loop;
                        }
                    }
                }
            }
        }
    }

    private killEnemy() {
        this.player.tile.killEnemy();
        for (const item of this.player.tile) {
            if (item.isPickable) {
                this.player.addItem(item);
                this.player.tile.removeItem(item);
            }
        };
        this.nextPlayer();
        this.setNormalState();
    }

    private get win() {
        if (!this.keysFound || !this.fuelFound || !this.bossKilled) {
            return false;
        }
        const endTiles = this.tiles.filter(t => t.type === TileType.END);
        for (const tile of endTiles) {
            if (tile.hasAnyItems) return false;
        }
        for (const player of Object.values(Players)) {
            if (!this.foundPlayers.has(player)) return false;
            if (player.isDead) continue;

            if (endTiles.every((tile) => {
                const path = Pathfinding.findPath(player.tile, tile);
                return path.length === 0;
            })) {
                return false;
            }
        }
        return true;
    }

    protected draw(ctx: CanvasRenderingContext2D): void {
        ctx.scale(1 / this.width, 1 / this.height);

        ctx.lineWidth = 2;
        ctx.fillStyle = 'rgba(0, 255, 0, 0.5)';

        if (this.state === GameState.NORMAL) {
            this.availableTiles.forEach(t =>
                ctx.fillRect(t.centerX - t.width / 2, t.centerY - t.height / 2, t.width, t.height)
            );
        }

        const w = this.tileSize * 0.8;
        [...this.players, this.player].forEach((player) => {
            const [x, y] = player.displayPosition;
            ctx.drawImage(player.type, x - w / 2, y - w / 2, w, w);
        });

        this.activeTiles.forEach(t => t.render(ctx));

        ctx.lineWidth = 5;
        ctx.strokeStyle = 'yellow';

        ctx.strokeRect(this.player.tile.centerX - w / 2, this.player.tile.centerY - w / 2, w, w);

        let x = this.width + this.tileSize / 2 + 10;
        let y = this.height - this.tileSize / 2;
        for (const player of Object.values(Players)) {
            this.drawItem(ctx, player.type, x, y, w, this.foundPlayers.has(player));
            x += this.tileSize;
        }

        x = this.width + this.tileSize / 2 + 10;
        y = this.height - 3 * this.tileSize / 2;
        for (const [item, found] of zip(
            [ItemType.ITEM_KEYS, ItemType.ITEM_FUEL, ItemType.ENEMY_BOSS],
            [this.keysFound, this.fuelFound, this.bossKilled],
        )) {
            this.drawItem(ctx, item, x, y, w, found);
            x += this.tileSize;
        }

        x = this.width + this.tileSize / 2 + 10;
        y = this.height - 5 * this.tileSize / 2;
        for (const tile of this.tiles) {
            if (tile.type !== TileType.END) continue;

            this.drawItem(ctx, ItemType.ENEMY_ZOMBIE, x, y, w, !tile.hasAnyItems);
            x += this.tileSize;
        }
    }

    private drawItem(
        ctx: CanvasRenderingContext2D,
        img: HTMLImageElement,
        x: number, y: number, w: number,
        found: boolean,
    ) {
        ctx.drawImage(img, x - w / 2, y - w / 2, w, w);

        if (!found) {
            ctx.fillStyle = `rgba(255, 255, 255, 0.6)`;
            ctx.fillRect(x - w / 2, y - w / 2, w, w);
        }
    }

    public override update(dt: number) {
        this.players.forEach((player) => player.update(dt));
        this.enemies.forEach((tile) => tile.enemy!.update(dt));
    }
}