// Based on https://en.wikipedia.org/wiki/Perlin_noise

#include <cstring>
#include <SDL/SDL.h>
#include <emscripten.h>

constexpr int screen_width_{ 800 };
constexpr int screen_height_{ 600 };

constexpr int world_width_{ 256 };
constexpr int world_height_{ 256 };

SDL_Surface* surface_{ nullptr };
Uint32 pixels_[world_width_ * world_height_]{};

struct Vec2 {
    float x, y;
};

// Function to linearly interpolate between a0 and a1. 
// Weight w should be in the range [0.0, 1.0].
float Interpolate(float a0, float a1, float w) {
    // Use [[Smootherstep]] for a smooth result with a second derivative equal to zero on boundaries:
    return (a1 - a0) * ((w * (w * 6.0 - 15.0) + 10.0) * w * w * w) + a0;
}

Uint32 InterpolateColor(SDL_PixelFormat* format, Uint32 color1, Uint32 color2, float t) {
    Uint8 r1{ 0 };
    Uint8 g1{ 0 };
    Uint8 b1{ 0 };
    SDL_GetRGB(color1, format, &r1, &g1, &b1);
    Uint8 r2{ 0 };
    Uint8 g2{ 0 };
    Uint8 b2{ 0 };
    SDL_GetRGB(color2, format, &r2, &g2, &b2);
    return SDL_MapRGB(format, Interpolate(r1, r2, t), Interpolate(g1, g2, t), Interpolate(b1, b2, t));
}

// Create pseudorandom direction vector
Vec2 RandomGradient(int ix, int iy) {
    // No precomputed gradients mean this works for any number of grid coordinates
    const unsigned w{ 8 * sizeof(unsigned) };
    const unsigned s{ w / 3 }; // rotation width
    unsigned a{ (unsigned) ix }; 
    unsigned b{ (unsigned) iy };
    a *= 3284157443; b ^= a << s | a >> (w-s);
    b *= 1911520717; a ^= b << s | b >> (w-s);
    a *= 2048419325;
    float random{ (float)(a * (3.14159265 / ~(~0u >> 1))) }; // in [0, 2*Pi]
    Vec2 v{ cos(random), sin(random) };
    return v;
}

// Computes the dot product of the distance and gradient vectors.
float DotGridGradient(int ix, int iy, float x, float y) {
    // Get gradient from integer coordinates
    Vec2 gradient{ RandomGradient(ix, iy) };

    // Compute the distance vector
    float dx{ x - (float)ix };
    float dy{ y - (float)iy };

    // Compute the dot-product
    return (dx*gradient.x + dy*gradient.y);
}

// Compute Perlin noise at coordinates x, y
float Perlin(float x, float y) {
    // Determine grid cell coordinates
    int x0{ (int)floor(x) };
    int x1{ x0 + 1 };
    int y0{ (int)floor(y) };
    int y1{ y0 + 1 };

    // Determine interpolation weights
    // Could also use higher order polynomial/s-curve here
    float sx{ x - (float)x0 };
    float sy{ y - (float)y0 };

    // Interpolate between grid point gradients
    float n0{ DotGridGradient(x0, y0, x, y) };
    float n1{ DotGridGradient(x1, y0, x, y) };
    float ix0{ Interpolate(n0, n1, sx) };

    n0 = DotGridGradient(x0, y1, x, y);
    n1 = DotGridGradient(x1, y1, x, y);
    float ix1{ Interpolate(n0, n1, sx) };

    return Interpolate(ix0, ix1, sy); // Will return in range -1 to 1.
}

Uint32 GetColor(float p) {
    auto fmt{ surface_->format };

    // setup five colors for gradient
    constexpr float t0{ 0.0f };
    constexpr float t1{ 0.4f };
    constexpr float t2{ 0.5f };
    constexpr float t3{ 0.6f };
    constexpr float t4{ 1.0f };

    Uint32 c0{ SDL_MapRGB(fmt, 0, 0, 0) };
    Uint32 c1{ SDL_MapRGB(fmt, 0, 0, 130) };
    Uint32 c2{ SDL_MapRGB(fmt, 130, 0, 0) };
    Uint32 c3{ SDL_MapRGB(fmt, 0, 0, 0) };
    Uint32 c4{ SDL_MapRGB(fmt, 255, 255, 255) };

    auto scale = [p](float prev_t, float curr_t) { 
        return (p - prev_t) * (1.0f / (curr_t - prev_t)); 
    };

    if (p <= t1) {
        return InterpolateColor(fmt, c0,  c1, scale(t0, t1));
    }
    else if (p <= t2) {
        return InterpolateColor(fmt, c1,  c2, scale(t1, t2));
    }
    else if (p <= t3) {
        return InterpolateColor(fmt, c2, c3, scale(t2, t3));
    }
    else {
        return InterpolateColor(fmt, c3, c4, scale(t3, t4));
    }
}

void Step(void* arg) {
    SDL_Surface* screen{ static_cast<SDL_Surface*>(arg) };

    static float angle{ 0.0f };
    angle += 0.005f;

    float x_offset_start{ 13.0f * cos(angle) };
    float y_offset_start{ 13.0f * sin(angle) };
    
    static float zoom{ 0.0f };
    zoom += 0.01f;

    float inc{ 0.04f + 0.05f * (sin(zoom) * 0.5f + 0.5f) };
    float y_offset{ y_offset_start };

    // compute and draw perlin for every pixel
    for (int y{ 0 }; y < world_height_; ++y) {
        float x_offset{ x_offset_start };
        for (int x{ 0 }; x < world_width_; ++x) {
            float p{ Perlin(x_offset, y_offset) * 0.5f + 0.5f };
            pixels_[y * world_width_ + x] = GetColor(p);
            x_offset += inc;
        }
        y_offset += inc;
    }

    if (SDL_MUSTLOCK(surface_)) SDL_LockSurface(surface_);
    memcpy(surface_->pixels, pixels_, sizeof(Uint32) * world_width_ * world_height_);
    if (SDL_MUSTLOCK(surface_)) SDL_UnlockSurface(surface_);

    SDL_Rect dst{ 0, 0, screen_width_, screen_height_ };
    SDL_BlitScaled(surface_, nullptr, screen, &dst);
    SDL_Flip(screen);
}

extern "C" int main(int argc, char** argv) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Surface* screen{ SDL_SetVideoMode(screen_width_, screen_height_, 32, SDL_SWSURFACE) };
    auto fmt{ screen->format };
    surface_ = SDL_CreateRGBSurface(SDL_SWSURFACE, world_width_, world_height_, 
        fmt->BitsPerPixel, fmt->Rmask, fmt->Gmask, fmt->Bmask, fmt->Amask);
    emscripten_set_main_loop_arg(Step, screen, 0, 1);
    SDL_Quit();
    return 0;
}
