#include "maze.hpp"
#include <assert.h>
#include <algorithm>
#include <optional>
#include <utility>


/**
 * @file
 * @brief Procedurally generate a maze and translate it to object and light models.
 *
 * This is the most messy part, but requires mostly trivial math.
 * As only done at startup, no performance concerns here.
 */


/** @brief Local helper for discrete 2D coordinate. */
struct Coord : public vertex2_t<dcoord_t> {
    bool operator==(const Coord& other) const {
        return this->x == other.x && this->y == other.y;
    }
};


/** @brief Two 2D integer coordinates span an area, inclusive per convention. */
struct Area {
    enum Corner { TR = 0, BR, BL, TL };
    enum Side { T = 0, R, B, L };

    Coord start;
    Coord end;

    Area(Coord start, Coord end) : start(start), end(end) {
        assert(start.x <= end.x && start.y <= end.y);
    }

    dcoord_t width() const {
        return end.x - start.x + 1;
    }
    dcoord_t height() const {
        return end.y - start.y + 1;
    }
    Coord dir() const {
        return Coord{end.x - start.x, end.y - start.y};
    }
    vertex2_t<fcoord_t> center() const {
        return {(fcoord_t)start.x + ((fcoord_t)(width() - 1) / 2.0F),
                (fcoord_t)start.y + ((fcoord_t)(height() - 1) / 2.0F)};
    }

    bool operator==(const Area& other) const {
        return this->start == other.start && this->end == other.end;
    }

    bool borders(const Coord& c) const {
        return ((c.x == this->start.x || c.x == this->end.x) && c.y >= this->start.y && c.y <= this->end.y) ||
               ((c.y == this->start.y || c.y == this->end.y) && c.x >= this->start.x && c.x <= this->end.x);
    }

    static vertex_t get_vertex(const Coord& pos, const vertex_t& off, const vertex_t& scale) {
        return vertex_t{(fcoord_t)pos.x * scale.x + off.x, (fcoord_t)pos.y * scale.y + off.y, off.z};
    }

    ray_t get_ray(const vertex_t& off, const vertex_t& scale) const {
        return ray_t{get_vertex(start, off, scale), get_vertex(dir(), off * -2.0F, scale),
                     get_vertex(end, off * -1.0F, scale)};
    }

    ray_t get_ray_x(const vertex_t& off, const vertex_t& scale) const {
        return ray_t{get_vertex(start, {off.x, off.y, off.z >= 0.0F ? 0.0F : -off.z}, scale),
                     {(fcoord_t)dir().x * scale.x - (off.x * 2.0F), 0.0F, off.z < 0.0F ? std::fabs(off.z) : off.z},
                     get_vertex(end, {-off.x, off.y, off.z >= 0.0F ? off.z : 0.0F}, scale)};
    }

    ray_t get_ray_y(const vertex_t& off, const vertex_t& scale) const {
        return ray_t{get_vertex(start, {off.x, off.y, off.z >= 0.0F ? 0.0F : -off.z}, scale),
                     {0.0F, (fcoord_t)dir().y * scale.y - (off.y * 2.0F), off.z < 0.0F ? std::fabs(off.z) : off.z},
                     get_vertex(end, {off.x, -off.y, off.z >= 0.0F ? off.z : 0.0F}, scale)};
    }

    Area get_corner(Coord split, Corner corner) const {
        switch (corner) {
            case Corner::TR:
                return Area{Coord{split.x, split.y}, Coord{end.x, end.y}};
            case Corner::BR:
                return Area{Coord{split.x, start.y}, Coord{end.x, split.y}};
            case Corner::BL:
                return Area{Coord{start.x, start.y}, Coord{split.x, split.y}};
            case Corner::TL:
                return Area{Coord{start.x, split.y}, Coord{split.x, end.y}};
            default:
                assert(false);
                return Area{{}, {}};
        }
    }

    Area get_corner(dcoord_t split, Side corner) const {
        switch (corner) {
            case Side::T:
                return Area{Coord{start.x, split}, Coord{end.x, end.y}};
            case Side::R:
                return Area{Coord{split, start.y}, Coord{end.x, end.y}};
            case Side::B:
                return Area{Coord{start.x, start.y}, Coord{end.x, split}};
            case Side::L:
                return Area{Coord{start.x, start.y}, Coord{split, end.y}};
            default:
                assert(false);
                return Area{{}, {}};
        }
    }

    Area get_border(Side corner) const {
        switch (corner) {
            case Side::T:
                return Area{Coord{start.x, end.y}, Coord{end.x, end.y}};
            case Side::R:
                return Area{Coord{end.x, start.y}, Coord{end.x, end.y}};
            case Side::B:
                return Area{Coord{start.x, start.y}, Coord{end.x, start.y}};
            case Side::L:
                return Area{Coord{start.x, start.y}, Coord{start.x, end.y}};
            default:
                assert(false);
                return Area{{}, {}};
        }
    }
};


/** @brief Tree of rooms per level, each one possibly subdivided by splits. */
struct Room : public Area {
    static int get_id() {
        static int last_id = 0;
        return last_id++;
    }

    std::pair<int, int> id; ///< iteration depth, auto increment
    const Room* parent;
    std::vector<std::unique_ptr<Room>> children{};

    Room(Coord start, Coord end, int depth, const Room* p) : Area(start, end), id({depth, get_id()}), parent(p) {}
    Room(Area area, int depth, const Room* p) : Room(area.start, area.end, depth, p) {}
};


/** @brief Initial maze generation yields a tree of rooms plus door positions. */
struct Maze {
    Room room;
    std::vector<Coord> doors{};
    Maze(Coord dim) : room({0, 0}, dim, 0, nullptr) {}
};


/**
 * @brief Basis for the level is a generated maze, which consists of subdivided rooms, walls, and doors.
 *
 * https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_division_method
 */
class MazeGenerator {
  private:
    PRNG rnd;

    void generate_r(Room* room, std::vector<Coord>* doors) {
        if (room->width() > med.x && room->height() > med.y) {
            split_x(room, doors);
        } else if (room->height() > max.y) {
            split_h(room, doors);
        } else if (room->width() > max.x) {
            split_v(room, doors);
        }
    }

    void split_x(Room* room, std::vector<Coord>* doors) {
        Coord split;
        Coord split_x_start, split_x_end;
        Coord split_y_start, split_y_end;
        do {
            split = {rnd.get(room->start.x + min.x, room->end.x - min.x),
                     rnd.get(room->start.y + min.y, room->end.y - min.y)};
            split_y_start = Coord{room->start.x, split.y};
            split_y_end = Coord{room->end.x, split.y};
            split_x_start = Coord{split.x, room->start.y};
            split_x_end = Coord{split.x, room->end.y};
        } while (std::find(doors->begin(), doors->end(), split_y_start) != doors->end() ||
                 std::find(doors->begin(), doors->end(), split_y_end) != doors->end() ||
                 std::find(doors->begin(), doors->end(), split_x_start) != doors->end() ||
                 std::find(doors->begin(), doors->end(), split_x_end) != doors->end());

        const int omit_door = (int)rnd.get(4U);
        for (const auto side : {Area::Side::T, Area::Side::R, Area::Side::B, Area::Side::L}) {
            if (side == omit_door) {
                continue;
            }
            switch (side) {
                case Area::Side::T:
                    doors->push_back(Coord{split.x, rnd.get(split.y + 1, room->end.y - 1)});
                    break;
                case Area::Side::B:
                    doors->push_back(Coord{split.x, rnd.get(room->start.y + 1, split.y - 1)});
                    break;
                case Area::Side::L:
                    doors->push_back(Coord{rnd.get(room->start.x + 1, split.x - 1), split.y});
                    break;
                case Area::Side::R:
                    doors->push_back(Coord{rnd.get(split.x + 1, room->end.x - 1), split.y});
                    break;
            }
        }

        for (const auto corner : {Area::Corner::TR, Area::Corner::BR, Area::Corner::BL, Area::Corner::TL}) {
            room->children.push_back(std::make_unique<Room>(room->get_corner(split, corner), room->id.first + 1, room));
            generate_r(room->children.back().get(), doors);
        }
        std::shuffle(room->children.begin(), room->children.end(), PRNG(rnd));
    }

    void split_h(Room* room, std::vector<Coord>* doors) {
        dcoord_t split_y{};
        Coord split_start, split_end;
        do {
            split_y = rnd.get(room->start.y + min.y, room->end.y - min.y);
            split_start = Coord{room->start.x, split_y};
            split_end = Coord{room->end.x, split_y};
        } while (std::find(doors->begin(), doors->end(), split_start) != doors->end() ||
                 std::find(doors->begin(), doors->end(), split_end) != doors->end());

        doors->push_back(Coord{rnd.get(room->start.x + 1, room->end.x - 1), split_y});

        for (const auto corner : {Area::Side::T, Area::Side::B}) {
            room->children.push_back(
                std::move(std::make_unique<Room>(room->get_corner(split_y, corner), room->id.first + 1, room)));
            generate_r(room->children.back().get(), doors);
        }
        std::shuffle(room->children.begin(), room->children.end(), PRNG(rnd));
    }

    void split_v(Room* room, std::vector<Coord>* doors) {
        dcoord_t split_x{};
        Coord split_start, split_end;
        do {
            split_x = rnd.get(room->start.x + min.x, room->end.x - min.x);
            split_start = Coord{split_x, room->start.y};
            split_end = Coord{split_x, room->end.y};
        } while (std::find(doors->begin(), doors->end(), split_start) != doors->end() ||
                 std::find(doors->begin(), doors->end(), split_end) != doors->end());

        doors->push_back(Coord{split_x, rnd.get(room->start.y + 1, room->end.y - 1)});

        for (const auto corner : {Area::Side::L, Area::Side::R}) {
            room->children.push_back(
                std::move(std::make_unique<Room>(room->get_corner(split_x, corner), room->id.first + 1, room)));
            generate_r(room->children.back().get(), doors);
        }
        std::shuffle(room->children.begin(), room->children.end(), PRNG(rnd));
    }

  public:
    const Coord min, med, max;

    MazeGenerator(PRNG&& seed, Coord min, Coord med, Coord max) : rnd(seed), min(min), med(med), max(max) {
        assert(min.x >= 5 && min.y >= 5); // 3 + 2
        assert(med.x >= min.x * 2 && med.y >= min.y * 2);
        assert(max.x > med.x && max.y > med.y);
    }

    std::unique_ptr<const Maze> generate(Coord dim) {
        assert(dim.x >= min.x + 2 && dim.y >= min.y + 2); // TODO: suffices?
        std::unique_ptr<Maze> maze = std::make_unique<Maze>(dim);
        generate_r(&maze->room, &maze->doors);
        return maze;
    }
};


Level::Level(PRNG&& seed, fcoord_t radius, res_t res, vertex2_t<fcoord_t> viewport)
    : texture_factory(PRNG(seed.last()), res, viewport), light_factory(PRNG(seed.last()), radius) {}


/**
 * @brief From a tree of rooms and doors, generate a level by building models for walls and lights.
 *
 * One of the most messy parts, but requires mostly trivial math.
 */
class LevelGenerator {
  private:
    const PRNG::result_type seed;
    PRNG rnd;
    MazeGenerator generator;
    const vertex2_t<dcoord_t> dim;
    const vertex_t tile_dim;

    std::unique_ptr<Level> generate(const Maze& maze) {
        std::unique_ptr<Level> level = std::make_unique<Level>(
            PRNG(rnd), (fcoord_t)generator.med.x * tile_dim.x / 2.0F, res_t{dim.x * 2, dim.y * 2},
            vertex2_t<fcoord_t>{(fcoord_t)dim.x * tile_dim.x, (fcoord_t)dim.y * tile_dim.y});

        collect_rooms(&maze.room, level->rooms);
        generate_r(level.get(), &maze.room, maze.doors);

        level->start = pick_start(&maze.room);
        level->end = pick_end(&maze.room);

        const vertex_t end_pos{level->end.x * tile_dim.x, level->end.y * tile_dim.y, tile_dim.z / 2.0F};
        level->lights.push_back({end_pos, level->light_factory.make(tile_dim.x, vertex_t::setted(-1.5F))});
        level->lights.push_back({end_pos, level->light_factory.make((fcoord_t)generator.max.x * tile_dim.x / 4.0F,
                                                                    vertex_t::setted(1.01F))});

        return level;
    }

    void collect_rooms(const Room* room, std::vector<vertex_t>& rooms) const {
        if (room->children.empty()) {
            const vertex2_t<fcoord_t> center = room->center();
            rooms.push_back({center.x * tile_dim.x, center.y * tile_dim.y, tile_dim.z / 2.0F});
        }
        for (const auto& child : room->children) {
            collect_rooms(child.get(), rooms);
        }
    }

    vertex2_t<fcoord_t> pick_start(const Room* room) const {
        while (!room->children.empty()) { // first leaf
            room = room->children.front().get();
        }
        return room->center();
    }

    vertex2_t<fcoord_t> pick_end(const Room* room) const {
        while (!room->children.empty()) { // last leaf
            room = room->children.back().get();
        }
        return room->center();
    }

    void generate_r(Level* level, const Room* room, const std::vector<Coord>& doors) {
        if (!room->children.empty()) {
            for (const auto& child : room->children) {
                generate_r(level, child.get(), doors);
            }
            return;
        }

        const vertex_t wall_dim{tile_dim.x / 4.0F, tile_dim.y / 4.0F, tile_dim.z};
        generate_walls(level, room, doors, wall_dim);
#if USE_LIGHTS
        generate_lights(level, room, doors, wall_dim);
#endif
    }

    void generate_walls(Level* level, const Room* room, const std::vector<Coord>& doors,
                        const vertex_t& wall_dim) const {
        const vertex_t margin = micro_coord; // for more deterministic clipping
        const Texture* texture = level->texture_factory.make(room->id);

        const ray_t inside_ray = room->get_ray((wall_dim - margin).with_z(), tile_dim);
        level->objects.push_back(std::make_unique<SidedAxisPlane>(inside_ray.pos, inside_ray.dir, true, texture));

        for (const auto side : {Area::Side::T, Area::Side::B, Area::Side::L, Area::Side::R}) {
            const Area border = room->get_border(side);
            const std::optional<std::pair<Coord, const Room*>> door_pos =
                find_door(border.start, border.end, room, doors);
            const bool dir_pos = side == Area::Side::B || side == Area::Side::L;

            ray_t border_ray{};
            switch (side) {
                case Area::Side::T:
                    border_ray = border.get_ray_x({wall_dim.x, -wall_dim.y, wall_dim.z}, tile_dim);
                    break;
                case Area::Side::B:
                    border_ray = border.get_ray_x({wall_dim.x, wall_dim.y, wall_dim.z}, tile_dim);
                    break;
                case Area::Side::L:
                    border_ray = border.get_ray_y({wall_dim.x, wall_dim.y - margin.y, wall_dim.z}, tile_dim);
                    break;
                case Area::Side::R:
                    border_ray = border.get_ray_y({-wall_dim.x, wall_dim.y - margin.y, wall_dim.z}, tile_dim);
                    break;
            }
            assert((border_ray.dst - border_ray.pos - border_ray.dir).len() == 0.0F);

            if (!door_pos.has_value()) {
                level->objects.push_back(
                    std::make_unique<SidedAxisPlane>(border_ray.pos, border_ray.dir, dir_pos, texture));
                continue;
            }

            assert(door_pos.value().second != nullptr);
            const Texture* texture_x = level->texture_factory.make(door_pos.value().second->id, room->id, 0);
            const Texture* texture_y = level->texture_factory.make(door_pos.value().second->id, room->id, 1);

            const Area door = Area{door_pos.value().first, door_pos.value().first};
            ray_t door_ray{};
            switch (side) {
                case Area::Side::T:
                    door_ray = door.get_ray_x({-tile_dim.x / 2.0F, -wall_dim.y, -wall_dim.z}, tile_dim);
                    break;
                case Area::Side::B:
                    door_ray = door.get_ray_x({-tile_dim.x / 2.0F, wall_dim.y, -wall_dim.z}, tile_dim);
                    break;
                case Area::Side::L:
                    door_ray = door.get_ray_y({wall_dim.x, -tile_dim.y / 2.0F + margin.y, -wall_dim.z}, tile_dim);
                    break;
                case Area::Side::R:
                    door_ray = door.get_ray_y({-wall_dim.x, -tile_dim.y / 2.0F + margin.y, -wall_dim.z}, tile_dim);
                    break;
            }

            level->objects.push_back(
                std::make_unique<SidedAxisPlane>(border_ray.pos, border_ray.pos.to(door_ray.pos), dir_pos, texture));
            level->objects.push_back(
                std::make_unique<SidedAxisPlane>(door_ray.dst, door_ray.dst.to(border_ray.dst), dir_pos, texture));

            if (side == Area::Side::B) {
                ray_t floor_ray = door.get_ray({-tile_dim.x / 2.0F, -wall_dim.y, 0.0F}, tile_dim);
                ray_t side_ray = door.get_ray_y({-tile_dim.x / 2.0F, -wall_dim.y - margin.y, 1.0F}, tile_dim); // L
                ray_t side_n_ray = door.get_ray_y({tile_dim.x / 2.0F, -wall_dim.y - margin.y, 1.0F}, tile_dim); // R
                level->objects.push_back(
                    std::make_unique<SidedAxisPlane>(floor_ray.pos, floor_ray.dir, true, texture_y));
                level->objects.push_back(std::make_unique<SidedAxisPlane>(side_ray.pos, side_ray.dir, true, texture_x));
                level->objects.push_back(
                    std::make_unique<SidedAxisPlane>(side_n_ray.pos, side_n_ray.dir, false, texture_x));
            } else if (side == Area::Side::L) {
                ray_t floor_ray = door.get_ray({-wall_dim.x, -tile_dim.y / 2.0F, 0.0F}, tile_dim);
                ray_t side_ray = door.get_ray_x({-wall_dim.x, -tile_dim.y / 2.0F, 1.0F}, tile_dim); // B
                ray_t side_n_ray = door.get_ray_x({wall_dim.x, tile_dim.y / 2.0F, 1.0F}, tile_dim); // T
                level->objects.push_back(
                    std::make_unique<SidedAxisPlane>(floor_ray.pos, floor_ray.dir, true, texture_x));
                level->objects.push_back(std::make_unique<SidedAxisPlane>(side_ray.pos, side_ray.dir, true, texture_x));
                level->objects.push_back(
                    std::make_unique<SidedAxisPlane>(side_n_ray.dst, side_n_ray.dir * -1.0F, false, texture_x));
            }
        }
    }

    void generate_lights(Level* level, const Room* room, const std::vector<Coord>& doors, const vertex_t& wall_dim) {
        std::vector<Area::Side> sides{Area::Side::T, Area::Side::B, Area::Side::L, Area::Side::R};
        std::shuffle(sides.begin(), sides.end(), PRNG(rnd));
        sides.resize(rnd.get(0U, 4U));
        for (const auto side : sides) {
            const Area border = room->get_border(side);
            const std::optional<std::pair<Coord, const Room*>> door_pos =
                find_door(border.start, border.end, room, doors);

            const Coord room_pos{rnd.get(room->start.x + 1, room->end.x - 1),
                                 rnd.get(room->start.y + 1, room->end.y - 1)};
            const vertex_t pos{(fcoord_t)room_pos.x * tile_dim.x, (fcoord_t)room_pos.y * tile_dim.y, wall_dim.z / 2.0F};
            const vertex_t off{tile_dim.x / 4.0F, tile_dim.y / 4.0F, 0.0F}; // proximity to wall
            const fcoord_t radius = level->light_factory.get_radius();

            vertex_t light_pos{};
            switch (side) {
                case Area::Side::T:
                    if (!door_pos.has_value() || std::fabs((fcoord_t)door_pos->first.x * tile_dim.x - pos.x) > radius) {
                        light_pos = {pos.x, (fcoord_t)room->end.y * tile_dim.y - wall_dim.y - off.y, pos.z};
                    } else {
                        continue;
                    }
                    break;
                case Area::Side::B:
                    if (!door_pos.has_value() || std::fabs((fcoord_t)door_pos->first.x * tile_dim.x - pos.x) > radius) {
                        light_pos = {pos.x, (fcoord_t)room->start.y * tile_dim.y + wall_dim.y + off.y, pos.z};
                    } else {
                        continue;
                    }
                    break;
                case Area::Side::L:
                    if (!door_pos.has_value() || std::fabs((fcoord_t)door_pos->first.y * tile_dim.y - pos.y) > radius) {
                        light_pos = {(fcoord_t)room->start.x * tile_dim.x + wall_dim.x + off.x, pos.y, pos.z};
                    } else {
                        continue;
                    }
                    break;
                case Area::Side::R:
                    if (!door_pos.has_value() || std::fabs((fcoord_t)door_pos->first.y * tile_dim.y - pos.y) > radius) {
                        light_pos = {(fcoord_t)room->end.x * tile_dim.x - wall_dim.x - off.x, pos.y, pos.z};
                    } else {
                        continue;
                    }
                    break;
                default:
                    assert(false);
                    break;
            }

            level->lights.push_back({light_pos, level->light_factory.make({room->id.first, room->id.second + side})});
        }
    }

    static std::optional<std::pair<Coord, const Room*>> find_door(Coord start, Coord end, const Room* curr,
                                                                  const std::vector<Coord>& doors) {
        const Room* root = curr;
        while (root->parent != nullptr) {
            root = root->parent;
        }

        for (auto x = start.x; x <= end.x; ++x) {
            for (auto y = start.y; y <= end.y; ++y) {
                Coord pos = Coord{x, y};
                assert(curr->borders(pos));
                if (std::find(doors.begin(), doors.end(), pos) != doors.end()) {
                    return std::pair<Coord, const Room*>(pos, find_room(pos, root, curr));
                }
            }
        }
        return std::nullopt;
    }

    static const Room* find_room(Coord door, const Room* root, const Room* curr) {
        if (root->borders(door) && root != curr && root->children.empty()) {
            return root;
        }
        for (const auto& child : root->children) {
            const Room* result = find_room(door, child.get(), curr);
            if (result != nullptr) {
                return result;
            }
        }
        return nullptr;
    }

  public:
    LevelGenerator(PRNG::result_type seed, vertex2_t<dcoord_t> min, vertex2_t<dcoord_t> med, vertex2_t<dcoord_t> max,
                   vertex2_t<dcoord_t> dim, vertex_t tile_dim)
        : seed(seed),
          rnd(seed),
          generator{PRNG(rnd), {min.x, min.y}, {med.x, med.y}, {max.x, max.y}},
          dim{dim},
          tile_dim{tile_dim} {}

    std::unique_ptr<Level> generate() {
        LOG("Generating level: %u", seed);
        std::unique_ptr<const Maze> maze = generator.generate({dim.x, dim.y});
        std::unique_ptr<Level> level = generate(*maze);
        LOG("Scene %d/%d with %zu rooms, %zu objects, %zu lights, %zu textures", dim.x, dim.y, level->rooms.size(),
            level->objects.size(), level->lights.size(), level->texture_factory.size());
        return level;
    }
};


std::unique_ptr<const Level> generate(PRNG::result_type seed, vertex2_t<dcoord_t> min, vertex2_t<dcoord_t> med,
                                      vertex2_t<dcoord_t> max, vertex2_t<dcoord_t> dim, vertex_t tile_dim) {
    return LevelGenerator(seed, min, med, max, dim, tile_dim).generate();
}