#pragma once

#include "forward.h"
#include "../generational/map.hpp"
#include "./Registry.hpp"
#include "../geometry/math.hpp"
#include <memory>

namespace world {
    class Chunk;
    struct Elements;

    struct transform {
        transform(const world_pos& p = world_pos(0), const pivot& r = identity_pivot):
            position(p), rotation(r) { }
        world_pos position;
        pivot rotation;

        world_pos computeChild(const world_pos& relative) const {
            return position + (lpivot)rotation * relative;
        }
        transform computeChild(const transform& relative) const {
            return transform(computeChild(relative.position), relative.rotation * rotation);
        }

        inline bool operator==(const transform &t) const { return position == t.position && rotation == t.rotation; }
        inline bool operator!=(const transform &t) const { return !operator==(t); }
    };
    struct velocity {
        velocity(const offset_pos& p = offset_pos(0), const pivot& r = identity_pivot):
            position(p), rotation(r) { }
        offset_pos position;
        pivot rotation;

        inline bool operator==(const velocity &v) const { return position == v.position && rotation == v.rotation; }
        inline bool operator!=(const velocity &v) const { return !operator==(v); }
    };
    const auto FIXED = velocity();


    /// Element position
    struct NodeBody {
    public:
        transform absolute;
    };

    //MAYBE: separate NodeBody and dense maps of Elements
    class Element;
    struct Node: NodeBody {
        Node(const transform& t, const std::shared_ptr<Element>& c): content(c) {
            absolute = t;
        }
        std::shared_ptr<Element> content;
    };
    using node_id = generational::key<world::Node>;
    using node_ref = generational::ref<world::Node>;

    /// Abstract element content
    class Element {
    public:
        virtual ~Element() { }
        /// Remove itself from specific maps
        virtual void free(const node_id& i, Elements&) = 0;

        /// Axis-align bounding box relative to center
        virtual aabb getBoundingBox(const Elements& l) const = 0;

        //TODO: add physics queries
        //TODO: getMass
    };

    template<class El>
    struct NodeOf: Node {
        static_assert(std::is_base_of<Element, El>::value);
        static constexpr NodeOf *Make(Node *n) { return reinterpret_cast<NodeOf<El>*>(n); }
        static constexpr const NodeOf *Make(const Node *n) { return reinterpret_cast<const NodeOf<El>*>(n); }
        inline constexpr std::shared_ptr<El> get() const { return std::static_pointer_cast<El>(content); }
    };

    /// Element hierarchical relative data
    struct Hierarchy {
    public:
        Hierarchy(const transform& relative, const velocity& vel):
            relative(relative), vel(vel) { }

        transform relative;
        velocity vel;
    };

    struct relative_pos {
        node_id parent;
        world_pos position;
    };
    struct relative_transform {
        node_id parent;
        transform relative;
    };

    /// Single region element
    class Part final : public Element {
    public:
        Part(glm::usvec3 s): size(s) { }
        //TODO: add scale

        void free(const node_id& i, Elements&) override;

        aabb getBoundingBox(const Elements&) const override {
            return aabb(glm::lfvec3(0), size);
        }

        /// Linear 3d grid of Chunk
        /// Size: chunkSize()
        using region_t = std::vector<std::shared_ptr<world::EdittableChunk>>;
        region_t chunks;

        bool empty() const { return chunks.empty(); }
        bool loaded() const {
            return chunks.size() >= static_cast<size_t>(chunkCount()) && std::all_of(chunks.begin(), chunks.end(),
                [](const std::shared_ptr<world::EdittableChunk> &ptr) {return ptr != nullptr; });
        }
        void allocate() {
            chunks.resize(chunkCount());
        }
        static constexpr chunk_pos ChunkSize(const glm::usvec3& s) { return chunk_pos(1) + glm::divide(s); }
        constexpr chunk_pos chunkSize() const { return ChunkSize(size); }
        size_t chunkCount() const {
            const auto size = chunkSize();
            return size.x * size.y * size.z;
        }
        constexpr bool inRange(const chunk_pos& c) const {
            return glm::all(glm::greaterThanEqual(c, chunk_pos(0))) &&
                   glm::all(glm::lessThan(c, chunkSize()));
        }
        constexpr size_t getIdx(const chunk_pos& c) const {
            return glm::toIdx(c, chunkSize());
        }
        constexpr region_chunk_pos getPos(size_t i) const {
            return glm::fromIdx(i, chunkSize());
        }
        std::shared_ptr<world::EdittableChunk> findInRange(const chunk_pos& c) const {
            assert(inRange(c));
            const auto idx = getIdx(c);
            if (idx < chunks.size())
                return chunks.at(idx);

            return nullptr;
        }

        glm::usvec3 size;
    };
    using part_id = generational::key<world::Part>;
    using part_ref = generational::ref<world::Part>;

    class Instance;
    struct Model: world::module::model {
        Model(const data& m, const geometry::faabb& col, glm::vec3 sc = glm::vec3(1), bool t = false):
            model("", m, col, sc), temporary(t) { }

        /// Is removed with last instance
        bool temporary;

        generational::map<Instance> instances;
        /// Shared value for models without unique parameters
        std::shared_ptr<Instance> origin;
    };
    using model_id = generational::key<world::Model>;
    /// Instanced model element
    class Instance final: public Element {
    public:
        Instance(const model_id& t): type(t) { }

        /// Model id
        model_id type;

        void free(const node_id& i, Elements&) override;

        aabb getBoundingBox(const Elements &l) const override;
    };
    using instance_id = generational::key<world::Instance>;
    using instance_ref = generational::ref<world::Instance>;
}
