Advent of Code 2020 solutions in Zig
const std = @import("std");

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

const TileData = [10][10]bool;
const Tile = struct {
    id: u16,
    data: TileData,

    // rotating left
    fn rotate(self: *@This()) void {
        var rotated: TileData = undefined;
        for (self.data) |line, row| {
            for (line) |item, col| {
                rotated[self.data.len - 1 - col][row] = item;
            }
        }
        self.data = rotated;
    }

    fn flip(self: *@This(), axis: u1) void {
        var flipped: TileData = undefined;
        switch (axis) {
            0 => return,
            1 => {
                // horizontal flip
                for (self.data) |slice, row| {
                    flipped[self.data.len - 1 - row] = slice;
                }
            },
        }
        self.data = flipped;
    }
};

const Image = struct {
    tiles: []Tile,
    size: u8 = undefined,
    visited: std.AutoHashMap(u16, void),
    current_tile: [2]u8 = .{ 0, 0 },
    order: std.ArrayList(usize),

    fn checkLeft(self: @This(), candidate: Tile) bool {
        // left column is always good
        if (self.current_tile[1] == 0) return true;

        // last item's right side should be equal to candidate left side
        for (candidate.data) |_, row| {
            if (self.tiles[self.order.items[self.order.items.len - 1]]
                .data[row][candidate.data.len - 1] != candidate.data[row][0]) return false;
        }

        return true;
    }

    fn checkUpper(self: @This(), candidate: Tile) bool {
        // top row is always good
        if (self.current_tile[0] == 0) return true;

        // index of the upper element in self.order
        const upper = (self.current_tile[0] - 1) * self.size + self.current_tile[1];

        // compare upper last line with candidate's first line
        return std.mem.eql(
            bool,
            &self.tiles[self.order.items[upper]].data[candidate.data.len - 1],
            &candidate.data[0],
        );
    }

    fn solve(self: *@This()) bool {
        // all item in place
        if (self.current_tile[0] == self.size) {
            return true;
        }

        var idx: usize = 0;
        while (idx < self.tiles.len) : (idx += 1) {
            const candidate = self.tiles[idx];
            if (self.visited.contains(candidate.id)) {
                // next 8 item has the same id, skip them!
                // while adds one more, so it is 7 here
                idx += 7;
                continue;
            }

            if (!self.checkUpper(candidate)) continue;
            if (!self.checkLeft(candidate)) continue;

            self.order.append(idx) catch unreachable;
            self.visited.put(candidate.id, {}) catch unreachable;

            // move current_tile forward
            const old_tile = self.current_tile;
            if (self.current_tile[1] != self.size - 1) {
                self.current_tile[1] += 1;
            } else {
                self.current_tile[0] += 1;
                self.current_tile[1] = 0;
            }

            if (self.solve()) {
                return true;
            } else {
                // restore items
                _ = self.order.pop();
                _ = self.visited.remove(candidate.id);
                self.current_tile = old_tile;
            }
        }
        return false;
    }

    fn bigImage(self: @This()) [8 * SIZE][8 * SIZE]bool {
        var ret: [8 * SIZE][8 * SIZE]bool = undefined;
        for (self.order.items) |oidx, i| {
            const start_row = i / SIZE * 8;
            const start_col = i % SIZE * 8;
            // std.debug.print("idx: {d} srow: {d} scol: {d}\n", .{oidx, start_row, start_col});
            for (self.tiles[oidx].data) |line, row| {
                if (row == 0 or row == 9) continue;
                for (line[1..9]) |item, col| {
                    ret[start_row + row - 1][start_col + col] = item;
                }
            }
        }
        return ret;
    }
};

fn bigRotate(big: [8 * SIZE][8 * SIZE]bool) [8 * SIZE][8 * SIZE]bool {
    var rotated: [8 * SIZE][8 * SIZE]bool = undefined;
    for (big) |line, row| {
        for (line) |item, col| {
            rotated[big.len - 1 - col][row] = item;
        }
    }
    return rotated;
}

fn bigFlip(big: [8 * SIZE][8 * SIZE]bool, axis: u1) [8 * SIZE][8 * SIZE]bool {
    var flipped: [8 * SIZE][8 * SIZE]bool = undefined;
    switch (axis) {
        0 => return big,
        1 => {
            // horizontal flip
            for (big) |slice, row| {
                flipped[big.len - 1 - row] = slice;
            }
        },
    }
    return flipped;
}

pub fn first(allocator: ?std.mem.Allocator) anyerror!usize {
    var image = Image{
        .tiles = undefined,
        .visited = std.AutoHashMap(u16, void).init(allocator.?),
        .order = std.ArrayList(usize).init(allocator.?),
    };

    image.tiles = try parseInput(allocator.?, @embedFile(PATH));
    defer {
        allocator.?.free(image.tiles);
        image.visited.deinit();
        image.order.deinit();
    }
    image.size = @intCast(u8, std.math.sqrt(image.tiles.len / 8));

    if (!image.solve()) unreachable;

    var mul: usize = 1;
    for ([_]usize{
        0,
        image.size - 1,
        image.size * image.size - image.size,
        image.size * image.size - 1,
    }) |order_no| {
        mul *= image.tiles[image.order.items[order_no]].id;
    }

    return mul;
}

const SIZE = 12;

pub fn second(allocator: ?std.mem.Allocator) anyerror!usize {
    var image = Image{
        .tiles = undefined,
        .visited = std.AutoHashMap(u16, void).init(allocator.?),
        .order = std.ArrayList(usize).init(allocator.?),
    };

    image.tiles = try parseInput(allocator.?, @embedFile(PATH));
    defer {
        allocator.?.free(image.tiles);
        image.visited.deinit();
        image.order.deinit();
    }
    image.size = @intCast(u8, std.math.sqrt(image.tiles.len / 8));

    if (!image.solve()) unreachable;

    var big = image.bigImage();

    var monsters: usize = 0;
    var habitat: usize = 0;
    for ([_]u1{ 0, 1 }) |flip| {
        big = bigFlip(big, flip);
        for ([_]u2{ 0, 1, 2, 3 }) |rotate| {
            big = bigRotate(big);
            // Monster search...
            for (big) |line, row| {
                ears: for (line) |item, col| {
                    if (flip == 0 and rotate == 0 and item == true) habitat += 1;
                    // ear is on .{-1, 18}
                    if (col < 18) continue;
                    // after ear we move one right
                    if (col >= big.len - 1) continue;
                    // after ear we move two down
                    if (row >= big.len - 2) continue;
                    if (item == true) {
                        const tailRow = row + 1;
                        const tailCol = col - 18;
                        for (monster_offsets) |diff| {
                            if (big[tailRow + diff[0]][tailCol + diff[1]] != true) continue :ears;
                        }
                        monsters += 1;
                    }
                }
            }
            if (monsters > 0) return habitat - monsters * 15;
        }
    }
    unreachable;
}

//                   O
// X    ##    ##    ###
//  #  #  #  #  #  #
//
// check for ears (marked with O first), then move .{1, -18} to tail (marked with X)
// and try these positions...
// zig fmt: off
const monster_offsets = [_][2]u8{
    .{ 0, 0 }, .{ 0, 5 }, .{ 0, 6 }, .{ 0, 11 }, .{ 0, 12 }, .{ 0, 17 }, .{ 0, 18 }, .{ 0, 19 },
    .{ 1, 1 }, .{ 1, 4 }, .{ 1, 7 }, .{ 1, 10 }, .{ 1, 13 }, .{ 1, 16 } };
// zig fmt: on

fn parseInput(allocator: std.mem.Allocator, input: Str) anyerror![]Tile {
    var ret = std.ArrayList(Tile).init(allocator);

    var lines = std.mem.split(u8, input, "\n");

    var t: Tile = undefined;
    var l_no: usize = 0;

    while (lines.next()) |line| {
        if (line.len == 0) {
            try flipRotate(&ret, &t);
            l_no = 0;
            t = undefined;
            continue;
        }
        switch (line[0]) {
            'T' => {
                t.id = try std.fmt.parseUnsigned(u16, line[5..9], 10);
            },
            '.', '#' => {
                for (line) |ch, idx| {
                    switch (ch) {
                        '.' => t.data[l_no][idx] = false,
                        '#' => t.data[l_no][idx] = true,
                        else => unreachable,
                    }
                }
                l_no += 1;
            },
            else => unreachable,
        }
    }

    // add the last item...
    try flipRotate(&ret, &t);

    return ret.toOwnedSlice();
}

fn flipRotate(ret: *std.ArrayList(Tile), t: *Tile) !void {
    for ([_]u1{ 0, 1 }) |flip| {
        t.flip(flip);
        for ([_]u2{ 0, 1, 2, 3 }) |_| {
            t.rotate();
            try ret.append(t.*);
        }
    }
}

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

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

const test_input =
    \\Tile 2311:
    \\..##.#..#.
    \\##..#.....
    \\#...##..#.
    \\####.#...#
    \\##.##.###.
    \\##...#.###
    \\.#.#.#..##
    \\..#....#..
    \\###...#.#.
    \\..###..###
    \\
    \\Tile 1951:
    \\#.##...##.
    \\#.####...#
    \\.....#..##
    \\#...######
    \\.##.#....#
    \\.###.#####
    \\###.##.##.
    \\.###....#.
    \\..#.#..#.#
    \\#...##.#..
    \\
    \\Tile 1171:
    \\####...##.
    \\#..##.#..#
    \\##.#..#.#.
    \\.###.####.
    \\..###.####
    \\.##....##.
    \\.#...####.
    \\#.##.####.
    \\####..#...
    \\.....##...
    \\
    \\Tile 1427:
    \\###.##.#..
    \\.#..#.##..
    \\.#.##.#..#
    \\#.#.#.##.#
    \\....#...##
    \\...##..##.
    \\...#.#####
    \\.#.####.#.
    \\..#..###.#
    \\..##.#..#.
    \\
    \\Tile 1489:
    \\##.#.#....
    \\..##...#..
    \\.##..##...
    \\..#...#...
    \\#####...#.
    \\#..#.#.#.#
    \\...#.#.#..
    \\##.#...##.
    \\..##.##.##
    \\###.##.#..
    \\
    \\Tile 2473:
    \\#....####.
    \\#..#.##...
    \\#.##..#...
    \\######.#.#
    \\.#...#.#.#
    \\.#########
    \\.###.#..#.
    \\########.#
    \\##...##.#.
    \\..###.#.#.
    \\
    \\Tile 2971:
    \\..#.#....#
    \\#...###...
    \\#.#.###...
    \\##.##..#..
    \\.#####..##
    \\.#..####.#
    \\#..#.#..#.
    \\..####.###
    \\..#.#.###.
    \\...#.#.#.#
    \\
    \\Tile 2729:
    \\...#.#.#.#
    \\####.#....
    \\..#.#.....
    \\....#..#.#
    \\.##..##.#.
    \\.#.####...
    \\####.#.#..
    \\##.####...
    \\##..#.##..
    \\#.##...##.
    \\
    \\Tile 3079:
    \\#.#.#####.
    \\.#..######
    \\..#.......
    \\######....
    \\####.#..#.
    \\.#...#.##.
    \\#.#####.##
    \\..#.###...
    \\..#.......
    \\..#.###...
;