import type Tile from "./tile";

namespace Pathfinding {
    /**
     * A* search from start to end Tile.
     * @returns array of Tiles from start to end (inclusive), or [] if no path.
     */
    export function findPath(start: Tile, end: Tile, forEnemy = false): Tile[] {
        // The set of discovered nodes to be evaluated
        const openSet: Set<Tile> = new Set([start]);

        // For each node, the cost of getting from start to that node.
        const gScore = new Map<Tile, number>();
        gScore.set(start, 0);

        // For each node, the total cost of getting from start to goal
        // by passing by that node: gScore + heuristic estimate to goal.
        const fScore = new Map<Tile, number>();
        fScore.set(start, heuristic(start, end));

        // For path reconstruction: map each node to the node it can most efficiently be reached from.
        const cameFrom = new Map<Tile, Tile>();

        let closestTile: Tile = start;
        let closestDistance = heuristic(closestTile, end);

        while (openSet.size > 0) {
            // Get the node in openSet having the lowest fScore
            let current: Tile | null = null;
            let currentF = Infinity;
            for (const tile of openSet) {
                const f = fScore.get(tile) ?? Infinity;
                if (f < currentF) {
                    currentF = f;
                    current = tile;
                }
            }
            if (!current) break;

            const dist = heuristic(current, end);
            if (closestDistance < dist) {
                closestTile = current;
                closestDistance = dist;
            }

            // If we’ve reached the goal, reconstruct and return the path.
            if (current === end) {
                return reconstructPath(cameFrom, current);
            }

            openSet.delete(current);

            if (current !== start && !forEnemy && current.hasAnyItems) continue;

            for (const neighbor of current.activeConnections) {
                // tentative gScore is current’s gScore plus cost to move to neighbor
                const tentativeG = (gScore.get(current) ?? Infinity) + distance(current, neighbor);

                if (tentativeG < (gScore.get(neighbor) ?? Infinity)) {
                    // This path to neighbor is better than any previous one. Record it!
                    cameFrom.set(neighbor, current);
                    gScore.set(neighbor, tentativeG);
                    fScore.set(neighbor, tentativeG + heuristic(neighbor, end));
                    if (!openSet.has(neighbor)) {
                        openSet.add(neighbor);
                    }
                }
            }
        }

        // Open set is empty but goal was never reached
        if (forEnemy) {
            return reconstructPath(cameFrom, closestTile);
        }

        return [];
    }

    /** Heuristic: Manhattan distance between tiles. */
    function heuristic(a: Tile, b: Tile): number {
        return Math.abs(a.centerX - b.centerX) + Math.abs(a.centerY - b.centerY);
    }

    /** Actual cost between two connected tiles (Euclidean). */
    function distance(a: Tile, b: Tile): number {
        const dx = a.centerX - b.centerX;
        const dy = a.centerY - b.centerY;
        return Math.hypot(dx, dy);
    }

    /** Reconstructs path from cameFrom map, ending at `current`. */
    function reconstructPath(
        cameFrom: Map<Tile, Tile>,
        current: Tile
    ): Tile[] {
        const path: Tile[] = [current];
        while (cameFrom.has(current)) {
            current = cameFrom.get(current)!;
            path.push(current);
        }
        return path.reverse();
    }

    export function findPossibleMoves(start: Tile, steps: number): Tile[] {
        const result = new Set<Tile>();
        for (const tile of findPossibleTiles(start, steps)) {
            const path = findPath(start, tile);
            if (path.length > 1 && path.length <= steps + 1) {
                result.add(tile);
            }
        }

        return Array.from(result);
    }

    function findPossibleTiles(start: Tile, steps: number): Set<Tile> {
        const result = new Set<Tile>([start]);
        if (steps > 0) {
            for (const tile of start.connections) {
                findPossibleTiles(tile, steps - 1).forEach(t => result.add(t));
            }
        }

        return result;
    }
}

export default Pathfinding;