//  File:    plot.cpp
//  Author:  Andreas Wittmann <andreas.wittmann@wittnet.at>

//  Comment: SDL_VIDEODRIVER=dummy PLOT_HEADLESS=video/image/single-image PLOT_FRAMES=360 ./anim

#include <sys/stat.h>
#include <sys/types.h>

#ifndef __linux
#include <windows.h>
#endif

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>

#include <algorithm>

#include "plot.hpp"

SDL_Window* g_window;
SDL_Renderer* g_renderer;

#define FONT_INCLUDE "vera_ttf.c"
#define FONT      vera_ttf
#define FONT_SIZE vera_ttf_size
extern unsigned char FONT[];
extern unsigned int FONT_SIZE;

// Achtung: Die Schriften werden ganz unten im Source direkt eingebunden!
TTF_Font* font = NULL;

SDL_Texture* g_textures[16];

int plot_x_ofs = 0;
int plot_y_ofs = 0;
int plot_width = 0;
int plot_height = 0;
int plot_mode = 0;

int g_running = 1;
bool g_headless = false;
int g_headless_mode = 0;
uint64_t g_current_frame = 1;
int g_max_frames = 360;           // Perfekter Default-Wert!

SDL_Color g_draw_color = {0, 0, 0, 255};

// Funktioniert so nur unter Linux (Sollte aber auch mit MinGW klappen)
void setup() __attribute__((weak));
void frame() __attribute__((weak));
void cleanup() __attribute__((weak));
bool event(SDL_Event* e) __attribute__((weak));

int PlotToScreenX(double x)
{
    if (plot_mode == PLOT_MODE_CROSS) {
        return (int)(plot_x_ofs + x);
    }

    return (int)x;
}

int PlotToScreenY(double y)
{
    if (plot_mode == PLOT_MODE_CROSS) {
        return (int)(plot_y_ofs - y);
    }

    if (plot_mode == PLOT_MODE_MATH) {
        return (int)(plot_height - y);
    }

    return (int)y;
}

void PlotCross()
{
    PlotPoint a;
    PlotPoint b;
    a.x = -(plot_width / 2);
    a.y = 0;
    b.x = plot_width / 2;
    b.y = 0;
    SDL_SetRenderDrawColor(g_renderer, 200, 200, 200, 255);
    PlotLine(a, b);
    a.x = 0;
    a.y = -(plot_height / 2);
    b.x = 0;
    b.y = plot_height / 2;
    PlotLine(a, b);
    // Farbe wieder auf ursprünglichen Wert setzen.
    SDL_SetRenderDrawColor(g_renderer, g_draw_color.r, g_draw_color.g, g_draw_color.b, g_draw_color.a);
}

void PlotColor(Uint8 r, Uint8 g, Uint8 b)
{
    SDL_SetRenderDrawColor(g_renderer, r, g, b, 255);
    g_draw_color = {r, g, b, 255};
}

void PlotColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a)
{
    SDL_SetRenderDrawColor(g_renderer, r, g, b, a);
    g_draw_color = {r, g, b, a};
}

void PlotPixel(double x, double y, double cr, double cg, double cb)
{
    printf("TODO: Obsolete function. Don't use colors in parameter!\n");
    SDL_SetRenderDrawColor(g_renderer, cr * 255, cg * 255, cb * 255, 255);
    SDL_RenderDrawPoint(g_renderer, PlotToScreenX(x), PlotToScreenY(y));
}

void PlotPixel(double x, double y)
{
    SDL_RenderDrawPoint(g_renderer, PlotToScreenX(x), PlotToScreenY(y));
}

void PlotPixel(PlotPoint a)
{
    SDL_RenderDrawPoint(g_renderer, PlotToScreenX(a.x), PlotToScreenY(a.y));
}

void PlotRectangle(PlotPoint a, PlotPoint b)
{
    int sx0 = PlotToScreenX(a.x);
    int sy0 = PlotToScreenY(a.y);
    int sx1 = PlotToScreenX(b.x);
    int sy1 = PlotToScreenY(b.y);

    int x0 = std::min(sx0, sx1);
    int y0 = std::min(sy0, sy1);
    int x1 = std::max(sx0, sx1);
    int y1 = std::max(sy0, sy1);

    SDL_Rect rect{ x0, y0, (x1 - x0) + 1, (y1 - y0) + 1 };
    SDL_RenderDrawRect(g_renderer, &rect);
}

void PlotRectangle(double x1, double y1, double x2, double y2)
{
    int sx0 = PlotToScreenX(x1);
    int sy0 = PlotToScreenY(y1);
    int sx1 = PlotToScreenX(x2);
    int sy1 = PlotToScreenY(y2);

    int tx0 = std::min(sx0, sx1);
    int ty0 = std::min(sy0, sy1);
    int tx1 = std::max(sx0, sx1);
    int ty1 = std::max(sy0, sy1);

    SDL_Rect rect{ tx0, ty0, (tx1 - tx0) + 1, (ty1 - ty0) + 1 };
    SDL_RenderDrawRect(g_renderer, &rect);
}

void PlotFilledRectangle(PlotPoint a, PlotPoint b)
{
    int sx0 = PlotToScreenX(a.x);
    int sy0 = PlotToScreenY(a.y);
    int sx1 = PlotToScreenX(b.x);
    int sy1 = PlotToScreenY(b.y);

    int x0 = std::min(sx0, sx1);
    int y0 = std::min(sy0, sy1);
    int x1 = std::max(sx0, sx1);
    int y1 = std::max(sy0, sy1);

    SDL_Rect rect{ x0, y0, (x1 - x0) + 1, (y1 - y0) + 1 };
    SDL_RenderFillRect(g_renderer, &rect);
}

void PlotFilledRectangle(double x1, double y1, double x2, double y2)
{
    int sx0 = PlotToScreenX(x1);
    int sy0 = PlotToScreenY(y1);
    int sx1 = PlotToScreenX(x2);
    int sy1 = PlotToScreenY(y2);

    int tx0 = std::min(sx0, sx1);
    int ty0 = std::min(sy0, sy1);
    int tx1 = std::max(sx0, sx1);
    int ty1 = std::max(sy0, sy1);

    SDL_Rect rect{ tx0, ty0, (tx1 - tx0) + 1, (ty1 - ty0) + 1 };
    SDL_RenderFillRect(g_renderer, &rect);
}

// Diese Funktion ist extrem "teuer" da sie nichts cached und Speicher laufend
// alloziert und wieder freigibt. Für einfache Demos genügt es aber vorerst!
void PlotText(double x, double y, const char* format, ...)
{
    char text[1024];

    va_list args;
    va_start(args, format);
    vsnprintf(text, sizeof(text), format, args);
    va_end(args);

    SDL_Surface* surf = TTF_RenderUTF8_Blended(font, text, g_draw_color);
    if (!surf) {
        return;
    }

    SDL_Texture* tex = SDL_CreateTextureFromSurface(g_renderer, surf);
    SDL_FreeSurface(surf);

    if (!tex) {
        return;
    }

    int w, h;
    SDL_QueryTexture(tex, nullptr, nullptr, &w, &h);

    SDL_Rect dst = { PlotToScreenX(x), PlotToScreenY(y), w, h };
    SDL_RenderCopy(g_renderer, tex, nullptr, &dst);

    SDL_DestroyTexture(tex);
}

void PlotLine(PlotPoint a, PlotPoint b)
{
    SDL_RenderDrawLine(g_renderer,
                       PlotToScreenX(a.x),
                       PlotToScreenY(a.y),
                       PlotToScreenX(b.x),
                       PlotToScreenY(b.y));
}
                  
void PlotLine(double x1, double y1, double x2, double y2)
{
    SDL_RenderDrawLine(g_renderer,
                       PlotToScreenX(x1),
                       PlotToScreenY(y1),
                       PlotToScreenX(x2),
                       PlotToScreenY(y2));
}

void PlotTriangle(PlotPoint a, PlotPoint b, PlotPoint c)
{
    SDL_Vertex vertices[3];
    vertices[0].position = {PlotToScreenX(a.x), PlotToScreenY(a.y)};
    vertices[0].tex_coord = {0.0f, 0.0f};
    vertices[0].color     = g_draw_color;
    vertices[1].position = {PlotToScreenX(b.x), PlotToScreenY(b.y)};
    vertices[1].tex_coord = {1.0f, 0.0f};
    vertices[1].color     = g_draw_color;
    vertices[2].position = {PlotToScreenX(c.x), PlotToScreenY(c.y)};
    vertices[2].tex_coord = {0.0f, 1.0f};
    vertices[2].color     = g_draw_color;
    SDL_RenderGeometry(g_renderer, nullptr, vertices, 3, nullptr, 0);
}

void PlotTriangle(PlotPoint a, PlotPoint b, PlotPoint c, SDL_Texture* texture)
{
    SDL_Vertex vertices[3];
    vertices[0].position = {PlotToScreenX(a.x), PlotToScreenY(a.y)};
    vertices[0].tex_coord = {0.0f, 0.0f};
    vertices[0].color     = {255, 255, 255, 255};
    vertices[1].position = {PlotToScreenX(b.x), PlotToScreenY(b.y)};
    vertices[1].tex_coord = {1.0f, 0.0f};
    vertices[1].color     = {255, 255, 255, 255};
    vertices[2].position = {PlotToScreenX(c.x), PlotToScreenY(c.y)};
    vertices[2].tex_coord = {0.0f, 1.0f};
    vertices[2].color     = {255, 255, 255, 255};
    SDL_RenderGeometry(g_renderer, texture, vertices, 3, nullptr, 0);
}

void PlotCanvas(int w, int h, int mode)
{
    SDL_CreateWindowAndRenderer(w, h, 0, &g_window, &g_renderer);

    plot_mode = mode;

    if (mode == PLOT_MODE_CROSS) {
        plot_x_ofs = w / 2;
        plot_y_ofs = h / 2;
    } else {
        plot_x_ofs = 0;
        plot_y_ofs = 0;
    }

    plot_width = w;
    plot_height = h;
}

void PlotMode(int mode)
{
    plot_mode = mode;
    if (mode == PLOT_MODE_CROSS) {
        plot_x_ofs = plot_width / 2;
        plot_y_ofs = plot_height / 2;
    } else {
        plot_x_ofs = 0;
        plot_y_ofs = 0;
    }
}

void PlotBackground(Uint8 r, Uint8 g, Uint8 b)
{
    SDL_SetRenderDrawColor(g_renderer, r, g, b, 255);
    SDL_RenderClear(g_renderer);
    // Farbe wieder auf ursprünglichen Wert setzen.
    SDL_SetRenderDrawColor(g_renderer, g_draw_color.r, g_draw_color.g, g_draw_color.b, g_draw_color.a);
}

static Uint32 g_frame_duration_ms = 1000 / 60; // default 60 FPS

void PlotSetFrameDurationMS(Uint32 ms)
{
    if (ms == 0) {
        // 0 ms ist sinnlos, also fallback auf 60 FPS
        g_frame_duration_ms = 1000 / 60;
    } else {
        g_frame_duration_ms = ms;
    }
}

void PlotSetFPS(int fps)
{
    if (fps <= 0) {
        // Schutz: fallback auf 60 FPS
        g_frame_duration_ms = 1000 / 60;
    } else {
        g_frame_duration_ms = 1000u / (Uint32)fps;
    }
}

void PlotSetFrameLimit(int frames)
{
    g_max_frames = frames;
}

static int PlotReadEnvInt(const char* name, int defaultValue)
{
    const char* value = getenv(name);

    if (!value || !*value) {
        return defaultValue;
    }

    return atoi(value);
}

static void PlotReadEnvironment()
{
    const char* mode = getenv("PLOT_HEADLESS");

    if (!mode) {
        return;
    }

    g_headless = true;

    if (strcmp(mode, "image") == 0) {
        g_headless_mode = PLOT_HEADLESS_MODE_IMAGE;
    } else if (strcmp(mode, "video") == 0) {
        g_headless_mode = PLOT_HEADLESS_MODE_VIDEO;
    } else if (strcmp(mode, "single-image") == 0) {
        g_headless_mode = PLOT_HEADLESS_MODE_SINGLE_IMAGE;
    } else {
        g_headless_mode = PLOT_HEADLESS_MODE_VIDEO;
    }

    g_max_frames = PlotReadEnvInt("PLOT_FRAMES", 360);
}

#ifndef __linux
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
#else
int main(int argc, char* argv[])
#endif
{
    SDL_Init(SDL_INIT_VIDEO);
    PlotReadEnvironment();
    printf("Plot Video driver: %s\n", SDL_GetCurrentVideoDriver());

    // Mit dummy kann man die grafische Ausgabe verhindern. offscreen ist
    // gesetzt wenn DISPLAY nicht gesetzt wurde.
    if ((strcmp(SDL_GetCurrentVideoDriver(), "dummy") == 0) || (strcmp(SDL_GetCurrentVideoDriver(), "offscreen") == 0)) {
        printf("headless mode\n");
        g_headless = true;
        if (g_headless_mode == PLOT_HEADLESS_MODE_NULL) {
            g_headless_mode = PLOT_HEADLESS_MODE_VIDEO;
        }
        if (g_headless_mode == PLOT_HEADLESS_MODE_IMAGE) {
#ifdef __linux
            mkdir("/tmp/output", 0755);
#endif
        }
    } else {
        printf("GUI mode\n");
    }

    TTF_Init();

    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");

    SDL_RWops* rw = SDL_RWFromConstMem(FONT, FONT_SIZE);
    if (!rw) {
        return 1;
    }

    // Font aus RW öffnen (2. Parameter: size, letzter: autoFreeRW=1 oder 0)
    font = TTF_OpenFontRW(rw, 1, 20); // 1 = RWops wird nachher automatisch freigegeben
    if (!font) {
        return 1;
    }

    TTF_SetFontKerning(font, 1);
    TTF_SetFontHinting(font, TTF_HINTING_NORMAL);

    PlotSetFPS(60);
    if (setup) {
        setup();
    } else {
        PlotCanvas(640, 480);
    }
    PlotColor(0, 0, 0, 255);    // Initial-Zeichenfarbe
    PlotBackground(255, 255, 255);
    SDL_RenderPresent(g_renderer);

    SDL_Event event;
    FILE* pipe = NULL;
    if (g_headless) {
        if (g_headless_mode == PLOT_HEADLESS_MODE_VIDEO) {
            pipe = popen("ffmpeg -y "
                         "-loglevel error "
                         "-f rawvideo "
                         "-pixel_format rgba "
                         "-video_size 640x480 "
                         "-framerate 25 "
                         "-i - "
                         "-pix_fmt yuv420p "
                         "-profile:v baseline "
                         "-movflags +faststart "
                         "-c:v libx264 /tmp/output.mp4",
                         "w");
        }
    }
    // Alle bis hierher angestauten Events verwerfen, bevor die eigentliche
    // Frame-Loop beginnt. Damit werden Tasten-Events welche direkt beim Starten
    // passierten verworfen (Passiert wenn per Emacs/Dired gestartet wird)
    while (SDL_PollEvent(&event)) {
        // absichtlich leer
    }
    while (g_running) {
        Uint32 frameStart = SDL_GetTicks();

        while (SDL_PollEvent(&event)) {
            if (::event) {
                if (::event(&event)) continue;
            }
            if (event.type == SDL_QUIT) {
                g_running = 0;
            }
            if (event.type == SDL_KEYDOWN) {
                SDL_Keycode key = event.key.keysym.sym;
                switch (key) {
                case SDLK_LSHIFT:
                case SDLK_RSHIFT:
                case SDLK_LCTRL:
                case SDLK_RCTRL:
                case SDLK_LALT:
                case SDLK_RALT:
                case SDLK_LGUI:
                case SDLK_RGUI:
                    break; // ignorieren
                default:
                    g_running = 0; // normale Taste
                    break;
                }
            }
        }

        frame();

        if (g_headless) {
            // Es ist wichtig das die Pixel VOR dem Aufruf von
            // SDL_RenderPresent(g_renderer) ausgelesen werden da danach die
            // Pixeldaten nicht mehr gültig sein müssen! Es können dadurch dann
            // grafische Fehler entstehen!
            SDL_Surface* surface =
                SDL_CreateRGBSurfaceWithFormat(0,
                                               plot_width,
                                               plot_height,
                                               32,
                                               SDL_PIXELFORMAT_RGBA32);
            
            SDL_RenderReadPixels(g_renderer,
                                 NULL,
                                 SDL_PIXELFORMAT_RGBA32,
                                 surface->pixels,
                                 surface->pitch);

            if (g_headless_mode == PLOT_HEADLESS_MODE_IMAGE) {
                char filename[256];

                snprintf(filename,
                         sizeof(filename),
                         "/tmp/output/frame_%04d.png",
                         g_current_frame);

                IMG_SavePNG(surface, filename);
            } else if (g_headless_mode == PLOT_HEADLESS_MODE_SINGLE_IMAGE) {
                IMG_SavePNG(surface, "/tmp/output.png");
                g_running = false;
            } else {
                fwrite(surface->pixels, surface->h * surface->pitch, 1, pipe);
            }
            SDL_FreeSurface(surface);
        }

        // Es wird auch gerendert wenn headless gesetzt ist, nur ohne FPS-Limit!
        // Soll tatsächlich keine grafische Ausgabe erfolgen so muss die
        // Umgebungsvariable SDL_VIDEODRIVER auf dummy gesetzt werden!
        SDL_RenderPresent(g_renderer);

        if (g_headless == false) {
            Uint32 frameTime = SDL_GetTicks() - frameStart;
            if (frameTime < g_frame_duration_ms) {
                SDL_Delay(g_frame_duration_ms - frameTime);
            }
        } else {
            if (g_current_frame >= g_max_frames) g_running = false;
        }
        g_current_frame++;

    }
    if (g_headless) {
        if (g_headless_mode == PLOT_HEADLESS_MODE_VIDEO) {
            pclose(pipe);
        }
    }

    if (cleanup) {
        cleanup();
    }
    
    TTF_CloseFont(font);
    SDL_DestroyRenderer(g_renderer);
    SDL_DestroyWindow(g_window);
    TTF_Quit();
    SDL_Quit();

    return 0;
}

uint32_t PlotGetTicks()
{
    return SDL_GetTicks();
}

#include FONT_INCLUDE
