module common;

import std.algorithm.iteration : filter, map;
import std.algorithm.searching : minElement, canFind;
import std.array : split;
import std.container.array : Array;
import std.container.rbtree : redBlackTree;
import std.container.binaryheap : BinaryHeap;
import std.conv : to;
import std.stdio;
import std.string : stripRight;
import std.typecons : tuple, Tuple;

private alias Buffer = Array!(Array!(uint));

struct CaveMap
{
    private Buffer positions;
    private const ulong tile_width;
    private const ulong tile_height;

    this(string path)
    {
        positions = parseFile(path);
    }

    this(string path, uint tiles)
    {
        auto buffer = parseFile(path);
        tile_height = buffer.length;
        tile_width = buffer[0].length;

        tileRight(buffer, tiles);
        tileDown(buffer, tiles);

        positions = buffer;
    }

    private void tileRight(Buffer buffer, uint tiles)
    {
        foreach (tile_x; 1 .. tiles)
        {
            foreach (y; 0 .. tile_height)
            {
                foreach (x; 0 .. tile_width)
                {
                    auto next_val = (buffer[y][x] + tile_x - 1) % 9;
                    next_val = next_val == 0 ? 1 : next_val + 1;
                    buffer[y].insertBack(next_val);
                }
            }
        }
    }

    private void tileDown(Buffer buffer, uint tiles)
    {
        foreach (tile_y; 1 .. tiles)
        {
            const tile_y_begin = tile_y * tile_height;
            foreach (y; 0 .. tile_height)
            {
                buffer.insertBack(Array!uint());
                foreach (tile_x; 0 .. tiles)
                {
                    const tile_x_begin = tile_x * tile_width;
                    foreach (x; 0 .. tile_width)
                    {
                        auto next_val = (buffer[y][tile_x_begin + x] + tile_y - 1) % 9;
                        next_val = next_val == 0 ? 1 : next_val + 1;
                        buffer[tile_y_begin + y].insertBack(next_val);
                    }
                }
            }
        }
    }

    private Array!(Array!uint) parseFile(string path)
    {
        auto file = File(path, "r");

        auto buffer = Buffer();
        string line;
        while ((line = file.readln()) !is null)
        {
            auto line_buffer = Array!uint();

            foreach (weight; split(stripRight(line), "").map!(to!uint))
            {
                line_buffer.insertBack(weight);
            }

            buffer.insertBack(line_buffer);
        }

        return buffer;

    }

    void toString(scope void delegate(const(char)[]) sink) const
    {
        foreach (y; 0 .. positions.length)
        {
            foreach (x; 0 .. positions[y].length)
                sink(to!string(positions[y][x]));
            sink("\n");
        }
    }
}

private alias NodeId = Tuple!(uint, uint);

private struct Node
{
    private NodeId id;
    private uint weight;

    void toString(scope void delegate(const(char)[]) sink) const
    {
        sink("(");
        sink(to!string(this.id[0]));
        sink(",");
        sink(to!string(this.id[1]));
        sink(";");
        sink(to!string(this.weight));
        sink(")");
    }
}

struct Cave
{
    private Array!Node[NodeId] nodes;
    private NodeId start_node;
    private NodeId end_node;

    this(CaveMap cave_map)
    {
        auto node_id = NodeId(0, 0);
        start_node = node_id;

        uint y = 0;
        foreach (row; cave_map.positions)
        {
            uint x = 0;
            foreach (weight; row)
            {
                node_id = tuple(x, y);
                nodes[node_id] = getNeighbors(x, y, cave_map.positions);

                x++;
            }

            y++;
        }

        end_node = node_id;
    }

    uint lowestRisk()
    {
        auto safest_path = dijkstraFaster();
        return safest_path[1][end_node];
    }

    private Tuple!(NodeId[NodeId], uint[NodeId]) dijkstraFaster()
    {
        uint[NodeId] distance;
        NodeId[NodeId] previous;

        foreach (node_id; nodes.byKey())
            distance[node_id] = uint.max;

        distance[start_node] = 0;

        auto queue = redBlackTree(tuple(distance[start_node], start_node));

        foreach (_, node; queue)
        {
            if (node == end_node)
                break;

            foreach (neighbor; nodes[node])
            {
                auto total_distance = distance[node] + neighbor.weight;

                if (total_distance < distance[neighbor.id])
                {
                    queue.removeKey(tuple(distance[neighbor.id], neighbor.id));
                    distance[neighbor.id] = total_distance;
                    previous[neighbor.id] = node;
                    queue.insert(tuple(distance[neighbor.id], neighbor.id));
                }

            }
        }

        return tuple(previous, distance);
    }

    private Tuple!(NodeId[NodeId], uint[NodeId]) dijkstraSlow()
    {
        bool[NodeId] unvisited;
        uint[NodeId] distance;
        NodeId[NodeId] previous;

        foreach (node_id; nodes.byKey())
        {
            unvisited[node_id] = true;
            distance[node_id] = uint.max;
        }

        distance[start_node] = 0;

        unvisited.rehash;
        distance.rehash;

        while (unvisited.length != 0)
        {
            auto cur_node = unvisited.keys.minElement!(x => distance[x]);

            if (cur_node == end_node)
            {
                break;
            }

            unvisited.remove(cur_node);

            foreach (neighbor; nodes[cur_node])
            {
                if ((neighbor.id in unvisited) is null)
                    continue;

                auto total_distance = distance[cur_node] + neighbor.weight;

                if (total_distance < distance[neighbor.id])
                {
                    distance[neighbor.id] = total_distance;
                    previous[neighbor.id] = cur_node;
                }
            }
        }

        return tuple(previous, distance);
    }

    private immutable int[2][4] DIRECTIONS = [
        [-1, 0], [1, 0],
        [0, -1], [0, 1],
    ];

    private Array!Node getNeighbors(uint x, uint y, Buffer buf)
    {
        auto nodes = Array!Node();

        foreach (dir; DIRECTIONS)
        {
            int dx = x + dir[0];
            int dy = y + dir[1];

            if (dx < 0 || dy < 0)
                continue;
            if (dx >= buf[0].length() || dy >= buf.length())
                continue;

            Node node = {id: tuple(dx, dy), buf[dy][dx]};

            nodes.insertBack(node);
        }

        return nodes;
    }

    void toString(scope void delegate(const(char)[]) sink) const
    {
        foreach (id; this.nodes.byKey())
        {
            auto nodes = this.nodes[id];
            sink("(");
            sink(to!string(id[0]));
            sink(",");
            sink(to!string(id[1]));
            sink(") { ");
            foreach (node; nodes)
            {
                sink(to!string(node));
                sink(" ");
            }
            sink("}\n");
        }
    }
}