/**
 * DOOM for ESP32-S3 - Arduino Framework Port
 * Video system implementation
 *
 * Replaces ESP-IDF heap_caps_malloc with standard malloc/PSRAM
 */

#include "config.h"
#include <stdlib.h>
#include <string.h>

#include "m_argv.h"
#include "doomstat.h"
#include "doomdef.h"
#include "doomtype.h"
#include "v_video.h"
#include "r_draw.h"
#include "d_main.h"
#include "d_event.h"
#include "i_video.h"
#include "z_zone.h"
#include "s_sound.h"
#include "sounds.h"
#include "w_wad.h"
#include "st_stuff.h"
#include "lprintf.h"

// Arduino/ESP32 includes for memory allocation and GPIO
#include "esp_heap_caps.h"
#include "driver/gpio.h"

// =============================================================================
// BUTTON CONFIGURATION - Modify these pins according to your wiring
// =============================================================================
// Connect each button between the GPIO pin and GND (buttons are active LOW)
//
// Suggested wiring:
//   Button    GPIO    Function
//   ------    ----    --------
//   UP        1       Move forward
//   DOWN      3       Move backward
//   LEFT      8       Turn left
//   RIGHT     9       Turn right
//   FIRE      10      Fire weapon
//   USE       11      Open doors/Use
//   ENTER     12      Menu select/Start game
//   STRAFE    13      Hold to strafe instead of turn
//
// Note: Buttons should connect GPIO to GND when pressed (internal pull-up used)
// =============================================================================

// Button pins - UPDATED to avoid conflict with display pins (8,9,10,11,12)
#define BTN_UP      1      // Forward
#define BTN_DOWN    3      // Backward
#define BTN_LEFT    14     // Turn left (was 8 - DC pin conflict!)
#define BTN_RIGHT   21     // Turn right (was 9 - RST pin conflict!)
#define BTN_FIRE    47     // Fire weapon (was 10 - CS pin conflict!)
#define BTN_USE     48     // Use/Open doors (was 11 - MOSI pin conflict!)
#define BTN_ENTER   45     // Menu enter (was 12 - CLK pin conflict!)
#define BTN_STRAFE  13     // Strafe modifier

// Key mappings (from doomdef.h)
#define KEY_UP      KEYD_UPARROW     // 0xad
#define KEY_DOWN    KEYD_DOWNARROW   // 0xaf
#define KEY_LEFT    KEYD_LEFTARROW   // 0xac
#define KEY_RIGHT   KEYD_RIGHTARROW  // 0xae
#define KEY_FIRE    KEYD_RCTRL       // Fire
#define KEY_USE     KEYD_SPACEBAR    // Use/Open
#define KEY_ENTER   KEYD_ENTER       // Menu select
#define KEY_STRAFE  KEYD_RALT        // Strafe modifier

// Button state tracking
static uint8_t btn_state_prev = 0xFF;  // All buttons released (active LOW)
static int buttons_initialized = 0;

// External LCD functions
extern void spi_lcd_wait_finish(void);
extern void spi_lcd_send(uint16_t *scr);

// Global variables
int use_fullscreen = 0;
int use_doublebuffer = 0;
int usejoystick = 0;
int joyleft, joyright, joyup, joydown;

// LCD palette (RGB565, byte-swapped for SPI)
int16_t lcdpal[256];

// Screen buffer
static void *screen0 = NULL;

// Initialize button GPIOs
static void init_buttons(void) {
    if (buttons_initialized) return;

    // Configure button GPIOs as inputs with pull-up
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BTN_UP) | (1ULL << BTN_DOWN) |
                        (1ULL << BTN_LEFT) | (1ULL << BTN_RIGHT) |
                        (1ULL << BTN_FIRE) | (1ULL << BTN_USE) |
                        (1ULL << BTN_ENTER) | (1ULL << BTN_STRAFE),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io_conf);

    lprintf(LO_INFO, "Buttons initialized: UP=%d DOWN=%d LEFT=%d RIGHT=%d FIRE=%d USE=%d ENTER=%d STRAFE=%d\n",
            BTN_UP, BTN_DOWN, BTN_LEFT, BTN_RIGHT, BTN_FIRE, BTN_USE, BTN_ENTER, BTN_STRAFE);

    buttons_initialized = 1;
}

// Helper to post key events
static void post_key_event(int key, int pressed) {
    event_t ev;
    ev.type = pressed ? ev_keydown : ev_keyup;
    ev.data1 = key;
    ev.data2 = 0;
    ev.data3 = 0;
    D_PostEvent(&ev);
}

void I_StartTic(void) {
    // Initialize buttons on first call
    if (!buttons_initialized) {
        init_buttons();
    }

    // Read current button states (active LOW - 0 = pressed)
    uint8_t btn_state = 0;
    if (gpio_get_level(BTN_UP) == 0)     btn_state |= (1 << 0);
    if (gpio_get_level(BTN_DOWN) == 0)   btn_state |= (1 << 1);
    if (gpio_get_level(BTN_LEFT) == 0)   btn_state |= (1 << 2);
    if (gpio_get_level(BTN_RIGHT) == 0)  btn_state |= (1 << 3);
    if (gpio_get_level(BTN_FIRE) == 0)   btn_state |= (1 << 4);
    if (gpio_get_level(BTN_USE) == 0)    btn_state |= (1 << 5);
    if (gpio_get_level(BTN_ENTER) == 0)  btn_state |= (1 << 6);
    if (gpio_get_level(BTN_STRAFE) == 0) btn_state |= (1 << 7);

    // Check for state changes and post events
    if (btn_state != btn_state_prev) {
        // UP button
        if ((btn_state & (1 << 0)) != (btn_state_prev & (1 << 0))) {
            post_key_event(KEY_UP, btn_state & (1 << 0));
        }
        // DOWN button
        if ((btn_state & (1 << 1)) != (btn_state_prev & (1 << 1))) {
            post_key_event(KEY_DOWN, btn_state & (1 << 1));
        }
        // LEFT button
        if ((btn_state & (1 << 2)) != (btn_state_prev & (1 << 2))) {
            post_key_event(KEY_LEFT, btn_state & (1 << 2));
        }
        // RIGHT button
        if ((btn_state & (1 << 3)) != (btn_state_prev & (1 << 3))) {
            post_key_event(KEY_RIGHT, btn_state & (1 << 3));
        }
        // FIRE button
        if ((btn_state & (1 << 4)) != (btn_state_prev & (1 << 4))) {
            post_key_event(KEY_FIRE, btn_state & (1 << 4));
        }
        // USE button
        if ((btn_state & (1 << 5)) != (btn_state_prev & (1 << 5))) {
            post_key_event(KEY_USE, btn_state & (1 << 5));
        }
        // ENTER button
        if ((btn_state & (1 << 6)) != (btn_state_prev & (1 << 6))) {
            post_key_event(KEY_ENTER, btn_state & (1 << 6));
        }
        // STRAFE button
        if ((btn_state & (1 << 7)) != (btn_state_prev & (1 << 7))) {
            post_key_event(KEY_STRAFE, btn_state & (1 << 7));
        }

        btn_state_prev = btn_state;
    }
}

void I_ShutdownGraphics(void) {
    // Nothing to clean up
}

void I_UpdateNoBlit(void) {
    // Nothing to do
}

void I_StartFrame(void) {
    // Nothing to do
}

int I_StartDisplay(void) {
    spi_lcd_wait_finish();
    return true;
}

void I_EndDisplay(void) {
    // Nothing to do
}

void I_FinishUpdate(void) {
    // Send frame buffer to LCD
    spi_lcd_send((uint16_t *)screens[0].data);
}

void I_SetPalette(int pal) {
    int pplump = W_GetNumForName("PLAYPAL");
    const byte *palette = W_CacheLumpNum(pplump);
    palette += pal * (3 * 256);

    // Convert 24-bit RGB to 16-bit RGB565
    for (int i = 0; i < 256; i++) {
        int r = palette[0] >> 3;  // 5 bits
        int g = palette[1] >> 2;  // 6 bits
        int b = palette[2] >> 3;  // 5 bits
        lcdpal[i] = (r << 11) | (g << 5) | b;  // No byte swap - we send MSB first
        palette += 3;
    }

    W_UnlockLumpNum(pplump);
}

void I_PreInitGraphics(void) {
    lprintf(LO_INFO, "I_PreInitGraphics: allocating screen buffer\n");

    // Allocate screen buffer in internal RAM for DMA compatibility
    // Use heap_caps_malloc which is available in Arduino-ESP32
    screen0 = heap_caps_malloc(SCREENWIDTH * SCREENHEIGHT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);

    if (!screen0) {
        // Fallback to regular malloc if internal RAM is full
        lprintf(LO_INFO, "Internal RAM full, trying regular malloc\n");
        screen0 = malloc(SCREENWIDTH * SCREENHEIGHT);
    }

    if (!screen0) {
        lprintf(LO_FATAL, "Failed to allocate screen buffer!\n");
        return;
    }

    lprintf(LO_INFO, "Screen buffer allocated at %p\n", screen0);
}

void I_SetRes(void) {
    // Set screen dimensions for first three screens
    for (int i = 0; i < 3; i++) {
        screens[i].width = SCREENWIDTH;
        screens[i].height = SCREENHEIGHT;
        screens[i].byte_pitch = SCREENPITCH;
        screens[i].short_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE16);
        screens[i].int_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE32);
    }

    // Status bar screen
    screens[4].width = SCREENWIDTH;
    screens[4].height = (ST_SCALED_HEIGHT + 1);
    screens[4].byte_pitch = SCREENPITCH;
    screens[4].short_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE16);
    screens[4].int_pitch = SCREENPITCH / V_GetModePixelDepth(VID_MODE32);

    // Set main screen data pointer
    screens[0].not_on_heap = true;
    screens[0].data = screen0;

    lprintf(LO_INFO, "I_SetRes: Using resolution %dx%d\n", SCREENWIDTH, SCREENHEIGHT);
}

void I_InitGraphics(void) {
    static int firsttime = 1;

    if (firsttime) {
        firsttime = 0;
        atexit(I_ShutdownGraphics);
        lprintf(LO_INFO, "I_InitGraphics: %dx%d\n", SCREENWIDTH, SCREENHEIGHT);
        I_UpdateVideoMode();
    }
}

void I_UpdateVideoMode(void) {
    lprintf(LO_INFO, "I_UpdateVideoMode: %dx%d\n", SCREENWIDTH, SCREENHEIGHT);

    // Use 8-bit indexed color mode
    video_mode_t mode = VID_MODE8;

    V_InitMode(mode);
    V_DestroyUnusedTrueColorPalettes();
    V_FreeScreens();

    I_SetRes();

    V_AllocScreens();

    R_InitBuffer(SCREENWIDTH, SCREENHEIGHT);
}
