#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>

#include <xcb/xcb.h>
#define XK_MISCELLANY
#include <X11/keysymdef.h>

#define MAX_ROMSIZE 0x8000
#include "emul.h"
#include "sms_vdp.h"
#include "sms_ports.h"
#include "sms_pad.h"
#include "sms_spi.h"
#include "ps2_kbd.h"
#include "sdc.h"

#define RAMSTART 0xc000
#define VDP_CMD_PORT 0xbf
#define VDP_DATA_PORT 0xbe
#define PORTS_CTL_PORT 0x3f
#define PORTS_IO1_PORT 0xdc
#define PORTS_IO2_PORT 0xdd
#define SDC_CTL 0x05
#define SDC_SPI 0x04

static xcb_connection_t    *conn;
static xcb_screen_t        *screen;

/* graphics contexts */
static xcb_gcontext_t       fg;
/* win */
static xcb_drawable_t       win;

// pixels to draw. We draw them in one shot.
static xcb_rectangle_t rectangles[(32*8)*(24*8)];

static Machine *m;
static VDP vdp;
static bool vdp_changed;
static Ports ports;
static Pad pad;
static Kbd kbd;
static bool use_kbd = false;
static SDC sdc;
static SPI spi;

static uint8_t iord_vdp_cmd()
{
   return tms_cmd_rd(&vdp.tms);
}

static uint8_t iord_vdp_data()
{
   return vdp_data_rd(&vdp);
}

static uint8_t iord_ports_io1()
{
   return ports_A_rd(&ports);
}

static uint8_t iord_ports_io2()
{
   return ports_B_rd(&ports);
}

static uint8_t iord_pad()
{
   return pad_rd(&pad);
}

static uint8_t iord_kbd()
{
   return kbd_rd(&kbd);
}

static void iowr_vdp_cmd(uint8_t val)
{
   vdp_cmd_wr(&vdp, val);
}

static void iowr_vdp_data(uint8_t val)
{
   vdp_changed = true;
   vdp_data_wr(&vdp, val);
}

static void iowr_ports_ctl(uint8_t val)
{
   ports_ctl_wr(&ports, val);
}

static byte iord_spi()
{
   return spi_rd(&spi);
}

static byte spix_sdc(byte val) { return sdc_spix(&sdc, val); }

void create_window()
{
   uint32_t mask;
   uint32_t values[2];

   /* Create the window */
   win = xcb_generate_id(conn);
   mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
   values[0] = screen->white_pixel;
   values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS |
       XCB_EVENT_MASK_KEY_RELEASE;
   xcb_create_window(
       conn,
       screen->root_depth,
       win,
       screen->root,
       0, 0,
       500, 500,
       10,
       XCB_WINDOW_CLASS_INPUT_OUTPUT,
       screen->root_visual,
       mask, values);

   fg = xcb_generate_id(conn);
   mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;
   values[0] = screen->black_pixel;
   values[1] = 0;
   xcb_create_gc(conn, fg, screen->root, mask, values);

   /* Map the window on the screen */
   xcb_map_window(conn, win);
}

// To make things simple with X11, we only support monochrome display, which is
// inverted: As soon as the color of the pixel is non-black, we show a black
// pixel. If the pixel is white, we show black.
void draw_pixels()
{
   xcb_get_geometry_reply_t *geom;

   geom = xcb_get_geometry_reply(conn, xcb_get_geometry(conn, win), NULL);

   xcb_clear_area(
       conn, 0, win, 0, 0, geom->width, geom->height);
   // Figure out inner size to maximize our screen's aspect ratio
   int psize = geom->height / vdp.tms.height;
   if (geom->width / vdp.tms.width < psize) {
       // width is the constraint
       psize = geom->width / vdp.tms.width;
   }
   int innerw = psize * vdp.tms.width;
   int innerh = psize * vdp.tms.height;
   int innerx = (geom->width - innerw) / 2;
   int innery = (geom->height - innerh) / 2;
   free(geom);
   int drawcnt = 0;
   for (int i=0; i<vdp.tms.width; i++) {
       for (int j=0; j<vdp.tms.height; j++) {
           if (vdp_pixel(&vdp, i, j)) {
               int x = innerx + (i*psize);
               int y = innery + (j*psize);
               rectangles[drawcnt].x = x;
               rectangles[drawcnt].y = y;
               rectangles[drawcnt].height = psize;
               rectangles[drawcnt].width = psize;
               drawcnt++;
           }
       }
   }
   if (drawcnt) {
       xcb_poly_fill_rectangle(
           conn, win, fg, drawcnt, rectangles);
   }
   vdp_changed = false;
   xcb_flush(conn);
}

// Returns true to exist event loop
static bool _handle_keypress(xcb_generic_event_t *e)
{
   xcb_key_press_event_t *ev = (xcb_key_press_event_t *)e;
   if (ev->detail == 0x09) { // ESC
       return true;
   }
   bool ispressed = e->response_type == XCB_KEY_PRESS;
   // change keycode into symbol
   xcb_get_keyboard_mapping_reply_t* km = xcb_get_keyboard_mapping_reply(
       conn, xcb_get_keyboard_mapping(conn, ev->detail, 1), NULL);
   if (km->length) {
       xcb_keysym_t* keysyms = (xcb_keysym_t*)(km + 1);
       if (use_kbd) {
           if ((keysyms[0] == XK_Shift_L) || (keysyms[0] == XK_Shift_R)) {
               kbd_pressshift(&kbd, ispressed);
           } else if (ispressed) {
               fprintf(stderr, "pressing %x\n", keysyms[0]);
               kbd_presskey(&kbd, keysyms[0]);
           }
       } else { // pad
           switch (keysyms[0]) {
               case 'w':
                   pad_setbtn(&pad, PAD_BTN_UP, ispressed);
                   break;
               case 'a':
                   pad_setbtn(&pad, PAD_BTN_LEFT, ispressed);
                   break;
               case 's':
                   pad_setbtn(&pad, PAD_BTN_DOWN, ispressed);
                   break;
               case 'd':
                   pad_setbtn(&pad, PAD_BTN_RIGHT, ispressed);
                   break;
               case 'h':
                   pad_setbtn(&pad, PAD_BTN_A, ispressed);
                   break;
               case 'j':
                   pad_setbtn(&pad, PAD_BTN_B, ispressed);
                   break;
               case 'k':
                   pad_setbtn(&pad, PAD_BTN_C, ispressed);
                   break;
               case 'l':
                   pad_setbtn(&pad, PAD_BTN_START, ispressed);
                   break;
           }
       }
   }
   free(km);
   return false;
}

void event_loop()
{
   while (1) {
       for (int i=0; i<100; i++) {
           if (!emul_step()) {
               fprintf(stderr, "CPU halted, quitting\n");
               usleep(1000 * 1000);
               break;
           }
           spi_pulse(&spi);
       }
       if (vdp_changed) {
           // To avoid overdrawing, we'll let the CPU run a bit to finish its
           // drawing operation.
           for (int i=0; i<10000; i++) {
               if (!emul_step()) {
                   fprintf(stderr, "CPU halted, quitting\n");
                   usleep(1000 * 1000);
                   break;
               }
               spi_pulse(&spi);
           }
           draw_pixels();
       }
       // A low tech way of checking when the window was closed. The proper way
       // involving WM_DELETE is too complicated.
       xcb_get_geometry_reply_t *geom;
       geom = xcb_get_geometry_reply(conn, xcb_get_geometry(conn, win), NULL);
       if (geom == NULL) {
           return;     // window has been closed.
       } else {
           free(geom);
       }
       xcb_generic_event_t *e = xcb_poll_for_event(conn);
       if (!e) {
           continue;
       }
       switch (e->response_type & ~0x80) {
       /* ESC to exit */
       case XCB_KEY_RELEASE:
       case XCB_KEY_PRESS:
           if (_handle_keypress(e)) return;
           break;
       case XCB_EXPOSE: {
           draw_pixels();
           break;
       }
       default: {
           break;
       }
       }
       free(e);
   }
}

static void usage()
{
   fprintf(stderr, "Usage: ./sms [-k] [-c sdcard.img] /path/to/rom\n");
}

static byte spi_dbg(byte val)
{
   fprintf(stderr, "SPI XCH: %x\n", val);
   return val+1;
}

int main(int argc, char *argv[])
{
   if (argc < 2) {
       usage();
       return 1;
   }
   vdp_init(&vdp);
   vdp_changed = false;
   ports_init(&ports);
   pad_init(&pad, &ports.THA);
   kbd_init(&kbd, &ports.THA);
   sdc_init(&sdc);
   spi_init(&spi, &ports.THB, &ports.TRB, spix_sdc);

   int ch;
   while ((ch = getopt(argc, argv, "kc:")) != -1) {
       switch (ch) {
           case 'k':
               use_kbd = true;
               break;
           case 'c':
               fprintf(stderr, "Setting up SD card image with %s\n", optarg);
               sdc.fp = fopen(optarg, "r+");
               if (sdc.fp == NULL) {
                   fprintf(stderr, "Can't open file\n");
                   return 1;
               }
               break;
       }
   }
   if (optind != argc-1) {
       usage();
       return 1;
   }
   FILE *fp = fopen(argv[optind], "r");
   if (fp == NULL) {
       fprintf(stderr, "Can't open %s\n", argv[1]);
       return 1;
   }
   m = emul_init(argv[optind], 0);
   if (m == NULL) return 1;
   m->ramstart = RAMSTART;
   if (use_kbd) {
       ports.portA_rd = iord_kbd;
   } else {
       ports.portA_rd = iord_pad;
   }
   ports.portB_rd = iord_spi;

   m->iord[VDP_CMD_PORT] = iord_vdp_cmd;
   m->iord[VDP_DATA_PORT] = iord_vdp_data;
   m->iord[PORTS_IO1_PORT] = iord_ports_io1;
   m->iord[PORTS_IO2_PORT] = iord_ports_io2;
   m->iord[PORTS_CTL_PORT] = iord_noop;
   m->iowr[VDP_CMD_PORT] = iowr_vdp_cmd;
   m->iowr[VDP_DATA_PORT] = iowr_vdp_data;
   m->iowr[PORTS_CTL_PORT] = iowr_ports_ctl;

   conn = xcb_connect(NULL, NULL);
   screen = xcb_setup_roots_iterator(xcb_get_setup(conn)).data;
   create_window();
   draw_pixels();
   event_loop();
   emul_printdebug();
   if (sdc.fp) {
       fclose(sdc.fp);
   }
   return 0;
}