#include "z64jpeg.h"

#include "stdbool.h"
#include "scheduler.h"
#include "sys_ucode.h"
#include "macros.h"

#define MARKER_ESCAPE 0x00
#define MARKER_SOI 0xD8
#define MARKER_SOF 0xC0
#define MARKER_DHT 0xC4
#define MARKER_DQT 0xDB
#define MARKER_DRI 0xDD
#define MARKER_SOS 0xDA
#define MARKER_APP0 0xE0
#define MARKER_APP1 0xE1
#define MARKER_APP2 0xE2
#define MARKER_COM 0xFE
#define MARKER_EOI 0xD9

extern u64 njpgdspMainTextStart[];
extern u64 njpgdspMainDataStart[];

/**
 * Configures and schedules a JPEG decoder task and waits for it to finish.
 */
void Jpeg_ScheduleDecoderTask(JpegContext* jpegCtx) {
    static OSTask sJpegTask = {
        M_NJPEGTASK,                         // type
        0,                                   // flags
        NULL,                                // ucode_boot
        0,                                   // ucode_boot_size
        njpgdspMainTextStart,                // ucode
        SP_UCODE_SIZE,                       // ucode_size
        njpgdspMainDataStart,                // ucode_data
        SP_UCODE_DATA_SIZE,                  // ucode_data_size
        NULL,                                // dram_stack
        0,                                   // dram_stack_size
        NULL,                                // output_buff
        NULL,                                // output_buff_size
        NULL,                                // data_ptr
        sizeof(JpegTaskData),                // data_size
        NULL,                                // yield_data_ptr
        sizeof(jpegCtx->workBuf->yieldData), // yield_data_size
    };

    JpegWork* workBuf = jpegCtx->workBuf;
    s32 pad[2];

    workBuf->taskData.address = &workBuf->data;
    workBuf->taskData.mode = jpegCtx->mode;
    workBuf->taskData.mbCount = 4;
    workBuf->taskData.qTableYPtr = &workBuf->qTableY;
    workBuf->taskData.qTableUPtr = &workBuf->qTableU;
    workBuf->taskData.qTableVPtr = &workBuf->qTableV;

    sJpegTask.t.flags = 0;
    sJpegTask.t.ucode_boot = SysUcode_GetUCodeBoot();
    sJpegTask.t.ucode_boot_size = SysUcode_GetUCodeBootSize();
    sJpegTask.t.yield_data_ptr = (u64*)&workBuf->yieldData;
    sJpegTask.t.data_ptr = (u64*)&workBuf->taskData;

    jpegCtx->scTask.next = NULL;
    jpegCtx->scTask.flags = OS_SC_NEEDS_RSP;
    jpegCtx->scTask.msgQ = &jpegCtx->mq;
    jpegCtx->scTask.msg = NULL;
    jpegCtx->scTask.framebuffer = NULL;
    jpegCtx->scTask.list = sJpegTask;

    osSendMesg(&gScheduler.cmdQueue, (OSMesg*)&jpegCtx->scTask, OS_MESG_BLOCK);
    Sched_SendNotifyMsg(&gScheduler); // osScKickEntryMsg
    osRecvMesg(&jpegCtx->mq, NULL, OS_MESG_BLOCK);
}

/**
 * Copies a 16x16 block of decoded image data to the Z-buffer.
 */
void Jpeg_CopyToZbuffer(u16* src, u16* zbuffer, s32 x, s32 y) {
    u16* dst = zbuffer + (((y * SCREEN_WIDTH) + x) * 16);
    s32 i;

    for (i = 0; i < 16; i++) {
        dst[0] = src[0];
        dst[1] = src[1];
        dst[2] = src[2];
        dst[3] = src[3];
        dst[4] = src[4];
        dst[5] = src[5];
        dst[6] = src[6];
        dst[7] = src[7];
        dst[8] = src[8];
        dst[9] = src[9];
        dst[10] = src[10];
        dst[11] = src[11];
        dst[12] = src[12];
        dst[13] = src[13];
        dst[14] = src[14];
        dst[15] = src[15];

        src += 16;
        dst += SCREEN_WIDTH;
    }
}

/**
 * Reads an u16 from a possibly unaligned address in memory.
 *
 * Replaces unaligned 16-bit reads with a pair of aligned reads, allowing for reading the possibly
 * unaligned values in JPEG header files.
 */
u16 Jpeg_GetUnalignedU16(u8* ptr) {
    if (((uintptr_t)ptr & 1) == 0) {
        // Read the value normally if it's aligned to a 16-bit address.
        return *(u16*)ptr;
    } else {
        // Read unaligned values using two separate aligned memory accesses when it's not.
        return *(u16*)(ptr - 1) << 8 | (*(u16*)(ptr + 1) >> 8);
    }
}

/**
 * Parses the markers in the JPEG file, storing information such as the pointer to the image data
 * in `jpegCtx` for later processing.
 */
void Jpeg_ParseMarkers(u8* ptr, JpegContext* jpegCtx) {
    u32 exit = false;

    jpegCtx->dqtCount = 0;
    jpegCtx->dhtCount = 0;

    while (true) {
        if (exit) {
            break;
        }

        // 0xFF indicates the start of a JPEG marker, so look for the next.
        if (*ptr++ == 0xFF) {
            switch (*ptr++) {
                case MARKER_ESCAPE:
                    // Compressed value 0xFF is stored as 0xFF00 to escape it, so ignore it.
                    break;

                case MARKER_SOI:
                    // Start of Image
                    break;

                case MARKER_APP0:
                    // Application marker for JFIF
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;

                case MARKER_APP1:
                    // Application marker for EXIF
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;

                case MARKER_APP2:
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;

                case MARKER_DQT:
                    // Define Quantization Table, stored for later processing
                    jpegCtx->dqtPtr[jpegCtx->dqtCount++] = ptr + 2;
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;

                case MARKER_DHT:
                    // Define Huffman Table, stored for later processing
                    jpegCtx->dhtPtr[jpegCtx->dhtCount++] = ptr + 2;
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;

                case MARKER_DRI:
                    // Define Restart Interval
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;

                case MARKER_SOF:
                    // Start of Frame, stores important metadata of the image.
                    // Only used for extracting the sampling factors (jpegCtx->mode).

                    if (ptr[9] == 0x21) {
                        // component Y : V0 == 1
                        jpegCtx->mode = 0;
                    } else if (ptr[9] == 0x22) {
                        // component Y : V0 == 2
                        jpegCtx->mode = 2;
                    }
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;

                case MARKER_SOS:
                    // Start of Scan marker, indicates the start of the image data.
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    jpegCtx->imageData = ptr;
                    break;

                case MARKER_EOI:
                    // End of Image
                    exit = true;
                    break;

                default:
                    ptr += Jpeg_GetUnalignedU16(ptr);
                    break;
            }
        }
    }
}

s32 Jpeg_Decode(void* data, void* zbuffer, void* work, u32 workSize) {
    s32 y;
    s32 x;
    s32 j;
    s32 i;
    JpegContext jpegCtx;
    JpegHuffmanTable hTables[4];
    JpegDecoder decoder;
    JpegDecoderState state;
    JpegWork* workBuff = work;

    if (workSize < sizeof(JpegWork)) {
        return -1;
    }

    osCreateMesgQueue(&jpegCtx.mq, &jpegCtx.msg, 1);
    Sched_FlushTaskQueue();

    jpegCtx.workBuf = workBuff;

    Jpeg_ParseMarkers(data, &jpegCtx);

    switch (jpegCtx.dqtCount) {
        case 1:
            JpegUtils_ProcessQuantizationTable(jpegCtx.dqtPtr[0], &workBuff->qTableY, 3);
            break;

        case 2:
            JpegUtils_ProcessQuantizationTable(jpegCtx.dqtPtr[0], &workBuff->qTableY, 1);
            JpegUtils_ProcessQuantizationTable(jpegCtx.dqtPtr[1], &workBuff->qTableU, 1);
            JpegUtils_ProcessQuantizationTable(jpegCtx.dqtPtr[1], &workBuff->qTableV, 1);
            break;

        case 3:
            JpegUtils_ProcessQuantizationTable(jpegCtx.dqtPtr[0], &workBuff->qTableY, 1);
            JpegUtils_ProcessQuantizationTable(jpegCtx.dqtPtr[1], &workBuff->qTableU, 1);
            JpegUtils_ProcessQuantizationTable(jpegCtx.dqtPtr[2], &workBuff->qTableV, 1);
            break;

        default:
            return -1;
    }

    switch (jpegCtx.dhtCount) {
        case 1:
            if (JpegUtils_ProcessHuffmanTable(jpegCtx.dhtPtr[0], &hTables[0], workBuff->codesLengths, workBuff->codes,
                                              4)) {
                return -1;
            }
            break;

        case 4:
            // This chained if-else has printfs inside it on debug
            if (JpegUtils_ProcessHuffmanTable(jpegCtx.dhtPtr[0], &hTables[0], workBuff->codesLengths, workBuff->codes,
                                              1)) {
            } else if (JpegUtils_ProcessHuffmanTable(jpegCtx.dhtPtr[1], &hTables[1], workBuff->codesLengths,
                                                     workBuff->codes, 1)) {
            } else if (JpegUtils_ProcessHuffmanTable(jpegCtx.dhtPtr[2], &hTables[2], workBuff->codesLengths,
                                                     workBuff->codes, 1)) {
            } else if (!JpegUtils_ProcessHuffmanTable(jpegCtx.dhtPtr[3], &hTables[3], workBuff->codesLengths,
                                                      workBuff->codes, 1)) {
                break;
            }
            return -1;

        default:
            return -1;
    }

    decoder.imageData = jpegCtx.imageData;
    decoder.mode = jpegCtx.mode;
    decoder.unk_05 = 2;

    decoder.hTablePtrs[0] = &hTables[0];
    decoder.hTablePtrs[1] = &hTables[1];
    decoder.hTablePtrs[2] = &hTables[2];
    decoder.hTablePtrs[3] = &hTables[3];
    decoder.unk_18 = 0;

    x = y = 0;
    for (i = 0; i < 300; i += 4) {
        if (!JpegDecoder_Decode(&decoder, (u16*)workBuff->data, 4, (i != 0), &state)) {
            Jpeg_ScheduleDecoderTask(&jpegCtx);

            for (j = 0; j < 4; j++) {
                Jpeg_CopyToZbuffer(workBuff->data[j], zbuffer, x, y);
                x++;

                if (x >= 20) {
                    x = 0;
                    y++;
                }
            }
        }
    }

    return 0;
}
