const std = @import("std");

const path = "data/day04/input.txt";

const retSize = u16;

const grid_item_size = u7;
const Grid = struct {
    boardMap: std.AutoHashMap(grid_item_size, Point),
    row: [5]u3 = .{ 0, 0, 0, 0, 0 },
    col: [5]u3 = .{ 0, 0, 0, 0, 0 },
    pub fn getValue(self: @This()) retSize {
        var sum: retSize = 0;

        var it = self.boardMap.iterator();
        while (it.next()) |item| {
            if (!item.value_ptr.marked) {
                sum += item.key_ptr.*;
            }
        }
        return sum;
    }
};

const Point = struct {
    row: u3, // < 5
    col: u3, // < 5
    marked: bool = false,
};

const Bingo = struct {
    numbers: std.ArrayList(grid_item_size),
    boards: std.ArrayList(Grid),
};

fn first() anyerror!retSize {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    var bg = try parseInput(allocator);
    defer { // cleanup code
        for (bg.boards.items) |_, idx| {
            bg.boards.items[idx].boardMap.deinit();
        }
        bg.boards.deinit();
        bg.numbers.deinit();
    }

    for (bg.numbers.items) |num| {
        for (bg.boards.items) |_, idx| {
            // std.debug.print("{any} {any}\n", .{ num, idx });

            var curr = &bg.boards.items[idx];
            const coord = curr.boardMap.get(num) orelse continue;

            curr.row[coord.row] += 1;
            curr.col[coord.col] += 1;

            try curr.boardMap.put(num, .{ .row = coord.row, .col = coord.col, .marked = true });

            // std.debug.print("{any} {any} {any}\n", .{num, curr.row, curr.col});
            if ((curr.row[coord.row] == 5) or (curr.col[coord.col] == 5)) {
                return num * curr.getValue();
            }
        }
    }

    unreachable;
}

fn second() anyerror!retSize {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    var bg = try parseInput(allocator);
    defer { // cleanup code
        for (bg.boards.items) |_, idx| {
            bg.boards.items[idx].boardMap.deinit();
        }
        bg.boards.deinit();
        bg.numbers.deinit();
    }

    var to_remove = std.ArrayList(usize).init(allocator);
    defer to_remove.deinit();

    for (bg.numbers.items) |num| {
        for (bg.boards.items) |_, idx| {
            var curr = &bg.boards.items[idx];
            const coord = curr.boardMap.get(num) orelse continue;

            curr.row[coord.row] += 1;
            curr.col[coord.col] += 1;

            try curr.boardMap.put(num, .{ .row = coord.row, .col = coord.col, .marked = true });

            if ((curr.row[coord.row] == 5) or (curr.col[coord.col] == 5)) {
                if (bg.boards.items.len == 1) {
                    return num * curr.getValue();
                } else {
                    try to_remove.append(idx);
                }
            }
        }

        std.sort.sort(usize, to_remove.items, {}, comptime std.sort.desc(usize));
        for (to_remove.items) |idx| {
            bg.boards.items[idx].boardMap.deinit();
            _ = bg.boards.swapRemove(idx);
        }
        try to_remove.resize(0);
    }

    unreachable;
}

fn parseInput(allocator: std.mem.Allocator) anyerror!Bingo {
    const input = @embedFile(path);
    var lines = std.mem.split(u8, input, "\n");

    var ret = Bingo{
        .numbers = std.ArrayList(grid_item_size).init(allocator),
        .boards = std.ArrayList(Grid).init(allocator),
    };

    var gridRow: u3 = 0;
    var gridCol: u3 = 0;
    var g = Grid{ .boardMap = std.AutoHashMap(grid_item_size, Point).init(allocator) };

    var idx: usize = 0;
    while (lines.next()) |line| : (idx += 1) {
        // read in numbers
        if (idx == 0) {
            var nums = std.mem.tokenize(u8, line, ",");
            while (nums.next()) |num| {
                try ret.numbers.append(try std.fmt.parseUnsigned(grid_item_size, num, 10));
            }
            continue;
        }

        // std.debug.print("line: {any}\n", .{line});

        // read in Grids
        var items = std.mem.tokenize(u8, line, " ");
        while (items.next()) |item| {
            const value = try std.fmt.parseUnsigned(grid_item_size, item, 10);

            // std.debug.print("{any} -> {any} {any}\n", .{value, gridRow, gridCol});
            try g.boardMap.put(value, .{ .row = gridRow, .col = gridCol });

            gridCol += 1;
        }
        gridCol = 0;

        if (line.len != 0) gridRow += 1;

        // append grid
        if (gridRow == 5) {
            try ret.boards.append(g);
            gridRow = 0;
            gridCol = 0;
            // create new grid
            g = Grid{ .boardMap = std.AutoHashMap(grid_item_size, Point).init(allocator) };
        }
    }

    return ret;
}

pub fn main() anyerror!void {
    var timer = try std.time.Timer.start();

    _ = try first();
    const f = timer.lap() / 1000;
    _ = try second();
    const s = timer.lap() / 1000;

    std.debug.print("Day 4 \t\tfirst: {d}us \t\tsecond: {d}us\n", .{ f, s });
}

test "first" {
    try std.testing.expectEqual(@as(retSize, 11536), try first(std.testing.allocator));
}

test "second" {
    try std.testing.expectEqual(@as(retSize, 1284), try second());
}