const std = @import("std");

const Str = []const u8;

const PATH = "input/day07.txt";

pub fn first(allocator: std.mem.Allocator) !usize {
    var known = std.StringHashMap(usize).init(allocator);
    defer known.deinit();
    return try resolve(&known, @embedFile(PATH));
}

pub fn second(allocator: std.mem.Allocator) !usize {
    var known = std.StringHashMap(usize).init(allocator);
    defer known.deinit();
    try known.put("b", 956);
    return try resolve(&known, @embedFile(PATH));
}

fn resolve(known: *std.StringHashMap(usize), input: Str) !usize {
    var lines = std.mem.tokenize(u8, input, "\n");

    while (true) {
        const size = known.count();
        lines.reset();
        while (lines.next()) |line| {
            var words = std.mem.tokenize(u8, line, " ");

            if (std.mem.containsAtLeast(u8, line, 1, "NOT")) {
                _ = words.next(); // NOT
                const wire = words.next().?;
                const val = known.get(wire) orelse continue;
                _ = words.next().?; // drop '->'
                const target = words.next().?;

                try known.put(target, ~val);
            } else if (std.mem.containsAtLeast(u8, line, 1, "AND") or
                std.mem.containsAtLeast(u8, line, 1, "OR") or
                std.mem.containsAtLeast(u8, line, 1, "LSHIFT") or
                std.mem.containsAtLeast(u8, line, 1, "RSHIFT"))
            {
                const left = words.next().?;
                const operand = words.next().?;
                const right = words.next().?;
                _ = words.next(); // drop '->'
                const target = words.next().?;

                try handleOp(known, left, operand, right, target);
            } else {
                const wire = words.next().?;
                const val = std.fmt.parseUnsigned(usize, wire, 10) catch known.get(wire) orelse continue;
                _ = words.next(); // drop '->'
                const target = words.next().?;

                // Do not overwrite 'b' value - Part 2
                if (std.mem.eql(u8, target, "b") and known.contains("b")) continue;

                try known.put(target, val);
            }
        }

        if (known.get("a")) |val| return val;

        if (size == known.count()) unreachable;
    }

    unreachable;
}

fn handleOp(known: *std.StringHashMap(usize), o1: Str, op: Str, o2: Str, target: Str) !void {
    const left = std.fmt.parseUnsigned(usize, o1, 10) catch known.get(o1) orelse return;
    const right = std.fmt.parseUnsigned(usize, o2, 10) catch known.get(o2) orelse return;

    if (std.mem.eql(u8, op, "AND")) {
        try known.put(target, left & right);
    } else if (std.mem.eql(u8, op, "OR")) {
        try known.put(target, left | right);
    } else if (std.mem.eql(u8, op, "RSHIFT")) {
        try known.put(target, left >> @intCast(u6, right));
    } else if (std.mem.eql(u8, op, "LSHIFT")) {
        try known.put(target, left << @intCast(u6, right));
    } else {
        unreachable;
    }
}

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

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