#pragma once
#include <assert.h>
#include <cstdint>
#include <map>
#include <vector>
#include "vertex.hpp"


/**
 * @brief Linear Congruential Generator
 *
 * Once seeded, gives stable pseudo-random numbers.
 * Concept: https://en.cppreference.com/w/cpp/named_req/UniformRandomBitGenerator
 */
class PRNG {
  public:
    typedef uint32_t result_type;

    static constexpr result_type min() {
        return 0;
    }

    static constexpr result_type max() {
        return UINT32_MAX;
    }

    result_type operator()() {
        return get();
    }

  private:
    result_type seed;

    /** @brief Pop a new pseudo-random value. */
    result_type get();

  public:
    PRNG(result_type seed) : seed(seed) {}

    /** @brief Sub-generator derived once for arbitrary number of follow-up calls. */
    PRNG(PRNG& parent) : PRNG(~parent.get()) {}

    /** @brief max (exclusive) wrapper for convenience. */
    result_type get(result_type max);

    /** @brief min/max (inclusive) wrapper for convenience. */
    result_type get(result_type min, result_type max);
    /** @brief min/max (inclusive) wrapper for signed integers (still unsigned results). */
    int get(int min, int max);

    /** @brief float values in [0.0, 1.0] */
    fvalue_t getf();

    result_type last() const; ///< current state
    static result_type hash(result_type); ///< underlying permutation
};


/** @brief 2D perlin noise implementation. */
class Perlin2D final {
  private:
    const vertex2_t<fcoord_t> viewport;
    Buffer2D<vertex2_t<fcoord_t>> buf;

    /** @brief Avoid blocky artifacts by smoothing polynomial instead of linear boundaries. */
    static INLINE constexpr fvalue_t ease(fvalue_t t) {
#if USE_PERLIN_INTERPOLATE
        return t * t * t * (t * (t * 6.0F - 15.0F) + 10.0F); // 6t^5 - 15t^4 + 10t^3
#else
        return t;
#endif
    }

    /** @brief Linear or polynomial interpolation between the two values. */
    static INLINE constexpr fvalue_t interpolate(fvalue_t a, fvalue_t b, fvalue_t v) {
        return (b - a) * ease(v) + a;
    }

    /** @brief Dot product as weight for one of the random vectors. */
    INLINE constexpr fvalue_t dot_gradient(dcoord_t x, dcoord_t y, fvalue_t wx, fvalue_t wy) const {
        const vertex2_t<fcoord_t> gradient = buf.at(x, y);
        return (gradient.x * wx) + (gradient.y * wy);
    }

    /** @brief Perlin noise implementation, yielding [-1, 1] for float coordinates. */
    INLINE constexpr fcoord_t at(fcoord_t x, fcoord_t y) const {
        const dcoord_t x0 = (dcoord_t)std::floor(x);
        const dcoord_t x1 = x0 + 1;
        const dcoord_t y0 = (dcoord_t)std::floor(y);
        const dcoord_t y1 = y0 + 1;
        assert(0 <= x0 && x1 < buf.width());
        assert(0 <= y0 && y1 < buf.height());

        const fcoord_t dist_x0 = x - (fcoord_t)x0;
        const fcoord_t dist_x1 = x - (fcoord_t)x1;
        const fcoord_t dist_y0 = y - (fcoord_t)y0;
        const fcoord_t dist_y1 = y - (fcoord_t)y1;

        const fcoord_t a = dot_gradient(x0, y0, dist_x0, dist_y0);
        const fcoord_t b = dot_gradient(x1, y0, dist_x1, dist_y0);
        const fcoord_t c = dot_gradient(x0, y1, dist_x0, dist_y1);
        const fcoord_t d = dot_gradient(x1, y1, dist_x1, dist_y1);

        return interpolate(interpolate(a, b, dist_x0), interpolate(c, d, dist_x0), dist_y0);
    }

  public:
    Perlin2D(PRNG&& rnd, res_t res, vertex2_t<fcoord_t> viewport) : viewport(viewport), buf(res.w, res.h) {
        for (dcoord_t x = 0; x < buf.width(); ++x) {
            for (dcoord_t y = 0; y < buf.height(); ++y) {
                buf.at(x, y) = {rnd.getf() * 2.0F - 1.0F, rnd.getf() * 2.0F - 1.0F};
            }
        }
    }

    /** @brief Perlin noise interface, yielding [-1, 1] for the given viewport x/y coordinates. */
    INLINE constexpr fcoord_t at(const vertex_t& pos) const {
        return at(pos.x / viewport.x * (fcoord_t)(buf.width() - 2), pos.y / viewport.y * (fcoord_t)(buf.height() - 2));
    }
};


/** @brief Abstract base for all textures, which yield RGB values for (u/v or absolute) scene hits. */
class Texture {
  public:
    /** @brief @ref Texture interface, [0, 1] RGB at given u/v coordinates or world coordinate. */
    virtual vertex_t get_color(const uv_t&, const vertex_t&) const = 0;
};


/** @brief Unconditionally give a predefined color. */
class ColorTexture final : public Texture {
  private:
    const vertex_t color;

  public:
    ColorTexture(vertex_t color) : color(color) {}

    INLINE vertex_t get_color(const uv_t& uv, const vertex_t& pos) const override {
        return color;
    }
};


/** @brief Interpolate between two colors by perlin noise value. */
class NoiseTexture final : public Texture {
  private:
    const vertex_t color_lo;
    const vertex_t color_hi;
    const vertex_t spread; // min, max, factor
    const bool iso; ///< Isobar at 0.5 instead of min/max interpolation.
    const Perlin2D& noise;

  public:
    NoiseTexture(vertex_t color_lo, vertex_t color_hi, fvalue_t max, bool iso, const Perlin2D& noise)
        : color_lo{color_lo},
          color_hi{color_hi},
          spread{iso ? -max : -max / 2.0F, iso ? max : max / 2.0F, iso ? 1.0F / max : 2.0F / max},
          iso(iso),
          noise(noise) {
        assert(0.0F < max && max <= 0.5F);
    }

    INLINE vertex_t get_color(const uv_t& uv, const vertex_t& pos) const override {
        fvalue_t noise_val = std::max(spread.v[0], std::min(spread.v[1], noise.at(pos))) * spread.v[2];
        if (iso) {
            noise_val = 1.0F - std::abs(noise_val); // zero gradient isobar
        } else {
            noise_val = noise_val / 2.0F + 0.5F; // [-1,1] -> [0, 1]
        }
        return ((color_lo * (1.0F - noise_val)) + (color_hi * noise_val)) / 2.0F;
    }
};


/**
 * @brief Interpolate between two other @ref Texture instances along the given axis.
 *
 * Used between 'rooms' of a level, for a smoother 'door' transition.
 */
class GradientTexture final : public Texture {
  private:
    const Texture* lo;
    const Texture* hi;
    const int component;

  public:
    GradientTexture(const Texture* lo, const Texture* hi, int component) : lo(lo), hi(hi), component(component) {
        assert(0 <= component && component <= 1);
    }

    INLINE vertex_t get_color(const uv_t& uv, const vertex_t& pos) const override {
        assert(0.0F <= uv.u && uv.u <= 1.0F);
        assert(0.0F <= uv.v && uv.v <= 1.0F);
#if USE_TEXTURE_GRADIENT
        return (lo->get_color(uv, pos) * (1.0F - uv.V[component])) + (hi->get_color(uv, pos) * (uv.V[component]));
#else
        return uv.v[component] < 0.5 ? lo->get_color(uv, pos) : hi->get_color(uv, pos);
#endif
    }
};


/** @brief Light as emitted from a point source, the only implementation atm. */
class PointLight final {
  private:
    fcoord_t rad;
    fcoord_t sqrad;
    vertex_t color;

  public:
    PointLight(fcoord_t rad, vertex_t color) : rad(rad), sqrad(rad * rad), color(color) {}

    /** @brief radius */
    INLINE constexpr fcoord_t get_max_dist() const {
        return rad;
    }

    /** @brief radius^2, for optimized comparisons that don't need sqrt() */
    INLINE constexpr fcoord_t get_max_sqdist() const {
        return sqrad;
    }

    /** @brief color value as [0, 1] RGB */
    INLINE constexpr const vertex_t& get_color() const {
        return color;
    }
};


/** @brief Generate and maintain @ref Texture instances in different 'variants', acc. to @ref LevelGenerator. */
class TextureFactory {
  private:
    const PRNG::result_type seed;

    const Perlin2D noise; // global noise
    std::vector<std::unique_ptr<const Texture>> textures{};
    std::map<std::pair<int, int>, const Texture*> cache{}; // caches more for locality than memory

  public:
    TextureFactory(PRNG&& rnd, res_t res, vertex2_t<fcoord_t> viewport);
    size_t size() const;

    const Texture* make(const std::pair<int, int>& variant);
    const Texture* make(const std::pair<int, int>& variant_lo, const std::pair<int, int>& variant_hi, int component);
};


/** @brief Generate and maintain @ref PointLight instances in different 'variants', acc. to @ref LevelGenerator. */
class LightFactory {
  private:
    const PRNG::result_type seed;

    const fcoord_t radius;
    std::vector<std::unique_ptr<const PointLight>> lights{};
    std::map<std::pair<int, int>, const PointLight*> cache{};

  public:
    LightFactory(PRNG&& rnd, fcoord_t radius);
    fcoord_t get_radius() const;

    const PointLight* make(const std::pair<int, int>& variant);
    const PointLight* make(fcoord_t rad, const vertex_t& color);
};