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

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

const Amphipod = enum {
    A,
    B,
    C,
    D,
};

const hallway_length = 7;
const siderooms = 4;
const sideroom_depth = 2;

const PathType = u4;

const State = struct {
    hallway: [hallway_length]?Amphipod = .{null} ** hallway_length,
    rooms: [siderooms][sideroom_depth]?Amphipod,
    fn print(self: @This()) []u8 {
        const blank =
            \\#############
            \\#...........#
            \\###.#.#.#.###
            \\  #.#.#.#.#
            \\  #########
        ;
        var buffer: [100]u8 = undefined;
        var mod: []u8 = buffer[0..blank.len];
        std.mem.copy(u8, mod, blank);
        for (self.hallway) |amp, idx| {
            if (amp != null) {
                const i: usize = switch (idx) {
                    0 => 14 + 1,
                    1 => 14 + 2,
                    2 => 14 + 4,
                    3 => 14 + 6,
                    4 => 14 + 8,
                    5 => 14 + 10,
                    6 => 14 + 11,
                    else => unreachable,
                };
                const a: u8 = switch (amp.?) {
                    .A => 'A',
                    .B => 'B',
                    .C => 'C',
                    .D => 'D',
                };
                mod[i] = a;
            }
        }
        for (self.rooms) |room, rid| {
            for (room) |amp, aid| {
                if (amp != null) {
                    const a: u8 = switch (amp.?) {
                        .A => 'A',
                        .B => 'B',
                        .C => 'C',
                        .D => 'D',
                    };
                    mod[14 + 14 + rid * 2 + 3 + 14 * aid] = a;
                }
            }
        }
        return mod;
    }
    fn sideRoomDone(self: @This(), room: usize) bool {
        std.debug.assert(room < siderooms);
        for (self.rooms[room]) |amp| {
            // https://github.com/ziglang/zig/issues/6059
            if (amp == null) {
                return false;
            } else if (amp != @intToEnum(Amphipod, room)) return false;
        }
        return true;
    }
    fn done(self: @This()) bool {
        // check if hallway is empty
        for (self.hallway) |item| {
            if (item != null) return false;
        }

        // every room is done
        for (self.rooms) |_, idx| {
            if (!self.sideRoomDone(idx)) return false;
        }
        return true;
    }
    fn sideRoomFreeSpace(self: @This(), room: usize) ?u2 {
        var ret: u2 = 0;

        std.debug.assert(room < siderooms);
        for (self.rooms[room]) |amp| {
            if (amp == null) {
                ret += 1;
            } else if (amp != @intToEnum(Amphipod, room)) {
                return null;
            }
        }

        std.debug.assert(ret <= sideroom_depth);
        return ret;
    }
    fn checkSideRoom(self: @This()) ?StateQueue {
        for (self.rooms) |_, idx| {
            const free = self.sideRoomFreeSpace(idx) orelse continue;
            if (free == 0) continue;

            // std.debug.print("FREE: {d}\n", .{free});

            for (self.hallway) |amp, hwidx| {
                if (amp == @intToEnum(Amphipod, idx)) {
                    const steps = (self.checkHWtoRoom(hwidx, idx) orelse continue) + free;

                    var ret: StateQueue = undefined;

                    ret.energy = (std.math.powi(usize, 10, @enumToInt(amp.?)) catch unreachable) * steps;

                    ret.state = self;
                    ret.state.hallway[hwidx] = null;
                    ret.state.rooms[idx][free - 1] = amp;

                    return ret;
                }
            }

            for (self.rooms) |other, oidx| {
                if (idx == oidx) continue;

                var pos: PathType = 0;
                for (other) |amp| {
                    if (amp == null) {
                        pos += 1;
                        continue;
                    }
                    if (amp == @intToEnum(Amphipod, idx)) {
                        var steps: PathType = undefined;
                        if (idx < oidx) { // moving left
                            const hwidx = oidx + 2;
                            // std.debug.print("L->R from: {d} to: {d}\n", .{ hwidx, idx });
                            steps = (self.checkHWtoRoom(hwidx, idx) orelse break) + free + pos;
                        } else { // moving right
                            const hwidx = oidx + 1;
                            // std.debug.print("R->L from: {d} to: {d}\n", .{ hwidx, idx });
                            steps = (self.checkHWtoRoom(hwidx, idx) orelse break) + free + pos;
                        }

                        // we can move amp to his sideroom
                        var ret: StateQueue = undefined;

                        ret.energy = (std.math.powi(usize, 10, @enumToInt(amp.?)) catch unreachable) * steps;

                        ret.state = self;
                        ret.state.rooms[oidx][pos] = null;
                        ret.state.rooms[idx][free - 1] = amp;

                        return ret;
                    }
                    // only first item can move, no need to check others
                    break;
                }
            }
        }

        return null;
    }
    fn checkHWtoRoom(self: @This(), from: usize, to: usize) ?PathType {
        // #############
        // #01.2.3.4.56#
        // ###0#1#2#3###
        //   #0#1#2#3#
        //   #########

        std.debug.assert(to < 4);
        std.debug.assert(from < 8);

        var steps: PathType = 0;
        if (from -| to >= 2) {
            var idx: usize = from -| 1;
            while (idx >= to + 2) : (idx -= 1) {
                // std.debug.print("LEFT from: {d} to: {d} step: {d}\n", .{ from, to, idx });
                if (self.hallway[idx] == null) steps += 2 else return null;
            }
        } else {
            var idx: usize = from + 1;
            while (idx <= to + 1) : (idx += 1) {
                // std.debug.print("RIGHT from: {d} to: {d} step: {d}\n", .{ from, to, idx });
                if (self.hallway[idx] == null) steps += 2 else return null;
            }
        }
        // room entrance point
        if (from != 6 and from != 0) steps += 1;

        return steps;
    }
    fn getValidStates(self: @This(), allocator: std.mem.Allocator) ![]StateQueue {
        var ret = std.ArrayList(StateQueue).init(allocator);

        // Amphipod can move to its final place, always a good move
        if (self.checkSideRoom()) |item| {
            try ret.append(item);
            return ret.toOwnedSlice();
        }

        // we already checked the side rooms, so we only have to collect
        // each rooms first item's possible hallway positions
        for (self.rooms) |room, idx| {
            // std.debug.print("ROOM->HW check {d} room\n", .{idx});
            for (room) |amp, pos| {
                if (amp == null) continue;

                // do not move items in right room
                // except: we should move away to let wrong placed amp out
                if (amp == @intToEnum(Amphipod, idx)) {
                    var tainted: bool = false;

                    var next: PathType = @intCast(PathType, pos) + 1;
                    while (next < sideroom_depth) : (next += 1) {
                        if (room[next] != @intToEnum(Amphipod, idx)) tainted = true;
                    }
                    if (!tainted) break;
                }

                for (self.hallway) |hw, hwidx| {
                    if (hw == null) { // destination hallway spot is free
                        // std.debug.print("checking hallway spot {d} with amp {any}\n", .{hwidx, amp});
                        const steps = (self.checkHWtoRoom(hwidx, idx) orelse continue) + pos + 1;

                        var st: StateQueue = undefined;

                        st.energy = steps * try std.math.powi(usize, 10, @enumToInt(amp.?));

                        st.state = self;
                        st.state.hallway[hwidx] = amp;
                        st.state.rooms[idx][pos] = null;

                        try ret.append(st);
                    }
                }
                // only first item can move
                break;
            }
        }

        return ret.toOwnedSlice();
    }
};

const StateQueue = struct {
    state: State,
    energy: usize,
};

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

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

    try std.testing.expectEqual(@as(usize, 16244), ret);

    std.debug.print("Day 23a result: {d} \t\ttime: {d}us\n", .{ ret, t });
}

fn lessThan(context: void, a: StateQueue, b: StateQueue) std.math.Order {
    _ = context;
    if (a.energy == b.energy) {
        return .eq;
    } else if (a.energy < b.energy) {
        return .lt;
    } else if (a.energy > b.energy) {
        return .gt;
    } else unreachable;
}

pub fn first(allocator: ?std.mem.Allocator) !usize {
    var queue = std.PriorityQueue(StateQueue, void, lessThan).init(allocator.?, {});
    defer queue.deinit();

    var visited = std.AutoHashMap(State, void).init(allocator.?);
    defer visited.deinit();

    // var backtrack = std.AutoHashMap(StateQueue, StateQueue).init(allocator);
    // defer backtrack.deinit();

    var init_state: StateQueue = undefined;
    init_state.state = try parseInput();
    init_state.energy = 0;

    try queue.add(init_state);

    while (queue.count() > 0) {
        // var steps: usize = 0;
        // while (steps < 1) : (steps += 1) {
        const curr = queue.remove();

        // std.debug.print("{d}\n{s}\n", .{ curr.energy, curr.state.print() });
        // std.debug.print("{d} ", .{curr.energy});

        // skip if already checked
        if (visited.contains(curr.state)) continue;
        try visited.put(curr.state, {});

        // Amphipods are in place, return energy
        if (curr.state.done()) {
            // var bt: StateQueue = curr;
            // while (backtrack.contains(bt)) {
            //     std.debug.print("{d}\n{s}\n", .{bt.energy, bt.state.print()});
            //     bt = backtrack.get(bt).?;
            // }
            return curr.energy;
        }

        var possible_states = try curr.state.getValidStates(allocator.?);
        defer allocator.?.free(possible_states);

        for (possible_states) |*sq| {
            sq.energy += curr.energy;
            // std.debug.print("{d}\n{s}\n", .{ sq.energy, sq.state.print() });
            try queue.add(sq.*);
            // try backtrack.put(sq.*, curr);
        }
    }

    unreachable;
}

fn parseInput() !State {
    const input = @embedFile(path);
    var lines = std.mem.split(u8, input, "\n");

    var ret = State{ .rooms = undefined };

    var line: usize = 0;
    var counter: usize = 0;
    while (lines.next()) |l| : (line += 1) {
        if (line == 2 or line == 3) {
            for (l) |ch, idx| {
                if (idx == 3 or idx == 5 or idx == 7 or idx == 9) {
                    const amp: Amphipod = switch (ch) {
                        'A' => .A,
                        'B' => .B,
                        'C' => .C,
                        'D' => .D,
                        else => unreachable,
                    };
                    // std.debug.print("{d} {d}\n", .{counter%siderooms, counter/siderooms});
                    ret.rooms[counter % siderooms][counter / siderooms] = amp;
                    counter += 1;
                }
            }
        }
    }

    return ret;
}

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

test "checkHWtoRoom" {
    const room = [sideroom_depth]?Amphipod{ null, null };
    const st = State{
        .hallway = [_]?Amphipod{ null, null, null, null, null, null, null },
        .rooms = .{ room, room, room, room },
    };
    try std.testing.expectEqual(@as(PathType, 5), st.checkHWtoRoom(5, 1).?);
    try std.testing.expectEqual(@as(PathType, 6), st.checkHWtoRoom(6, 1).?);
    try std.testing.expectEqual(@as(PathType, 2), st.checkHWtoRoom(6, 3).?);
    try std.testing.expectEqual(@as(PathType, 1), st.checkHWtoRoom(1, 0).?);
    try std.testing.expectEqual(@as(PathType, 2), st.checkHWtoRoom(0, 0).?);
    try std.testing.expectEqual(@as(PathType, 8), st.checkHWtoRoom(0, 3).?);
    try std.testing.expectEqual(@as(PathType, 6), st.checkHWtoRoom(0, 2).?);
}