#include "texture.hpp"


PRNG::result_type PRNG::hash(PRNG::result_type value) {
    return value * 1103515245U + 12345U;
}


PRNG::result_type PRNG::get() {
    return seed = hash(seed);
}


PRNG::result_type PRNG::last() const {
    return seed;
}


PRNG::result_type PRNG::get(result_type max) {
    assert(max > 0);
    return get() % max;
}


PRNG::result_type PRNG::get(result_type min, result_type max) {
    assert(max >= min);
    return get(max - min + 1) + min;
}


int PRNG::get(int min, int max) {
    // NB: safe cast, as pos max must fit unsigned and returned unsigned must fit int
    assert(min >= 0);
    assert(max >= 0);
    return static_cast<int>(get(static_cast<result_type>(min), static_cast<result_type>(max)));
}


fvalue_t PRNG::getf() {
    return static_cast<fvalue_t>(get()) / static_cast<fvalue_t>(max());
}


/** @brief Choose a color theme for the given seed and subsequently pick colors from that palette. */
class Theme {
  protected:
    // clang-format off
    static constexpr std::array<std::array<unsigned, 11>, 14> sequentials = {{
        // https://github.com/qrohlf/trianglify/blob/master/src/utils/colorbrewer.js
        {0x7f3b08U, 0xb35806U, 0xe08214U, 0xfdb863U, 0xfee0b6U, 0xf7f7f7U, 0xd8daebU, 0xb2abd2U, 0x8073acU, 0x542788U, 0x2d004bU}, // PuOr
        {0x543005U, 0x8c510aU, 0xbf812dU, 0xdfc27dU, 0xf6e8c3U, 0xf5f5f5U, 0xc7eae5U, 0x80cdc1U, 0x35978fU, 0x01665eU, 0x003c30U}, // BrBG
        {0x40004bU, 0x762a83U, 0x9970abU, 0xc2a5cfU, 0xe7d4e8U, 0xf7f7f7U, 0xd9f0d3U, 0xa6dba0U, 0x5aae61U, 0x1b7837U, 0x00441bU}, // PRGn
        {0x8e0152U, 0xc51b7dU, 0xde77aeU, 0xf1b6daU, 0xfde0efU, 0xf7f7f7U, 0xe6f5d0U, 0xb8e186U, 0x7fbc41U, 0x4d9221U, 0x276419U}, // PiYG
        {0x67001fU, 0xb2182bU, 0xd6604dU, 0xf4a582U, 0xfddbc7U, 0xf7f7f7U, 0xd1e5f0U, 0x92c5deU, 0x4393c3U, 0x2166acU, 0x053061U}, // RdBu
        {0x67001fU, 0xb2182bU, 0xd6604dU, 0xf4a582U, 0xfddbc7U, 0xffffffU, 0xe0e0e0U, 0xbababaU, 0x878787U, 0x4d4d4dU, 0x1a1a1aU}, // RdGy
        {0xa50026U, 0xd73027U, 0xf46d43U, 0xfdae61U, 0xfee090U, 0xffffbfU, 0xe0f3f8U, 0xabd9e9U, 0x74add1U, 0x4575b4U, 0x313695U}, // RdYlBu
        {0x9e0142U, 0xd53e4fU, 0xf46d43U, 0xfdae61U, 0xfee08bU, 0xffffbfU, 0xe6f598U, 0xabdda4U, 0x66c2a5U, 0x3288bdU, 0x5e4fa2U}, // Spectral
        {0xa50026U, 0xd73027U, 0xf46d43U, 0xfdae61U, 0xfee08bU, 0xffffbfU, 0xd9ef8bU, 0xa6d96aU, 0x66bd63U, 0x1a9850U, 0x006837U}, // RdYlGn
        // https://github.com/plotly/plotly.py/blob/master/packages/python/plotly/_plotly_utils/colors/sequential.py
        {0x440154U, 0x482878U, 0x3e4989U, 0x31688eU, 0x26828eU, 0xffffffU, 0x1f9e89U, 0x35b779U, 0x6ece58U, 0xb5de2bU, 0xfde725U}, // Viridis
        {0x00224eU, 0x123570U, 0x3b496cU, 0x575d6dU, 0x707173U, 0xffffffU, 0x8a8678U, 0xa59c74U, 0xc3b369U, 0xe1cc55U, 0xfee838U}, // Cividis
        {0x0d0887U, 0x46039fU, 0x7201a8U, 0x9c179eU, 0xbd3786U, 0xffffffU, 0xd8576bU, 0xed7953U, 0xfb9f3aU, 0xfdca26U, 0xf0f921U}, // Plasma
        {0x2a23a0U, 0x0f4799U, 0x125f8eU, 0x267489U, 0x358888U, 0x419d85U, 0x51b27cU, 0x6fc66bU, 0xa0d65bU, 0xd4e170U, 0xfdee99U}, // haline
        {0x245668U, 0x0f7279U, 0x0d8f81U, 0x39ab7eU, 0xffffffU, 0xffffffU, 0xffffffU, 0x39ab7eU, 0x6ec574U, 0xa9dc67U, 0xedef5dU}, // Aggrnyl
    }};
    // clang-format on

  private:
    const size_t seed;

    static constexpr vertex_t parse_color(unsigned color) {
        return vertex_t{(fvalue_t)(color >> 16) / 255.0F, (fvalue_t)((color >> 8) & 0xffU) / 255.0F,
                        (fvalue_t)(color & 0xffU) / 255.0F};
    }

    static vertex_t scale_color(vertex_t color) {
#if 1
        return color * (1.0F / std::max(color.r, std::max(color.g, color.b)));
#else
        return color + (1.0F - std::max(color.r, std::max(color.g, color.b)));
#endif
    }

    vertex_t get_color(ssize_t index, fvalue_t saturation = 1.0F) const {
        const auto& sequential = sequentials.at(seed % sequentials.size());
        assert((size_t)std::abs(index) < sequential.size());
        assert(0.0F <= saturation && saturation <= 1.0F);
        const size_t off = index >= 0 ? (size_t)index : sequential.size() - (size_t)std::abs(index);
        const vertex_t color = parse_color(sequential.at(seed % 2 == 0 ? off : sequential.size() - off - 1));
        return (scale_color(color) * saturation) + (ones * (1.0F - saturation));
    }

  public:
    Theme(PRNG::result_type seed) : seed(static_cast<size_t>(seed)) {}

    vertex_t bg_color() const {
        return vertex_t::setted(0.4F);
    }

    vertex_t bg_color_lo(int variant, fvalue_t intensity) const {
        return get_color(0, intensity);
    }

    vertex_t bg_color_hi(int variant, fvalue_t intensity) const {
        return get_color(-1, intensity);
    }

    vertex_t light_color(int variant) const {
        switch (variant % 4) {
            case 0:
                return get_color(2);
            case 1:
                return get_color(3);
            case 2:
                return get_color(-4);
            default:
                return get_color(-3);
        }
    }
};


TextureFactory::TextureFactory(PRNG&& rnd, res_t res, vertex2_t<fcoord_t> viewport)
    : seed(rnd.last()), noise(std::move(rnd), {rnd.get(res.x / 2, res.x), rnd.get(res.y / 2, res.y)}, viewport) {}


size_t TextureFactory::size() const {
    return textures.size();
}


const Texture* TextureFactory::make(const std::pair<int, int>& variant) {
    auto hit = cache.find(variant);
    if (hit == cache.end()) {
        const Theme theme(seed);
#if USE_TEXTURE >= 2
        textures.push_back(std::make_unique<NoiseTexture>(theme.bg_color_lo(variant.second, USE_TEXTURE_SATURATION),
                                                          theme.bg_color_hi(variant.second, USE_TEXTURE_SATURATION),
                                                          0.15F, variant.first % 2 == 0, noise));
#elif USE_TEXTURE >= 1
        textures.push_back(std::make_unique<ColorTexture>(theme.bg_color_hi(variant.second, USE_TEXTURE_SATURATION)));
#else
        textures.push_back(std::make_unique<ColorTexture>(theme.bg_color()));
#endif
        hit = cache.insert({variant, textures.back().get()}).first;
    }
    return hit->second;
}

const Texture* TextureFactory::make(const std::pair<int, int>& variant_lo, const std::pair<int, int>& variant_hi,
                                    int component) {
    if (variant_lo != variant_hi) {
        textures.push_back(std::make_unique<GradientTexture>(make(variant_lo), make(variant_hi), component));
        return textures.back().get();
    }
    return make(variant_lo);
}


LightFactory::LightFactory(PRNG&& rnd, fcoord_t radius) : seed(rnd.last()), radius(radius) {}


fcoord_t LightFactory::get_radius() const {
    return radius;
}


const PointLight* LightFactory::make(const std::pair<int, int>& variant) {
    auto hit = cache.find(variant);
    if (hit == cache.end()) {
        hit = cache.insert({variant, make(radius, Theme(seed).light_color(variant.second))}).first;
    }
    return hit->second;
}


const PointLight* LightFactory::make(fcoord_t rad, const vertex_t& color) {
    lights.push_back(std::make_unique<PointLight>(rad, color));
    return lights.back().get();
}