const std = @import("std");


const Symbol = enum {
    Barret,
    Chalice,
    Flask,
    Gallion,
    Hourglass,
    Scatter,
    Witch,
};

const Variant = union(enum) {
    Multiplier: u32,
    Respin,
    Symbol: Symbol,
    Empty,

    const Self = @This();

    pub fn new_random() Self {
        const rand = std.crypto.random;
        const symbol = rand.enumValue(Symbol);
        return Self{ .Symbol = symbol };
    }
};

const Collection = struct {
    Symbol: Variant,
    Piece: [2]u8,
};


pub const Board = struct {
    fields: [7][7]Variant,
    multipler: u32,

    const Self = @This();

    pub fn init() Board {
        const rand = std.crypto.random;
        var buffer: [6]u8 = undefined;
        rand.bytes(&buffer);

        var board = Self {
            .fields = [7][7]Variant{
                [7]Variant{Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty},
                [7]Variant{Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty},
                [7]Variant{Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty},
                [7]Variant{Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty},
                [7]Variant{Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty},
                [7]Variant{Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty},
                [7]Variant{Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty, Variant.Empty},
            },
            .multipler = 1,
        };
        board.fields[buffer[0] % 7][buffer[1] % 7] = Variant.Respin;
        board.fields[buffer[2] % 7][buffer[3] % 7] = Variant.Respin;
        board.fields[buffer[4] % 7][buffer[5] % 7] = Variant.Respin;
        board.fill();
        return board;
    }

    pub fn fill(self: *Board) void {
        const rand = std.crypto.random;
        var buffer: [6]u8 = undefined;
        rand.bytes(&buffer);
        for (self.fields, 0..) |row, i| {
            for (row, 0..) |field, j| {
                if (field == Variant.Empty) {
                    // TODO: add probability distribution for Respins
                    self.fields[i][j] = Variant{ .Symbol = rand.enumValue(Symbol) };
                }
            }
        }
    }

    pub fn calculate(self: *Board, components: *std.ArrayList(u8)) !void {
        var visited: [7][7]bool = [7][7]bool{
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
        };
        for (0..self.fields.len) |i| {
            for (0..self.fields[0].len) |j| {
                if (self.fields[i][j] != Variant.Multiplier and self.fields[i][j] != Variant.Respin and self.fields[i][j] != Variant.Empty) {
                    const size = try self.dfs(&visited, @intCast(i), @intCast(j), self.fields[i][j].Symbol);
                    if (size >= 5) {
                        // std.debug.print("fuction returned {} for Symbol {}\n", .{size, self.fields[i][j].Symbol});
                        try components.append(@as(u8, @truncate((@as(u8, @truncate(i)) << 3 | j))));
                    }
                }
            }
        }
    }

    pub fn calculate2(self: *Board, allocator: std.mem.Allocator) !void {
        var collections = std.ArrayList(std.ArrayList(Collection)).init(allocator);
        var visited: [7][7]bool = [7][7]bool{
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
            [7]bool{false, false, false, false, false, false, false},
        };
        var collections_index: ?u8 = null;
        for (0..self.fields.len) |i| {
            for (0..self.fields[0].len) |j| {
                if (self.fields[i][j] != Variant.Multiplier and self.fields[i][j] != Variant.Respin and self.fields[i][j] != Variant.Empty) {
                    // const size = try self.dfs(&visited, @intCast(i), @intCast(j), self.fields[i][j].Symbol);
                    // if (size >= 5) {
                    //     // std.debug.print("fuction returned {} for Symbol {}\n", .{size, self.fields[i][j].Symbol});
                    //     try components.append(@as(u8, @truncate((@as(u8, @truncate(i)) << 3 | j))));
                    //     
                    // }
                    const fields = try self.dfs2(&visited, @intCast(i), @intCast(j), self.fields[i][j].Symbol, allocator);
                    if (fields.items.len >= 5) {
                        if (collections_index == null) {
                            collections_index.? = 0;
                        } else {
                            collections_index.? += 1;
                        }
                        try collections.append(std.ArrayList(Collection).init(allocator));
                        for (fields.items) |field| {
                            for (collections, 0..) |_, k| {
                                if (k == collections_index) {
                                    if (self.fields[field[0]][field[1]] == Variant.Multiplier) {
                                        collections.at(collections.items.len - 1).append(Collection {
                                            .Symbol = Variant.Multiplier,
                                            .Piece = field,
                                        });
                                    } else {
                                        collections.append(Collection{
                                            .Symbol = self.fields[i][j].Symbol,
                                            .Piece = field,
                                        });
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    pub fn dfs2(self: *Board, visited: *[7][7]bool, i: u8, j: u8, cell_type: Symbol, allocator: std.mem.Allocator) !std.ArrayList([2]u8) {
        var stack = std.ArrayList([2]u8).init(allocator);
        try stack.append([2]u8{i, j});
        var size: u8 = 0;
        while (stack.items.len > 0) {
            const cords = stack.getLast();
            if (cords[0] >= 7 or cords[1] >= 7 or
                visited[cords[0]][cords[1]] or (
                    switch (self.fields[cords[0]][cords[1]]) {
                        Variant.Empty => unreachable,
                        Variant.Multiplier => false,
                        Variant.Respin => true,
                        Variant.Symbol => self.fields[cords[0]][cords[1]].Symbol != cell_type
                    }
                )
            ) {
                if (cords[0] <= 6 and cords[1] <= 6 and self.fields[cords[0]][cords[1]] == Variant.Respin) {
                    visited[cords[0]][cords[1]] = true;
                }
                continue;
            }
            visited[cords[0]][cords[1]] = true;
            size += 1;

            if (cords[0] > 0) {
                try stack.append([2]u8{cords[0] - 1, cords[1]});
            }
            try stack.append([2]u8{cords[0] + 1, cords[1]});
            if (cords[1] > 0) {
                try stack.append([2]u8{cords[0], cords[1] - 1});
            }
            try stack.append([2]u8{cords[0], cords[1] + 1});
            // std.debug.print(
            //     "increased size to {}, for symbol {} at position {}x{}, new stack size is {}\n",
            //     .{size, cell_type, cords[0], cords[1], stack.items.len}
            // );
        }
        return stack;
    }


    pub fn dfs(self: *Board, visited: *[7][7]bool, i: u8, j: u8, cell_type: Symbol) !u32 {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        const allocator = gpa.allocator();

        var stack = std.ArrayList([2]u8).init(allocator);
        try stack.append([2]u8{i, j});
        var size: u8 = 0;
        while (stack.items.len > 0) {
            const cords = stack.pop();
            if (cords[0] >= 7 or cords[1] >= 7 or
                visited[cords[0]][cords[1]] or (
                    switch (self.fields[cords[0]][cords[1]]) {
                        Variant.Empty => unreachable,
                        Variant.Multiplier => false,
                        Variant.Respin => true,
                        Variant.Symbol => self.fields[cords[0]][cords[1]].Symbol != cell_type
                    }
                )
            ) {
                if (cords[0] <= 6 and cords[1] <= 6 and self.fields[cords[0]][cords[1]] == Variant.Respin) {
                    visited[cords[0]][cords[1]] = true;
                }
                continue;
            }
            visited[cords[0]][cords[1]] = true;
            size += 1;

            if (cords[0] > 0) {
                try stack.append([2]u8{cords[0] - 1, cords[1]});
            }
            try stack.append([2]u8{cords[0] + 1, cords[1]});
            if (cords[1] > 0) {
                try stack.append([2]u8{cords[0], cords[1] - 1});
            }
            try stack.append([2]u8{cords[0], cords[1] + 1});
            // std.debug.print(
            //     "increased size to {}, for symbol {} at position {}x{}, new stack size is {}\n",
            //     .{size, cell_type, cords[0], cords[1], stack.items.len}
            // );
        }
        return size;
    }

    pub fn print(self: Self) !void {
        const stdout_file = std.io.getStdOut().writer();
        var bw = std.io.bufferedWriter(stdout_file);
        const stdout = bw.writer();

        try stdout.print("Board:\n", .{});

        for (self.fields) |row| {
            for (row) |field| {
                switch (field) {
                    Variant.Multiplier => try stdout.print("M", .{}),
                    Variant.Respin => try stdout.print("R", .{}),
                    Variant.Empty => try stdout.print("E", .{}),
                    Variant.Symbol => {
                        switch (field.Symbol) {
                            Symbol.Barret => try stdout.print("B", .{}),
                            Symbol.Chalice => try stdout.print("C", .{}),
                            Symbol.Flask => try stdout.print("F", .{}),
                            Symbol.Gallion => try stdout.print("G", .{}),
                            Symbol.Hourglass => try stdout.print("H", .{}),
                            Symbol.Scatter => try stdout.print("S", .{}),
                            Symbol.Witch => try stdout.print("W", .{}),
                        }
                    }
                }
            }
            try stdout.print("\n", .{});
        }
        try bw.flush();
    }

    pub fn print5(self: Self) !void {
        const symbols = [10][5][5]u8{
            [5][5]u8{
                [5]u8{'|', '-', '-', '-', '|'},
                [5]u8{'|', ' ', ' ', ' ', '|'},
                [5]u8{'M', 'M', 'M', 'M', 'M'},
                [5]u8{'|', ' ', ' ', ' ', '|'},
                [5]u8{'|', '_', '_', '_', '|'},
            },
            [5][5]u8{
                [5]u8{'\\', ' ', ' ', ' ', '/'},
                [5]u8{' ', '\\', ' ', '/', ' '},
                [5]u8{' ', ' ', 'x', ' ', ' '},
                [5]u8{' ', '/', ' ', '\\', ' '},
                [5]u8{'/', ' ', ' ', ' ', '\\'},
            },
            [5][5]u8{
                [5]u8{' ', ' ', ' ', ' ', ' '},
                [5]u8{' ', ' ', ' ', ' ', ' '},
                [5]u8{' ', ' ', ' ', ' ', ' '},
                [5]u8{' ', ' ', ' ', ' ', ' '},
                [5]u8{' ', ' ', ' ', ' ', ' '},
            },
            [5][5]u8{
                [5]u8{' ', '|', '-', '\\', ' '},
                [5]u8{' ', '|', ' ', '|', ' '},
                [5]u8{' ', '|', '-', ' ', ' '},
                [5]u8{' ', '|', ' ', '|', ' '},
                [5]u8{' ', '|', '-', '/', ' '},
            },
            [5][5]u8{
                [5]u8{' ', '/', '-', '\\', ' '},
                [5]u8{' ', '|', ' ', ' ', ' '},
                [5]u8{' ', '|', ' ', ' ', ' '},
                [5]u8{' ', '|', ' ', ' ', ' '},
                [5]u8{' ', '\\', '-', '/', ' '},
            },
            [5][5]u8{
                [5]u8{' ', '|', '-', '-', ' '},
                [5]u8{' ', '|', ' ', ' ', ' '},
                [5]u8{' ', '|', '-', ' ', ' '},
                [5]u8{' ', '|', ' ', ' ', ' '},
                [5]u8{' ', '|', ' ', ' ', ' '},
            },
            [5][5]u8{
                [5]u8{' ', ' ', '-', ' ', ' '},
                [5]u8{' ', '/', ' ', '\\', ' '},
                [5]u8{'|', ' ', ' ', ' ', '|'},
                [5]u8{' ', '\\', ' ', '/', ' '},
                [5]u8{' ', ' ', '_', ' ', ' '},
            },
            [5][5]u8{
                [5]u8{' ', '|', ' ', '|', ' '},
                [5]u8{' ', '|', ' ', '|', ' '},
                [5]u8{' ', '|', '-', '|', ' '},
                [5]u8{' ', '|', ' ', '|', ' '},
                [5]u8{' ', '|', ' ', '|', ' '},
            },
            [5][5]u8{
                [5]u8{' ', '/', '-', '\\', ' '},
                [5]u8{' ', '|', ' ', ' ', ' '},
                [5]u8{' ', ' ', '-', '\\', ' '},
                [5]u8{' ', ' ', ' ', '|', ' '},
                [5]u8{' ', '\\', '_', '/', ' '},
            },
            [5][5]u8{
                [5]u8{'|', ' ', '|', ' ', '|'},
                [5]u8{'|', ' ', '|', ' ', '|'},
                [5]u8{'|', ' ', '|', ' ', '|'},
                [5]u8{'|', ' ', '|', ' ', '|'},
                [5]u8{'\\', '/', ' ', '\\', '/'},
            },
        };
        var output = std.mem.zeroes([7][7][5][5]u8);

        const stdout_file = std.io.getStdOut().writer();
        var bw = std.io.bufferedWriter(stdout_file);
        const stdout = bw.writer();
        try stdout.print("\n", .{});
        for (self.fields, 0..) |row, i| {
            for (row, 0..) |field, j| {
                switch (field) {
                    Variant.Multiplier => output[i][j] = symbols[0],
                    Variant.Respin => output[i][j] = symbols[1],
                    Variant.Empty => output[i][j] = symbols[2],
                    Variant.Symbol => {
                        switch (field.Symbol) {
                            Symbol.Barret => output[i][j] = symbols[3],
                            Symbol.Chalice => output[i][j] = symbols[4],
                            Symbol.Flask => output[i][j] = symbols[5],
                            Symbol.Gallion => output[i][j] = symbols[6],
                            Symbol.Hourglass => output[i][j] = symbols[7],
                            Symbol.Scatter => output[i][j] = symbols[8],
                            Symbol.Witch => output[i][j] = symbols[9],
                        }
                    }
                }
            }
        }

        // iterate over rows on board
        for (0..output.len) |i| {
            // iterate over fields on row
            for (0..output[0][0].len) |j| {
                // iterate over slices of output symbol
                for (0..output[0].len) |k| {
                    // for (0..7) |k2| {
                        if (k == 0) {
                            try stdout.print("   ", .{});
                        }
                        if (output[i][k][j][0] != 'M') {
                            try stdout.print("{s}", .{output[i][k][j]});
                        } else {
                            try stdout.print("|{}|", .{self.fields[i][k].Multiplier});
                        }
                        if (k == 6) {
                            try stdout.print("\n", .{});
                        } else {
                            try stdout.print("   |   ", .{});
                        }
                    // }
                }
            }
            if (i != 6) {
                try stdout.print(
                    "===================================================================================\n",
                    .{}
                );
            }
        }
        try stdout.print("\n", .{});
        try bw.flush();
    }
};


