const std = @import("std");
const Span = @import("lib/zig-regex/src/regex.zig").Span;
const Regex = @import("lib/zig-regex/src/regex.zig").Regex;

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

const RetType = u12;
const SnailType = u6;
const Str = []const u8;

const explode_limit = 5;

const RegExp = struct {
    number: Regex,
    last_number: Regex,
    exp_number: Regex,
};

var reg_exp: RegExp = undefined;

pub fn main() !void {
    var timer = try std.time.Timer.start();
    const ret = try first();
    const t = timer.lap() / 1000;

    try std.testing.expectEqual(@as(RetType, 3675), ret);

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

pub fn first() !RetType {
    const input = @embedFile(path);
    var in = std.mem.tokenize(u8, input, "\n");

    const allocator = std.testing.allocator;
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();

    reg_exp.number = try Regex.compile(arena.allocator(), "(\\d+)");
    reg_exp.last_number = try Regex.compile(arena.allocator(), "(\\d+)[^\\d+]+$");
    reg_exp.exp_number = try Regex.compile(arena.allocator(), "^\\[(\\d+)[^\\d]+(\\d+)");

    var ret: []const u8 = "";
    while (in.next()) |line| {
        if (ret.len == 0) {
            ret = try reduce(arena.allocator(), &line[0..]);
        } else {
            var buf: [150]u8 = undefined;
            const next = try std.fmt.bufPrint(&buf, "[{s},{s}]", .{ ret, line });
            ret = try reduce(arena.allocator(), &next[0..]);
        }
    }

    var pos: usize = 0;
    return (try magnitude(&ret, &pos));
}

fn magnitude(input: *Str, pos: *usize) anyerror!RetType {
    var left: isize = -1;
    var right: isize = -1;

    var right_side: bool = false;

    while (pos.* < input.len) : (pos.* += 1) {
        if (input.*[pos.*] == ']') {
            break;
        }

        if (input.*[pos.*] == '[') {
            pos.* += 1;
            if (right_side) {
                right = try magnitude(input, pos);
            } else {
                left = try magnitude(input, pos);
            }
            continue;
        }

        switch (input.*[pos.*]) {
            ',' => right_side = true,
            '0',
            '1',
            '2',
            '3',
            '4',
            '5',
            '6',
            '7',
            '8',
            '9',
            => {
                if (right_side) {
                    right = try std.fmt.parseUnsigned(isize, input.*[pos.* .. pos.* + 1], 10);
                } else {
                    left = try std.fmt.parseUnsigned(isize, input.*[pos.* .. pos.* + 1], 10);
                }
            },
            else => unreachable,
        }
    }

    if (left == -1 or right == -1) {
        return @intCast(RetType, left + right + 1);
    }

    return @intCast(RetType, 3 * left + 2 * right);
}

fn reduce(a: std.mem.Allocator, input: *Str) anyerror!Str {
    if (checkExplode(a, input)) {
        _ = try reduce(a, input);
    } else if (checkSplit(a, input)) {
        _ = try reduce(a, input);
    }
    return input.*;
}

fn checkExplode(a: std.mem.Allocator, input: *Str) bool {
    var count: u5 = 0;
    for (input.*) |ch, i| {
        if (ch == '[') {
            count += 1;
            if (count == explode_limit) {
                explode(a, input, i) catch unreachable;
                return true;
            }
        } else if (ch == ']') {
            count -= 1;
        }
    }
    return false;
}

fn checkSplit(a: std.mem.Allocator, input: *Str) bool {
    return split(a, input) catch unreachable;
}

fn split(a: std.mem.Allocator, input: *Str) !bool {
    // std.debug.print("split: {s}\n", .{input.*});

    var two_digit = try Regex.compile(a, "\\d\\d+");
    defer two_digit.deinit();
    const two = (try two_digit.captures(input.*)) orelse return false;
    const split_coords = two.boundsAt(0).?;

    var ret = std.ArrayList(u8).init(a);

    try ret.appendSlice(input.*[0..split_coords.lower]);

    var buf: [2]u8 = undefined;
    const split_num = try std.fmt.parseUnsigned(SnailType, input.*[split_coords.lower..split_coords.upper], 10);
    try ret.append('[');
    try ret.appendSlice(try std.fmt.bufPrint(&buf, "{d}", .{split_num / 2}));
    try ret.append(',');
    try ret.appendSlice(try std.fmt.bufPrint(&buf, "{d}", .{std.math.ceil(@intToFloat(f32, split_num) / 2)}));
    try ret.append(']');

    try ret.appendSlice(input.*[split_coords.upper..]);

    input.* = ret.toOwnedSlice();

    return true;
}

fn explode(a: std.mem.Allocator, input: *Str, pivot: usize) !void {
    // std.debug.print("explode: {s} {d}\n", .{ input.*, pivot });
    const exp_nums = (try reg_exp.exp_number.captures(input.*[pivot..])).?;

    // Exploding left item
    const exp_left = [2]usize{ pivot + exp_nums.boundsAt(1).?.lower, pivot + exp_nums.boundsAt(1).?.upper };
    // Exploding right item
    const exp_right = [2]usize{ pivot + exp_nums.boundsAt(2).?.lower, pivot + exp_nums.boundsAt(2).?.upper };

    // std.debug.print("expLeft: {s} expRight: {s}\n", .{ input.*[exp_left[0]..exp_left[1]], input.*[exp_right[0]..exp_right[1]] });

    var new_string = std.ArrayList(u8).init(a);

    if (try reg_exp.last_number.captures(input.*[0..pivot])) |lefts| {
        if (lefts.boundsAt(1)) |l| {
            // std.debug.print("{any} {s}\n", .{ l, input.*[l.lower..l.upper] });
            try new_string.appendSlice(input.*[0..l.lower]);

            const left_num = try std.fmt.parseUnsigned(SnailType, input.*[l.lower..l.upper], 10);
            const exp_left_num = try std.fmt.parseUnsigned(SnailType, input.*[exp_left[0]..exp_left[1]], 10);
            var buf: [2]u8 = undefined;
            try new_string.appendSlice(try std.fmt.bufPrint(&buf, "{d}", .{left_num + exp_left_num}));

            try new_string.appendSlice(input.*[l.upper .. exp_left[0] - 1]);
        }
    } else {
        try new_string.appendSlice(input.*[0 .. exp_left[0] - 1]);
    }

    try new_string.append('0');

    if (try reg_exp.number.captures(input.*[exp_right[1] + 1 ..])) |rights| {
        if (rights.boundsAt(0)) |r_raw| {
            const r = Span{ .lower = r_raw.lower + exp_right[1] + 1, .upper = r_raw.upper + exp_right[1] + 1 };

            // std.debug.print("{any} {s}\n", .{ r, input.*[r.lower..r.upper] });
            try new_string.appendSlice(input.*[exp_right[1] + 1 .. r.lower]);

            const right_num = try std.fmt.parseUnsigned(SnailType, input.*[r.lower..r.upper], 10);
            const exp_left_num = try std.fmt.parseUnsigned(SnailType, input.*[exp_right[0]..exp_right[1]], 10);
            var buf: [2]u8 = undefined;
            try new_string.appendSlice(try std.fmt.bufPrint(&buf, "{d}", .{right_num + exp_left_num}));

            try new_string.appendSlice(input.*[r.upper..]);
        }
    } else {
        try new_string.appendSlice(input.*[exp_right[1] + 1 ..]);
    }

    input.* = new_string.toOwnedSlice();
}

test "day18a" {
    try std.testing.expectEqual(@as(RetType, 3675), try first(std.testing.allocator));
}