// ---------------------------------------------------------------------------
// MinHeap
// ---------------------------------------------------------------------------

import type { Point } from "@common/geometry";

export interface HeapItem<N> {
    node: N;
    nodeKey: string;
    g: number; // cost from start
    f: number; // g + h
}

export class MinHeap<N> {
    private data: HeapItem<N>[] = [];

    get size(): number { return this.data.length; }

    push(item: HeapItem<N>): void {
        this.data.push(item);
        this._bubbleUp(this.data.length - 1);
    }

    pop(): HeapItem<N> | undefined {
        if (this.data.length === 0) return undefined;
        const top = this.data[0];
        const last = this.data.pop()!;
        if (this.data.length > 0) {
            this.data[0] = last;
            this._sinkDown(0);
        }
        return top;
    }

    private _bubbleUp(i: number): void {
        while (i > 0) {
            const parent = (i - 1) >> 1;
            if (this._less(i, parent)) {
                this._swap(i, parent);
                i = parent;
            } else break;
        }
    }

    private _sinkDown(i: number): void {
        const n = this.data.length;
        for (; ;) {
            let best = i;
            const l = 2 * i + 1, r = 2 * i + 2;
            if (l < n && this._less(l, best)) best = l;
            if (r < n && this._less(r, best)) best = r;
            if (best === i) break;
            this._swap(i, best);
            i = best;
        }
    }

    // Lower f wins; equal f → higher g wins (prefer nodes closer to goal).
    private _less(a: number, b: number): boolean {
        const da = this.data[a], db = this.data[b];
        return da.f < db.f || (da.f === db.f && da.g > db.g);
    }

    private _swap(a: number, b: number): void {
        [this.data[a], this.data[b]] = [this.data[b], this.data[a]];
    }
}

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface Edge<N> {
    node: N;
    /** Traversal cost — must be > 0 for A* to be optimal. */
    cost: number;
}

export interface DijkstraOptions<N> {
    /**
     * Return the reachable neighbours of a node, with edge costs.
     * Costs must be non-negative.
     */
    neighbours: (node: N) => Iterable<Edge<N>>;

    /**
     * Unique string key for a node.
     * Default: `String(node)` — override whenever two distinct nodes could
     * serialise the same way (objects, arrays, coordinate tuples…).
     */
    key?: (node: N) => string;

    /**
     * Maximum number of nodes to expand before stopping.
     * Nodes beyond the frontier are left unreachable (`distance` → `undefined`).
     * Default: `Infinity`.
     */
    maxExpansions?: number;
}

export interface AStarOptions<N> extends DijkstraOptions<N> {
    /**
     * Admissible heuristic — estimated cost from `a` to `b`.
     * Must never exceed the true shortest cost (can under-estimate freely).
     * Use `() => 0` for Dijkstra's algorithm.
     */
    heuristic: (from: N, to: N) => number;
}

export interface AStarResult<N> {
    /** Ordered list of nodes from start (inclusive) to goal (inclusive). */
    path: N[];
    /** Total accumulated edge cost along the path. */
    cost: number;
    /** Number of nodes popped from the open set during this search. */
    expansions: number;
}

export interface Dijkstra2DOptions {
    /**
     * Return true if the cell (toX, toY) can be entered from (fromX, fromY).
     * The from-coordinates let you model direction-dependent rules such as
     * one-way ramps, wall-hugging costs, or diagonal corner-cutting checks.
     */
    passable: (toX: number, toY: number, fromX: number, fromY: number) => boolean;

    /**
     * Cost to move from one cell to a neighbour.
     * Receives the same arguments as `passable` after the passability check.
     * Default: 1 for cardinal moves, √2 for diagonal moves.
     */
    cost?: (toX: number, toY: number, fromX: number, fromY: number) => number;

    /**
     * Which neighbour directions to consider.
     * - 4-directional (N S E W, default)
     * - 8-directional
     */
    directions?: 4 | 8;

    /** Forwarded to AStar. */
    maxExpansions?: number;
}

export interface AStar2DOptions extends Dijkstra2DOptions { }

const DIRS_4: Point[] = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }];
const DIRS_8: Point[] = [...DIRS_4, { x: -1, y: -1 }, { x: -1, y: 1 }, { x: 1, y: -1 }, { x: 1, y: 1 }];

export const buildDijkstraOptionsFrom2D = (options: Dijkstra2DOptions): DijkstraOptions<Point> => {
    const dirs = options.directions === 8 ? DIRS_8 : DIRS_4;
    const costFn = options.cost
        ?? ((tx, ty, fx, fy) => (tx !== fx && ty !== fy ? Math.SQRT2 : 1));

    return {
        neighbours: ({ x, y }) => {
            const edges: Edge<Point>[] = [];
            for (const { x: dx, y: dy } of dirs) {
                const nx = x + dx, ny = y + dy;
                if (options.passable(nx, ny, x, y)) {
                    edges.push({ node: { x: nx, y: ny }, cost: costFn(nx, ny, x, y) });
                }
            }
            return edges;
        },
        key: ({ x, y }) => `${x},${y}`,
        maxExpansions: options.maxExpansions,
    }
}

export const buildAStarOptionsFrom2D = (options: AStar2DOptions): AStarOptions<Point> => {
    const dirs = options.directions;

    return {
        ...buildDijkstraOptionsFrom2D(options),
        // Octile heuristic — consistent for 8-directional grids;
        // falls back to Manhattan for cardinal-only (diagonal term is 0).
        heuristic: ({ x: ax, y: ay }, { x: bx, y: by }) => {
            const dx = Math.abs(ax - bx), dy = Math.abs(ay - by);
            return dirs === 8
                ? (dx + dy) + (Math.SQRT2 - 2) * Math.min(dx, dy) // octile
                : dx + dy;                                          // manhattan
        },
    }
}
