#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <algorithm>

#include <linux/fb.h>
#include <linux/joystick.h>

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>

#include "apu/apu.h"
#include "blit.h"
#include "conffile.h"
#include "controls.h"
#include "display.h"
#include "gfx.h"
#include "hifb.hpp"
#include "hq2x.h"
#include "memmap.h"
#include "snes9x.h"

#define MAP_KEY(x, y)      \
  {                        \
    s9xcommand_t cmd;      \
    \ 
  cmd = S9xGetCommandT(y); \
    S9xMapInput(x, &cmd);  \
  }

HI_U32 g_pSnesBackBufferPhys;
void* g_pSnesBackBufferVirt;

HI_U32 g_pScaleBufferPhys;
void* g_pScaleBufferVirt;

uint8 js_mod[8] = {0, 0, 0, 0, 0, 0, 0, 0};
int js_fd[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
const char* js_device[8] = {"/dev/js0", "/dev/js1", "/dev/js2", "/dev/js3",
                            "/dev/js4", "/dev/js5", "/dev/js6", "/dev/js7"};
bool8 js_unplugged[8] = {FALSE, FALSE, FALSE, FALSE,
                         FALSE, FALSE, FALSE, FALSE};

void S9xCloseSnapshotFile(STREAM file) {}
void S9xExit(void) {}
void S9xAutoSaveSRAM(void) {}
void S9xToggleSoundChannel(int c) {}
void S9xSyncSpeed(void) {}
void S9xMessage(int type, int number, const char* message) {}
void S9xParsePortConfig(ConfigFile& conf, int pass) {}

bool8 S9xMapInput(const char* n, s9xcommand_t* cmd) {
  int i, j, d;
  char* c;
  char buf[4] = "M1+";

  if (!strncmp(n, "PseudoPointer", 13) && n[13] >= '1' && n[13] <= '8' &&
      n[14] == '\0')
    return (S9xMapPointer(PseudoPointerBase + (n[13] - '1'), *cmd, false));

  if (!strncmp(n, "PseudoButton", 12) && isdigit(n[12]) &&
      (j = strtol(n + 12, &c, 10)) < 256 && (c == NULL || *c == '\0'))
    return (S9xMapButton(PseudoButtonBase + j, *cmd, false));

  if (n[0] != 'J' || !isdigit(n[1]) || !isdigit(n[2]) || n[3] != ':')
    return false;

  d = ((n[1] - '0') * 10 + (n[2] - '0')) << 24;
  d |= 0x80000000;
  i = 4;
  if (!strncmp(n + i, "X+", 2)) {
    d |= 0x4000;
    i += 2;
  } else {
    for (buf[1] = '1'; buf[1] <= '8'; buf[1]++) {
      if (!strncmp(n + i, buf, 3)) {
        d |= 1 << (buf[1] - '1' + 16);
        i += 3;
      }
    }
  }

  if (!strncmp(n + i, "Axis", 4)) {
    d |= 0x8000;
    i += 4;
  } else if (n[i] == 'B')
    i++;
  else
    return false;

  d |= j = strtol(n + i, &c, 10);
  if ((c != NULL && *c != '\0') || j > 0x3fff)
    return false;

  if (d & 0x8000)
    return (S9xMapAxis(d, *cmd, false));

  return (S9xMapButton(d, *cmd, false));
}

void InitJoysticks(void) {
  int version;
  unsigned char axes, buttons;

  if ((js_fd[0] = open(js_device[0], O_RDONLY | O_NONBLOCK)) == -1) {
    fprintf(stderr, "joystick: No joystick found.\n");
    return;
  }

  if (ioctl(js_fd[0], JSIOCGVERSION, &version) == -1) {
    fprintf(stderr,
            "joystick: You need at least driver version 1.0 for joystick "
            "support.\n");
    close(js_fd[0]);
    return;
  }

  for (int i = 1; i < 8; i++)
    js_fd[i] = open(js_device[i], O_RDONLY | O_NONBLOCK);

  char name[130];
  bzero(name, 128);

  if (ioctl(js_fd[0], JSIOCGNAME(128), name) > 0) {
    printf("Using %s (%s) as joystick1\n", name, js_device[0]);

    for (int i = 1; i < 8; i++) {
      if (js_fd[i] > 0) {
        ioctl(js_fd[i], JSIOCGNAME(128), name);
        printf("  and %s (%s) as joystick%d\n", name, js_device[i], i + 1);
      }
    }
  } else {
    ioctl(js_fd[0], JSIOCGAXES, &axes);
    ioctl(js_fd[0], JSIOCGBUTTONS, &buttons);
    printf("Using %d-axis %d-button joystick (%s) as joystick1\n", axes,
           buttons, js_device[0]);

    for (int i = 1; i < 8; i++) {
      if (js_fd[i] > 0) {
        ioctl(js_fd[i], JSIOCGAXES, &axes);
        ioctl(js_fd[i], JSIOCGBUTTONS, &buttons);
        printf("  and %d-axis %d-button joystick (%s) as joystick%d\n", axes,
               buttons, js_device[i], i + 1);
      }
    }
  }
}

bool8 ReadJoysticks(void) {
  // track if ANY joystick event happened this frame
  int js_latch = FALSE;
  struct js_event js_ev;

  for (int i = 0; i < 8; i++) {
    // Try to reopen unplugged sticks
    if (js_unplugged[i]) {
      js_fd[i] = open(js_device[i], O_RDONLY | O_NONBLOCK);
      if (js_fd[i] >= 0) {
        fprintf(stderr, "Joystick %d reconnected.\n", i);
        js_unplugged[i] = FALSE;
        js_latch = TRUE;
      }
    }

    // skip sticks without valid file desc
    if (js_fd[i] < 0)
      continue;

    while (read(js_fd[i], &js_ev, sizeof(struct js_event)) ==
           sizeof(struct js_event)) {
      switch (js_ev.type) {
        case JS_EVENT_AXIS:
          S9xReportAxis(0x8000c000 | (i << 24) | js_ev.number, js_ev.value);
          S9xReportAxis(
              0x80008000 | (i << 24) | (js_mod[i] << 16) | js_ev.number,
              js_ev.value);
          js_latch = TRUE;
          break;

        case JS_EVENT_BUTTON:
        case JS_EVENT_BUTTON | JS_EVENT_INIT:
          S9xReportButton(0x80004000 | (i << 24) | js_ev.number, js_ev.value);
          S9xReportButton(
              0x80000000 | (i << 24) | (js_mod[i] << 16) | js_ev.number,
              js_ev.value);
          js_latch = TRUE;
          break;
      }
    }

    /* EAGAIN is returned when the queue is empty */
    if (errno != EAGAIN) {
      // Error reading joystick.
      fprintf(stderr, "Error reading joystick %d!\n", i);

      // Mark for reconnect attempt.
      js_unplugged[i] = TRUE;

      for (unsigned int j = 0; j < 16; j++) {
        // Center all axis
        S9xReportAxis(0x8000c000 | (i << 24) | j, 0);
        S9xReportAxis(0x80008000 | (i << 24) | (js_mod[i] << 16) | j, 0);
        // Unpress all buttons.
        S9xReportButton(0x80004000 | (i << 24) | j, 0);
        S9xReportButton(0x80000000 | (i << 24) | (js_mod[i] << 16) | j, 0);
      }

      js_latch = TRUE;
    }
  }
  return js_latch;
}

const char* S9xStringInput(const char* message) {
  static char buffer[256];

  printf("%s: ", message);
  fflush(stdout);

  if (fgets(buffer, sizeof(buffer) - 2, stdin))
    return (buffer);

  return (NULL);
}

bool8 S9xContinueUpdate(int width, int height) {
  return true;
}

bool S9xPollButton(uint32 id, bool* pressed) {
  return false;
}

bool S9xPollAxis(uint32 id, int16* value) {
  return false;
}

bool8 S9xOpenSnapshotFile(const char* filepath, bool8 read_only, STREAM* file) {
  return true;
}

bool8 S9xInitUpdate(void) {
  return true;
}

bool8 S9xDeinitUpdate(int width, int height) {
  TDE2_RECT_S stSrcRect, stDstRect, stScaleRect;
  TDE2_SURFACE_S stSrc, stDst, stScale;
  TDE2_OPT_S stOpt = {TDE2_ALUCMD_NONE};
  TDE_HANDLE s32Handle;
  HI_S32 s32Ret;

  /* 1. start job */
  s32Handle = HI_TDE2_BeginJob();
  if (HI_ERR_TDE_INVALID_HANDLE == s32Handle) {
    SAMPLE_PRT("start job failed!\n");
    return false;
  }

  /* copy (scale and pixel convert) from snes buffer to scale buffer... */

  stSrcRect.s32Xpos = 0;
  stSrcRect.s32Ypos = 0;
  stSrcRect.u32Height = SNES_HEIGHT;
  stSrcRect.u32Width = SNES_WIDTH;

  stSrc.enColorFmt = TDE2_COLOR_FMT_RGB565;
  stSrc.u32Width = SNES_WIDTH;
  stSrc.u32Height = SNES_HEIGHT;
  stSrc.u32Stride = 2 * SNES_WIDTH;
  stSrc.u32PhyAddr = g_pSnesBackBufferPhys;

  stScaleRect.s32Xpos = 0;
  stScaleRect.s32Ypos = 0;
  stScaleRect.u32Height = SCALE_HEIGHT;
  stScaleRect.u32Width = SCALE_WIDTH;

  stScale.enColorFmt = TDE2_COLOR_FMT_ARGB1555;
  stScale.u32Width = SCALE_WIDTH;
  stScale.u32Height = SCALE_HEIGHT;
  stScale.u32Stride = SCALE_WIDTH * 2;
  stScale.u32PhyAddr = g_pScaleBufferPhys;

  stDstRect.s32Xpos = (SCREEN_WIDTH - SCALE_WIDTH) / 2;
  stDstRect.s32Ypos = 0;
  stDstRect.u32Height = SCREEN_HEIGHT;
  stDstRect.u32Width = SCREEN_WIDTH;

  stDst.enColorFmt = TDE2_COLOR_FMT_ARGB1555;
  stDst.u32Width = SCREEN_WIDTH;
  stDst.u32Height = SCREEN_HEIGHT;
  stDst.u32Stride = SCREEN_WIDTH * 2;
  stDst.u32PhyAddr = g_stCanvasBuf.stCanvas.u32PhyAddr;
  stOpt.bResize = HI_TRUE;

  s32Ret = HI_TDE2_Bitblit(s32Handle, &stScale, &stScaleRect, &stSrc,
                           &stSrcRect, &stScale, &stScaleRect, &stOpt);

  if (s32Ret < 0) {
    SAMPLE_PRT("HI_TDE2_Bitblit:%d failed,ret=0x%x!\n", __LINE__, s32Ret);
    HI_TDE2_CancelJob(s32Handle);
    return false;
  }

  s32Ret =
      HI_TDE2_QuickCopy(s32Handle, &stScale, &stScaleRect, &stDst, &stDstRect);

  if (s32Ret < 0) {
    SAMPLE_PRT("HI_TDE2_QuickCopy:%d failed,ret=0x%x!\n", __LINE__, s32Ret);
    HI_TDE2_CancelJob(s32Handle);
    return false;
  }

  /* 3. submit job */
  s32Ret = HI_TDE2_EndJob(s32Handle, HI_FALSE, HI_TRUE, 10);
  if (s32Ret < 0) {
    SAMPLE_PRT("Line:%d,HI_TDE2_EndJob failed,ret=0x%x!\n", __LINE__, s32Ret);
    HI_TDE2_CancelJob(s32Handle);
    return false;
  }
  
  RefreshScreen();
  return true;
}

bool8 S9xOpenSoundDevice(void) {
  return false;
}

const char* S9xGetFilename(const char* ex, s9x_getdirtype dirtype) {
  static char s[PATH_MAX + 1];
  char drive[_MAX_DRIVE + 1], dir[_MAX_DIR + 1], fname[_MAX_FNAME + 1],
      ext[_MAX_EXT + 1];

  _splitpath(Memory.ROMFilename, drive, dir, fname, ext);
  snprintf(s, PATH_MAX + 1, "%s%s%s%s", S9xGetDirectory(dirtype), SLASH_STR,
           fname, ex);

  return (s);
}

const char* S9xGetFilenameInc(const char* ex, enum s9x_getdirtype dirtype) {
  static char s[PATH_MAX + 1];
  char drive[_MAX_DRIVE + 1], dir[_MAX_DIR + 1], fname[_MAX_FNAME + 1],
      ext[_MAX_EXT + 1];

  unsigned int i = 0;
  const char* d;
  struct stat buf;

  _splitpath(Memory.ROMFilename, drive, dir, fname, ext);
  d = S9xGetDirectory(dirtype);

  do
    snprintf(s, PATH_MAX + 1, "%s%s%s.%03d%s", d, SLASH_STR, fname, i++, ex);
  while (stat(s, &buf) == 0 && i < 1000);
  return (s);
}

const char* S9xGetDirectory(enum s9x_getdirtype dirtype) {
  static char s[PATH_MAX + 1];
  switch (dirtype) {
    case DEFAULT_DIR:
      strncpy(s, getenv("HOME"), PATH_MAX + 1);
      s[PATH_MAX] = 0;
      break;

    case HOME_DIR:
      strncpy(s, getenv("HOME"), PATH_MAX + 1);
      s[PATH_MAX] = 0;
      break;

    case ROMFILENAME_DIR:
      strncpy(s, Memory.ROMFilename, PATH_MAX + 1);
      s[PATH_MAX] = 0;

      for (int i = strlen(s); i >= 0; i--) {
        if (s[i] == SLASH_CHAR) {
          s[i] = 0;
          break;
        }
      }

      break;

    default:
      s[0] = 0;
      break;
  }
  return (s);
}

const char* S9xBasename(const char* path) {
  const char* p;

  if ((p = strrchr(path, '/')) != NULL || (p = strrchr(path, '\\')) != NULL)
    return (p + 1);

  return (path);
}

int main(int argc, char* argv[]) {
  /* default settings to get snes emulation working... */
  memset(&Settings, 0, sizeof(Settings));
  Settings.MouseMaster = TRUE;
  Settings.SuperScopeMaster = TRUE;
  Settings.JustifierMaster = TRUE;
  Settings.MultiPlayer5Master = TRUE;
  Settings.FrameTimePAL = 20000;
  Settings.FrameTimeNTSC = 16667;
  Settings.SixteenBitSound = TRUE;
  Settings.Stereo = TRUE;
  Settings.SoundPlaybackRate = 48000;
  Settings.SoundInputRate = 31950;
  Settings.SupportHiRes = FALSE;
  Settings.Transparency = TRUE;
  Settings.AutoDisplayMessages = TRUE;
  Settings.InitialInfoStringTimeout = 120;
  Settings.HDMATimingHack = 100;
  Settings.BlockInvalidVRAMAccessMaster = TRUE;
  Settings.SkipFrames = AUTO_FRAMERATE;
  Settings.TurboSkipFrames = 15;

  HI_MPI_SYS_MmzAlloc(&g_pScaleBufferPhys, &g_pScaleBufferVirt, NULL, NULL,
                      2 * SCALE_WIDTH * SCALE_HEIGHT);

  HI_MPI_SYS_MmzAlloc(&g_pSnesBackBufferPhys, &g_pSnesBackBufferVirt, NULL,
                      NULL, 2 * SNES_WIDTH * SNES_HEIGHT);

  GFX.Screen = (uint16_t*)g_pSnesBackBufferVirt;
  GFX.Pitch = 2 * SNES_WIDTH;
  std::printf("[+] init back buffer and scale buffer...\n");

  S9xLoadConfigFiles(argv, argc);
  Memory.Init();
  S9xInitAPU();
  S9xInitSound(0);
  S9xSetSoundMute(TRUE);
  S9xGraphicsInit();
  S9xBlitFilterInit();
  S9xBlit2xSaIFilterInit();
  S9xBlitHQ2xFilterInit();
  HiInitDisplay();
  std::printf("[+] init snes emulator...\n");

  Memory.LoadROM("super_mario.smc");
  InitJoysticks();

  S9xSetController(0, CTL_JOYPAD, 0, 0, 0, 0);
  S9xSetController(1, CTL_JOYPAD, 1, 0, 0, 0);

  S9xUnmapAllControls();
  MAP_KEY("J00:Axis0", "Joypad1 Axis Left/Right T=50%");
  MAP_KEY("J00:Axis1", "Joypad1 Axis Up/Down T=50%");
  MAP_KEY("J00:B0", "Joypad1 X");
  MAP_KEY("J00:B1", "Joypad1 A");
  MAP_KEY("J00:B2", "Joypad1 B");
  MAP_KEY("J00:B3", "Joypad1 Y");
  MAP_KEY("J00:B6", "Joypad1 L");
  MAP_KEY("J00:B7", "Joypad1 R");
  MAP_KEY("J00:B8", "Joypad1 Select");
  MAP_KEY("J00:B11", "Joypad1 Start");

  std::printf("[+] setup joystick... emulating...\n");
  while (true) {
    ReadJoysticks();
    S9xMainLoop();
  }
}