export class MinHeap<T> {

    private heap: T[] = [];
    private compare: (a: T, b: T) => number;

    constructor(compare: (a: T, b: T) => number) {
        this.compare = compare;
    }

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

    public add(elem: T) {
        this.heap.push(elem);
        this.heapifyUp();
    }

    public pop(): T | undefined {
        if (this.heap.length === 0) {
            return undefined;
        }
        const ret = this.heap[0];
        this.heap[0] = this.heap[this.heap.length - 1];
        this.heap.pop();
        this.heapifyDown();
        return ret;
    }

    private heapifyUp() {
        let index = this.heap.length - 1;
        while (index > 0) {
            const parentIndex = Math.floor((index - 1) / 2);
            if (this.compare(this.heap[index], this.heap[parentIndex]) > 0) {
                break;
            }
            // swap
            const temp = this.heap[index];
            this.heap[index] = this.heap[parentIndex];
            this.heap[parentIndex] = temp;
            index = parentIndex;
        }
    }

    private heapifyDown() {
        let index = 0;
        while (index < this.heap.length) {
            const leftIndex = 2 * index + 1;
            const rightIndex = 2 * index + 2;
            let minIndex = leftIndex;
            if (leftIndex >= this.heap.length) {
                break;
            }
            if (rightIndex < this.heap.length && this.compare(this.heap[rightIndex], this.heap[minIndex]) < 0) {
                minIndex = rightIndex;
            }
            if (this.compare(this.heap[index], this.heap[minIndex]) < 0) {
                break;
            }
            // swap
            const temp = this.heap[index];
            this.heap[index] = this.heap[minIndex];
            this.heap[minIndex] = temp;
            index = minIndex;
        }
    }
}

type CostMap = number[][];

export type Coord = [number, number];
export type Dir = "left" | "right" | "up" | "down";
export type State = {
    pos: Coord,
    prevDir: Dir,
    dist: number,
};

export function readCostMap(input: string): CostMap {
    const costMap = input.split("\n").map(x => x.trim()).filter(x => x.length > 0).map(x => x.split("").map(x => parseInt(x)));
    return costMap
}

function reflectDir(dir: Dir): Dir {
    switch (dir) {
        case "left": return "right";
        case "right": return "left";
        case "up": return "down";
        case "down": return "up";
    }
}

function nextCoord(pos: Coord, dir: Dir): Coord {
    switch (dir) {
        case "left": return [pos[0] - 1, pos[1]];
        case "right": return [pos[0] + 1, pos[1]];
        case "up": return [pos[0], pos[1] - 1];
        case "down": return [pos[0], pos[1] + 1];
    }
}

export function nextCrucibleStates(pos: Coord, prevDir: Dir, step: number): State[] {
    const availableDirs = ["left", "right", "up", "down"] satisfies Dir[];
    return availableDirs.filter(dir => {
        return dir !== reflectDir(prevDir) && (
            dir !== prevDir || step < 3
        );
    }).map(dir => ({
        pos: nextCoord(pos, dir),
        prevDir: dir,
        dist: dir === prevDir ? step + 1 : 1,
    }));
}

export function nextUltraCrucibleStates(pos: Coord, prevDir: Dir, step: number): State[] {
    const availableDirs = ["left", "right", "up", "down"] satisfies Dir[];
    return availableDirs.filter(dir => {
        return dir !== reflectDir(prevDir) && (
            dir !== prevDir || step < 10
        ) && (
            dir === prevDir || step > 3
        );
    }).map(dir => ({
        pos: nextCoord(pos, dir),
        prevDir: dir,
        dist: dir === prevDir ? step + 1 : 1,
    }));
}

export function solve(costMap: CostMap, finalPos: Coord): number {
    const [height, width] = [costMap.length, costMap[0].length];
    const minHeap = new MinHeap<[State, number]>((a, b) => a[1] - b[1]);

    minHeap.add([{ pos: [0, 0], prevDir: "down", dist: 0 }, 0]);
    const minCost = new Map<string, number>();

    while (minHeap.size > 0) {
        const [state, cost] = minHeap.pop()!;
        const curMinCost = minCost.get(`${state.pos.join(",")};${state.prevDir},${state.dist}`);
        if (curMinCost !== undefined && curMinCost < cost) {
            continue;
        }
        const nextAvailableStates = nextCrucibleStates(state.pos, state.prevDir, state.dist)
            .filter(x => x.pos[0] >= 0 && x.pos[0] < width && x.pos[1] >= 0 && x.pos[1] < height);
        for (const nextState of nextAvailableStates) {
            const key = `${nextState.pos.join(",")};${nextState.prevDir},${nextState.dist}`;
            const nextCost = cost + costMap[nextState.pos[1]][nextState.pos[0]];
            const curMinCost = minCost.get(key);
            if (curMinCost === undefined || curMinCost > nextCost) {
                minCost.set(key, nextCost);
                minHeap.add([nextState, nextCost]);
            }
        }
    }
    let ret = Number.MAX_SAFE_INTEGER;
    for (const [key, value] of minCost) {
        if (key.split(";")[0] === finalPos.join(",")) {
            ret = Math.min(ret, value);
        }
    }
    return ret;
}

export function solve_2(costMap: CostMap, finalPos: Coord): number {
    const [height, width] = [costMap.length, costMap[0].length];
    const minHeap = new MinHeap<[State, number]>((a, b) => a[1] - b[1]);

    minHeap.add([{ pos: [0, 0], prevDir: "down", dist: 0 }, 0]);
    minHeap.add([{ pos: [0, 0], prevDir: "right", dist: 0 }, 0]);
    const minCost = new Map<string, number>();

    while (minHeap.size > 0) {
        const [state, cost] = minHeap.pop()!;
        const curMinCost = minCost.get(`${state.pos.join(",")};${state.prevDir},${state.dist}`);
        if (curMinCost !== undefined && curMinCost < cost) {
            continue;
        }
        const nextAvailableStates = nextUltraCrucibleStates(state.pos, state.prevDir, state.dist)
            .filter(x => x.pos[0] >= 0 && x.pos[0] < width && x.pos[1] >= 0 && x.pos[1] < height);
        for (const nextState of nextAvailableStates) {
            const key = `${nextState.pos.join(",")};${nextState.prevDir},${nextState.dist}`;
            const nextCost = cost + costMap[nextState.pos[1]][nextState.pos[0]];
            const curMinCost = minCost.get(key);
            if (curMinCost === undefined || curMinCost > nextCost) {
                minCost.set(key, nextCost);
                minHeap.add([nextState, nextCost]);
            }
        }
    }
    // console.log([...minCost.entries()]);
    let ret = Number.MAX_SAFE_INTEGER;
    for (const [key, value] of minCost) {
        if (key.split(";")[0] === finalPos.join(",") && 
        parseInt(key.split(";")[1].split(",")[1]) >= 4) {
            ret = Math.min(ret, value);
        }
    }
    return ret;
}