const std = @import("std");

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

pub fn first(allocator: std.mem.Allocator) !usize {
    var sues = SueList.init(allocator);
    defer sues.deinit();

    try parseInput(&sues, @embedFile(PATH));

    std.debug.assert(sues.items.len == 500);

    inline for (@typeInfo(Sue).Struct.fields) |fld| {
        comptime if (std.mem.eql(u8, "id", fld.name)) continue;

        var next_sues = SueList.init(allocator);
        for (sues.items) |sue| {
            if (@field(sue, fld.name)) |val| {
                if (val == @field(target_sue, fld.name)) {
                    try next_sues.append(sue);
                }
            } else {
                try next_sues.append(sue);
            }
        }

        sues.deinit();
        sues = next_sues;
    }

    std.debug.assert(sues.items.len == 1);

    return sues.items[0].id;
}

pub fn second(allocator: std.mem.Allocator) !usize {
    var sues = SueList.init(allocator);
    defer sues.deinit();

    try parseInput(&sues, @embedFile(PATH));

    std.debug.assert(sues.items.len == 500);

    inline for (@typeInfo(Sue).Struct.fields) |fld| {
        comptime if (std.mem.eql(u8, "id", fld.name)) continue;

        var next_sues = SueList.init(allocator);
        for (sues.items) |sue| {
            if (@field(sue, fld.name)) |val| {
                if (std.mem.eql(u8, "cats", fld.name) or std.mem.eql(u8, "trees", fld.name)) {
                    if (val > @field(target_sue, fld.name).?) try next_sues.append(sue);
                } else if (std.mem.eql(u8, "pomeranians", fld.name) or std.mem.eql(u8, "goldfish", fld.name)) {
                    if (val < @field(target_sue, fld.name).?) try next_sues.append(sue);
                } else {
                    if (val == @field(target_sue, fld.name)) {
                        try next_sues.append(sue);
                    }
                }
            } else {
                try next_sues.append(sue);
            }
        }

        sues.deinit();
        sues = next_sues;
    }

    std.debug.assert(sues.items.len == 1);

    return sues.items[0].id;
}

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

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

const target_sue: Sue = .{
    .id = 0,
    .children = 3,
    .cats = 7,
    .samoyeds = 2,
    .pomeranians = 3,
    .akitas = 0,
    .vizslas = 0,
    .goldfish = 5,
    .trees = 3,
    .cars = 2,
    .perfumes = 1,
};

const SueValue = u4;
const Sue = struct {
    id: u9,
    children: ?SueValue = null,
    cats: ?SueValue = null,
    samoyeds: ?SueValue = null,
    pomeranians: ?SueValue = null,
    akitas: ?SueValue = null,
    vizslas: ?SueValue = null,
    goldfish: ?SueValue = null,
    trees: ?SueValue = null,
    cars: ?SueValue = null,
    perfumes: ?SueValue = null,
};
const SueList = std.ArrayList(Sue);

fn parseInput(sl: *SueList, input: Str) !void {
    var lines = std.mem.tokenize(u8, input, "\n");
    while (lines.next()) |line| {
        var s: Sue = .{ .id = 0 };
        const pivot = std.mem.indexOf(u8, line, ":").?;
        s.id = try std.fmt.parseUnsigned(u9, line[4..pivot], 10);

        var tags = std.mem.tokenize(u8, line[pivot + 2 ..], ", ");
        while (tags.next()) |tag| {
            // std.debug.print("{s}\n", .{tag});
            switch (tag[0]) {
                'a'...'z' => {
                    const val = try std.fmt.parseUnsigned(SueValue, tags.next().?, 10);
                    switch (tag[0]) {
                        'a' => s.akitas = val,
                        'c' => {
                            switch (tag[1]) {
                                'a' => {
                                    switch (tag[2]) {
                                        'r' => s.cars = val,
                                        't' => s.cats = val,
                                        else => unreachable,
                                    }
                                },
                                'h' => s.children = val,
                                else => unreachable,
                            }
                        },
                        'g' => s.goldfish = val,
                        'p' => {
                            switch (tag[1]) {
                                'e' => s.perfumes = val,
                                'o' => s.pomeranians = val,
                                else => unreachable,
                            }
                        },
                        's' => s.samoyeds = val,
                        't' => s.trees = val,
                        'v' => s.vizslas = val,
                        else => unreachable,
                    }
                },
                else => unreachable,
            }
        }
        try sl.append(s);
    }
}