#ifndef PERLIN_H
#define PERLIN_H

#include <vector>
#include <algorithm>
#include <random>

#include <glm/glm.hpp>


// Doesn't need to be super fast because assumedly I'm not making Perlin noise _that_ often
std::vector<int> makePermutation() {
    std::vector<int> permutation;

    for (int i = 0; i < 256; i++) {
        permutation.push_back(i);
    }

    std::random_device rd;
    std::mt19937 g(rd());

    std::shuffle(permutation.begin(), permutation.end(), g);

    for (int i = 0; i < 256; i++) {
        permutation.push_back(permutation[i]);
    }

    return permutation;
}

glm::vec2 getConstantVector(int v) {
    int h = v & 3;

    if (h == 0) {
        return glm::vec2(1.0f, 1.0f);
    } else if (h == 1) {
        return glm::vec2(-1.0f, 1.0f);
    } else if (h == 2) {
        return glm::vec2(-1.0f, -1.0f);
    }

    return glm::vec2(1.0f, -1.0f);
}

float fade(float t) {
    return ((6 * t - 15) * t + 10) * t * t * t;
}

// linear interpolation
float lerp(float t, float a1, float a2) {
    return a1 + t * (a2 - a1);
}

float perlinNoise2D(const float x, const float y, const std::vector<int>& permutation) {
    const int X = (int)floorf(x) & 255;
    const int Y = (int)floorf(y) & 255;

    const float xf = x - floorf(x);
    const float yf = y - floorf(y);

    const glm::vec2 topRight    = glm::vec2(xf - 1.0f, yf - 1.0f);
    const glm::vec2 topLeft     = glm::vec2(xf, yf - 1.0f);
    const glm::vec2 bottomRight = glm::vec2(xf - 1.0f, yf);
    const glm::vec2 bottomLeft  = glm::vec2(xf, yf);

    const int valueTopRight    = permutation[permutation[X+1]+Y+1];
    const int valueTopLeft     = permutation[permutation[X]+Y+1];
    const int valueBottomRight = permutation[permutation[X+1]+Y];
    const int valueBottomLeft  = permutation[permutation[X]+Y];

    const auto dotTopRight    = glm::dot(topRight, getConstantVector(valueTopRight));
    const auto dotTopLeft     = glm::dot(topLeft, getConstantVector(valueTopLeft));
    const auto dotBottomRight = glm::dot(bottomRight, getConstantVector(valueBottomRight));
    const auto dotBottomLeft  = glm::dot(bottomLeft, getConstantVector(valueBottomLeft));

    const float u = fade(xf);
    const float v = fade(yf);

    return lerp(
        u,
        lerp(v, dotBottomLeft, dotTopLeft),
        lerp(v, dotBottomRight, dotTopRight)
    );
}

#endif

