const std = @import("std");

const Str = []const u8;
const PATH = "input/day18.txt";

const ROWS = 100;
const COLS = 100;
const ROUND = 100;

pub fn first(allocator: std.mem.Allocator) !usize {
    _ = allocator;

    var gif = GIF.init(@embedFile(PATH));

    var next_grid: [ROWS][COLS]State = undefined;

    var round: usize = 0;
    while (round < ROUND) : (round += 1) {
        for (gif.grid) |line, row| {
            for (line) |light, col| {
                const nbrs = gif.countNeighbor(row, col);
                if (light == .on) {
                    next_grid[row][col] = if (nbrs == 2 or nbrs == 3) .on else .off;
                } else {
                    next_grid[row][col] = if (nbrs == 3) .on else .off;
                }
            }
        }

        gif.grid = next_grid;
    }

    var count: usize = 0;
    for (gif.grid) |line| {
        for (line) |ch| {
            if (ch == .on) count += 1;
        }
    }

    return count;
}

pub fn second(allocator: std.mem.Allocator) !usize {
    _ = allocator;

    var gif = GIF.init(@embedFile(PATH));
    gif.alwaysOn();

    var next_grid: [ROWS][COLS]State = undefined;

    var round: usize = 0;
    while (round < ROUND) : (round += 1) {
        for (gif.grid) |line, row| {
            for (line) |light, col| {
                const nbrs = gif.countNeighbor(row, col);
                if (light == .on) {
                    next_grid[row][col] = if (nbrs == 2 or nbrs == 3) .on else .off;
                } else {
                    next_grid[row][col] = if (nbrs == 3) .on else .off;
                }
            }
        }

        gif.grid = next_grid;
        gif.alwaysOn();
    }

    var count: usize = 0;
    for (gif.grid) |line| {
        for (line) |ch| {
            if (ch == .on) count += 1;
        }
    }

    return count;
}

test "day18a" {
    try std.testing.expectEqual(@as(usize, 1061), try first(std.testing.allocator));
}

test "day18b" {
    try std.testing.expectEqual(@as(usize, 1006), try second(std.testing.allocator));
}

const State = enum {
    on,
    off,
};

const GIF = struct {
    grid: [ROWS][COLS]State,

    const neighbors = [8][2]i2{
        .{ -1, 0 },
        .{ 1, 0 },
        .{ 0, -1 },
        .{ 0, 1 },
        .{ -1, -1 },
        .{ -1, 1 },
        .{ 1, -1 },
        .{ 1, 1 },
    };

    fn init(in: Str) @This() {
        return @This(){
            .grid = parseInput(in),
        };
    }

    fn parseInput(in: Str) [ROWS][COLS]State {
        var grid: [ROWS][COLS]State = undefined;

        var lines = std.mem.tokenize(u8, in, "\n");

        var row: usize = 0;
        while (lines.next()) |line| : (row += 1) {
            for (line) |ch, col| {
                grid[row][col] = switch (ch) {
                    '.' => .off,
                    '#' => .on,
                    else => unreachable,
                };
            }
        }

        return grid;
    }

    fn countNeighbor(self: @This(), row: usize, col: usize) u4 {
        var ret: u4 = 0;

        for (neighbors) |n| {
            const dr = @intCast(isize, row) + n[0];
            if (dr < 0 or dr >= ROWS) continue;

            const dc = @intCast(isize, col) + n[1];
            if (dc < 0 or dc >= COLS) continue;

            if (self.grid[@intCast(usize, dr)][@intCast(usize, dc)] == .on) ret += 1;
        }

        return ret;
    }

    fn alwaysOn(self: *@This()) void {
        self.grid[0][0] = .on;
        self.grid[0][COLS - 1] = .on;
        self.grid[ROWS - 1][0] = .on;
        self.grid[ROWS - 1][COLS - 1] = .on;
    }
};

const test_input =
    \\.#.#.#
    \\...##.
    \\#....#
    \\..#...
    \\#.#..#
    \\####..
;