const std = @import("std");
const mem = std.mem;
const math = std.math;

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

const Cuboid = struct {
    state: bool,
    xmin: isize,
    xmax: isize,
    ymin: isize,
    ymax: isize,
    zmin: isize,
    zmax: isize,
};

const Cubes = []Cuboid;

const SubCubeType = i3;
const SubCubes = std.AutoArrayHashMap(Cuboid, SubCubeType);

pub fn main() !void {
    var buf: [3_000_000]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buf);
    var arena = std.heap.ArenaAllocator.init(fba.allocator());
    defer arena.deinit();

    var timer = try std.time.Timer.start();
    const ret = try second(arena.allocator());
    const t = timer.lap() / 1000;

    try std.testing.expectEqual(@as(isize, 1267133912086024), ret);

    std.debug.print("Day 22b result: {d}time: {d}us\n", .{ ret, t });
}

pub fn second(allocator: ?std.mem.Allocator) !isize {
    const input = try parseInput(allocator.?);
    defer allocator.?.free(input);

    // original and subcubes
    var cubes = SubCubes.init(allocator.?);
    defer cubes.deinit();

    for (input) |cube| {
        var overlaps = std.AutoHashMap(Cuboid, SubCubeType).init(allocator.?);
        defer overlaps.deinit();

        var it = cubes.iterator();
        while (it.next()) |orig| {
            const ovrlp = overlap(cube, orig.key_ptr.*) orelse continue;

            // this way we do not count a subcube twice
            const res = try overlaps.getOrPut(ovrlp);
            if (res.found_existing) {
                res.value_ptr.* -= orig.value_ptr.*;
            } else {
                res.value_ptr.* = -orig.value_ptr.*;
            }
        }

        // add cubes from input to overlaps array
        if (cube.state) try cubes.put(cube, 1);

        // set the main array values based on overlaps
        var oit = overlaps.iterator();
        while (oit.next()) |o| {
            const item = try cubes.getOrPut(o.key_ptr.*);
            if (item.found_existing) {
                item.value_ptr.* += o.value_ptr.*;
            } else {
                item.value_ptr.* = o.value_ptr.*;
            }
        }
    }

    var sum: isize = 0;

    var it = cubes.iterator();
    while (it.next()) |c| {
        const cube = c.key_ptr.*;
        sum += c.value_ptr.* *
            (cube.xmax - cube.xmin + 1) *
            (cube.ymax - cube.ymin + 1) *
            (cube.zmax - cube.zmin + 1);
    }

    return sum;
}

fn overlap(a: Cuboid, b: Cuboid) ?Cuboid {
    if ((a.xmax < b.xmin or a.xmin > b.xmax) or
        (a.ymax < b.ymin or a.ymin > b.ymax) or
        (a.zmax < b.zmin or a.zmin > b.zmax)) return null;
    return Cuboid{ .state = false, .xmin = math.max(a.xmin, b.xmin), .xmax = math.min(a.xmax, b.xmax), .ymin = math.max(a.ymin, b.ymin), .ymax = math.min(a.ymax, b.ymax), .zmin = math.max(a.zmin, b.zmin), .zmax = math.min(a.zmax, b.zmax) };
}

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

    var ret = std.ArrayList(Cuboid).init(a);

    while (lines.next()) |line| {
        if (line.len == 0) break;
        var cube: Cuboid = undefined;
        var st_coord = mem.tokenize(u8, line, " ");

        // set cuboid state
        if (mem.eql(u8, st_coord.next().?, "on")) cube.state = true else cube.state = false;

        var axes = mem.tokenize(u8, st_coord.next().?, ",");
        var cntr: u2 = 0;
        while (axes.next()) |ax| : (cntr += 1) {
            const axis = ax[2..]; // strip x=, y=, z=
            var min_max = mem.tokenize(u8, axis, "..");

            // coordinates
            if (cntr == 0) {
                cube.xmin = try std.fmt.parseInt(isize, min_max.next().?, 10);
                cube.xmax = try std.fmt.parseInt(isize, min_max.next().?, 10);
            }
            if (cntr == 1) {
                cube.ymin = try std.fmt.parseInt(isize, min_max.next().?, 10);
                cube.ymax = try std.fmt.parseInt(isize, min_max.next().?, 10);
            }
            if (cntr == 2) {
                cube.zmin = try std.fmt.parseInt(isize, min_max.next().?, 10);
                cube.zmax = try std.fmt.parseInt(isize, min_max.next().?, 10);
            }
        }

        try ret.append(cube);
    }

    return ret.toOwnedSlice();
}

test "day22b" {
    try std.testing.expectEqual(@as(isize, 1267133912086024), try second(std.testing.allocator));
}