/****************************************************************************
 *
 *  $RCSfile: acdctl.c,v $
 *
 *  acdctl - apple cinema display control
 *  Original Author: Caskey Dickson <caskey@technocage.com> 2005-07-09
 *  Modifications: Michael Hanselmann <acdctl@hansmi.ch> 2005-08-09
 *  Copyright 2005 TechnoCage, Inc.  All Rights Reserved
 *  $Id: acdctl.c,v 1.4 2005/08/14 12:46:20 caskey Exp $
 *
 *   This program is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU General Public License
 *   as published by the Free Software Foundation; either version 2
 *   of the License, or (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software Foundation, 
 *   Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 *****************************************************************************/


#include <usb.h> 
#include <string.h> 
#include <stdint.h> 
#include <getopt.h> 
#include <asm/errno.h> 
#include <stdio.h> 

#define VERSION_MAJOR 1
#define VERSION_MINOR 1

#define ACD_BRIGHTNESS_CONTROL 0x10
#define ACD_BRIGHTNESS_MAX 255

#define APPLE_VENDOR 0x05ac

#define USB_TIMEOUT (10 * 1000) // At HZ = 1000, this are 10 seconds

int debug = 0;
int verbose = 0;
int list_displays_mode = 0;
int set_brightness_mode = 0;
int show_brightness_mode = 0;
int config_brightness = 127;

#define X214_MAX 4
int get_x214_mode = 0;
int set_x214_mode = 0;
int set_x214_data = 0;

#define X228_MAX 255
int get_x228_mode = 0;
int set_x228_mode = 0;
int set_x228_data = 0;

#define X231_MAX 3
int get_x231_mode = 0;
int set_x231_mode = 0;
int set_x231_data = 0;


/**
 * A structure containing details about supported displays.
 * Since apple lieks to re-use product names, the year field is used
 * to distinguish between them.
 */
struct display_dev {
  int vendor;
  int product;
  const char* description;
  int year;
};

struct display_dev displays[] = {
  { APPLE_VENDOR, 0x9218, "Apple Cinema HD Display 23\"", 2002 },
  { APPLE_VENDOR, 0x921d, "Apple Cinema Display 20\"", 0 },
  { 0, 0, "User Specified", 0 },
  { 0, 0, NULL, 0 },
};

ssize_t userDisplayIndex = (sizeof(displays)/sizeof(struct display_dev)) - 2;

void title() {
  printf(
  "\n"
  " acdctl %d.%d - apple cinema display controller\n"
  "\n"
  "  This tool is designed to control the brightness settings of an\n"
  "  Apple Cinema Display via its USB interface.\n"
  "  Copyright 2005 by TechnoCage, Inc.  All Rights Reserved\n"
  "    <http://www.technocage.com/~caskey/acdctl/>\n"
  "\n"
  , VERSION_MAJOR, VERSION_MINOR
  );
  }
void license() {
  title();
  printf(
 "   This program is free software; you can redistribute it and/or\n"
 "   modify it under the terms of the GNU General Public License\n"
 "   as published by the Free Software Foundation; either version 2\n"
 "   of the License, or (at your option) any later version.\n"
 "\n"
 "   This program is distributed in the hope that it will be useful,\n"
 "   but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
 "   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
 "   GNU General Public License for more details.\n"
 "\n"
 "   You should have received a copy of the GNU General Public License\n"
 "   along with this program; if not, write to the Free Software Foundation, \n"
 "   Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\n"
  "\n"
 );
}

void listSupportedDisplays() {
  struct display_dev* next = displays;
  ssize_t displayCount = 0;

  printf("\nSupported Displays:\n\n");
  while(next->description && next->vendor && next->product) {
    displayCount++;
    printf((next->year?"  ID:%04x:%04x  %s (%d)\n":"  ID:%04x:%04x  %s\n"),
      next->vendor, next->product, next->description, next->year);
    next++;
  }
  printf("\n%d displays supported\n", displayCount);
}

void help(const char* program_name) {
  title();
  printf("Usage: %s <options>\n", program_name);
  printf(
  "\n"
  "  --list                 list all the ACDs that can be found\n"
  "  --brightness[=value]   set display brightness of ACD to value\n"
  "                         w/o value, current setting will be displayed\n"
  "\n"
  "  --brief                disable verbose mode (default)\n"
  "  --verbose              extra detail printed\n"
  "  --debug                lots of detail printed (does not set verbose)\n"
  "  --help                 this message\n"
  "  --license              display this software's license\n"
  "\n"
  " These options are experimental and have no discernable effect on the display\n"
  "  --x214[=value]         display or set report 214 to given value\n"
  "  --x231[=value]         display or set report 231 to given value\n"
  "  --x228[=value]         display or set report 228 to given value\n"
  "\n"
      );
  listSupportedDisplays();
}

/**
 * Set the control ID on the given device to the data array presented.
 * Assumes 2 byte array.
 */
int set_acd_control(struct usb_dev_handle* dev, int control, uint8_t* data) {
  int ret;

  int value;
  
  int hibyte = 0x0300;

  // Control 228 appears to require hibyte set to 0x0100
  if(control == 228) hibyte = 0x0100;

  value = hibyte | control;

  if(debug) {
    printf("Setting 0x%02x\n", control);
  }
  
  /* Read the current values out */
  ret = usb_control_msg(dev, 
      0x21,                 /* req. type 10100001 == GET_REPORT_REQUEST */
      0x09,                 /* request == GET_REPORT */
      value,                /* value  0x0300 = FEATURE & 0x10 = (Brightness)*/
      0,                    /* index (interface) */
      (char*)data,          /* bytes */
      2,                    /* size */
      USB_TIMEOUT);         /* timeout 0 = forever*/

  if(ret < 0) {
    fprintf(stderr, "Failed to send control message [0x%04x]: %d: %s\n",
        value, ret, strerror(-ret));
    return 0;
  }

  if(debug) {
    printf("Wrote %d bytes [%02x][%02x].\n", ret, data[0], data[1]);
  }

  return ret;
}

int get_acd_control(struct usb_dev_handle* dev, int control, uint8_t* data) {
  int hibyte = 0x0300;
  int ret, value;

  //if(control == 228) hibyte = 0x0100;

  value = hibyte | control;

  if(debug) {
    printf("Reading 0x%02x\n", control);
  }

  usb_clear_halt(dev, 0x81);

  /* Read the current values */
  ret = usb_control_msg(dev, 
              0xa1,     /* req. type 10100001 == GET_REPORT_REQUEST */
              0x01,     /* request == GET_REPORT */
              value,    /* value  0x0300 = FEATURE & 0x10 = (Brightness)*/
              0,      /* index (interface) */
              (char*)data,  /* bytes */
              2,      /* size */
              USB_TIMEOUT); /* timeout 0 = forever*/
  
  if(ret < 0) {
    fprintf(stderr, "Failed to send control message [0x%04x]: %d: %s\n",
        value, ret, strerror(-ret));
    return -1;
  }
  
  if(debug) {
    printf("Read %d bytes [%02x][%02x].\n",
         ret, data[0], data[1]);
  }

  return ret;
}

int get_acd_brightness(struct usb_dev_handle *dev) {
  uint8_t control_data[2];
  int ret;

  ret = get_acd_control(dev, ACD_BRIGHTNESS_CONTROL, control_data);
  if(ret <= 0) {
    return -1;
  }

  return control_data[1];
}

void set_acd_brightness(struct usb_dev_handle *dev, int level) {
  uint8_t control_data[2];
  int ret;

  if(level < 0 || level > ACD_BRIGHTNESS_MAX) {
    fprintf(stderr, 
        "Warning: Brightness (%d) out of range (0-%d). Using %d instead.\n",
        level, ACD_BRIGHTNESS_MAX, ACD_BRIGHTNESS_MAX);
    level = ACD_BRIGHTNESS_MAX;
  }

  /* Read the current values out to load control_data structure */
  ret = get_acd_control(dev, ACD_BRIGHTNESS_CONTROL, control_data);
  if(ret < 0) {
    return;
  }

  // set new brightness in 2nd byte
  control_data[1] = level;

  // discarding return value of set
  set_acd_control(dev, ACD_BRIGHTNESS_CONTROL, control_data);

  return;
}

void found_display(int index, struct usb_device *dev) {
  int ret;
  usb_dev_handle* display_device = usb_open(dev);

  if(display_device) {
//    ret = usb_claim_interface(display_device, 0);
//    if(ret < 0) {
//      fprintf(stderr, "Failed to claim interface 0: %s\n", strerror(-ret));
//      return;
//    }
  
    if(list_displays_mode) {
      printf("%04x:%04x %s (%d) on USB bus %s device %s \n",
          displays[index].vendor,
          displays[index].product,
          displays[index].description,
          displays[index].year,
          dev->bus->dirname, dev->filename
          );
    }

    uint8_t data[2];

    if(get_x214_mode) {
      if ((ret = get_acd_control(display_device, 214, data))) {
        printf("x214: [0x%02x][0x%02x] (max: 0x%02x)\n",
             data[0], data[1], X214_MAX);
      }
    }
    if(set_x214_mode && (ret ||
        (ret = get_acd_control(display_device, 214, data)))) {
      data[1] = set_x214_data;
      ret = set_acd_control(display_device, 214, data);
    }

    if(get_x228_mode) {
      if((ret =  get_acd_control(display_device, 0xe4, data))) {
        printf("x228: [0x%02x][0x%02x] (max: 0x%02x)\n",
             data[0], data[1], X228_MAX);
      }
    }
    if(set_x228_mode && (ret ||
        (ret = get_acd_control(display_device, 228, data)))) {
      data[1] = set_x228_data;
      ret = set_acd_control(display_device, 0xe4, data);
    }

    if(get_x231_mode) {
      if ((ret = get_acd_control(display_device, 231, data))) {
        printf("x231: [0x%02x][0x%02x] (max: 0x%02x)\n",
             data[0], data[1], X231_MAX);
      }
    }
    if(set_x231_mode && (ret ||
        (ret = get_acd_control(display_device, 231, data)))) {
      data[1] = set_x231_data;
      ret = set_acd_control(display_device, 231, data);
    }

    if(show_brightness_mode || set_brightness_mode) {
      if(verbose || show_brightness_mode) {
        printf("  Current brightness of display at %s:%s = %d\n",
             dev->bus->dirname, dev->filename,
             get_acd_brightness(display_device));
      }
    }
    if(set_brightness_mode) {
      if(verbose) {
        printf("  Setting brightness of display at %s:%s to %d\n",
             dev->bus->dirname, dev->filename,
             config_brightness);
      }

      set_acd_brightness(display_device, config_brightness);

      if(verbose) {
        printf("  New brightness of display at %s:%s = %d\n",
             dev->bus->dirname, dev->filename,
             get_acd_brightness(display_device));
      }
    }

    usb_close(display_device);
  }
}

int parse_argv(int argc, char **argv) {
  static struct option long_options[] = {
    /* These options set a flag. */
    { "verbose", no_argument, &verbose, 1 },
    { "brief",   no_argument, &verbose, 0 },
    { "debug", no_argument, &debug, 1 },

    { "list", no_argument, &list_displays_mode, 1 },

    { "help",  no_argument, 0, 'h' },
    { "license",  no_argument, 0, 'l' },

    { "vendor",  required_argument, 0, 'V' },
    { "product",  required_argument, 0, 'P' },

    /* These options don't set a flag.
       We distinguish them by their indices. */
    { "brightness",  optional_argument, 0, 'b' },
    { "x214",  optional_argument, 0, 'x' },
    { "x231",  optional_argument, 0, 'y' },
    { "x228",  optional_argument, 0, 'z' },
    { 0, 0, 0, 0 }
  };

  for(;;) {
    /* getopt_long stores the option index here. */
    int option_index = 0;
    int c = getopt_long(argc, argv, "abc:d:f:h", long_options, &option_index);

    /* Detect the end of the options. */
    if(c == -1) {
      break;
    }

    switch(c) {
      case 0:
        /* If this option set a flag, do nothing else now. */
        if(long_options[option_index].flag != 0) {
          break;
        }
        printf("option %s", long_options[option_index].name);
        if(optarg) {
          printf(" with arg %s", optarg);
        }
        putchar('\n');
        break;
      case 'V':
        displays[userDisplayIndex].vendor = strtol(optarg, 0, 16);
        break;
      case 'P':
        displays[userDisplayIndex].product = strtol(optarg, 0, 16);
        break;
      case 'b':
        if(optarg) {
          set_brightness_mode = 1;
          config_brightness = strtol(optarg, 0, 10);
        }else{
          show_brightness_mode = 1;
        }
        break;

      case 'l':
        license();
        break;

      case 'h':
        help(argv[0]);
        return 0;

      case 'x':
        if(optarg) {
          set_x214_mode = 1;
          set_x214_data = strtol(optarg, 0, 10);
        }else{
          get_x214_mode = 1;
        }
        break;

      case 'y':
        if(optarg) {
          set_x231_mode = 1;
          set_x231_data = strtol(optarg, 0, 10);
        }else{
          get_x231_mode = 1;
        }
        break;

      case 'z':
        if(optarg) {
          set_x228_mode = 1;
          set_x228_data = strtol(optarg, 0, 10);
        }else{
          get_x228_mode = 1;
        }
        break;

      case '?':
         /* getopt_long already printed an error message. */
         break;

      default:
         return 0;
    }
  }

  /* Print any remaining command line arguments (not options). */
  if(optind < argc) {
    printf("Non-option ARGV-elements: ");
    while(optind < argc)
      printf ("%s ", argv[optind++]);
    putchar('\n');
    return 0;
  }

  return 1;
}

int main(int argc, char **argv) {
  struct usb_bus* busses;
  struct usb_bus* bus;

  if(!parse_argv(argc, argv)) return 1;

  // usb_set_debug(255);

  usb_init();
  usb_find_busses();
  usb_find_devices();
  busses = usb_get_busses();

  if(verbose) {
    puts("Searching for USB devices ...");
  }

  for(bus = busses; bus; bus = bus->next) {
    struct usb_device* dev;
  
    for(dev = bus->devices; dev; dev = dev->next) {
      if(dev->descriptor.bDeviceClass == USB_CLASS_PER_INTERFACE) {
        int i = 0;
        for(; displays[i].vendor && displays[i].product; ++i) {
          if(dev->descriptor.idVendor == displays[i].vendor &&
             dev->descriptor.idProduct == displays[i].product) {
            found_display(i, dev);
          }
        }
      }
    }
  }

  if(verbose) {
    puts("Finished.");
  }

  return 0;
}
