import { GdRoot } from "../data/GdRoot";
import { EDirection } from "../util/EDirection";
import { Hex } from "../util/Hex";
import { PathFinding } from "../util/PathFinding";
import { ECellType } from "./ECellType";
import { SimCell } from "./SimCell";
import { SimEnemy } from "./SimEnemy";
import { SimProjectile } from "./SimProjectile";

export class SimLevel {
	paused: boolean = true;
	currentStep: number = -1;
	highlightedIndex: number = -1;
	currency: number = -1;
	stride: number = -1;
	cells: SimCell[] = [];
	enemies: SimEnemy[] = [];
	projectiles: SimProjectile[] = [];
	nextWaveStep: number = -1;
	lastEnemySpawnStep: number = 0;
	currentWave: number = -1;
	enemiesLeftToSpawn: number = 0;
	index: number = 0;
	radius: number = 0;

    constructor(gdRoot: GdRoot, levelIdx: number) {
        const gdLevel = gdRoot.levels[levelIdx];
        this.index = levelIdx;
        this.currency = gdLevel.currency;
        this.enemiesLeftToSpawn = gdLevel.waves[0].amount;

        this.radius = gdLevel.radius;
        this.stride = 2 * this.radius + 1;
        const h0 = new Hex(0, 0);
        for (let y = -this.radius; y <= this.radius; ++y) {
            for (let x = -this.radius; x <= this.radius; ++x) {
                const hex = new Hex(x, y);
                const distance = Hex.distance(hex, h0);
                const type = distance >= this.radius ? ECellType.Blocked : ECellType.Free;
                const cellIndex = this.getCellIndex(hex);
                this.cells[cellIndex] = new SimCell(hex, distance, type, cellIndex);
            }
        }

        gdLevel.walls.forEach((wall: Hex) => {
            const cellIndex = this.getCellIndex(wall);
            this.cells[cellIndex].type = ECellType.Blocked;
        });

        gdLevel.enemySpawns.forEach((hex: Hex) => {
            const cellIndex = this.getCellIndex(hex);
            this.cells[cellIndex].type = ECellType.Entry;
        });

        gdLevel.enemyTargets.forEach((hex: Hex) => {
            const cellIndex = this.getCellIndex(hex);
            this.cells[cellIndex].type = ECellType.Entry;
        });

        this.cells.forEach((cell: SimCell) => {
            this.updateBlockedType(cell);
        });

        this.updatePaths(gdRoot);
    };

    public getCellIndex(hex: Hex) {
        const x = hex.col + this.radius;
        const y = hex.row + this.radius;
        if (x < 0 || x >= this.stride || y < 0 || y >= this.stride) {
            return -1;
        }
        return y * this.stride + x;
    }

    public getNeighbourCell(cell: SimCell, direction: EDirection) {
        const hex = cell.hex;
        const neighbourHex = Hex.neighbour(hex, direction);
        const neighbourIndex = this.getCellIndex(neighbourHex);
        return this.cells[neighbourIndex];
    }

    public updateBlockedType(cell: SimCell) {
        if (cell.type == ECellType.Free) {
            cell.blockedType = -1;
            return;
        }

        if (cell.type == ECellType.Entry && cell.blockedType != -1) {
            return;
        }

        let blockedType = 0;
        for (let direction = 0; direction < 6; ++direction) {
            const neighbourCell = this.getNeighbourCell(cell, direction);
            if (!!neighbourCell && neighbourCell.type == ECellType.Free) {
                blockedType |= 1 << direction;
            }
        }

        cell.blockedType = blockedType;
    }

    public reservePaths(gdRoot: GdRoot, hexToBlock: Hex | null): boolean {
        try {
            if (!!hexToBlock) {
                const reservedCell = this.cells[this.getCellIndex(hexToBlock)];
                if (reservedCell.type != ECellType.Free || !!reservedCell.tower)
                    return false;

                reservedCell.type = ECellType.Reserved;
            }

            return this.updatePaths(gdRoot);
        }
        finally {
            if (!!hexToBlock) {
                const reservedCell = this.cells[this.getCellIndex(hexToBlock)];
                if (reservedCell.type == ECellType.Reserved)
                    reservedCell.type = ECellType.Free
            }
        }
    }

    public updatePaths(gdRoot: GdRoot): boolean {
        const newRoutePaths: number[][] = [];
        const newEnemyPaths: number[][] = [];
        const gdLevel = gdRoot.levels[this.index];
        let invalid = false;

        for (const routeIdx in gdLevel.enemyRoutes) {
            const route = gdLevel.enemyRoutes[routeIdx];
            const enemySpawnHex = gdLevel.enemySpawns[route[0]];
            const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)];
            const enemyTargetHex = gdLevel.enemyTargets[route[1]];
            const enemyTargetCell = this.cells[this.getCellIndex(enemyTargetHex)];
            const path = PathFinding.bfs(this, enemySpawnCell.index, enemyTargetCell.index);
            if (path == null) {
                invalid = true;
                break;
            }
            newRoutePaths[routeIdx] = path;
        }

        if (invalid) {
            return false;
        }

        for (let idx in this.enemies) {
            const simEnemy = this.enemies[idx];
            const hex = Hex.fromWorld(simEnemy.position);
            const startIndex = this.getCellIndex(hex);
            const endIndex = this.getCellIndex(simEnemy.endHex);
            const path = PathFinding.bfs(this, startIndex, endIndex);
            if (path == null) {
                invalid = true;
                break;
            }
            newEnemyPaths[idx] = path;
        }

        if (invalid) {
            return false;
        }

        for (const routeIdx in gdLevel.enemyRoutes) {
            const route = gdLevel.enemyRoutes[routeIdx];
            const enemySpawnHex = gdLevel.enemySpawns[route[0]];
            const enemySpawnCell = this.cells[this.getCellIndex(enemySpawnHex)];
            enemySpawnCell.pathsToTarget[routeIdx] = newRoutePaths[routeIdx];
        }

        this.enemies.forEach((simEnemy: SimEnemy, idx: number) => {
            simEnemy.path = newEnemyPaths[idx];
            simEnemy.currentPathIndex = 0;
            simEnemy.onPathUpdated(this);
        });

        return true;
    }
}
