//================
//SDL input driver
//================
//Keyboard and mouse are controlled directly via Xlib,
//as SDL cannot capture input from windows it does not create itself.
//SDL is used only to handle joysticks / gamepads.

#include <SDL/SDL.h>
#include <sys/ipc.h>
#include <sys/shm.h>

namespace ruby {

struct pInputSDL {
  #include "xlibkeys.hpp"

  struct {
    Display *display;
    Window rootwindow;
    Cursor InvisibleCursor;
    SDL_Joystick *gamepad[Joypad::Count];

    unsigned screenwidth, screenheight;
    unsigned relativex, relativey;
    bool mouseacquired;

    //mouse device settings
    int accel_numerator;
    int accel_denominator;
    int threshold;
  } device;

  struct {
    uintptr_t handle;
  } settings;

  bool cap(const string& name) {
    if(name == Input::Handle) return true;
    if(name == Input::KeyboardSupport) return true;
    if(name == Input::MouseSupport) return true;
    if(name == Input::JoypadSupport) return true;
    return false;
  }

  any get(const string& name) {
    if(name == Input::Handle) return (uintptr_t)settings.handle;
    return false;
  }

  bool set(const string& name, const any &value) {
    if(name == Input::Handle) {
      settings.handle = any_cast<uintptr_t>(value);
      return true;
    }

    return false;
  }

  bool acquire() {
    if(acquired()) return true;

    if(XGrabPointer(device.display, settings.handle, True, 0, GrabModeAsync, GrabModeAsync,
    device.rootwindow, device.InvisibleCursor, CurrentTime) == GrabSuccess) {
      //backup existing cursor acceleration settings
      XGetPointerControl(device.display, &device.accel_numerator, &device.accel_denominator, &device.threshold);

      //disable cursor acceleration
      XChangePointerControl(device.display, True, False, 1, 1, 0);

      //center cursor (so that first relative poll returns 0, 0 if mouse has not moved)
      XWarpPointer(device.display, None, device.rootwindow, 0, 0, 0, 0, device.screenwidth / 2, device.screenheight / 2);

      return device.mouseacquired = true;
    } else {
      return device.mouseacquired = false;
    }
  }

  bool unacquire() {
    if(acquired()) {
      //restore cursor acceleration and release cursor
      XChangePointerControl(device.display, True, True, device.accel_numerator, device.accel_denominator, device.threshold);
      XUngrabPointer(device.display, CurrentTime);
      device.mouseacquired = false;
    }
    return true;
  }

  bool acquired() {
    return device.mouseacquired;
  }

  bool poll(int16_t *table) {
    memset(table, 0, Scancode::Limit * sizeof(int16_t));

    //========
    //Keyboard
    //========

    x_poll(device.display, table);

    //=====
    //Mouse
    //=====

    Window root_return, child_return;
    int root_x_return = 0, root_y_return = 0;
    int win_x_return = 0, win_y_return = 0;
    unsigned int mask_return = 0;
    XQueryPointer(device.display, settings.handle,
      &root_return, &child_return, &root_x_return, &root_y_return,
      &win_x_return, &win_y_return, &mask_return);

    if(acquired()) {
      XWindowAttributes attributes;
      XGetWindowAttributes(device.display, settings.handle, &attributes);

      //absolute -> relative conversion
      table[mouse(0).axis(0)] = (int16_t)(root_x_return - device.screenwidth  / 2);
      table[mouse(0).axis(1)] = (int16_t)(root_y_return - device.screenheight / 2);

      if(table[mouse(0).axis(0)] != 0 || table[mouse(0).axis(1)] != 0) {
        //if mouse movement occurred, re-center mouse for next poll
        XWarpPointer(device.display, None, device.rootwindow, 0, 0, 0, 0, device.screenwidth / 2, device.screenheight / 2);
      }
    } else {
      table[mouse(0).axis(0)] = (int16_t)(root_x_return - device.relativex);
      table[mouse(0).axis(1)] = (int16_t)(root_y_return - device.relativey);

      device.relativex = root_x_return;
      device.relativey = root_y_return;
    }

    //manual device polling is limited to only five buttons ...
    table[mouse(0).button(0)] = (bool)(mask_return & Button1Mask);
    table[mouse(0).button(1)] = (bool)(mask_return & Button2Mask);
    table[mouse(0).button(2)] = (bool)(mask_return & Button3Mask);
    table[mouse(0).button(3)] = (bool)(mask_return & Button4Mask);
    table[mouse(0).button(4)] = (bool)(mask_return & Button5Mask);

    //=========
    //Joypad(s)
    //=========

    SDL_JoystickUpdate();
    for(unsigned i = 0; i < Joypad::Count; i++) {
      if(!device.gamepad[i]) continue;

      //POV hats
      unsigned hats = min((unsigned)Joypad::Hats, SDL_JoystickNumHats(device.gamepad[i]));
      for(unsigned hat = 0; hat < hats; hat++) {
        uint8_t state = SDL_JoystickGetHat(device.gamepad[i], hat);
        if(state & SDL_HAT_UP   ) table[joypad(i).hat(hat)] |= Joypad::HatUp;
        if(state & SDL_HAT_RIGHT) table[joypad(i).hat(hat)] |= Joypad::HatRight;
        if(state & SDL_HAT_DOWN ) table[joypad(i).hat(hat)] |= Joypad::HatDown;
        if(state & SDL_HAT_LEFT ) table[joypad(i).hat(hat)] |= Joypad::HatLeft;
      }

      //axes
      unsigned axes = min((unsigned)Joypad::Axes, SDL_JoystickNumAxes(device.gamepad[i]));
      for(unsigned axis = 0; axis < axes; axis++) {
        table[joypad(i).axis(axis)] = (int16_t)SDL_JoystickGetAxis(device.gamepad[i], axis);
      }

      //buttons
      for(unsigned button = 0; button < Joypad::Buttons; button++) {
        table[joypad(i).button(button)] = (bool)SDL_JoystickGetButton(device.gamepad[i], button);
      }
    }

    return true;
  }

  bool init() {
    SDL_InitSubSystem(SDL_INIT_JOYSTICK);
    SDL_JoystickEventState(SDL_IGNORE);

    device.display = XOpenDisplay(0);
    device.rootwindow = DefaultRootWindow(device.display);
    XWindowAttributes attributes;
    XGetWindowAttributes(device.display, device.rootwindow, &attributes);
    device.screenwidth  = attributes.width;
    device.screenheight = attributes.height;
    x_init(device.display);

    //Xlib: "because XShowCursor(false) would be too easy."
    //create a fully transparent cursor named InvisibleCursor,
    //for use while acquire() / XGrabPointer() is active.
    Pixmap pixmap;
    XColor black, unused;
    static char invisible_data[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
    Colormap colormap = DefaultColormap(device.display, DefaultScreen(device.display));
    XAllocNamedColor(device.display, colormap, "black", &black, &unused);
    pixmap = XCreateBitmapFromData(device.display, settings.handle, invisible_data, 8, 8);
    device.InvisibleCursor = XCreatePixmapCursor(device.display, pixmap, pixmap, &black, &black, 0, 0);
    XFreePixmap(device.display, pixmap);
    XFreeColors(device.display, colormap, &black.pixel, 1, 0);

    device.mouseacquired = false;
    device.relativex = 0;
    device.relativey = 0;

    unsigned joypads = min((unsigned)Joypad::Count, SDL_NumJoysticks());
    for(unsigned i = 0; i < joypads; i++) device.gamepad[i] = SDL_JoystickOpen(i);

    return true;
  }

  void term() {
    unacquire();
    XFreeCursor(device.display, device.InvisibleCursor);

    for(unsigned i = 0; i < Joypad::Count; i++) {
      if(device.gamepad[i]) SDL_JoystickClose(device.gamepad[i]);
      device.gamepad[i] = 0;
    }

    SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
    XCloseDisplay(device.display);
  }

  pInputSDL() {
    for(unsigned i = 0; i < Joypad::Count; i++) device.gamepad[i] = 0;
    settings.handle = 0;
  }
};

DeclareInput(SDL)

};
