/* -*- C -*-
  __   _
  |_) /|  Copyright (C) 2007  |  richard@
  | \/¯|  Richard Atterer     |  atterer.net
  ¯ '` ¯
  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License, version 2. See the file
  COPYING for details.

*/

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <usb.h>
#include <unistd.h>
#include <sys/types.h>

/* #define DEBUG */
#define VERSION "1.0"
/*______________________________________________________________________*/

# define BUFLEN 256
static char buf[BUFLEN];

/* Start control transfer to mouse, with given index. The answer of the mouse
   always appears to be the same 4 bytes for me: 4b 16 17 a0 - we ignore the
   answer. */
int myGetString(usb_dev_handle *dh, int index) {
  int bytes;
# ifdef DEBUG
  fprintf(stderr, "    Querying index %02x - ", index);
# endif
  bytes = usb_get_string(dh, index, 0x409, buf, BUFLEN);
  if (bytes < 0) {
    fputs(usb_strerror(), stderr);
    fputc('\n', stderr);
    if (bytes == -1 && errno == EPERM)
      fputs("You either need to be root or configure udev "
            "to give you access to the device.\n", stderr);
  } else {
#   ifdef DEBUG
    int j;
    fprintf(stderr, "got %d bytes:", bytes);
    for (j = 0; j < bytes; ++j)
      fprintf(stderr, " %02x", (unsigned)(buf[j] & 0xff));
    fputc('\n', stderr);
#   endif
  }
  return bytes;
}
/*______________________________________________________________________*/

/* Slide mouse found, send commands to it. Returns 0 if OK, -1 on error. */
int setupDevice(struct usb_device *dev, int argc, char* argv[]) {
  usb_dev_handle *dh;
  int i;
  
# ifdef DEBUG
  fprintf(stderr, "    Found Hama S1 Gaming Mouse!\n");
# endif
  
  dh = usb_open(dev);

  /* Reverse-engineered values of string indices:

     0x51 UNKNOWN - is issued by Hama software when it starts up
     0xb5 Change functionality of thumb button: Holding down button and using
          scroll wheel cycles through different resolutions
          (button creates no event when clicked, but scrolling creates button
          4 events; 2nd thumb button creates button 3 events.)

     0xb6 Change functionality of thumb button: Button is normal mouse button
          (1st thumb creates button 2, 2nd thumb creates button 3)
     0x08 Switch resolution to 400 dpi, colour blue
     0x0c Switch resolution to 800 dpi, colour green
     0x11 Switch resolution to 1200 dpi, colour cyan
     0x14 Switch resolution to 1600 dpi, colour red

     0xb7 Change functionality of thumb buttons: 2 buttons switch to 2
          different resolutions (1st thumb creates button 4, 2nd no events)
     0x20 Two buttons, two resolutions: 400 / 800 dpi
     0x22 Two buttons, two resolutions: 400 / 1200 dpi
     0x24 Two buttons, two resolutions: 400 / 1600 dpi
     0x2b Two buttons, two resolutions: 800 / 1200 dpi
     0x2d Two buttons, two resolutions: 800 / 1600 dpi
     0x32 Two buttons, two resolutions: 1200 / 1600 dpi

     Bugs:

     1) In 0xb5, scroll wheel both cycles res AND generates "scroll up"
        events for OS.

     2) (Also happens under Windows!) Previous setting is used as basis for
        cycling. If e.g. 1600dpi is set after plugging in, we do 0x0c to
        switch to 800, then cycle down by one resolution, the mouse will
        switch to 1200 dpi ("one down from 1600") instead of 400 ("one down
        from 800").

     3) Mouse remapping does not work, thumb buttons can only cause button
        2/3 clicks. Kernel mouse driver hack probably needed for this.
        xmodmap -e "pointer = 1 2 3 4 5 6 7 8 9 10 11 12"
  */
  for (i = 1; i < argc; ++i) {
    int ret;
    /* // Allow sending arbitrary hex indices:
    sscanf(argv[i], "%x", &index);
    ret = myGetString(dh, index);*/

    // Scroll wheel changes DPI
    if (strcmp(argv[i], "scroll") == 0) {
      ret = myGetString(dh, 0xb5);

    // Fixed DPI
    } else if (strcmp(argv[i], "400") == 0) {
      ret = myGetString(dh, 0xb6);
      if (ret >= 0) ret = myGetString(dh, 0x08);
    } else if (strcmp(argv[i], "800") == 0) {
      ret = myGetString(dh, 0xb6);
      if (ret >= 0) ret = myGetString(dh, 0x0c);
    } else if (strcmp(argv[i], "1200") == 0) {
      ret = myGetString(dh, 0xb6);
      if (ret >= 0) ret = myGetString(dh, 0x11);
    } else if (strcmp(argv[i], "1600") == 0) {
      ret = myGetString(dh, 0xb6);
      if (ret >= 0) ret = myGetString(dh, 0x14);

    // Two different DPI values, switch via thumb buttons
    } else if (strcmp(argv[i], "400+800") == 0) {
      ret = myGetString(dh, 0xb7);
      if (ret >= 0) ret = myGetString(dh, 0x20);
    } else if (strcmp(argv[i], "400+1200") == 0) {
      ret = myGetString(dh, 0xb7);
      if (ret >= 0) ret = myGetString(dh, 0x22);
    } else if (strcmp(argv[i], "400+1600") == 0) {
      ret = myGetString(dh, 0xb7);
      if (ret >= 0) ret = myGetString(dh, 0x24);
    } else if (strcmp(argv[i], "800+1200") == 0) {
      ret = myGetString(dh, 0xb7);
      if (ret >= 0) ret = myGetString(dh, 0x2b);
    } else if (strcmp(argv[i], "800+1600") == 0) {
      ret = myGetString(dh, 0xb7);
      if (ret >= 0) ret = myGetString(dh, 0x2d);
    } else if (strcmp(argv[i], "1200+1600") == 0) {
      ret = myGetString(dh, 0xb7);
      if (ret >= 0) ret = myGetString(dh, 0x32);
    } else {
      fprintf(stderr, "Unknown command '%s'\n", argv[i]);
      ret = -EINVAL;
    }
    if (ret < 0) {
      usb_close(dh);
      return ret;
    }
  }

  if (usb_close(dh) < 0) {
    fprintf(stderr, "usb_close() failed\n");
    return -1;
  }
  return 0;
}
/*______________________________________________________________________*/

/* Scan through all slide mice and attempt to send commands to them. Return 0
   if at least one succeeds, 1 if none such mouse was found, 2 if there was
   an error sending commands. */
int scanAllDevices(uint16_t idVendor, uint16_t idProduct, int argc, char* argv[]) {
  struct usb_bus *busses;
  struct usb_bus *bus;
  int result = 2;
  int found = 0;
  
  busses = usb_get_busses();

  // Scan busses
  for (bus = busses; bus; bus = bus->next) {
    struct usb_device *dev;
#   ifdef DEBUG
    fprintf(stderr, "Scanning bus %s\n", bus->dirname);
#   endif

    for (dev = bus->devices; dev; dev = dev->next) {
#     ifdef DEBUG
      fprintf(stderr, "  Scanning device %04x:%04x: %s\n",
              dev->descriptor.idVendor, dev->descriptor.idProduct,
              dev->filename);
#     endif
      if (dev->descriptor.idVendor == idVendor
          && dev->descriptor.idProduct == idProduct) {
        // Mouse found!
        int ret = setupDevice(dev, argc, argv);
        ++found;
        if (ret == 0) result = 0;
      }
    }
  }
  if (found == 0) {
    fprintf(stderr, "No Hama SLide S1 USB mouse detected! "
                    "(Looked for USB vendor/device %04x:%04x)\n", idVendor, idProduct);
    result = 1;
  }
  return result;
}
/*______________________________________________________________________*/

static const char* const helpMessage = 
"hama-slide-mouse-control version "VERSION"\n"
"Copyright 2007 Richard Atterer, released under GPL v2.\n"
"\n"
"Syntax: hama-slide-mouse-control [-d <idVendor>:<idProduct>] [0 or more commands]\n"
"  -d <idVendor>:<idProduct>\n"
"                Specify USB device (default: -d 056e:001c) - see \"lsusb\" output\n"
"Available commands:\n"
"  scroll        Scroll wheel selects DPI while button 4 is pressed\n"
"  400           Fixed value of 400 dpi (blue), buttons 4+5 act like 2+3\n"
"  800           Fixed value of 800 dpi (green), buttons 4+5 act like 2+3\n"
"  1200          Fixed value of 1200 dpi (cyan), buttons 4+5 act like 2+3\n"
"  1600          Fixed value of 1600 dpi (red), buttons 4+5 act like 2+3\n"
"  400+800       Buttons 4+5 select 400 dpi and 800 dpi\n"
"  400+1200      Buttons 4+5 select 400 dpi and 1200 dpi\n"
"  400+1600      Buttons 4+5 select 400 dpi and 1600 dpi\n"
"  800+1200      Buttons 4+5 select 800 dpi and 1200 dpi\n"
"  800+1600      Buttons 4+5 select 800 dpi and 1600 dpi\n"
"  1200+1600     Buttons 4+5 select 1200 dpi and 1600 dpi\n"
"You can supply several commands, e.g. '800 scroll'.\n"
;

int main(int argc, char* argv[]) {

  if (argc >= 2) {
    if (strcmp(argv[1], "-h") == 0
        || strcmp(argv[1], "--help") == 0) {
      fputs(helpMessage, stdout);
      return 0;
    } else if (strcmp(argv[1], "-v") == 0
               || strcmp(argv[1], "--version") == 0) {
      fputs(VERSION "\n", stdout);
      return 0;
    }
  }

  // Look for any -d option preceding the commands
  unsigned idVendor = 0x056e;
  unsigned idProduct = 0x001c;
  if (argc >= 2 && strcmp(argv[1], "-d") == 0) {
    // -d switch is only allowed if not running suid root
    if (getuid() != geteuid()) {
      fputs("Usage of -d is disallowed when this program is run suid root.\n", stderr);
      return 2;
    }
    if (argc == 2) {
      fputs("Missing argument to -d option.\n", stderr);
      return 2;
    }
    int len = -1;
    sscanf(argv[2], "%4x:%4x%n", &idVendor, &idProduct, &len);
    if (len != (int)strlen(argv[2]) || idVendor == 0) {
      fputs("Argument to -d option has wrong format. "
            "Use \"-d 056e:001c\" or similar - see the output of \"lsusb\".\n", stderr);
      return 2;
    }
    argc -= 2;
    argv += 2;
  }
# ifdef DEBUG
  fprintf(stderr, "Device: %04x:%04x\n", idVendor, idProduct);
# endif

  usb_init();
  usb_find_busses();
  usb_find_devices();

  return scanAllDevices(idVendor, idProduct, argc, argv);
}
