#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>

#include "stb_image.h"


#if 0
static void PrintLastError() {
    DWORD errorCode = GetLastError(); // Get the last error code
    if (errorCode == 0) {
        printf("No error occurred.\n");
        return;
    }

    LPVOID lpMsgBuf;
    DWORD bufLen;

    // Format the message
    bufLen = FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        errorCode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPSTR)&lpMsgBuf,
        0, NULL);

    // Print the error message
    if (bufLen) {
        printf("Error %d: %s", errorCode, (LPSTR)lpMsgBuf);
        LocalFree(lpMsgBuf); // Free the buffer allocated by FormatMessage
    } else {
        printf("Failed to format message for error %d.\n", errorCode);
    }
}
#endif


int main()
{
    // Get console handle
    HANDLE cmd = GetStdHandle(STD_OUTPUT_HANDLE);

    // Enable console colors
    DWORD cmdMode;
    GetConsoleMode(cmd, &cmdMode);
    cmdMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
    SetConsoleMode(cmd, cmdMode);

    // Get the current module handle
    HMODULE thisExe = GetModuleHandle(NULL);

    // Find resource
    HRSRC resHandle = FindResourceA(thisExe, "VIDEO", "GIF");

    // Get video resource
    HGLOBAL resDataHandle = LoadResource(thisExe, resHandle);

    // Get resource size
    DWORD resSize = SizeofResource(thisExe, resHandle);

    // Load resource data
    void* resData = LockResource(resDataHandle);

    // Load GIF
    int *gifDelays, gifWidth, gifHeight, gifLength, gifComp;
    uint8_t* gif = stbi_load_gif_from_memory(resData, resSize, &gifDelays, &gifWidth, &gifHeight, &gifLength, &gifComp, 3);
    if (!gif)
        puts(stbi_failure_reason());

    // Unload resource data
    UnlockResource(resDataHandle);

    // Prepare data
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    int start_time;
    int end_time;
    int delta;
    int gifTimer;
    int gifPos;
    int stride;
    int cmdWidth, cmdHeight;
    int oldCmdWidth = 0;
    int oldCmdHeight = 0;
    bool full_render = true;
    bool skipped;
    int lastx, lasty;
    uint8_t* frame;
    uint8_t* displayBuf = NULL;  // Working frame data
    uint8_t* screenBuf  = NULL;  // Present frame data
    char*    renderBuf  = NULL;  // Render  frame data
    char* renderBufCur;
    const float fGifWidth  = (float)gifWidth;
    const float fGifHeight = (float)gifHeight;

    // display and screen buffers are oversized, to avoid resizing at runtime.
    // Screen resolution SHOULD be enough...
    // x16 for good mesure :D

    // Rewind gif and play music
main_start:
    gifTimer = 0;
    gifPos = 0;
    PlaySound(TEXT("AUDIO"), thisExe, SND_RESOURCE|SND_LOOP|SND_ASYNC);

main_loop:
    // Prepare delta
    start_time = GetTickCount();

    // Get Console Size
    GetConsoleScreenBufferInfo(cmd, &csbi);
    cmdWidth = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    cmdHeight = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
    cmdHeight *= 2;
    stride = cmdWidth * 3;

    // If resized, reallocate buffers and do a full render
    if (cmdWidth != oldCmdWidth || cmdHeight != oldCmdHeight)
    {
        displayBuf = realloc(displayBuf, cmdWidth * cmdHeight * 3);
        screenBuf  = realloc(screenBuf,  cmdWidth * cmdHeight * 3);
        renderBuf  = realloc(renderBuf,  cmdWidth * cmdHeight * 40 + 1);  // 40 is the length of escape
                                                                          // code, +1 for null terminator
        full_render = true;
    }
    oldCmdWidth = cmdWidth;
    oldCmdHeight = cmdHeight;

    // Select frame
    frame = gif + (gifWidth * gifHeight * 3 * gifPos);

    // Project frame to display buffer
    int by;
    for (by = 0; by < cmdHeight; by++)
    {
        int bx;
        for (bx = 0; bx < cmdWidth; bx++)
        {
            int gx = (int)(fGifWidth / cmdWidth * bx + 0.5f);
            int gy = (int)(fGifHeight / cmdHeight * by + 0.5f);
            int displayOffset = (by * cmdWidth + bx) * 3;
            int gifOffset = (gy * gifWidth + gx) * 3;
            displayBuf[displayOffset + 0] = frame[gifOffset + 0];
            displayBuf[displayOffset + 1] = frame[gifOffset + 1];
            displayBuf[displayOffset + 2] = frame[gifOffset + 2];
        }
    }

    // Render to console
    skipped = true;
    renderBufCur = renderBuf;
    lastx = -1;
    lasty = -1;
    for (int by = 0; by < cmdHeight; by+=2)
    for (int bx = 0; bx < cmdWidth; bx++)
    {
        // Check if pixel needs rendering
        bool render_needed = false;

        int offset = (by * cmdWidth + bx) * 3;
        uint8_t dRed      = displayBuf[offset + 0];
        uint8_t dGreen    = displayBuf[offset + 1];
        uint8_t dBlue     = displayBuf[offset + 2];
        uint8_t sRed      = screenBuf[offset + 0];
        uint8_t sGreen    = screenBuf[offset + 1];
        uint8_t sBlue     = screenBuf[offset + 2];
        uint8_t dRedNxt   = displayBuf[offset + stride + 0];
        uint8_t dGreenNxt = displayBuf[offset + stride + 1];
        uint8_t dBlueNxt  = displayBuf[offset + stride + 2];
        uint8_t sRedNxt   = screenBuf[offset + stride + 0];
        uint8_t sGreenNxt = screenBuf[offset + stride + 1];
        uint8_t sBlueNxt  = screenBuf[offset + stride + 2];

        if (
            full_render
            || dRed != sRed || dGreen != sGreen || dBlue != sBlue
            || dRedNxt != sRedNxt || dGreenNxt != sGreenNxt || dBlueNxt != sBlueNxt
        )
            render_needed = true;

        // If render not needed, skip
        if (!render_needed)
            continue;

        // Move cursor to correct pos
        if (bx != lastx+1 || by != lasty)
            renderBufCur += sprintf(renderBufCur, "\x1B[%d;%dH", by>>1, bx+1);

        // Render pixel
        renderBufCur += sprintf(
            renderBufCur,
            "\x1B[38;2;%d;%d;%dm\x1B[48;2;%d;%d;%dm\xDF",
            dRed,
            dGreen,
            dBlue,
            dRedNxt,
            dGreenNxt,
            dBlueNxt
        );
        lastx = bx;
        lasty = by;
    }
    *renderBufCur = '\0';
    full_render = false;

    // Swap buffers
    void* tempBuf = displayBuf;
    displayBuf = screenBuf;
    screenBuf = tempBuf;

    // Display rendered frame
    printf("%s", renderBuf);

    // Compute delta
    end_time = GetTickCount();
    delta = end_time - start_time;

    // Sleep
    int sleep_time = gifDelays[gifPos] - delta;
    if (sleep_time > 0)
        Sleep(sleep_time);

    // Increment GIF timer
    gifTimer += delta;
    if (sleep_time > 0)
        gifTimer += sleep_time;

    // Switch frame
    for (int total = 0, n = 0; total < gifTimer; n++)
    {
        gifPos = n+1;
        total += gifDelays[n];
    }
    if (gifPos >= gifLength)
        goto main_start;

    // Repeat
    goto main_loop;
}