// Published by devloop.org under MIT License, see: https://www.devloop.org/MIT-license.txt

#include <SDL/SDL.h>
#include <emscripten.h>
#include <emscripten/html5.h>
#include <cstring>
#include <math.h>
#include <random>

constexpr int SCREEN_WIDTH{ 800 };
constexpr int SCREEN_HEIGHT{ 600 };
constexpr int MAX_ITERATIONS{ 1000 };

SDL_Surface* screen{ nullptr };
Uint32* pixels{ nullptr };
double* iterations{ nullptr };
Uint32* palette{ nullptr };

struct ViewWindow {
    double x{ -2.5f };
    double y{ -1.0f };
    double w{ 3.5f };
    double h{ 2.0f };
} view_window;

// https://en.wikipedia.org/wiki/Linear_interpolation
float Lerp(float v0, float v1, float t) {
    return (1.0f - t) * v0 + t * v1;
}

Uint32 LerpColor(Uint32 color1, Uint32 color2, float t) {
    Uint8 r1{ 0 };
    Uint8 g1{ 0 };
    Uint8 b1{ 0 };
    SDL_GetRGB(color1, screen->format, &r1, &g1, &b1);
    Uint8 r2{ 0 };
    Uint8 g2{ 0 };
    Uint8 b2{ 0 };
    SDL_GetRGB(color2, screen->format, &r2, &g2, &b2);
    return SDL_MapRGB(screen->format, Lerp(r1, r2, t), Lerp(g1, g2, t), Lerp(b1, b2, t));
}

Uint32 GetColor(Uint32* pallete, double iteration) {
    auto iter{ (int)floor(iteration) };
    auto color1{ pallete[iter] };
    auto color2{ pallete[iter + 1] };
    auto iter_frac{ iteration - (double)iter };
    return LerpColor(color1, color2, iter_frac);
}

void RandomizePalette() {
    for (int i{ 0 }; i < MAX_ITERATIONS; ++i) {
        palette[i] = SDL_MapRGB(screen->format, rand() % 255, rand() % 255, rand() % 255);
    }
    palette[MAX_ITERATIONS] = SDL_MapRGB(screen->format, 0, 0, 0); // bg color
}

void RenderMandelbrot() {
    static int count{ 0 };
    if ((++count) % 60 == 0) {
        RandomizePalette();
    }
    for (int i{ 0 }; i < SCREEN_WIDTH * SCREEN_HEIGHT; ++i) {
        auto target_color{ GetColor(palette, iterations[i]) };
        pixels[i] = LerpColor(pixels[i], target_color, 0.1f);
    }
    SDL_LockSurface(screen);
    memcpy(screen->pixels, pixels, SCREEN_WIDTH * SCREEN_HEIGHT * sizeof(Uint32));
    SDL_UnlockSurface(screen);
    SDL_Flip(screen);
}

// https://en.wikipedia.org/wiki/Plotting_algorithms_for_the_Mandelbrot_set
void ComputeMandelbrot() {
    for (int py{ 0 }; py < SCREEN_HEIGHT; ++py) {
        for (int px{ 0 }; px < SCREEN_WIDTH; ++px) {
            double x0{ (double)px / SCREEN_WIDTH * view_window.w + view_window.x };
            double y0{ (double)py / SCREEN_HEIGHT * view_window.h + view_window.y };
            double x{ 0.0f };
            double y{ 0.0f };
            double iteration{ 0.0f };
            while (x*x + y*y <= (1 << 16) && iteration < MAX_ITERATIONS) {
                double xtemp{ x*x - y*y + x0 };
                y = 2*x*y + y0;
                x = xtemp;
                ++iteration;
            }
            if (iteration < MAX_ITERATIONS) {
                double log_zn{ log(x*x + y*y) / 2 };
                double nu{ log(log_zn / log(2)) / log(2) };
                iteration = iteration + 1 - nu;
            }
            iterations[py * SCREEN_WIDTH + px] = iteration;
        }
    }
}

void Zoom(long x, long y) {
    view_window.x += view_window.w * 0.1f;
    view_window.y += view_window.h * 0.1f;
    view_window.w *= 0.8f;
    view_window.h *= 0.8f;
    ComputeMandelbrot();
}

EM_BOOL OnTouchStart(int eventType, const EmscriptenTouchEvent *touchEvent, void *userData) {
    const auto* touch_point{ touchEvent->touches };
    Zoom(touch_point->targetX, touch_point->targetY);
    return true;
}

EM_BOOL OnMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) {
    if (mouseEvent->button == 0) {
        Zoom(mouseEvent->targetX, mouseEvent->targetY);
    }
    else if (mouseEvent->button == 2) {
        view_window = {}; // reset zoom
        ComputeMandelbrot();
    }
    return true;
}

int main(int argc, char* argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    screen = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32, SDL_SWSURFACE);
    pixels = new Uint32[SCREEN_WIDTH * SCREEN_HEIGHT];
    iterations = new double[SCREEN_WIDTH * SCREEN_HEIGHT];
    palette = new Uint32[MAX_ITERATIONS + 1];
    emscripten_set_touchstart_callback("canvas", nullptr, true, OnTouchStart);
    emscripten_set_mousedown_callback("canvas", nullptr, true, OnMouseDown);
    RandomizePalette();
    ComputeMandelbrot();
    emscripten_set_main_loop(RenderMandelbrot, 0, 1);
    SDL_Quit();
    return 0;
}
