Pathfinder library in zig
//! Pathfinder algorithms on a grid. The grid is stack-based, we only use heap allocations
//! in solvers.
const std = @import("std");

const Str = []const u8;

/// Various predefined valid neighbour directions on a 2D grid.
pub const D2 = struct {
    pub const horizontal = [_][2]i2{ .{ 0, -1 }, .{ 0, 1 } };
    pub const vertical = [_][2]i2{ .{ -1, 0 }, .{ 1, 0 } };
    pub const HV = horizontal ++ vertical;
    pub const diagonal = [_][2]i2{ .{ -1, -1 }, .{ -1, 1 }, .{ 1, -1 }, .{ 1, 1 } };
    pub const all = HV ++ diagonal;
};

/// Grid for various pathfinder algorithms.
/// `T` is the type of Grid items.
/// `GRID_ROWS` and `GRID_COLS` defines the Grid size.
pub fn Grid(comptime T: type, comptime GRID_ROWS: usize, comptime GRID_COLS: usize) type {
    return struct {
        grid: [GRID_ROWS][GRID_COLS]T = undefined,

        const QueueType = struct {
            state: [2]usize,
            cost: T,
        };

        pub fn setGrid(self: *@This(), grid: [GRID_ROWS][GRID_COLS]T) void {
            self.grid = grid;
        }

        /// Parses a grid-file located in `path`. The items must be separated with `sep`.
        pub fn parseGridFromCSV(self: *@This(), comptime path: Str, sep: Str) !void {
            const input = @embedFile(path);
            var lines = std.mem.tokenize(u8, input, "\n");

            var ret: [GRID_ROWS][GRID_COLS]T = undefined;

            var row: usize = 0;
            while (lines.next()) |line| : (row += 1) {
                var items = std.mem.tokenize(u8, line, sep);

                var col: usize = 0;
                while (items.next()) |item| : (col += 1) {
                    switch (@typeInfo(T)) {
                        .Int => |item_type| {
                            if (item_type.signedness == .unsigned) {
                                ret[row][col] = try std.fmt.parseUnsigned(T, item, 0);
                            } else {
                                ret[row][col] = try std.fmt.parseInt(T, item, 0);
                            }
                        },
                        .Float => ret[row][col] = try std.fmt.parseFloat(T, item),
                        else => @compileError("unsupported type: " ++ @typeName(T)),
                    }
                }
            }
            self.setGrid(ret);
        }

        fn lessThan(context: void, a: QueueType, b: QueueType) std.math.Order {
            _ = context;
            if (a.cost == b.cost) {
                return .eq;
            } else if (a.cost < b.cost) {
                return .lt;
            } else {
                return .gt;
            }
        }

        /// Finds shortest path between `start` and `goal` using Dijkstra's algorithm.
        /// Returns the path length or `error.PathNotFound`.
        pub fn dijkstra(
            self: *@This(),
            allocator: std.mem.Allocator,
            start: [2]usize,
            goal: [2]usize,
            dir: anytype,
        ) !T {
            var CostGrid = switch (@typeInfo(T)) {
                .Int => [_][GRID_COLS]T{[_]T{std.math.maxInt(T)} ** GRID_COLS} ** GRID_ROWS,
                .Float => [_][GRID_COLS]T{[_]T{std.math.floatMax(T)} ** GRID_COLS} ** GRID_ROWS,
                else => @compileError("unsupported type: " ++ @typeName(T)),
            };
            CostGrid[start[0]][start[1]] = 0;

            var queue = std.PriorityQueue(QueueType, void, comptime lessThan).init(allocator, {});
            defer queue.deinit();

            try queue.add(.{ .state = start, .cost = 0 });

            while (queue.removeOrNull()) |curr| {
                if (std.meta.eql(curr.state, goal)) {
                    return curr.cost;
                }
                for (dir) |d| {
                    const diffrow = @as(isize, @intCast(curr.state[0])) + d[0];
                    if (diffrow < 0 or diffrow >= GRID_ROWS) continue;
                    const dr = @as(usize, @intCast(diffrow));

                    const diffcol = @as(isize, @intCast(curr.state[1])) + d[1];
                    if (diffcol < 0 or diffcol >= GRID_COLS) continue;
                    const dc = @as(usize, @intCast(diffcol));

                    const cost = CostGrid[curr.state[0]][curr.state[1]] + self.grid[dr][dc];

                    if (cost < CostGrid[dr][dc]) {
                        CostGrid[dr][dc] = cost;
                        try queue.add(.{ .state = .{ dr, dc }, .cost = cost });
                    }
                }
            }
            return error.PathNotFound;
        }

        pub fn astar(
            self: *@This(),
            allocator: std.mem.Allocator,
            start: [2]usize,
            goal: [2]usize,
            dir: anytype,
            heuristics: anytype,
        ) !T {
            var CostGrid = switch (@typeInfo(T)) {
                .Int => [_][GRID_COLS]T{[_]T{std.math.maxInt(T)} ** GRID_COLS} ** GRID_ROWS,
                .Float => [_][GRID_COLS]T{[_]T{std.math.floatMax(T)} ** GRID_COLS} ** GRID_ROWS,
                else => @compileError("unsupported type: " ++ @typeName(T)),
            };
            CostGrid[start[0]][start[1]] = 0;

            var queue = std.PriorityQueue(QueueType, void, comptime lessThan).init(allocator, {});
            defer queue.deinit();

            try queue.add(.{ .state = start, .cost = 0 });

            while (queue.removeOrNull()) |curr| {
                if (std.meta.eql(curr.state, goal)) {
                    return curr.cost;
                }
                for (dir) |d| {
                    const diffrow = @as(isize, @intCast(curr.state[0])) + d[0];
                    if (diffrow < 0 or diffrow >= GRID_ROWS) continue;
                    const dr = @as(usize, @intCast(diffrow));

                    const diffcol = @as(isize, @intCast(curr.state[1])) + d[1];
                    if (diffcol < 0 or diffcol >= GRID_COLS) continue;
                    const dc = @as(usize, @intCast(diffcol));

                    const cost = CostGrid[curr.state[0]][curr.state[1]] + self.grid[dr][dc];

                    if (cost < CostGrid[dr][dc]) {
                        CostGrid[dr][dc] = cost;
                        try queue.add(
                            .{ .state = .{ dr, dc }, .cost = cost + try heuristics(.{ dr, dc }, goal) },
                        );
                    }
                }
            }
            return error.PathNotFound;
        }
    };
}