#pragma once

#include <variant>
#include "Noise.hpp"
#include "../../core/world/Voxel.hpp"
#include "../../core/data/math.hpp"

namespace world::generator {

    /// Abstract Noise generator
    class Abstract {
    public:
        /// Generate chunk voxels
        /// MAYBE: use template to avoid virtual
        virtual void generate(const chunk_pos &at, std::array<Voxel, CHUNK_SIZE> &out) = 0;
        /// Get gravity vector at given point
        virtual glm::vec3 getGravity(const voxel_pos& point) const = 0;
    };

    // Generate empty space
    class Void: public Abstract {
    public:
        struct Params { };
        Void() = default;

        void generate(const chunk_pos &, std::array<Voxel, CHUNK_SIZE> &out) override {
            out.fill(Voxel());
        }
        glm::vec3 getGravity(const voxel_pos&) const override { return glm::vec3(0); }
    };

    /// Endless cave network
    class Cave: public Abstract {
    public:
        struct Params {
            Params(int seed = 42, float density = 0, float gran = 30): seed(seed), density(density), granularity(gran) { }

            /// Random generator start
            int seed;
            /// Offset density overrage
            float density;
            /// Speed of density change
            float granularity;
        };
        Cave(const Params p): params(p), density(Noise::SimplexFractal(p.seed)), material(Noise::Cellular(p.seed * 5, .1)) { }

        void generate(const chunk_pos &pos, std::array<Voxel, CHUNK_SIZE> &out) override {
            auto densitySet = density.Get(pos, CHUNK_LENGTH);
            auto materialSet = material.Get(pos, CHUNK_LENGTH);
            for (size_t i = 0; i < CHUNK_SIZE; i++) {
                const auto density = std::clamp((densitySet.get()[i] + params.density) * params.granularity, 0.f, 1.f) * Voxel::DENSITY_MAX;
                const auto material = density > 0 ? 2 + std::clamp(static_cast<int>(std::lrint((materialSet.get()[i] + 1) / 2 * (materials::count - 3))),
                    0, materials::count - 3) : 1; //NOTE: map (approx -1, 1) to (1, mat_max)
                out[i] = Voxel(material, density);
            }
        }
        glm::vec3 getGravity(const voxel_pos&) const override { return glm::vec3(-1, 0, 0); }
    private:
        Params params;
        Noise density;
        Noise material;
    };

    enum class PlanetShape { Cube, Sphere };

    /// Abstract shaped planet generator
    template <PlanetShape PS>
    class Planet: public Abstract {
    public:
        struct Params: Cave::Params {
            Params(voxel_pos::value_type height, int seed = 42, float surface_roughness = .1, float depth_roughness = .05, float density = 0, float gran = 30):
                Cave::Params(seed, density, gran), height(height), surface_roughness(surface_roughness), depth_roughness(depth_roughness) { }

            /// Sea level (minimal density)
            voxel_pos::value_type height;
            /// Density decrease over height
            float surface_roughness;
            /// Density decrease under height
            float depth_roughness;
            /// Sea border depth
            float beach_depth = .01f;
            /// Sea ground displacement
            float beach_displacement = .01f;
        };
        Planet(const Params p) : params(p), density(Noise::SimplexFractal(p.seed)), displacement(Noise::Simplex(p.seed * 5, .01)) {}

        void generate(const chunk_pos &pos, std::array<Voxel, CHUNK_SIZE> &out) override {
            auto densitySet = density.Get(pos, CHUNK_LENGTH);
            auto displacementSet = displacement.Get(pos, CHUNK_LENGTH);
            for (size_t i = 0; i < CHUNK_SIZE; i++) {
                const auto heightRatio = static_cast<float>(getHeight(glm::multiply(pos) + glm::llvec3(glm::fromIdx(i))) - params.height) / params.height;

                const auto verticalDensityOffset = heightRatio / (heightRatio >= 0 ? params.surface_roughness : params.depth_roughness);
                const auto density = std::clamp((densitySet.get()[i] + params.density - verticalDensityOffset) * params.granularity, 0.f, 1.f) * Voxel::DENSITY_MAX;

                out[i] = [&]() -> Voxel {
                    if (density > 0) {
                        const auto material = [&]() -> int {
                            const auto noisedHeightRatio = heightRatio - displacementSet.get()[i] * params.beach_displacement;
                            if(noisedHeightRatio >= 0) {
                                return densitySet.get()[i] + params.density < ((heightRatio + 0.007f) / params.surface_roughness) ? materials::GRASS : materials::DIRT;
                            } else {
                                return noisedHeightRatio >= -params.beach_depth ? materials::SAND : materials::ROCK;
                            }
                        }();
                        return Voxel(material, density);
                    } else {
                        if (heightRatio >= 0) {
                            return Voxel(materials::AIR, (heightRatio < params.surface_roughness) * Voxel::DENSITY_MAX);
                        } else {
                            return Voxel(materials::WATER, std::clamp(-heightRatio * params.height, 0.f, 1.f) * Voxel::DENSITY_MAX);
                        }
                    }
                }();
            }
        }
        glm::vec3 getGravity(const voxel_pos& pos) const override {
            const auto heightRatio = static_cast<float>((getHeight(pos) - params.height) / params.height);
            const auto scale = params.height >> 7;
            const auto mass = scale * scale * scale; //FIXME: average material density
            return -getSurfaceDir(pos) * std::max<float>(0, 1.f - heightRatio * heightRatio) * glm::vec3(mass);
        }
    private:
        static constexpr PlanetShape shape = PS;
        static constexpr glm::f64 getHeight(const voxel_pos &p) {
            if constexpr(shape == PlanetShape::Cube) {
                return glm::max_axis(glm::abs(p));
            } else {
                return glm::length(glm::dvec3(p));
            }
        }
        /// Centre inverse direction
        // NOTE: So center point is consider mass center
        static constexpr glm::vec3 getSurfaceDir(const voxel_pos &p) {
            if constexpr(shape == PlanetShape::Cube) {
                return glm::inormalize(p);
            } else {
                return glm::normalize(glm::dvec3(p));
            }
        }
        Params params;
        Noise density;
        Noise displacement;
    };
    using CubicPlanet = Planet<PlanetShape::Cube>;
    using RoundPlanet = Planet<PlanetShape::Sphere>;

    using params = std::variant<Void::Params, Cave::Params, CubicPlanet::Params, RoundPlanet::Params>;
    inline std::unique_ptr<Abstract> load(const params &p) {
        const auto get_new = [](const params &p) -> Abstract* {
            if(std::holds_alternative<Void::Params>(p)) {
                return new Void();
            } else if(const auto c = std::get_if<Cave::Params>(&p)) {
                return new Cave(*c);
            } else if(const auto pc = std::get_if<CubicPlanet::Params>(&p)) {
                return new CubicPlanet(*pc);
            } else if(const auto ps = std::get_if<RoundPlanet::Params>(&p)) {
                return new RoundPlanet(*ps);
            }
            return nullptr;
        };
        return std::unique_ptr<Abstract>(get_new(p));
    }
}