solve_day19 :: (test: bool) {
    contents := read_entire_file(ifx test then "inputs/day19_test.txt" else "inputs/day19.txt");
    lines := split(contents, cast(u8) #char "\n");

    part1 := 0;
    part2 := 1;


    counter := 0;

    for lines {
        blueprint := parse_blueprint(it);

        res, count := run_blueprint(blueprint, 24) * (it_index + 1);

        part1 += res;
        counter += count;

        if it_index < 3 {
            res,count = run_blueprint(blueprint, 32);
            counter += count;
            part2 *= res;
        }
    }

    print("Part 1: %\n", part1);
    print("Part 2: %\n", part2);
    print("Counter: %\n", counter);
}

run_blueprint :: (blueprint: Blueprint, time: s64) -> s64, s64{
    queue: Deque(Permutation);
    init(*queue);
    resize(*queue, 2_000_000);
    defer deinit(*queue);

    deque_add_last(*queue, .{
        to_build = .ORE,
        time = time,
    });
    deque_add_last(*queue, .{
        to_build = .CLAY,
        time = time,
    });

    result := 0;


    counter := 0;
    max_deque := 0;
    while queue.count > 0 {
        max_deque = max(max_deque, queue.count);

        permutation := deque_remove_first(*queue);

        counter += 1;

        if permutation.time <= 0 || calc_max(permutation) <= result {
            continue;
        }


        while permutation.time > 0 {
            building := Robot_Type.NONE;

            if permutation.to_build == {
                case .ORE;
                    if has_enough(permutation, blueprint.cost[0]) {
                        building = .ORE;
                        subtract_cost(*permutation, blueprint.cost[0]);
                    }
                case .CLAY;
                    if has_enough(permutation, blueprint.cost[1]) {
                        building = .CLAY;
                        subtract_cost(*permutation, blueprint.cost[1]);
                    }
                case .OBSIDIAN;
                    if has_enough(permutation, blueprint.cost[2]) {
                        building = .OBSIDIAN;
                        subtract_cost(*permutation, blueprint.cost[2]);
                    }
                case .GEODE;
                    if has_enough(permutation, blueprint.cost[3]) {
                        building = .GEODE;
                        subtract_cost(*permutation, blueprint.cost[3]);
                    }
            }
            mine(*permutation);

            result = max(result, permutation.geode);

            if building == {
                case .ORE;      permutation.ore_robots      += 1;
                case .CLAY;     permutation.clay_robots     += 1;
                case .OBSIDIAN; permutation.obsidian_robots += 1;
                case .GEODE;    permutation.geode_robots    += 1;
            }

            if building != .NONE {
                if can_build(permutation, blueprint.cost[3]) {
                    new_perm := permutation;
                    new_perm.to_build = .GEODE;
                    deque_add_last(*queue, new_perm);
                } 
                if permutation.ore_robots < blueprint.max_ore_robots {
                    new_perm := permutation;
                    new_perm.to_build = .ORE;
                    deque_add_last(*queue, new_perm);
                }
                if permutation.clay_robots < blueprint.max_clay_robots {
                    new_perm := permutation;
                    new_perm.to_build = .CLAY;
                    deque_add_last(*queue, new_perm);
                }
                if permutation.obsidian_robots < blueprint.max_obsidian_robots && can_build(permutation, blueprint.cost[2]) {
                    new_perm := permutation;
                    new_perm.to_build = .OBSIDIAN;
                    deque_add_last(*queue, new_perm);
                }
                break;
            }
        }
    }

    return result, max_deque;
}

can_build :: (permutation: Permutation, robot: Robot) -> bool {
    if robot.clay_cost > 0 && permutation.clay_robots == 0  return false;
    if robot.obsidian_cost > 0 && permutation.obsidian_robots == 0  return false;

    return true;
}

mine :: (permutation: *Permutation) {
    permutation.time     -= 1;
    permutation.ore      += permutation.ore_robots;
    permutation.clay     += permutation.clay_robots;
    permutation.obsidian += permutation.obsidian_robots;
    permutation.geode    += permutation.geode_robots;
}

has_enough :: (permutation: Permutation, robot: Robot) -> bool {
    result := permutation.ore >= robot.ore_cost &&
           permutation.clay >= robot.clay_cost &&
           permutation.obsidian >= robot.obsidian_cost;


    return result;
}

subtract_cost :: (permutation: *Permutation, robot: Robot) {
    permutation.ore -= robot.ore_cost;
    permutation.clay -= robot.clay_cost;
    permutation.obsidian -= robot.obsidian_cost;
}

parse_blueprint :: (line: string) -> Blueprint {
    colon_index := find_index_from_left(line, cast(u8) #char ":");

    robots := split(slice(line, colon_index + 2, line.count - colon_index - 3), ". ");

    blueprint: Blueprint = .{
        cost = Robot.[
            parse_robot(robots[0]),
            parse_robot(robots[1]),
            parse_robot(robots[2]),
            parse_robot(robots[3]),
        ],
    };

    for blueprint.cost {
        blueprint.max_ore_robots = max(blueprint.max_ore_robots, it.ore_cost);
        blueprint.max_clay_robots = max(blueprint.max_clay_robots, it.clay_cost);
        blueprint.max_obsidian_robots = max(blueprint.max_obsidian_robots, it.obsidian_cost);
    }

    return blueprint; 
}

calc_max :: (permutation: Permutation) -> s64 {
    return permutation.geode + (permutation.time + 2 * permutation.geode_robots) * permutation.time / 2;
}

parse_robot :: (line: string) -> Robot {
    parts := split(line, cast(u8) #char " ");

    cost1 := string_to_int(parts[4], 10, s64);
    cost2 := -1;

    robot: Robot;

    if parts[5] == {
        case "ore";
            robot.ore_cost = cost1;
        case "clay";
            robot.clay_cost = cost1;
        case "obsidian";
            robot.obsidian_cost = cost1;
        case;
            assert(false, "Bad cost category '%'", parts[5]);
    }

    if parts.count == 9 {
        cost2 = string_to_int(parts[7], 10, s64);


        if parts[8] == {
            case "ore";
                robot.ore_cost = cost2;
            case "clay";
                robot.clay_cost = cost2;
            case "obsidian";
                robot.obsidian_cost = cost2;
            case;
                assert(false, "Bad cost category '%'", parts[8]);
        }
    }

    return robot;
}

Permutation :: struct {
    time:             s64 = 0;
    ore_robots:      s64 = 1;
    clay_robots:     s64 = 0;
    obsidian_robots: s64 = 0;
     geode_robots:    s64 = 0;
    ore:      s64;
    clay:     s64;
    obsidian: s64;
    geode:    s64;
    to_build: Robot_Type = .NONE;
}

Blueprint :: struct {
    max_ore_robots:      s64;
    max_clay_robots:     s64;
    max_obsidian_robots: s64;
    cost: [4]Robot;
}

Robot :: struct {
    ore_cost:      s64;
    clay_cost:     s64;
    obsidian_cost: s64;
}

Robot_Type :: enum {
    ORE      :: 0;
    CLAY     :: 1;
    OBSIDIAN :: 2;
    GEODE    :: 3;
    NONE     :: 4;
}