const std = @import("std");

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

pub fn first(allocator: std.mem.Allocator) !isize {
    var tk = TableKnights{ .pairs = undefined };

    tk.pairs = try parseInput(allocator, @embedFile(PATH));
    defer tk.pairs.deinit();

    tk.permute(0, tk.table.len);

    return tk.max;
}

pub fn second(allocator: std.mem.Allocator) !isize {
    var tk = TableKnights{ .pairs = undefined };

    tk.pairs = try parseInput(allocator, @embedFile(PATH));
    defer tk.pairs.deinit();

    tk.permute(0, tk.table.len);

    var min: isize = std.math.maxInt(isize);

    // find the least good pair, replace that with myself (zeros)
    var idx: usize = 0;
    while (idx < tk.best.len) : (idx += 1) {
        const val = tk.pairs.get(.{ tk.best[idx], tk.best[(idx + 1) % tk.best.len] }) orelse
            tk.pairs.get(.{ tk.best[(idx + 1) % tk.best.len], tk.best[idx] }).?;
        if (val < min) min = val;
    }

    return tk.max - min;
}

test "day13a" {
    try std.testing.expectEqual(@as(isize, 709), try first(std.testing.allocator));
}

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

const Pairs = std.AutoHashMap([2]u8, isize);
const TableKnights = struct {
    pairs: Pairs,
    table: [8]u8 = .{ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'M' },
    best: [8]u8 = undefined,
    max: isize = 0,

    fn permute(self: *@This(), start: usize, end: usize) void {
        if (start == end) {
            const value = self.calValue();
            if (value > self.max) {
                self.max = value;
                self.best = self.table;
            }
        } else {
            var idx: usize = start;
            while (idx < end) : (idx += 1) {
                std.mem.swap(u8, &self.table[start], &self.table[idx]);

                self.permute(start + 1, end);

                std.mem.swap(u8, &self.table[start], &self.table[idx]);
            }
        }
    }

    fn calValue(self: @This()) isize {
        var sum: isize = 0;

        var idx: usize = 0;
        while (idx < self.table.len) : (idx += 1) {
            sum += self.pairs.get(.{ self.table[idx], self.table[(idx + 1) % self.table.len] }) orelse
                self.pairs.get(.{ self.table[(idx + 1) % self.table.len], self.table[idx] }).?;
        }

        return sum;
    }
};

fn parseInput(allocator: std.mem.Allocator, in: Str) !Pairs {
    var pairs = Pairs.init(allocator);

    var lines = std.mem.tokenize(u8, in, "\n");
    while (lines.next()) |line| {
        var words = std.mem.tokenize(u8, line, " ");

        var pair: [2]u8 = undefined;
        pair[0] = words.next().?[0];

        _ = words.next(); // would

        // lose of gain
        const gain: isize = switch (words.next().?[0]) {
            'g' => try std.fmt.parseUnsigned(isize, words.next().?, 10),
            'l' => try std.fmt.parseUnsigned(isize, words.next().?, 10) * -1,
            else => unreachable,
        };

        // drop "happiness units by sitting next to"
        var idx: usize = 0;
        while (idx < 6) : (idx += 1) {
            _ = words.next();
        }

        pair[1] = words.next().?[0];

        if (pairs.contains(pair)) {
            pairs.getPtr(pair).?.* += gain;
        } else if (pairs.contains(.{ pair[1], pair[0] })) {
            pairs.getPtr(.{ pair[1], pair[0] }).?.* += gain;
        } else {
            try pairs.put(pair, gain);
        }
    }

    std.debug.assert(pairs.count() == 8 * 7 / 2);

    return pairs;
}