/* -*- linux-c -*-
 * vicam.c - ViCAM/3Com HomeConnect USB Camera Driver
 *
 * Copyright (c) 2002 John Tyner <jtyner@cs.ucr.edu>
 *
 * This driver is for the 3Com HomeConnect USB Camera and
 * as far as I know, the Vista Imaging ViCAM Camera. The
 * code is based on the driver created by the team at
 * homeconnectusb.sourceforge.net. They deserve the credit
 * for the decoding algorithm and for figuring out what values
 * and messages need to be sent to the camera and what data
 * is actually coming back.
 *
 * This file is woefully underdocumented and doesn't print
 * any information. I apologize for that, but the driver is
 * working (for me), and doesn't need them. *duck*
 */

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/usb.h>
#include <linux/errno.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/videodev.h>
#include <linux/vmalloc.h>
#include <linux/wrapper.h>

#include <asm/semaphore.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#include "vicam.h"

int reverse_rgb = 0;
int shutter_speed = 30;

static int vicam_send_control_msg( struct vicam_v4l_data *vicam_v4l_priv,
				   u8 request, u16 value, u16 size )
{
	struct usb_device *usb_dev = vicam_v4l_priv->usb_dev;
	int ret;

	/* for some reason we can't pass data directly
	 * to usb_control_msg. therefore, we assume that
	 * the caller has already placed their data into
	 * the cntrl_buf.
	 */

	ret = usb_control_msg(
		usb_dev,
		usb_sndctrlpipe( usb_dev, 0 ),
		request,
		USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE,
		value,
		0,
		vicam_v4l_priv->cntrl_buf,
		size,
		HZ );

	return min( ret, 0 );
}

static void vicam_urb_complete( struct urb *vicam_v4l_urb )
{
	struct vicam_v4l_data *vicam_v4l_priv;

	vicam_v4l_priv = vicam_v4l_urb->context;

	if ( !vicam_v4l_urb->status ) {
		set_bit( vicam_v4l_priv->current_frame,
			 &vicam_v4l_priv->decode_frames );
		tasklet_schedule( &vicam_v4l_priv->decode );
	}

	up( &vicam_v4l_priv->busy_mutex );
}

static int vicam_capture_image( struct vicam_v4l_data *vicam_v4l_priv,
				unsigned long frame_num )
{
	u8 * const request = vicam_v4l_priv->cntrl_buf;
	int err;

	if ( vicam_v4l_priv->speed < 2 || vicam_v4l_priv->speed > 60 ) {
		return -EINVAL;
	}

	memset( request, 0, 16 );

	request[0] = vicam_v4l_priv->gain;

	if ( vicam_v4l_priv->width <= 256 ) {
		request[1] |= ( vicam_v4l_priv->width > 128 ) ? 0x01 : 0x03;
	}

	if ( vicam_v4l_priv->height <= 180 ) {
		request[1] |= ( vicam_v4l_priv->height > 121 ) ? 0x20 : 0x30;
	}

	request[2] = 0x90;
	request[3] = 0x07;

	*( ( u16 * )( &request[6] ) ) =
		cpu_to_le16( 15600 / vicam_v4l_priv->speed - 1 );

	err = down_interruptible( &vicam_v4l_priv->busy_mutex );
	if ( err ) {
		return err;
	}

	vicam_v4l_priv->current_frame = frame_num;

	err = vicam_send_control_msg( vicam_v4l_priv,
				      VICAM_REQ_CAPTURE,
				      0x80,
				      16 );

	if ( err ) {
		goto done;
	}

	usb_fill_bulk_urb( vicam_v4l_priv->urb,
			   vicam_v4l_priv->usb_dev,
			   usb_rcvbulkpipe( vicam_v4l_priv->usb_dev,
					    vicam_v4l_priv->bulk_endpoint ),
			   vicam_v4l_priv->input_buf[frame_num],
			   vicam_v4l_priv->width * vicam_v4l_priv->height +
			   VICAM_HEADER_SIZE + VICAM_FOOTER_SIZE,
			   vicam_urb_complete,
			   vicam_v4l_priv );

	err = usb_submit_urb( vicam_v4l_priv->urb );

 done:
	if ( err ) {
		up( &vicam_v4l_priv->busy_mutex );
	}

	return err;
}

static void vicam_decode_frame( struct vicam_v4l_data *vicam_v4l_priv,
				unsigned long frame_num )
{
	const u32 width = vicam_v4l_priv->width,
		height = vicam_v4l_priv->height;
	u8 *ibuf, *obuf;
	int y;

	ibuf = &vicam_v4l_priv->input_buf[frame_num][VICAM_HEADER_SIZE];
	obuf = vicam_v4l_priv->frame_buf + ( VICAM_FRAME_SIZE * frame_num );

	for ( y = 0; y < height; y++, ibuf += width ) {
		int dy = width;
		int x_out;

		if ( unlikely( y >= height - 1 ) ) {
			dy = -dy;
		}

		for ( x_out = 0; x_out < 320; x_out++ ) {
			int dx, cx, cy, luma, x, i;
			int rgbc[3];
			u8 sp[4];

			x = ( x_out * width ) / 320;

			dx = 1;

			if ( unlikely( x >= width - 1 ) ) {
				dx = -dx;
			}

			i = ( y & 1 ) | ( ( x & 1 ) << 1 );

			sp[i ^ 0] = ibuf[x];
			sp[i ^ 1] = ibuf[x + dy];
			sp[i ^ 2] = ibuf[x + dx];
			sp[i ^ 3] = ibuf[x + dy + dx];

			cx = sp[3] - sp[1];
			cy = sp[2] - sp[0];

			rgbc[0] = cy + ( cx / 2 );

			cy /= -2;
			cx *= 13; cx /= 15;

			rgbc[1] = cy + cx;
			rgbc[2] = cy - cx;

			luma = ( int )( ibuf[x] + ibuf[x + dx] ) >> 1;

			for ( i = 0; i < 3; i++, obuf++ ) {
				int j = i;

				if ( reverse_rgb ) {
					j = ( 2 - i );
				}

				*obuf = clamp( luma + rgbc[j], 0, 255 );
			}
		}
	}

	up( &vicam_v4l_priv->decode_mutex[frame_num] );
}

static void vicam_bh_handler( unsigned long _vicam_v4l_priv )
{
	struct vicam_v4l_data *vicam_v4l_priv =
		( struct vicam_v4l_data * )( _vicam_v4l_priv );
	int i;

	do {
		i = ffs( vicam_v4l_priv->decode_frames );
		vicam_decode_frame( vicam_v4l_priv, i - 1 );
		clear_bit( i - 1, &vicam_v4l_priv->decode_frames );
	} while ( vicam_v4l_priv->decode_frames );
}

static int vicam_v4l_open( struct video_device *dev, int mode )
{
	struct vicam_v4l_data *vicam_v4l_priv = dev->priv;
	int err;

	err = down_interruptible( &vicam_v4l_priv->dev_mutex );
	if ( err ) {
		return err;
	}

	err = vicam_send_control_msg( vicam_v4l_priv, VICAM_REQ_POWER, 1, 0 );
	vicam_send_control_msg( vicam_v4l_priv, VICAM_REQ_LED, 3, 0 );

	if ( err ) {
		up( &vicam_v4l_priv->dev_mutex );
	}

	return err;
}

static void vicam_v4l_close( struct video_device *dev )
{
	struct vicam_v4l_data *vicam_v4l_priv = dev->priv;

	vicam_send_control_msg( vicam_v4l_priv, VICAM_REQ_POWER, 0, 0 );

	up( &vicam_v4l_priv->dev_mutex );
}

static long vicam_v4l_write( struct video_device *dev, const char *buf,
			     unsigned long count, int noblock )
{
	return -EPERM;
}

static int vicam_v4l_ioctl( struct video_device *dev, unsigned int cmd,
			    void *arg )
{
	struct vicam_v4l_data *vicam_v4l_priv;
	struct video_capability *vcap;
	struct video_channel *vchan;
	struct video_mbuf *vmbuf;
	struct video_picture *vpic;
	struct video_mmap *vmmap;
	struct video_window *vwin;
	void *buf;
	int buf_size, err, i;

	vicam_v4l_priv = dev->priv;

	err = 0;

	buf_size = max_t( int, sizeof( *vcap ),
			  max_t( int, sizeof( *vchan ),
				 max_t( int, sizeof( *vmbuf ),
					max_t( int, sizeof( *vpic ),
					       max_t( int, sizeof( *vmmap ),
						      sizeof( *vwin ) ) ) ) ) );

	if ( unlikely( buf_size > VICAM_CNTRL_BUF_SIZE ) ) {
		return -ENOMEM;
	}

	buf = vicam_v4l_priv->cntrl_buf;
	buf_size = 0;

	vcap  = buf;
	vchan = buf;
	vmbuf = buf;
	vpic  = buf;
	vmmap = buf;
	vwin  = buf;

	switch ( cmd ) {
	case VIDIOCGCAP:
		buf_size = sizeof( *vcap );

		memset( buf, 0, buf_size );

		strcpy( vcap->name, dev->name );
		vcap->type = dev->type;

		vcap->channels = 1;
		vcap->audios = 0;

		vcap->maxwidth = 320;
		vcap->maxheight = 242;

		vcap->minwidth = 320;
		vcap->minheight = 242;

		break;
	case VIDIOCGCHAN:
		if ( copy_from_user( vchan, arg, sizeof( *vchan ) ) ) {
			err = -EFAULT;
			break;
		}

		if ( vchan->channel ) {
			err = -EINVAL;
			break;
		}

		strcpy( vchan->name, dev->name );

		vchan->flags = 0;
		vchan->tuners = 0;

		vchan->type = VIDEO_TYPE_CAMERA;

		vchan->norm = 0;

		buf_size = sizeof( *vchan );

		break;
	case VIDIOCSCHAN:
		if ( copy_from_user( vchan, arg, sizeof( *vchan ) ) ) {
			err = -EFAULT;
			break;
		}

		if ( vchan->channel ) {
			err = -EINVAL;
			break;
		}

		break;
	case VIDIOCGPICT:
		buf_size = sizeof( *vpic );

		memset( vpic, 0, buf_size );

		vpic->brightness = vicam_v4l_priv->gain << 8;
		vpic->depth = 24;
		vpic->palette = VIDEO_PALETTE_RGB24;

		break;
	case VIDIOCSPICT:
		if ( copy_from_user( vpic, arg, sizeof( *vpic ) ) ) {
			err = -EFAULT;
			break;
		}

		if ( vpic->depth != 24 ||
		     vpic->palette != VIDEO_PALETTE_RGB24 ) {
			err = -EINVAL;
		}

		vicam_v4l_priv->gain = vpic->brightness >> 8;

		break;
	case VIDIOCGWIN:
		buf_size = sizeof( *vwin );

		memset( vwin, 0, buf_size );

		vwin->x = 0;
		vwin->y = 0;

		vwin->width = 320;
		vwin->height = vicam_v4l_priv->height;

		vwin->chromakey = 0;
		vwin->flags = 0;
		vwin->clips = NULL;
		vwin->clipcount = 0;

		break;
	case VIDIOCSWIN:
		if ( vwin->x != 0 || vwin->y != 0 ) {
			err = -EINVAL;
		}

		if ( vwin->width != 320 ||
		     vwin->height != vicam_v4l_priv->height ) {
			err = -EINVAL;
		}

		break;
	case VIDIOCGMBUF:
		buf_size = sizeof( *vmbuf );

		memset( vmbuf, 0, buf_size );

		vmbuf->frames = min( VICAM_NUM_FRAMES, VIDEO_MAX_FRAME );
		vmbuf->size = VICAM_FRAME_SIZE * vmbuf->frames;

		for ( i = 0; i < vmbuf->frames; i++ ) {
			vmbuf->offsets[i] = VICAM_FRAME_SIZE * i;
		}

		break;
	case VIDIOCMCAPTURE:
		if ( copy_from_user( vmmap, arg, sizeof( *vmmap ) ) ) {
			err = -EFAULT;
			break;
		}

		if ( vmmap->frame > VICAM_NUM_FRAMES ||
		     vmmap->format != VIDEO_PALETTE_RGB24 ) {
			err = -EINVAL;
			break;
		}

		err = vicam_capture_image( vicam_v4l_priv, vmmap->frame );

		break;
	case VIDIOCSYNC:
		if ( copy_from_user( &i, arg, sizeof( i ) ) ) {
			err = -EFAULT;
			break;
		}

		if ( i > VICAM_NUM_FRAMES ) {
			err = -EINVAL;
			break;
		}

		err = down_interruptible( &vicam_v4l_priv->decode_mutex[i] );

		break;
	default:
		err = -ENOIOCTLCMD;
		break;
	}

	if ( !err && buf_size ) {
		err = copy_to_user( arg, buf, buf_size ) ? -EFAULT : 0;
	}

	return err;
}

static int vicam_v4l_mmap( struct video_device *dev, const char *addr,
			   unsigned long size )
{
	struct vicam_v4l_data *vicam_v4l_priv = dev->priv;
	unsigned long start = ( unsigned long )( addr );
	unsigned long pos;

	if ( size > VICAM_FRAME_BUF_SIZE ) {
		return -EINVAL;
	}

	pos = ( unsigned long )( vicam_v4l_priv->frame_buf );

	while ( size > 0 ) {
		unsigned long page = kvirt_to_pa( pos );

		if ( remap_page_range( start,
				       page,
				       PAGE_SIZE,
				       PAGE_SHARED ) ){
			return -EAGAIN;
		}

		start += PAGE_SIZE;
		pos += PAGE_SIZE;

		/* in case size isn't a multiple of PAGE_SIZE */

		size -= min( size, PAGE_SIZE );
	}

	return 0;
}

static int __devinit vicam_v4l_initialize( struct video_device *dev )
{
	struct vicam_v4l_data *vicam_v4l_priv;
	int err;
	int i;

	vicam_v4l_priv = kmalloc( sizeof( *vicam_v4l_priv ), GFP_KERNEL );
	if ( !vicam_v4l_priv ) {
		return -ENOMEM;
	}

	memset( vicam_v4l_priv, 0, sizeof( *vicam_v4l_priv ) );

	err = -ENOMEM;

	vicam_v4l_priv->urb = usb_alloc_urb( 0 );
	if ( !vicam_v4l_priv->urb ) {
		goto done;
	}

	vicam_v4l_priv->frame_buf = rvmalloc( VICAM_FRAME_BUF_SIZE );
	if ( !vicam_v4l_priv->frame_buf ) {
		goto done;
	}

	vicam_v4l_priv->cntrl_buf = kmalloc( VICAM_CNTRL_BUF_SIZE,
					     GFP_KERNEL );
	if ( !vicam_v4l_priv->cntrl_buf ) {
		goto done;
	}

	for ( i = 0; i < VICAM_NUM_FRAMES; i++ ) {
		vicam_v4l_priv->input_buf[i] = kmalloc( VICAM_INPUT_SIZE,
							GFP_KERNEL );
		if ( !vicam_v4l_priv->input_buf[i] ) {
			goto done;
		}
	}

	/* the usb_device was passed to us from the vicam_usb_probe
	 * function through our dev->priv pointer.
	 */

	vicam_v4l_priv->usb_dev = dev->priv;
	dev->priv = vicam_v4l_priv;

	tasklet_init( &vicam_v4l_priv->decode,
		      vicam_bh_handler,
		      ( unsigned long )( vicam_v4l_priv ) );

	init_MUTEX( &vicam_v4l_priv->dev_mutex );
	init_MUTEX( &vicam_v4l_priv->busy_mutex );

	for ( i = 0; i < VICAM_NUM_FRAMES; i++ ) {
		init_MUTEX_LOCKED( &vicam_v4l_priv->decode_mutex[i] );
	}

	vicam_v4l_priv->width  = 512;
	vicam_v4l_priv->height = 242;
	vicam_v4l_priv->speed  = shutter_speed;

	for ( i = 0, err = 0; setup[i].firmware && !err; i++ ) {
		memcpy( vicam_v4l_priv->cntrl_buf,
			setup[i].firmware,
			setup[i].size );

		err = vicam_send_control_msg( vicam_v4l_priv,
					      VICAM_REQ_VENDOR,
					      0,
					      setup[i].size );
	}

 done:
	if ( err ) {
		for ( i = 0; i < VICAM_NUM_FRAMES; i++ ) {
			if ( vicam_v4l_priv->input_buf[i] ) {
				kfree( vicam_v4l_priv->input_buf[i] );
			}
		}

		if ( vicam_v4l_priv->cntrl_buf ) {
			kfree( vicam_v4l_priv->cntrl_buf );
		}

		if ( vicam_v4l_priv->frame_buf ) {
			rvfree( vicam_v4l_priv->frame_buf,
				VICAM_FRAME_BUF_SIZE );
		}

		if ( vicam_v4l_priv->urb ) {
			usb_free_urb( vicam_v4l_priv->urb );
		}

		kfree( vicam_v4l_priv );
	}

	return err;
}

static void * __devinit vicam_usb_probe( struct usb_device *dev,
					 unsigned int ifnum,
					 const struct usb_device_id *devid )
{
	struct vicam_usb_data *vicam_usb_priv;
	const struct usb_interface *usb_iface;
	int err;
	u8 endpt_addr, endpt_attr;

	vicam_usb_priv = NULL;

	usb_iface = usb_ifnum_to_if( dev, ifnum );

	if ( usb_iface->num_altsetting != 1 ) {
		return NULL;
	}

	endpt_addr = usb_iface->altsetting[0].endpoint[0].bEndpointAddress;
	endpt_attr = usb_iface->altsetting[0].endpoint[0].bmAttributes;

	if ( !( endpt_addr & USB_ENDPOINT_DIR_MASK ) ||
	     ( endpt_attr & USB_ENDPOINT_XFERTYPE_MASK ) !=
	     USB_ENDPOINT_XFER_BULK ) {
		return NULL;
	}

	vicam_usb_priv = kmalloc( sizeof( *vicam_usb_priv ), GFP_KERNEL );
	if ( !vicam_usb_priv ) {
		return NULL;
	}

	memset( vicam_usb_priv, 0, sizeof( *vicam_usb_priv ) );

	usb_string( dev, dev->descriptor.iProduct,
		    vicam_usb_priv->vicam_v4l_dev.name, 32 );

	vicam_usb_priv->vicam_v4l_dev.type       = VID_TYPE_CAPTURE;
	vicam_usb_priv->vicam_v4l_dev.hardware   = 0; /* need own id */

	vicam_usb_priv->vicam_v4l_dev.initialize = vicam_v4l_initialize;
	vicam_usb_priv->vicam_v4l_dev.open       = vicam_v4l_open;
	vicam_usb_priv->vicam_v4l_dev.close      = vicam_v4l_close;
	vicam_usb_priv->vicam_v4l_dev.write      = vicam_v4l_write;
	vicam_usb_priv->vicam_v4l_dev.ioctl      = vicam_v4l_ioctl;
	vicam_usb_priv->vicam_v4l_dev.mmap       = vicam_v4l_mmap;

	vicam_usb_priv->vicam_v4l_dev.owner      = THIS_MODULE;

	/* we need to get the usb_device struct to the
	 * vicam_v4l_initialize function [preferably]
	 * without using globals.
	 */

	vicam_usb_priv->vicam_v4l_dev.priv       = dev;

	err = video_register_device ( &vicam_usb_priv->vicam_v4l_dev,
				      VFL_TYPE_GRABBER, -1 );

	if ( err ) {
		kfree( vicam_usb_priv );
		vicam_usb_priv = NULL;
	} else {
		struct vicam_v4l_data *vicam_v4l_priv =
			vicam_usb_priv->vicam_v4l_dev.priv;

		vicam_v4l_priv->bulk_endpoint = endpt_addr;
	}

	return vicam_usb_priv;
}

static void __devexit vicam_usb_disconnect( struct usb_device *dev, void *data )
{
	struct vicam_usb_data *vicam_usb_priv;
	struct vicam_v4l_data *vicam_v4l_priv;
	int i;

	vicam_usb_priv = data;
	vicam_v4l_priv = vicam_usb_priv->vicam_v4l_dev.priv;

	/* what happens if a disconnect occurs while the device is open? */

	video_unregister_device( &vicam_usb_priv->vicam_v4l_dev );

	tasklet_kill( &vicam_v4l_priv->decode );

	usb_free_urb( vicam_v4l_priv->urb );
	rvfree( vicam_v4l_priv->frame_buf, VICAM_FRAME_BUF_SIZE );
	kfree( vicam_v4l_priv->cntrl_buf );

	for ( i = 0; i < VICAM_NUM_FRAMES; i++ ) {
		if ( vicam_v4l_priv->input_buf[i] ) {
			kfree( vicam_v4l_priv->input_buf[i] );
		}
	}

	kfree( vicam_v4l_priv );
	kfree( vicam_usb_priv );
}

static struct usb_device_id __devinitdata vicam_usb_table[] = {
	{ USB_DEVICE( USB_3COMHC_VENDOR_ID, USB_3COMHC_PRODUCT_ID ) },
	/* That's all folks */
	{ .match_flags = 0 }
};

static struct usb_driver vicam_usb_drv = {
	.name = "vicam_usb",
	.probe = vicam_usb_probe,
	.disconnect = __devexit_p( vicam_usb_disconnect ),
	.id_table = vicam_usb_table
};

static int __init vicam_usb_init( void )
{
	reverse_rgb = clamp( reverse_rgb, 0, 1 );
	shutter_speed = clamp( shutter_speed, 2, 60 );

	return usb_register( &vicam_usb_drv );
}

static void __exit vicam_usb_exit( void )
{
	usb_deregister( &vicam_usb_drv );
}

module_init( vicam_usb_init );
module_exit( vicam_usb_exit );

MODULE_DEVICE_TABLE( usb, vicam_usb_table );

MODULE_PARM( reverse_rgb, "i" );
MODULE_PARM_DESC( reverse_rgb, "Perform RGB to BGR conversion" );

MODULE_PARM( shutter_speed, "i" );

MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "John Tyner <jtyner@cs.ucr.edu>" );
MODULE_DESCRIPTION( "ViCAM USB/V4L Driver" );
