#pragma once

#include "position.h"
#include "actions.hpp"
#include <queue>

namespace world::iterator {

using pair = std::pair<glm::ivec3, float>;

class AbstractFill {
public:
    virtual bool next(pair&) = 0;

protected:
    static constexpr uint32_t Diam(uint16_t radius) { return radius * 2 + 1; }
};
class AbstractBorder {
public:
    virtual bool next(glm::ivec3&) = 0;
};

/// From -radius to radius
std::unique_ptr<AbstractFill> Get(action::Shape, uint16_t radius);
/// Border sampling from -radius to radius
std::unique_ptr<AbstractBorder> GetBorder(action::Shape, uint16_t radius);

template<typename Chunk, typename Area>
inline bool GetChunk(Area& chunks, const std::pair<chunk_pos, chunk_voxel_idx>& split, Voxel& out,
    typename std::shared_ptr<Chunk>& ck = nullptr, chunk_pos& ck_pos = chunk_pos(INT32_MAX)
) {
    if (split.first != ck_pos && chunks.inRange(split.first)) {
        if(auto it = chunks.find(split.first); it != chunks.end()) {
            ck = std::dynamic_pointer_cast<Chunk>(it->second);
            ck_pos = split.first;
        }
    }
    if (split.first == ck_pos) {
        out = ck->get(split.second);
        return true;
    }
    return false;
}

/// Apply shape on areas
template<typename Chunk, typename area_map, typename CB>
inline void Apply(area_map &areas, action::FillShape fill, const CB& callback) {
    const auto it = areas.find(fill.pos.first);
    if (it == areas.end())
        return;

    auto &chunks = it->second->setChunks();
    auto iterator = Get(fill.shape, fill.radius);
    pair point;
    typename std::shared_ptr<Chunk> ck = nullptr;
    chunk_pos ck_pos = chunk_pos(INT32_MAX);
    while (iterator->next(point)) {
        const voxel_pos offset = point.first;
        const auto split = glm::splitIdx(fill.pos.second + offset);
        if (Voxel prev; GetChunk<Chunk>(chunks, split, prev, ck, ck_pos)) {
            const auto next = prev.filled(fill.val, point.second);
            if (prev.value != next.value) {
                callback(ck, ck_pos, split.second, prev, next, glm::length2(offset) / fill.radius * .05f);
            }
        }
    }
}
/// Find parts using flood fill
template<typename Chunk, typename area_map, typename CB>
inline bool Split(area_map &areas, action::FillShape fill, size_t floodFillLimit, const CB& callback) {
    if (floodFillLimit == 0 || fill.val.is_solid())
        return false;

    const auto it = areas.find(fill.pos.first);
    if (it == areas.end())
        return false;

    // Check for subpart break
    robin_hood::unordered_set<voxel_pos> joints;
    auto &chunks = it->second->setChunks();
    auto iterator = GetBorder(fill.shape, fill.radius+1);
    glm::ivec3 point;
    typename std::shared_ptr<Chunk> ck = nullptr;
    chunk_pos ck_pos = chunk_pos(INT32_MAX);
    const auto cleanVoxel = Voxel(world::materials::AIR, fill.val.density());
    while (iterator->next(point)) {
        const auto full = fill.pos.second + voxel_pos(point);
        if (Voxel v; GetChunk<Chunk>(chunks, glm::splitIdx(full), v, ck, ck_pos) && v.is_solid())
            joints.insert(full);
    }
    bool added = false;
    while (!joints.empty()) {
        std::queue<voxel_pos> todo;
        todo.push(*joints.begin());
        robin_hood::unordered_set<voxel_pos> part;
        part.insert(todo.front());
        size_t i = 0;
        for (; !todo.empty() && i < floodFillLimit; i++) {
            const auto full = todo.front();
            todo.pop();
            joints.erase(full);
            if (Voxel v; GetChunk<Chunk>(chunks, glm::splitIdx(full), v, ck, ck_pos) && v.is_solid()) {
                constexpr std::array<voxel_pos, 6> DIRS = {
                    voxel_pos(1, 0, 0), voxel_pos(-1, 0, 0), voxel_pos(0, 1, 0),
                    voxel_pos(0, -1, 0), voxel_pos(0, 0, 1), voxel_pos(0, 0, -1)};
                    //MAYBE: diag
                for (auto dir : DIRS) {
                    const auto off = full + dir;
                    if (part.insert(off).second)
                        todo.push(off);
                }
            }
        }
        if (todo.empty()) {
            added = true;
            callback(part, cleanVoxel, chunks, ck, ck_pos, it->second);
        }
    }
    return added;
}

/// Call Apply then Split
template<typename Chunk, typename area_map, typename CB>
inline void ApplySplit(area_map &areas, action::FillShape fill, size_t floodFillLimit, const CB& callback) {
    Apply<Chunk>(areas, fill, callback);
    Split<Chunk>(areas, fill, floodFillLimit, [&](const robin_hood::unordered_set<voxel_pos>& part, Voxel next,
        world::ChunkContainer& chunks, std::shared_ptr<Chunk>& ck, chunk_pos& ck_pos, std::shared_ptr<Area>&) {
        for(auto full: part) {
            const auto split = glm::splitIdx(full);
            if (Voxel prev; world::iterator::GetChunk<Chunk>(chunks, split, prev, ck, ck_pos) && prev.is_solid()) {
                if (prev.value != next.value) {
                    callback(ck, ck_pos, split.second, prev, next, fill.radius * .05f);
                }
            }
        }
    });
}

class Cube final: public AbstractFill {
public:
    bool next(pair&) override;

    Cube(uint16_t radius): radius(radius), pos(-radius, -radius, -radius) { }

private:
    const uint16_t radius;
    glm::ivec3 pos;
};
class CubeBorder final: public AbstractBorder {
public:
    bool next(glm::ivec3&) override;

    CubeBorder(uint16_t radius): radius(radius), pos(-radius, -radius, -radius), face(0) { }

private:
    const uint16_t radius;
    glm::ivec3 pos;
    int face;
};

/// Interger sphere
class RawSphere final: public AbstractFill {
public:
    bool next(pair&) override;

    RawSphere(uint16_t radius): radius(radius), pos(-radius-1, 0, 0), dy(0), dz(0) { }

private:
    const uint16_t radius;
    glm::ivec3 pos;
    int dy;
    int dz;
};
class SphereBorder final: public AbstractBorder {
public:
    bool next(glm::ivec3&) override;

    SphereBorder(uint16_t radius): radius(radius), max_i(radius*3), max_j(radius*4), i(0), j(0) { }

private:
    const uint16_t radius;
    const uint32_t max_i;
    const uint32_t max_j;
    uint32_t i;
    uint32_t j;
};

/// Anti-aliased sphere
class SmoothSphere final: public AbstractFill {
public:
    bool next(pair&) override;

    SmoothSphere(uint16_t radius): radius(radius), pos(-radius-1, 0, 0), dy(0), dz(0) { }

private:
    const uint16_t radius;
    glm::ivec3 pos;
    double dy;
    double dz;
};
}