/*         ______   ___    ___
 *        /\  _  \ /\_ \  /\_ \
 *        \ \ \L\ \\//\ \ \//\ \      __     __   _ __   ___
 *         \ \  __ \ \ \ \  \ \ \   /'__`\ /'_ `\/\`'__\/ __`\
 *          \ \ \/\ \ \_\ \_ \_\ \_/\  __//\ \L\ \ \ \//\ \L\ \
 *           \ \_\ \_\/\____\/\____\ \____\ \____ \ \_\\ \____/
 *            \/_/\/_/\/____/\/____/\/____/\/___L\ \/_/ \/___/
 *                                           /\____/
 *                                           \_/__/
 *
 *      X-Windows mouse module.
 *
 *      By Peter Wang.
 *
 *      Original by Michael Bukin.
 *
 *      See readme.txt for copyright information.
 */


#define ALLEGRO_NO_COMPATIBILITY

#include "allegro5/allegro5.h"
#include "allegro5/internal/aintern.h"
#include "allegro5/internal/aintern_mouse.h"
#include "xglx.h"



typedef struct ALLEGRO_MOUSE_XWIN
{
   ALLEGRO_MOUSE parent;
   ALLEGRO_MOUSE_STATE state;
   int min_x, min_y;
   int max_x, max_y;
} ALLEGRO_MOUSE_XWIN;



static bool xmouse_installed = false;

/* the one and only mouse object */
static ALLEGRO_MOUSE_XWIN the_mouse;



/* forward declarations */
static bool xmouse_init(void);
static void xmouse_exit(void);
static ALLEGRO_MOUSE *xmouse_get_mouse(void);
static unsigned int xmouse_get_mouse_num_buttons(void);
static unsigned int xmouse_get_mouse_num_axes(void);
static bool xmouse_set_mouse_xy(int x, int y);
static bool xmouse_set_mouse_axis(int which, int z);
static bool xmouse_set_mouse_range(int x1, int y1, int x2, int y2);
static void xmouse_get_state(ALLEGRO_MOUSE_STATE *ret_state);

static void wheel_motion_handler(int dz, ALLEGRO_DISPLAY *display);
static int x_button_to_wheel(unsigned int x_button);
static unsigned int x_button_to_al_button(unsigned int x_button);
static void generate_mouse_event(unsigned int type,
                                 int x, int y, int z,
                                 int dx, int dy, int dz,
                                 unsigned int button,
                                 ALLEGRO_DISPLAY *display);



/* the driver vtable */
#define MOUSEDRV_XWIN  AL_ID('X','W','I','N')

static ALLEGRO_MOUSE_DRIVER mousedrv_xwin =
{
   MOUSEDRV_XWIN,
   "",
   "",
   "X-Windows mouse",
   xmouse_init,
   xmouse_exit,
   xmouse_get_mouse,
   xmouse_get_mouse_num_buttons,
   xmouse_get_mouse_num_axes,
   xmouse_set_mouse_xy,
   xmouse_set_mouse_axis,
   xmouse_set_mouse_range,
   xmouse_get_state
};



/* list the available drivers */
_DRIVER_INFO _al_xwin_mouse_driver_list[] =
{
   {  MOUSEDRV_XWIN, &mousedrv_xwin, true  },
   {  0,             NULL,           false }
};



/* xmouse_init:
 *  Initialise the driver.
 */
static bool xmouse_init(void)
{
   //FIXME
   //_al_xwin_enable_hardware_cursor(true);

   memset(&the_mouse, 0, sizeof the_mouse);

   _al_event_source_init(&the_mouse.parent.es);

   xmouse_installed = true;

   return true;
}



/* xmouse_exit:
 *  Shut down the mouse driver.
 */
static void xmouse_exit(void)
{
   if (!xmouse_installed)
      return;
   xmouse_installed = false;

   /* ... */

   _al_event_source_free(&the_mouse.parent.es);
}



/* xmouse_get_mouse:
 *  Returns the address of a ALLEGRO_MOUSE structure representing the mouse.
 */
static ALLEGRO_MOUSE *xmouse_get_mouse(void)
{
   ASSERT(xmouse_installed);

   return (ALLEGRO_MOUSE *)&the_mouse;
}



/* xmouse_get_mouse_num_buttons:
 *  Return the number of buttons on the mouse.
 */
static unsigned int xmouse_get_mouse_num_buttons(void)
{
   int num_buttons;
   unsigned char map[8];
   ALLEGRO_SYSTEM_XGLX *system = (void *)al_system_driver();

   ASSERT(xmouse_installed);

   _al_mutex_lock(&system->lock);
   num_buttons = XGetPointerMapping(system->x11display, map, sizeof(map));
   _al_mutex_unlock(&system->lock);

   if (num_buttons < 2) num_buttons = 2;
   if (num_buttons > 3) num_buttons = 3;

   return num_buttons;
}



/* xmouse_get_mouse_num_axes:
 *  Return the number of axes on the mouse.
 */
static unsigned int xmouse_get_mouse_num_axes(void)
{
   ASSERT(xmouse_installed);

   /* XXX get the right number later */
   return 3;
}



/* xmouse_set_mouse_xy:
 *  Set the mouse position.  Return true if successful.
 */
static bool xmouse_set_mouse_xy(int x, int y)
{
   if (!xmouse_installed)
      return false;

   // TODO: Multi display warps.

   ALLEGRO_SYSTEM_XGLX *system = (void *)al_system_driver();
   Display *display = system->x11display;
   ALLEGRO_DISPLAY_XGLX *d = (void *)al_get_current_display();

   int window_width = al_get_display_width();
   int window_height = al_get_display_height();
   if (x < 0 || y < 0 || x >= window_width || y >= window_height)
      return false;

   _al_mutex_lock(&system->lock);

   /* We prepend the mouse motion event generated by XWarpPointer
    * with our own event to distinguish real and generated
    * events.
    */
   XEvent event;
   memset(&event, 0, sizeof event);
   event.xclient.type = ClientMessage;
   event.xclient.serial = 0;
   event.xclient.send_event = True;
   event.xclient.display = display;
   event.xclient.window = d->window;
   event.xclient.message_type = system->AllegroAtom;
   event.xclient.format = 32;
   XSendEvent(display, d->window, False, NoEventMask, &event);

   XWarpPointer(display, None, d->window, 0, 0, 0, 0, x, y);
   _al_mutex_unlock(&system->lock);

   return true;
}



/* xmouse_set_mouse_axis:
 *  Set the mouse wheel position.  Return true if successful.
 */
static bool xmouse_set_mouse_axis(int which, int z)
{
   ASSERT(xmouse_installed);

   if (which != 2) {
      return false;
   }

   _al_event_source_lock(&the_mouse.parent.es);
   {
      int dz = (z - the_mouse.state.z);

      if (dz != 0) {
         the_mouse.state.z = z;

         generate_mouse_event(
            ALLEGRO_EVENT_MOUSE_AXES,
            the_mouse.state.x, the_mouse.state.y, the_mouse.state.z,
            0, 0, dz,
            0, NULL);
      }
   }
   _al_event_source_unlock(&the_mouse.parent.es);

   return true;
}



/* xmouse_set_mouse_range:
 */
static bool xmouse_set_mouse_range(int x1, int y1, int x2, int y2)
{
   // TODO
   (void)x1;
   (void)y1;
   (void)x2;
   (void)y2;
   return false;
}



/* xmouse_get_state:
 *  Copy the current mouse state into RET_STATE, with any necessary locking.
 */
static void xmouse_get_state(ALLEGRO_MOUSE_STATE *ret_state)
{
   ASSERT(xmouse_installed);

   _al_event_source_lock(&the_mouse.parent.es);
   {
      *ret_state = the_mouse.state;
   }
   _al_event_source_unlock(&the_mouse.parent.es);
}



/* _al_xwin_mouse_button_press_handler: [bgman thread]
 *  Called by _xwin_process_event() for ButtonPress events received from the X
 *  server.
 */
void _al_xwin_mouse_button_press_handler(int x_button,
   ALLEGRO_DISPLAY *display)
{
   int dz;
   unsigned int al_button;

   if (!xmouse_installed)
      return;

   /* Is this event actually generated for a mouse wheel movement? */
   /* TODO secondary wheel support */
   dz = x_button_to_wheel(x_button);
   if (dz != 0) {
      wheel_motion_handler(dz, display);
      return;
   }

   /* Is this button supported by the Allegro API? */
   al_button = x_button_to_al_button(x_button);
   if (al_button == 0)
      return;

   _al_event_source_lock(&the_mouse.parent.es);
   {
      the_mouse.state.buttons |= (1 << (al_button - 1));

      generate_mouse_event(
         ALLEGRO_EVENT_MOUSE_BUTTON_DOWN,
         the_mouse.state.x, the_mouse.state.y, the_mouse.state.z,
         0, 0, 0,
         al_button, display);
   }
   _al_event_source_unlock(&the_mouse.parent.es);
}



/* wheel_motion_handler: [bgman thread]
 *  Called by _al_xwin_mouse_button_press_handler() if the ButtonPress event
 *  received from the X server is actually for a mouse wheel movement.
 */
static void wheel_motion_handler(int dz, ALLEGRO_DISPLAY *display)
{
   _al_event_source_lock(&the_mouse.parent.es);
   {
      the_mouse.state.z += dz;

      generate_mouse_event(
         ALLEGRO_EVENT_MOUSE_AXES,
         the_mouse.state.x, the_mouse.state.y, the_mouse.state.z,
         0, 0, dz,
         0, display);
   }
   _al_event_source_unlock(&the_mouse.parent.es);
}



/* _al_xwin_mouse_button_release_handler: [bgman thread]
 *  Called by _xwin_process_event() for ButtonRelease events received from the
 *  X server.
 */
void _al_xwin_mouse_button_release_handler(int x_button,
   ALLEGRO_DISPLAY *display)
{
   int al_button;

   if (!xmouse_installed)
      return;

   al_button = x_button_to_al_button(x_button);
   if (al_button == 0)
      return;

   _al_event_source_lock(&the_mouse.parent.es);
   {
      the_mouse.state.buttons &=~ (1 << (al_button - 1));

      generate_mouse_event(
         ALLEGRO_EVENT_MOUSE_BUTTON_UP,
         the_mouse.state.x, the_mouse.state.y, the_mouse.state.z,
         0, 0, 0,
         al_button, display);
   }
   _al_event_source_unlock(&the_mouse.parent.es);
}



/* _al_xwin_mouse_motion_notify_handler: [bgman thread]
 *  Called by _xwin_process_event() for MotionNotify events received from the X
 *  server.
 */
void _al_xwin_mouse_motion_notify_handler(int x, int y,
   ALLEGRO_DISPLAY *display)
{
   ALLEGRO_DISPLAY_XGLX *glx = (void *)display;
   int event_type = ALLEGRO_EVENT_MOUSE_AXES;

   if (!xmouse_installed)
      return;

   /* Is this an event generated in response to al_set_mouse_xy? */
   if (glx->mouse_warp) {
      glx->mouse_warp = false;
      event_type = ALLEGRO_EVENT_MOUSE_WARPED;
   }

   _al_event_source_lock(&the_mouse.parent.es);
   int dx = x - the_mouse.state.x;
   int dy = y - the_mouse.state.y;

   the_mouse.state.x = x;
   the_mouse.state.y = y;

   generate_mouse_event(
      event_type,
      the_mouse.state.x, the_mouse.state.y, the_mouse.state.z,
      dx, dy, 0,
      0, display);

   _al_event_source_unlock(&the_mouse.parent.es);
}



/* x_button_to_wheel: [bgman thread]
 *  Return a non-zero number if an X mouse button number corresponds to the
 *  mouse wheel scrolling, otherwise return 0.
 */
static int x_button_to_wheel(unsigned int x_button)
{
   switch (x_button) {
      case Button4:
         return +1;
      case Button5:
         return -1;
      default:
         /* not a wheel */
         return 0;
   }
}



/* x_button_to_al_button: [bgman thread]
 *  Map a X button number to an Allegro button number.
 */
static unsigned int x_button_to_al_button(unsigned int x_button)
{
   switch (x_button) {
      case Button1:
         return 1;
      case Button2:
         return 3;
      case Button3:
         return 2;
      default:
         /* unknown or wheel */
         return 0;
   }
}



/* generate_mouse_event: [bgman thread]
 *  Helper to generate a mouse event.
 */
static void generate_mouse_event(unsigned int type,
                                 int x, int y, int z,
                                 int dx, int dy, int dz,
                                 unsigned int button,
                                 ALLEGRO_DISPLAY *display)
{
   ALLEGRO_EVENT event;

   if (!_al_event_source_needs_to_generate_event(&the_mouse.parent.es))
      return;

   event.mouse.type = type;
   event.mouse.timestamp = al_current_time();
   event.mouse.display = display;
   event.mouse.x = x;
   event.mouse.y = y;
   event.mouse.z = z;
   event.mouse.dx = dx;
   event.mouse.dy = dy;
   event.mouse.dz = dz;
   event.mouse.button = button;
   _al_event_source_emit_event(&the_mouse.parent.es, &event);
}



/* _al_xwin_mouse_switch_handler:
 *  Handle a focus switch event.
 */
void _al_xwin_mouse_switch_handler(ALLEGRO_DISPLAY *display,
   const XCrossingEvent *event)
{
   /* Ignore events where any of the buttons are held down. */
   if (event->state & (Button1Mask | Button2Mask | Button3Mask |
         Button4Mask | Button5Mask)) {
      return;
   }

   _al_event_source_lock(&the_mouse.parent.es);

   switch (event->type) {
      case EnterNotify:
         the_mouse.state.display = display;
         break;
      case LeaveNotify:
         the_mouse.state.display = NULL;
         break;
   }

   _al_event_source_unlock(&the_mouse.parent.es);
}



/*
 * Local Variables:
 * c-basic-offset: 3
 * indent-tabs-mode: nil
 * End:
 */
/* vim: set sts=3 sw=3 et: */
