/*
++++ fdrawterm ++++

reinhard@finalmedia.de

fdraw fuer direkte Terminalausgabe via ANSI Escape Sequenzen
Public Domain

Liest Befehle ueber stdin ein (c, m, r, p, s, o) und zeichnet in ein Terminal.
Nutzt Double Buffering und das Unicode-Zeichen '▄' (Oberer/Unterer Pixel kombiniert).

Compilieren mittels:
	musl-gcc -O2 -static-pie -fPIE -fstack-protector-strong \
	-D_FORTIFY_SOURCE=2 -Wl,-z,relro -Wl,-z,now \
	fdrawterm.c -o fdrawterm -lpthread
*/

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

typedef struct {
    uint8_t r, g, b;
} Pixel;

// Globale Variablen fuer Thread-Zugriff
Pixel *draw_buffer = NULL;
Pixel *send_buffer = NULL;
int width = 80;
int height = 50;
useconds_t frame_delay_us = 33333; // Standard fuer 30 FPS

pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;

// Hilfsfunktion zum sauberen Loeschen des Terminals und Cursor verstecken
void init_terminal() {
    printf("\033[?25l"); // Cursor verstecken
    printf("\033[2J");   // Bildschirm loeschen
    fflush(stdout);
}

void reset_terminal() {
    printf("\033[?25h\033[0m\n"); // Cursor wieder anzeigen, Farben zuruecksetzen
    fflush(stdout);
}

// Thread fuer die konstante FPS Ausgabe ins Terminal via ANSI
void* video_output_thread(void* arg) {
    (void)arg;

    // Lokaler temporaerer String-Buffer fuer schnelles Schreiben (I/O-Durchsatz minimieren)
    // Jedes Zeichenpaar (oben/unten) braucht max ~40 Bytes fuer Truecolor-ANSI
    size_t chunk_size = (size_t)width * (height / 2) * 50 + 64;
    char *output_chunk = malloc(chunk_size);
    if (!output_chunk) return NULL;

    while (1) {
        // Sicheres Kopieren des Zeichenpuffers (Double Buffering)
        pthread_mutex_lock(&buffer_mutex);
        memcpy(send_buffer, draw_buffer, (size_t)width * height * sizeof(Pixel));
        pthread_mutex_unlock(&buffer_mutex);

        char *ptr = output_chunk;

        // Cursor nach oben links setzen (Home ohne Clear verhindert Flackern)
        ptr += sprintf(ptr, "\033[H");

        // Zeichne das Terminal zeilenweise (Schritte von 2 Pixeln vertikal)
        for (int y = 0; y < height - 1; y += 2) {
            Pixel last_top = {0, 0, 0};
            Pixel last_bot = {0, 0, 0};
            int first_pixel = 1;

            for (int x = 0; x < width; x++) {
                Pixel top = send_buffer[y * width + x];
                Pixel bot = send_buffer[(y + 1) * width + x];

                // Nur ANSI-Codes ausgeben, wenn sich die Farben zum vorherigen Pixel geaendert haben (Kompression)
                if (first_pixel || top.r != last_top.r || top.g != last_top.g || top.b != last_top.b ||
                    bot.r != last_bot.r || bot.g != last_bot.g || bot.b != last_bot.b) {

                    // \033[38;2;R;G;Bm = Vordergrund (unten) | \033[48;2;R;G;Bm = Hintergrund (oben)
                    ptr += sprintf(ptr, "\033[48;2;%d;%d;%dm\033[38;2;%d;%d;%dm",
                                   top.r, top.g, top.b, bot.r, bot.g, bot.b);

                    last_top = top;
                    last_bot = bot;
                    first_pixel = 0;
                }
                // Unicode Halbblock (unten ausgefuellt, oben Hintergrund)
                strcpy(ptr, "▄");
                ptr += 3; // UTF-8 fuer ▄ belegt 3 Bytes
            }
            // Zeilenumbruch und Reset fuer die Terminalkante
            ptr += sprintf(ptr, "\033[0m\n");
        }

        // Gesamten Frame auf einmal rausschreiben
        size_t to_write = (size_t)(ptr - output_chunk);
        size_t written = fwrite(output_chunk, 1, to_write, stdout);
        if (written < to_write) {
            break;
        }
        fflush(stdout);

        usleep(frame_delay_us);
    }

    free(output_chunk);
    return NULL;
}

int main() {
    // Standard-Umgebungsvariablen auslesen oder Fallbacks setzen
    // Terminals brauchen kleinere Aufloesungen als Roh-Video streams!
    if (getenv("SCREEN_WIDTH")) width = atoi(getenv("SCREEN_WIDTH"));
    if (getenv("SCREEN_HEIGHT")) height = atoi(getenv("SCREEN_HEIGHT"));

    // Da wir 2 Pixel vertikal in ein Zeichen packen, muss die Hoehe gerade sein
    if (height % 2 != 0) height++;

    int fps = 30;
    if (getenv("SCREEN_FPS")) {
        fps = atoi(getenv("SCREEN_FPS"));
        if (fps < 1) fps = 1;
        if (fps > 300) fps = 300;
    }

    frame_delay_us = (useconds_t)(1000000 / fps);

    // Speicher reservieren
    draw_buffer = calloc((size_t)width * height, sizeof(Pixel));
    send_buffer = malloc((size_t)width * height * sizeof(Pixel));

    if (!draw_buffer || !send_buffer) {
        fprintf(stderr, "Fehler: Speicher konnte nicht alloziert werden.\n");
        return EXIT_FAILURE;
    }

    init_terminal();
    atexit(reset_terminal);

    // Video-Ausgabe-Thread starten
    pthread_t thread_id;
    if (pthread_create(&thread_id, NULL, video_output_thread, NULL) != 0) {
        fprintf(stderr, "Fehler beim Erstellen des ANSI-Threads.\n");
        free(draw_buffer);
        free(send_buffer);
        return EXIT_FAILURE;
    }

    char cmd[66];
    int arg1, arg2, arg3, arg4, arg5;
    int x = 0, y = 0;
    int ox = 0, oy = 0;
    uint8_t r = 255, g = 255, b = 255;

    // Hauptschleife verarbeitet Befehle von stdin
    while (fgets(cmd, sizeof(cmd), stdin) != NULL) {

	// Spiegeln von stdin auf stdout deaktiviert
        // fputs(cmd, stderr);

        if (sscanf(cmd, "s %d", &arg1) == 1) {
            usleep(arg1);
        }
        else if (sscanf(cmd, "c %d %d %d", &arg1, &arg2, &arg3) == 3) {
            r = (uint8_t)arg1; g = (uint8_t)arg2; b = (uint8_t)arg3;
        }
        else if (sscanf(cmd, "m %d %d", &arg1, &arg2) == 2) {
            x = arg1; y = arg2;
        }
        else if (sscanf(cmd, "o %d %d", &arg1, &arg2) == 2) {
            ox = arg1; oy = arg2;
        }
        else if (sscanf(cmd, "p %d %d %d %d %d", &arg1, &arg2, &arg3, &arg4, &arg5) == 5) {
            x = arg1; y = arg2;
            r = (uint8_t)arg3; g = (uint8_t)arg4; b = (uint8_t)arg5;
            int mx = ox + x;
            int my = oy + y;
            if (mx >= 0 && mx < width && my >= 0 && my < height) {
                pthread_mutex_lock(&buffer_mutex);
                draw_buffer[my * width + mx] = (Pixel){r, g, b};
                pthread_mutex_unlock(&buffer_mutex);
            }
        }
        else if (sscanf(cmd, "r %d %d", &arg1, &arg2) == 2) {
            pthread_mutex_lock(&buffer_mutex);
            for (int dx = 0; dx < arg1; dx++) {
                for (int dy = 0; dy < arg2; dy++) {
                    int mx = ox + x + dx;
                    int my = oy + y + dy;
                    if (mx >= 0 && mx < width && my >= 0 && my < height) {
                       draw_buffer[my * width + mx] = (Pixel){r, g, b};
                    }
                }
            }
            pthread_mutex_unlock(&buffer_mutex);
        }
    }

    pthread_cancel(thread_id);
    pthread_join(thread_id, NULL);
    free(draw_buffer);
    free(send_buffer);
    return EXIT_SUCCESS;
}


