const std = @import("std");

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

const retSize = u16;

const grid_size = 5;
const grid_item_size = u7;
const Board = struct {
    items: [grid_size][grid_size]grid_item_size,
    row: [grid_size]u5 = [_]u5{0} ** grid_size,
    col: [grid_size]u5 = [_]u5{0} ** grid_size,
    pub fn value(self: @This()) retSize {
        var ret: retSize = 0;
        for (self.items) |row, rowID| {
            for (row) |item, col| {
                if (self.row[rowID] & (@as(u5, 1) <<| col) == 0) {
                    ret += item;
                }
            }
        }
        return ret;
    }
};

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

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(Board).init(allocator),
    };

    var gridRow: u3 = 0;
    var gridCol: u3 = 0;
    var g = Board{ .items = undefined };

    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;
        }

        // 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);
            g.items[gridRow][gridCol] = value;
            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 = Board{ .items = undefined };
        }
    }

    return ret;
}

pub fn first(allocator: ?std.mem.Allocator) anyerror!retSize {
    var bg = try parseInput(allocator.?);
    defer { // cleanup code
        bg.boards.deinit();
        bg.numbers.deinit();
    }

    for (bg.numbers.items) |num| {
        boards_loop: for (bg.boards.items) |_, idx| {
            var grid = &bg.boards.items[idx];
            for (grid.items) |row, i| {
                for (row) |item, j| {
                    if (item == num) {
                        // increase row, col counter
                        grid.row[i] += @as(u5, 1) <<| j;
                        grid.col[j] += @as(u5, 1) <<| i;
                        // check if row or grid equals to 11111, return when true
                        if ((grid.row[i] == 0b11111) or (grid.col[j] == 0b11111)) {
                            return num * grid.value();
                        }
                        // break out to grid loop
                        continue :boards_loop;
                    }
                }
            }
        }
    }

    unreachable;
}

pub fn main() anyerror!void {
    var buf: [500 * grid_size * grid_size]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buf);

    var timer = try std.time.Timer.start();
    const ret = try first(fba.allocator());
    const f = timer.lap() / 1000;

    try std.testing.expectEqual(ret, @as(retSize, 11536));

    std.debug.print("Day 4a result: {d} \t\ttime: {d}us\n", .{ ret, f });
}

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