const std = @import("std");

const ArrayList = std.ArrayList;

const SymmetryResult = struct {
    part1: u32,
    part2: u32,
};

pub fn solve_part1(allocator: std.mem.Allocator, file: []const u8) !void {
    var inputs = try std.fs.cwd().openFile(file, .{});

    var contents = try inputs.readToEndAlloc(allocator, 100 * 1024);

    var section_splitter = std.mem.splitScalar(u8, contents[0 .. contents.len - 1], '\n');

    var sections = ArrayList(ArrayList([]const u8)).init(allocator);

    try sections.append(ArrayList([]const u8).init(allocator));

    {
        var section = &sections.items[0];

        while (section_splitter.next()) |line| {
            if (line.len == 0) {
                try sections.append(ArrayList([]const u8).init(allocator));
                section = &sections.items[sections.items.len - 1];
                continue;
            }

            try section.append(line);
        }
    }

    var timer = try std.time.Timer.start();
    var grid: [][50]u8 = try allocator.alloc([50]u8, 50);
    defer allocator.free(grid);

    var rows: []u32 = try allocator.alloc(u32, 50);
    defer allocator.free(rows);
    var num_rows: u32 = 0;

    var result_1: u64 = 0;
    var result_2: u64 = 0;

    for (sections.items) |section| {
        num_rows = 0;
        for (section.items, 0..) |line, i| {
            @memcpy(grid[i][0..line.len], line);
            std.mem.replaceScalar(u8, grid[i][0..line.len], '.', '0');
            std.mem.replaceScalar(u8, grid[i][0..line.len], '#', '1');
            rows[num_rows] = try std.fmt.parseInt(u32, grid[i][0..line.len], 2);
            num_rows += 1;
        }

        var result = find_symmetries(rows[0..num_rows]);

        result_1 += result.part1 * 100;
        result_2 += result.part2 * 100;

        transpose(grid, @intCast(section.items.len), @intCast(section.items[0].len));
        num_rows = 0;

        for (0..section.items[0].len) |i| {
            rows[num_rows] = try std.fmt.parseInt(u32, grid[i][0..section.items.len], 2);
            num_rows += 1;
        }

        result = find_symmetries(rows[0..num_rows]);

        result_1 += result.part1;
        result_2 += result.part2;
    }

    var time = timer.read();

    std.log.info("Part 1: {d}", .{result_1});
    std.log.info("Part 2: {d}", .{result_2});

    std.log.info("Time: {d}", .{time});
}

fn transpose(grid: [][50]u8, rows: u32, columns: u32) void {
    var max = @max(rows, columns);
    for (0..max) |row| {
        for (row..max) |column| {
            var t = grid[row][column];
            grid[row][column] = grid[column][row];
            grid[column][row] = t;
        }
    }
}

fn is_pow_2(val: u64) bool {
    return (val & (val - 1)) == 0 and val != 0;
}

fn find_symmetries(rows: []u32) SymmetryResult {
    var errors: u32 = 0;
    var error_val: u32 = 0;
    var result = SymmetryResult{
        .part1 = 0,
        .part2 = 0,
    };

    for (0..rows.len - 1) |row| {
        errors = 0;

        var j: u32 = 0;

        while (errors < 2 and row >= j and j + row + 1 < rows.len) : (j += 1) {
            if (rows[row - j] != rows[row + j + 1]) {
                errors += 1;
                error_val = rows[row - j] ^ rows[row + j + 1];
            }
        }

        if (errors == 1 and is_pow_2(error_val)) {
            result.part2 += @intCast(row + 1);
        }

        if (errors == 0) {
            result.part1 += @intCast(row + 1);
        }
    }

    return result;
}