/*
pccam880: A user space video4linux driver for the pccam880 (and
other) USB webcams
(c) Harmen van der Wal, 2004, 2006.
$Id: pccam880.c,v 1.4 2006/08/16 22:24:42 harmen Exp $
License: GPLv2
See http://www.harmwal.nl/pccam880/
*/

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <setjmp.h>
#include <string.h>
#include <unistd.h>
#include <usb.h>
#include <linux/ioctl.h>
#include <linux/videodev.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "jpeg.h"

#define ENOIOCTLCMD 515

#define DBG(fmt,args...)
//#define DBG(fmt,args...) fprintf(stderr, "%s %s (%d): "fmt, __FILE__, __FUNCTION__, __LINE__,##args)
#define WARN(fmt,args...) fprintf(stderr, "%s %s (%d): "fmt, __FILE__, __FUNCTION__, __LINE__,##args)

#define TIMEOUT 5000

struct usb_id {
  int vendor;
  int product;
};

struct usb_message {
  int dir;
  unsigned int value;
  unsigned int index;
  int len;
  unsigned char *buf;
};

struct buffer {
  void *buf;
  int write;
  int read;
  int size;
};

static struct usb_id id[] = {
  {-1, -1},         // dynamic entry
  {0x041e, 0x4024}, // Creative / PC-CAM 880
  // add other usb id here
  {}
};

/* USB sniffed blobs */

// unknown
static unsigned char payload1[] = {0x01, 0x00};
// image size?
static unsigned char payload2[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// compression rate?
static unsigned char payload3[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// unknown
static unsigned char payload4[] = {0x00, 0x00};

//  in value   index   len payload
static struct usb_message init_msg[] = {
  { 0, 0x1f30, 0x0000,  0, NULL },
  { 1, 0xd000, 0x0000,  4, NULL }, // 01 30 00 84
  { 1, 0xf800, 0x0000, 10, NULL }, // 00 52 18 02 9b 01 0e 02 17 00
  { 1, 0x9202, 0x0000,  2, NULL }, // 03 00
  { 1, 0x9203, 0x0000, 12, NULL }, // 40 01 f0 00 a0 00 78 00  80 02 e0 01
  { 0, 0x1f30, 0x0000,  0, NULL },
  { 1, 0xd000, 0x0000,  4, NULL }, // 01 30 00 84
  { 1, 0x0d00, 0x00aa,  2, NULL }, // 00 00
  { 1, 0xbf01, 0x0000,  2, NULL }, // 6c 00
  { 1, 0xbf00, 0x0000,  2, NULL }, // 00 00
  { 1, 0xb601, 0x0000, 10, NULL }, // 00 00 00 02 20 00 b2 03 b0 03
  { 0, 0xae00, 0x0000,  2, payload1}, // 01 00
  { 1, 0xb600, 0x0000, 10, NULL }, // 00 00 00 00 1e 00 ec 00 00 00
  {}
};

static struct usb_message open_msg[] = {
  { 0, 0x1f30, 0x0000,  0, NULL },
  { 1, 0xd000, 0x0000,  4, NULL }, // 01 30 00 84
  { 0, 0x3370, 0x0000, 10, payload2}, // 01 00 00 00 00 00 00 00 00 00
  { 0, 0x2000, 0x0000,  0, NULL },
  { 0, 0x2f0f, 0x0000,  0, NULL },
  { 0, 0x2610, 0x0000,  6, payload3}, // 00 00 00 00 00 00
  { 0, 0xe107, 0x0000,  0, NULL },
  { 0, 0x2502, 0x0000,  0, NULL },
  { 0, 0x1f70, 0x0000,  0, NULL },
  { 1, 0xd000, 0x0000,  4, NULL }, // 01 70 00 84
  { 0, 0x9a01, 0x0000,  2, payload4}, // 00 00
  {}
};

static struct usb_message close_msg[] = {
  { 0, 0x1f30, 0x0000,  0, NULL },
  { 1, 0xd000, 0x0000,  4, NULL }, // 01 30 00 00
  {}
};

/* v4l */

// Other modes the hardware is capable of are currently not supported
// by this driver

struct video_capability cap = {
  .channels = 1,
  .type = VID_TYPE_CAPTURE,
  .maxwidth = 320,
  .maxheight = 240,
  .minwidth = 320,
  .minheight = 240,      
};

struct video_picture pic = {
  .brightness = 0xffff,
  .hue = 0xffff,
  .colour = 0xffff,
  .contrast = 0xffff,
  .whiteness = 0xffff,
  .depth = 24,
  .palette = VIDEO_PALETTE_RGB24,
};

struct video_window window = {
  .width = 320,
  .height = 240,
};

struct video_channel channel = {
  .channel = 0,
  .type = VIDEO_TYPE_CAMERA,
};


/* libjpeg */

struct jpeg_decompress_struct cinfo;
struct my_error_mgr jerr;
struct buffer jpeg;

/* libusb */

usb_dev_handle *udev;

static usb_dev_handle *usb_open_id(struct usb_id id[])
{
  struct usb_bus *bus;
  struct usb_device *dev;
  usb_dev_handle *udev;
  int i;

  DBG("\n");

  usb_init();

  //usb_set_debug(127);

  usb_find_busses();
  usb_find_devices();
    
  bus = usb_get_busses();
  
  for (bus = usb_busses; bus; bus = bus->next) {
    for (dev = bus->devices; dev; dev = dev->next) {
      udev = usb_open(dev);
      if (udev) {
	i = 0;
	while (id[i].vendor != 0) {
	  DBG("%04x/%04x / %04x/%04x\n",
	      dev->descriptor.idVendor, dev->descriptor.idProduct,
	      id[i].vendor, id[i].product);
	  if (dev->descriptor.idVendor == id[i].vendor &&
	      dev->descriptor.idProduct == id[i].product) {
	    usb_claim_interface(udev, 0);
	    usb_get_string_simple(udev, 2, cap.name, sizeof(cap.name));
	    usb_get_string_simple(udev, 2, channel.name, sizeof(cap.name));
	    DBG("Supported camera found: %s\n", cap.name);	    
	    return udev;
	  }
	  i++;
	}
	usb_close(udev);
      }
    }
  }

  return NULL;
}

static int usb_ctrl(usb_dev_handle *udev, struct usb_message *msg)
{
  int result = -1;
  int i = 0;
  char buf[4096];

  if (udev == NULL)
    return result;

  do {

    DBG("%s value=%04x, index=%04x, buf=%p, len=%d\n",
	msg[i].dir ? "rcv" : "snd", 
	msg[i].value, msg[i].index, msg[i].buf, msg[i].len);
    if (msg[i].dir)
      msg[i].buf = buf;

    result = 
      usb_control_msg (udev, 
		       msg[i].dir ?
		       USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN :
		       USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
		       1 & 0xff,
		       msg[i].value, msg[i].index, msg[i].buf, msg[i].len, TIMEOUT);
    
    if (result < 0) {
      WARN("!result %d %s(0x%02x) value 0x%04x index 0x%04x len %d",
	   result, msg[i].dir ? "rcv" : "snd", 
	   msg[i].dir ?
	   USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN:
	   USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
	   msg[i].value, msg[i].index, msg[i].len);

      WARN("\n%s\n", usb_strerror());

      return result;

    } else {
      DBG("result %d %s value 0x%04x index 0x%04x len %d\n",
	  result, msg[i].dir ? "rcv" : "snd", msg[i].value, msg[i].index, msg[i].len);
    }

/*     int ii; */
/*     for (ii = 0; ii < msg[i].len; ii++) { */
/*       fprintf(stderr, " %02x", msg[i].buf[ii]); */
/*     } */
/*     fprintf(stderr, "\n"); */
    
  } while (msg[++i].value != 0);
  
  return result;
}

static int usb_read(usb_dev_handle *udev, void *buf, int count)
{
 DBG("count=%d\n", count);
  int len = usb_bulk_read(udev, 1 & 0xff, buf, count, TIMEOUT);
  DBG("len=%d\n", len);
  if (len < 0)
    WARN("\n%s\n", usb_strerror());    
  return len;
}

/* video format hacks */

// 16 bit endianess.
static void swap(unsigned char *buf, int len)
{
  int i;
  unsigned char c;
  
  for(i = 0; 2 * i + 2 <= len; i++) {
    c = buf[2 * i];
    buf[2 * i] = buf[2 * i + 1];
    buf[2 * i + 1] = c;
    //fprintf(stderr, "%02x ", buf[2 * i]);
    //fprintf(stderr, "%02x ", buf[2 * i + 1]);
  }
}

// de-smurf
static void rgb(unsigned char *buf, int size)
{
  int i;
  unsigned char c;
  for(i = 0; i < size - 3; i += 3) {
    c = buf[i];
    buf[i] = buf[i + 2];
    buf[i + 2] = c;
  }
}

// libjpeg fill_input_buffer callback
static int pccam880_read(void *buf, int count, void *arg)
{
  int len = 0, available;

  DBG("count=%d, size=%d\n", count, jpeg.size);

  // test with the jpeg image only
/*   memcpy(buf, jpeg.buf, count); */
/*   if (1) */
/*     return jpeg.size; */

  if (jpeg.write == jpeg.read) {
    jpeg.write = 0;
    jpeg.read = 0;
  }

  // Overwrite the jpeg image with live video data
  if (jpeg.write == 0) {
    len = usb_read(udev, jpeg.buf + 31, 64);
    DBG("len=%d\n", len);
    if (len < 64)
      return len;
    swap(jpeg.buf + 31, len);
    len = usb_read(udev, jpeg.buf + 96, 64);
    DBG("len=%d\n", len);
    if (len < 64)
      return len;
    swap(jpeg.buf + 96, len);
    jpeg.write = 613;
    while (1) {
      if (jpeg.size - jpeg.write < 4096) {
	jpeg.size += 4096 - jpeg.size + jpeg.write;
	jpeg.buf = realloc(jpeg.buf, jpeg.size);
      }
      len = usb_read(udev, jpeg.buf + jpeg.write, 4096);
      DBG("len=%d\n", len);
      if (len < 1)
	return len;
      swap(jpeg.buf + jpeg.write, len);

      jpeg.write += len;
      if (len < 4096)
	break;
    }
  }

  available = jpeg.write - jpeg.read;
  DBG("available=%d\n", available);
  if (available < count)
    count = available;

  DBG("jpeg.size=%d\n", jpeg.size);


  memcpy(buf, jpeg.buf + jpeg.read, count);
  jpeg.read += count;

  return count;
}

/* file io */

int shim_open()
{
  int fd, len, result = -1;
  char *str;

  DBG("\n");

  // dynamic usb id
  str = getenv("USB_VID");
  DBG("%s\n",str);
  if (str)
    id[0].vendor = strtol(str, NULL, 16);
  str = getenv("USB_PID");
  DBG("%s\n",str);
  if (str)
    id[0].product = strtol(str, NULL, 16);
  DBG("dynamic usb id 0x%04x/0x%04x\n", id[0].vendor, id[0].product);

  udev = usb_open_id(id);
  if (udev == NULL) {
    return -1;
  }

  DBG("\n");

  result = usb_ctrl(udev, init_msg);
  DBG("result=%d\n", result);
  if (result >= 0) {
    result = usb_ctrl(udev, open_msg);
    DBG("result=%d\n", result);
  }
  if (result < 0)
    return result;

  // Read jpeg headers from a jpeg image.
  str = getenv("JPEG");
  fd = open(str, O_RDONLY);
  if (fd < 0) {
    perror("open");
    usb_close(udev);
    return fd;
  }

  while (1) {
    if (jpeg.size - jpeg.write < 4096) {
      jpeg.size += 4096 - jpeg.size + jpeg.write;
      jpeg.buf = realloc(jpeg.buf, jpeg.size);
    }
    len = read(fd, jpeg.buf + jpeg.write, 4096);
    if (len < 1)
      break;
    jpeg.write += len;
  }
  jpeg.read = jpeg.write;
  close(fd);
  if (len < 0) {
    WARN("error reading file: %s", strerror(errno));
    return len;
  }
  if (jpeg.write < 631) {
    WARN("jpeg file too small %d", jpeg.write);
    return -1;
  }

  jpeg_init(&cinfo, &jerr, pccam880_read, NULL);

  DBG("result=%d\n", result);

  return result;
}

int shim_ioctl(unsigned long int request, char *argp){

  int result = 0;

  DBG("request=%d nr %d\n", (int)request, (int)_IOC_NR(request));

  switch(request) {

  case VIDIOCGCAP:
    /* Get capabilities */
    {
      DBG("\n");
      memcpy(argp, &cap, sizeof(struct video_capability));
      break;
    }
  case VIDIOCGCHAN:
      /* Get channel info (sources) */
      DBG("\n");
      memcpy(argp, &channel, sizeof(struct video_channel));
      break;
  case VIDIOCSCHAN:
    /* Set channel  */
    if (((struct video_channel *)argp)->channel != 0) {
      errno = EINVAL;
      result = -1;
    }
    break;
  case VIDIOCGTUNER:
    /* Get tuner abilities */
  case VIDIOCSTUNER:
    /* Tune the tuner for the current channel */
  case VIDIOCGPICT:
    /* Get picture properties */
    {
      DBG("\n");
      memcpy(argp, &pic, sizeof(struct video_picture));
      break;
    }
  case VIDIOCSPICT:
    /* Set picture properties */
    DBG("\n");
    if (memcmp(argp, &pic, sizeof(struct video_picture))) {
      errno = EINVAL;
      result = -1;
    }
    break;
  case VIDIOCCAPTURE:
    /* Start, end capture */
  case VIDIOCGWIN:
    /* Get the video overlay window */
    DBG("\n");
    memcpy(argp, &window, sizeof(struct video_window));
    break;
  case VIDIOCSWIN:
    /* Set the video overlay window - passes clip list for hardware smarts , chromakey etc */
    DBG("\n");
    if (memcmp(argp, &window, sizeof(struct video_window))) {
      errno = EINVAL;
      result = -1;
    }
    break;
  case VIDIOCGFBUF:
    /* Get frame buffer */
  case VIDIOCSFBUF:
     /* Set frame buffer - root only */
  case VIDIOCKEY:
    /* Video key event - to dev 255 is to all - cuts capture on all DMA windows with this key (0xFFFFFFFF == all) */
  case VIDIOCGFREQ:
    /* Set tuner */
  case VIDIOCSFREQ:
    /* Set tuner */
  case VIDIOCGAUDIO:
    /* Get audio info */
  case VIDIOCSAUDIO:
    /* Audio source, mute etc */
  case VIDIOCSYNC:
      /* Sync with mmap grabbing */
  case VIDIOCMCAPTURE:
    /* Grab frames */
  case VIDIOCGMBUF:
    /* Memory map buffer info */
  case VIDIOCGUNIT:
    /* Get attached units */
  case VIDIOCGCAPTURE:
    /* Get subcapture */
  case VIDIOCSCAPTURE:
    /* Set subcapture */
  case VIDIOCSPLAYMODE:
    /* Set output video mode/feature */
  case VIDIOCSWRITEMODE:
    /* Set write mode */
  case VIDIOCGPLAYINFO:
    /* Get current playback info from hardware */
  case VIDIOCSMICROCODE:
    /* Load microcode into hardware */
  case VIDIOCGVBIFMT:
    /* Get VBI information */
  case VIDIOCSVBIFMT:
    /* Set VBI information */
    errno = EINVAL;
    result = -1;
    break;

  default:
    DBG("\n");
    errno = ENOIOCTLCMD;
    result = -1;
 }

  DBG("result=%d error=%d %s\n", result, errno, strerror(errno));

  return result;
}

ssize_t shim_read(void *buf, size_t count)
{
  int len;

  // My camera throws lots of messed up frames at me.
  // I let libjpeg catch 'em.
  if (setjmp(jerr.setjmp_buffer)) {
    jpeg_cleanup(&cinfo);
    jpeg_init(&cinfo, &jerr, pccam880_read, NULL);
  }
  len = jpeg_read(&cinfo, buf, count);
  rgb(buf, count); // Maybe this could be set this straight earlier.
  return len;
}

int shim_close(){
  jpeg_cleanup(&cinfo);
  usb_ctrl(udev, close_msg);
  return usb_close(udev);
}
