import { GdLevel, GdRoot } from "../../GameData/index.js";
import { EDirection, Hex, PathFinding } from "../../Util/index.js";
import { ECellType, SimCell, SimEnemy, SimMain, SimProjectile } from "../index.js";

export class SimLevel {
    public static version: number = 2;

    private _paused: boolean = true;
    private _currentStep: number = -1;
    private _gdLevel: GdLevel;
    private _highlightedIndex: number = -1;
    private _currency: number = -1;
    private _stride: number = -1;
    private _simCells: SimCell[];
    private _simEnemies: SimEnemy[];
    private _simProjectiles: SimProjectile[];
    private _nextWaveStep: number = -1;
    private _lastEnemySpawnStep: number = 0;
    private _currentWave: number;
    private _enemiesLeftToSpawn: number;
    private _levelIdx: number;
    private _gdRoot: GdRoot;

    constructor(gdRoot: GdRoot, levelIdx: number) {
        this._levelIdx = levelIdx;
        this._gdRoot = gdRoot;
        this._gdLevel = gdRoot.levels[levelIdx];
        this._simCells = [];
        this._simEnemies = [];
        this._simProjectiles = [];
        this._currentStep = 0;
        this._currency = this._gdLevel.currency;
        this._currentWave = 0;
        this._enemiesLeftToSpawn = this._gdLevel.waves[0].amount;

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

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

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

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

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

        this.updatePaths();
    };

    public get gdLevel(): GdLevel {
        return this._gdLevel;
    }

    public get simCells(): SimCell[] {
        return this._simCells;
    }

    public get simEnemies(): SimEnemy[] {
        return this._simEnemies;
    }

    public get simProjectiles(): SimProjectile[] {
        return this._simProjectiles;
    }

    public get currentStep(): number {
        return this._currentStep;
    }

    public set currentStep(value: number) {
        this._currentStep = value;
    }

    public get currency(): number {
        return this._currency;
    }

    public set currency(value: number) {
        this._currency = value;
    }

    public get highlightedIndex(): number {
        return this._highlightedIndex;
    }

    public set highlightedIndex(value: number) {
        this._highlightedIndex = value;
    }

    public get paused(): boolean {
        return this._paused;
    }

    public set paused(value: boolean) {
        this._paused = value;
    }

    public get currentWave(): number {
        return this._currentWave;
    }

    public set currentWave(value: number) {
        this._currentWave = value;
    }

    public get nextWaveStep(): number {
        return this._nextWaveStep;
    }

    public set nextWaveStep(value: number) {
        this._nextWaveStep = value;
    }

    public get lastEnemySpawnStep(): number {
        return this._lastEnemySpawnStep;
    }

    public set lastEnemySpawnStep(value: number) {
        this._lastEnemySpawnStep = value;
    }

    public get enemiesLeftToSpawn(): number {
        return this._enemiesLeftToSpawn;
    }

    public set enemiesLeftToSpawn(value: number) {
        this._enemiesLeftToSpawn = value;
    }

    serialize(): string {
        const data = {
            version: SimLevel.version,
            levelIdx: this._levelIdx,
            paused: this._paused,
            gdLevel: this._gdLevel,
            currentStep: this._currentStep,
            highlightedIndex: this._highlightedIndex,
            currency: this._currency,
            stride: this._stride,
            cells: this._simCells,
            enemies: this._simEnemies,
            projectiles: this._simProjectiles,
            nextWaveStep: this._nextWaveStep,
            lastEnemySpawnStep: this._lastEnemySpawnStep,
            currentWave: this._currentWave,
            enemiesLeftToSpawn: this._enemiesLeftToSpawn
        };
        return btoa(JSON.stringify(data));
    }

    static deserialize(gdRoot: GdRoot, data: string): SimLevel | null {
        const parsedData = JSON.parse(atob(data));
        if (parsedData.version != SimLevel.version) {
            console.warn('Unsupported version:', parsedData.version);
            return null;
        }

        let level = new SimLevel(gdRoot, parsedData.levelIdx);
        level._paused = parsedData.paused;
        level._gdLevel = parsedData.gdLevel;
        level._currentStep = parsedData.currentStep;
        level._highlightedIndex = parsedData.highlightedIndex;
        level._currency = parsedData.currency;
        level._stride = parsedData.stride;
        level._simCells = parsedData.cells;
        level._simEnemies = parsedData.enemies;
        level._simProjectiles = parsedData.projectiles;
        level._nextWaveStep = parsedData.nextWaveStep;
        level._lastEnemySpawnStep = parsedData.lastEnemySpawnStep;
        level._currentWave = parsedData.currentWave;
        level._enemiesLeftToSpawn = parsedData.enemiesLeftToSpawn;
        return level;
    }

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

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

    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(hexToBlock: Hex | null): boolean {
        try {
            if (!!hexToBlock) {
                const reservedCell = this._simCells[this.getCellIndex(hexToBlock)];
                if (reservedCell.type != ECellType.Free || !!reservedCell.simTower)
                    return false;

                reservedCell.type = ECellType.Reserved;
            }

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

    public updatePaths(): boolean {
        const level = this._gdLevel;
        const newRoutePaths: number[][] = [];
        const newEnemyPaths: number[][] = [];
        let invalid = false;

        for (const routeIdx in level.enemyRoutes) {
            const route = level.enemyRoutes[routeIdx];
            const enemySpawnHex = level.enemySpawns[route[0]];
            const enemySpawnCell = this._simCells[this.getCellIndex(enemySpawnHex)];
            const enemyTargetHex = level.enemyTargets[route[1]];
            const enemyTargetCell = this._simCells[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._simEnemies) {
            const simEnemy = this._simEnemies[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 level.enemyRoutes) {
            const route = level.enemyRoutes[routeIdx];
            const enemySpawnHex = level.enemySpawns[route[0]];
            const enemySpawnCell = this._simCells[this.getCellIndex(enemySpawnHex)];
            enemySpawnCell.pathsToTarget[routeIdx] = newRoutePaths[routeIdx];
        }

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

        return true;
    }
}
