/**
 * DOOM for ESP32-S3 - Arduino Framework Port
 * System functions including flash/WAD access
 *
 * Replaces ESP-IDF partition functions with direct flash access
 */

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <sys/time.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_spi_flash.h"
#include "esp_partition.h"

// Undefine true/false to avoid conflict with DOOM's boolean type
#undef true
#undef false

// DOOM headers
#include "config.h"
#include "m_argv.h"
#include "lprintf.h"
#include "doomtype.h"
#include "doomdef.h"
#include "m_fixed.h"
#include "r_fps.h"
#include "i_system.h"
#include "i_joy.h"

// WAD partition addresses (from partitions.csv)
#ifndef IWAD_FLASH_ADDR
#define IWAD_FLASH_ADDR 0x120000
#endif
#ifndef PWAD_FLASH_ADDR
#define PWAD_FLASH_ADDR 0xFA0000
#endif

// WAD sizes (from partitions.csv)
#define IWAD_SIZE 0xE80000    // ~14.5MB
#define PWAD_SIZE 0x60000     // ~384KB

int realtime = 0;

void I_uSleep(unsigned long usecs) {
    vTaskDelay(usecs / 1000 / portTICK_PERIOD_MS);
}

static unsigned long getMsTicks() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_usec / 1000 + tv.tv_sec * 1000;
}

int I_GetTime_RealTime(void) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (tv.tv_sec * TICRATE + (tv.tv_usec * TICRATE) / 1000000);
}

const int displaytime = 0;

fixed_t I_GetTimeFrac(void) {
    unsigned long now = getMsTicks();
    fixed_t frac;

    if (tic_vars.step == 0)
        return FRACUNIT;

    frac = (fixed_t)((now - tic_vars.start + displaytime) * FRACUNIT / tic_vars.step);
    if (frac < 0) frac = 0;
    if (frac > FRACUNIT) frac = FRACUNIT;
    return frac;
}

void I_GetTime_SaveMS(void) {
    if (!movement_smooth) return;

    tic_vars.start = getMsTicks();
    tic_vars.next = (unsigned int)((tic_vars.start * tic_vars.msec + 1.0f) / tic_vars.msec);
    tic_vars.step = tic_vars.next - tic_vars.start;
}

unsigned long I_GetRandomTimeSeed(void) {
    return 4;  // per https://xkcd.com/221/
}

const char *I_GetVersionString(char *buf, size_t sz) {
    sprintf(buf, "%s v%s (ESP32-S3 Arduino Port)", PACKAGE, VERSION);
    return buf;
}

const char *I_SigString(char *buf, size_t sz, int signum) {
    return buf;
}

// File descriptor structure for WAD access
typedef struct {
    uint32_t flash_addr;     // Base address in flash
    uint32_t size;           // Size of WAD
    uint32_t offset;         // Current read offset
    const void *mmap_ptr;    // Memory-mapped pointer
    spi_flash_mmap_handle_t mmap_handle;
    int in_use;
} FileDesc;

#define MAX_N_FILES 2
static FileDesc fds[MAX_N_FILES];

// WAD names mapping
static const char *flash_wads[] = {
    "doom2.wad",
    "prboom-plus.wad"
};

// Flash addresses for each WAD
static const uint32_t flash_addrs[] = {
    IWAD_FLASH_ADDR,
    PWAD_FLASH_ADDR
};

// Flash sizes for each WAD
static const uint32_t flash_sizes[] = {
    IWAD_SIZE,
    PWAD_SIZE
};

int I_Open(const char *wad, int flags) {
    FileDesc *file = NULL;
    int fd = -1;

    // Find free file descriptor
    for (int i = 0; i < MAX_N_FILES; ++i) {
        if (!fds[i].in_use) {
            file = &fds[i];
            fd = i;
            break;
        }
    }

    if (file == NULL) {
        lprintf(LO_INFO, "I_Open: no free file descriptors for %s\n", wad);
        return -1;
    }

    // Find matching WAD
    int found = 0;
    for (int i = 0; i < sizeof(flash_wads) / sizeof(flash_wads[0]); ++i) {
        if (!strcasecmp(wad, flash_wads[i])) {
            file->flash_addr = flash_addrs[i];
            file->size = flash_sizes[i];
            file->offset = 0;
            found = 1;
            break;
        }
    }

    if (!found) {
        lprintf(LO_INFO, "I_Open: WAD %s not found in flash\n", wad);
        return -1;
    }

    // Memory-map the flash region
    esp_err_t err = spi_flash_mmap(
        file->flash_addr,
        file->size,
        SPI_FLASH_MMAP_DATA,
        &file->mmap_ptr,
        &file->mmap_handle
    );

    if (err != ESP_OK) {
        lprintf(LO_INFO, "I_Open: mmap failed for %s (err=%d)\n", wad, err);
        return -1;
    }

    file->in_use = 1;
    lprintf(LO_INFO, "I_Open: %s @ 0x%08X, size=%u, mmap=%p, fd=%d\n",
            wad, file->flash_addr, file->size, file->mmap_ptr, fd);

    return fd;
}

int I_Lseek(int ifd, off_t offset, int whence) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) {
        return -1;
    }

    if (whence == SEEK_SET) {
        fds[ifd].offset = offset;
    } else if (whence == SEEK_CUR) {
        fds[ifd].offset += offset;
    } else if (whence == SEEK_END) {
        fds[ifd].offset = fds[ifd].size + offset;
    }

    return fds[ifd].offset;
}

int I_Filelength(int ifd) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) {
        return 0;
    }
    return fds[ifd].size;
}

void I_Close(int fd) {
    if (fd < 0 || fd >= MAX_N_FILES || !fds[fd].in_use) {
        return;
    }

    if (fds[fd].mmap_ptr) {
        spi_flash_munmap(fds[fd].mmap_handle);
    }

    fds[fd].in_use = 0;
    fds[fd].mmap_ptr = NULL;
}

void *I_Mmap(void *addr, size_t length, int prot, int flags, int ifd, off_t offset) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) {
        return NULL;
    }
    return (byte *)fds[ifd].mmap_ptr + offset;
}

int I_Munmap(void *addr, size_t length) {
    // We don't actually unmap individual regions
    return 0;
}

void I_Read(int ifd, void *vbuf, size_t sz) {
    if (ifd < 0 || ifd >= MAX_N_FILES || !fds[ifd].in_use) {
        return;
    }

    memcpy(vbuf, (byte *)fds[ifd].mmap_ptr + fds[ifd].offset, sz);
    fds[ifd].offset += sz;
}

const char *I_DoomExeDir(void) {
    return "";
}

void *I_Realloc(void *ptr, size_t size) {
    void *new_ptr = realloc(ptr, size);

    if (size != 0 && new_ptr == NULL) {
        I_Error("I_Realloc: failed on reallocation of %zu bytes", size);
    }

    return new_ptr;
}

char *I_FindFile(const char *wfname, const char *ext) {
    char *findfile_name = malloc(strlen(wfname) + strlen(ext) + 1);
    sprintf(findfile_name, "%s%s", wfname, ext);

    // Check if this WAD exists in our flash
    for (int i = 0; i < sizeof(flash_wads) / sizeof(flash_wads[0]); ++i) {
        if (!strcasecmp(findfile_name, flash_wads[i])) {
            lprintf(LO_INFO, "I_FindFile: found %s\n", findfile_name);
            return findfile_name;
        }
    }

    free(findfile_name);
    lprintf(LO_INFO, "I_FindFile: %s not found\n", wfname);
    return NULL;
}

void I_SetAffinityMask(void) {
    // Not needed on ESP32
}

// Check if a pointer is valid (simple implementation)
int isValidPtr(void *ptr) {
    // On ESP32, we consider a pointer valid if it's not NULL
    // and within reasonable memory ranges
    if (ptr == NULL) return 0;

    uint32_t addr = (uint32_t)ptr;

    // Check if in DRAM (internal RAM)
    if (addr >= 0x3FC88000 && addr < 0x3FD00000) return 1;

    // Check if in PSRAM
    if (addr >= 0x3C000000 && addr < 0x3E000000) return 1;

    // Check if in flash mapped region
    if (addr >= 0x42000000 && addr < 0x44000000) return 1;

    return 0;
}
