#pragma once

#include "core/world/generator/Cave.hpp"
#include "core/geometry/math.hpp"
#include "./Core.hpp"

namespace world::generator {

    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.getBlock(pos, CHUNK_LENGTH);
            auto displacementSet = displacement.getBlock(pos, CHUNK_LENGTH);
            const auto& IDS = world::module::core::Core::Get()->ids;
            for (size_t i = 0; i < CHUNK_SIZE; i++) {
                const auto vPos = glm::fromIdx(i);
                const auto noise_i = glm::toIdx(vPos.z, vPos.y, vPos.x);

                const auto heightRatio = ((float)getHeight(glm::multiply(pos) + glm::llvec3(vPos)) - params.height) / params.height;

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

                out[i] = [&]() -> Voxel {
                    if (density > 0) {
                        const auto material = [&]() -> int {
                            const auto noisedHeightRatio = heightRatio - displacementSet.get()[noise_i] * params.beach_displacement;
                            if(noisedHeightRatio >= 0) {
                                return densitySet.get()[noise_i] + params.density < ((heightRatio + 0.007f) / params.surface_roughness) ? IDS.GRASS : IDS.DIRT;
                            } else {
                                return noisedHeightRatio >= -params.beach_depth ? IDS.SAND : IDS.ROCK;
                            }
                        }();
                        return Voxel(material, density);
                    } else {
                        if (heightRatio >= 0) {
                            return Voxel(IDS.AIR, (heightRatio < params.surface_roughness) * Voxel::DENSITY_MAX);
                        } else {
                            return Voxel(IDS.WATER, std::clamp<double>(-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);
        }
        std::optional<double> getCurvature() const override {
            if constexpr (shape == PlanetShape::Cube) {
                return params.height * (1.1 + params.surface_roughness);
            }
            return {};
        }
    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>;

}