/* video grab for linux ... uses the original v4l
 */

/*

    This file is part of VIPS.
    
    VIPS is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser 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

 */

/*

    These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <vips/intl.h>

#ifdef HAVE_VIDEODEV

/* Lots of debugging output.
#define DEBUG
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif /*HAVE_UNISTD_H*/
#include <errno.h>
#include <assert.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#include <fcntl.h>
#include <linux/types.h>
#include <linux/videodev.h>

#include <vips/vips.h>

#ifdef WITH_DMALLOC
#include <dmalloc.h>
#endif /*WITH_DMALLOC*/

/* Zero freed mem to help catch stray pointers.
 */
#ifdef NDEBUG
#define FREE( S ) { if( S ) { (void) im_free( (char *) (S) ); \
        (char *) (S) = NULL; } }
#else
#define FREE( S ) { if( S ) { memset( (char *)(S), 0, sizeof( *(S) ) ); \
        (void) im_free( (char *) (S) ); (S) = NULL; } }
#endif /*NDEBUG*/

#define FREEF( F, S ) { if( S ) { (void) F( S ); (S) = NULL; } }
#define FREEFI( F, S ) { if( S ) { (void) F( S ); (S) = 0; } }
#define SETSTR( S, V ) \
        { const char *sst = (V); FREE( S ); (S) = im_strdup( NULL, sst ); }

/* Max channels on a device.
 */
#define IM_MAXCHANNELS (10)

/* Video input sources, e.g. tuner, svideo, composite.
 */
#define TUNER (0)
#define COMPOSITE (1)
#define SVIDEO (2)

typedef struct lgrab {
	/* Mmap here, plus file descriptor.
	 */
	char *device;
	char *capture_buffer;
	int capture_size;
	int fd;

	/* Current settings.
	 */
	int c_channel;
	int c_width;
	int c_height;
	int c_ngrabs;

	/* Extract capabilities here.
	 */
	struct video_capability capability;
	struct video_channel channel[IM_MAXCHANNELS];
	struct video_window window;
	struct video_picture picture;
	struct video_mbuf mbuf;
	struct video_mmap mmap;
} LGrab;

#ifdef DEBUG
/* Decode various things ... capability bits etc.
 */
typedef struct {
        int value;
        const char *name;
        const char *description;
} Decode;

static const Decode decode_palette[] = {
	{ VIDEO_PALETTE_GREY,
		"VIDEO_PALETTE_GREY", "Linear greyscale" },
	{ VIDEO_PALETTE_HI240, 
		"VIDEO_PALETTE_HI240", "High 240 cube (BT848)" },
	{ VIDEO_PALETTE_RGB565, 
		"VIDEO_PALETTE_RGB565", "565 16 bit RGB" },
	{ VIDEO_PALETTE_RGB24, 
		"VIDEO_PALETTE_RGB24", "24bit RGB" },
	{ VIDEO_PALETTE_RGB32, 
		"VIDEO_PALETTE_RGB32", "32bit RGB" },
	{ VIDEO_PALETTE_RGB555, 
		"VIDEO_PALETTE_RGB555", "555 15bit RGB" },
	{ VIDEO_PALETTE_YUV422, 
		"VIDEO_PALETTE_YUV422", "YUV422 capture" },
	{ VIDEO_PALETTE_YUYV, 
		"VIDEO_PALETTE_YUYV", "" },
	{ VIDEO_PALETTE_UYVY, 
		"VIDEO_PALETTE_UYVY", "" },
	{ VIDEO_PALETTE_YUV420, 
		"VIDEO_PALETTE_YUV420", "" },
	{ VIDEO_PALETTE_YUV411, 
		"VIDEO_PALETTE_YUV411", "YUV411 capture" },
	{ VIDEO_PALETTE_RAW, 
		"VIDEO_PALETTE_RAW", "RAW capture (BT848)" },
	{ VIDEO_PALETTE_YUV422P, 
		"VIDEO_PALETTE_YUV422P", "YUV 4:2:2 Planar" },
	{ VIDEO_PALETTE_YUV411P, 
		"VIDEO_PALETTE_YUV411P", "YUV 4:1:1 Planar" },
	{ VIDEO_PALETTE_YUV420P, 
		"VIDEO_PALETTE_YUV420P", "YUV 4:2:0 Planar" },
	{ VIDEO_PALETTE_YUV410P, 
		"VIDEO_PALETTE_YUV410P", "YUV 4:1:0 Planar" }
};

static const Decode decode_type[] = {
	{ VIDEO_TYPE_TV, 
		"VIDEO_TYPE_TV", "TV" },
	{ VIDEO_TYPE_CAMERA, 
		"VIDEO_TYPE_CAMERA", "Camera" },
};

static const Decode decode_vtype[] = {
	{ VID_TYPE_CAPTURE,
		"VID_TYPE_CAPTURE", "Can capture to memory" },
	{ VID_TYPE_TUNER,
		"VID_TYPE_TUNER", "Has Tuner" },
	{ VID_TYPE_TELETEXT,
		"VID_TYPE_TELETEXT", "Has Teletext" },
	{ VID_TYPE_OVERLAY,
		"VID_TYPE_OVERLAY", "Chromakeyed overlay" },
	{ VID_TYPE_CLIPPING,
		"VID_TYPE_CLIPPING", "Overlay clipping" },
	{ VID_TYPE_FRAMERAM,
		"VID_TYPE_FRAMERAM", "Overlay overwrites frame buffer memory" },
	{ VID_TYPE_SCALES,
		"VID_TYPE_SCALES", "Hardware supports image scaling" },
	{ VID_TYPE_MONOCHROME,
		"VID_TYPE_MONOCHROME", "Image capture is grey scale only" },
	{ VID_TYPE_SUBCAPTURE,
		"VID_TYPE_SUBCAPTURE", "Capture sub-image" },
	{ VID_TYPE_MPEG_DECODER,
		"VID_TYPE_MPEG_DECODER", "Can decode MPEG streams" },
	{ VID_TYPE_MPEG_ENCODER,
		"VID_TYPE_MPEG_ENCODER", "Can encode MPEG streams" },
	{ VID_TYPE_MJPEG_DECODER,
		"VID_TYPE_MJPEG_DECODER", "Can decode MJPEG streams" },
	{ VID_TYPE_MJPEG_ENCODER,
		"VID_TYPE_MJPEG_ENCODER", "Can encode MJPEG streams" }
};

static const Decode decode_ctype[] = {
	{ VIDEO_VC_TUNER,
		"VIDEO_VC_TUNER", "Has tuner" },
	{ VIDEO_VC_AUDIO,
		"VIDEO_VC_AUDIO", "Has audio" }
};

/* Prettyprint a value.
 */
static void
decode_print( const Decode *decode, int ndecode, int value )
{
        int i;

        for( i = 0; i < ndecode; i++ )
                if( decode[i].value == value ) {
                        printf( "%s, %s",
                                decode[i].name, decode[i].description );
                        return;
                }

        printf( "unknown (%p)", value );
}

/* Prettyprint a set of flags.
 */
static void
decode_print_flags( const Decode *decode, int ndecode, int flags )
{
        int i;

        printf( "0x%x ", (unsigned int) flags );

        for( i = 0; i < ndecode; i++ )
                if( decode[i].value & flags ) {
                        printf( "[" );
                        decode_print( decode, ndecode,
                                decode[i].value & flags );
                        printf( "] " );
                        flags &= -1 ^ decode[i].value;
                }

        if( flags )
                printf( "[unknown extra flags 0x%x]", (unsigned int) flags );
}
#endif /*DEBUG*/

static int
lgrab_ioctl( LGrab *lg, int request, void *argp )
{
	if( !lg->fd ) {
		im_error( "lgrab_ioctl", "%s", _( "no file descriptor" ) );
		return( -1 );
	}

	if( ioctl( lg->fd, request, argp ) < 0 ) {
		im_error( "lgrab_ioctl", _( "ioctl(0x%x) failed: %s" ), 
			(unsigned int) request, strerror( errno ) );
		return( -1 );
	}

	return( 0 );
}

static void
lgrab_destroy( LGrab *lg )
{
	if( lg->fd != -1 ) {
		int zero = 0;

		(void) lgrab_ioctl( lg, VIDIOCCAPTURE, &zero );
		close( lg->fd );
		lg->fd = -1;
	}

	if( lg->capture_buffer ) {
		munmap( lg->capture_buffer, lg->capture_size );
		lg->capture_buffer = NULL;
	}

	FREE( lg->device );
	FREE( lg );
}

static LGrab *
lgrab_new( const char *device )
{
	LGrab *lg = IM_NEW( NULL, LGrab );
	int i;

	if( !lg )
		return( NULL );

	lg->device = NULL;
	lg->capture_buffer = NULL;
	lg->capture_size = 0;
	lg->fd = -1;

	lg->c_channel = -1;
	lg->c_width = -1;
	lg->c_height = -1;
	lg->c_ngrabs = 1;

	SETSTR( lg->device, device );
        if( !lg->device || (lg->fd = open( lg->device, O_RDWR )) == -1 ) {
		im_error( "lgrab_new", _( "cannot open video device \"%s\"" ),
			lg->device );
		lgrab_destroy( lg );
		return( NULL );
	}

        if( lgrab_ioctl( lg, VIDIOCGCAP, &lg->capability ) ) {
		im_error( "lgrab_new", 
			"%s", _( "cannot get video capability" ) );
		lgrab_destroy( lg );
		return( NULL );
	}

        /* Check that it can capture to memory.
	 */
        if( !(lg->capability.type & VID_TYPE_CAPTURE) ) {
                im_error( "lgrab_new", 
			"%s", _( "card cannot capture to memory" ) );
		lgrab_destroy( lg );
		return( NULL );
	}

        /* Read channel info.
	 */
        for( i = 0; i < IM_MIN( lg->capability.channels, IM_MAXCHANNELS ); 
		i++ ) {
                lg->channel[i].channel = i;
                if( lgrab_ioctl( lg, VIDIOCGCHAN, &lg->channel[i] ) ) {
			lgrab_destroy( lg );
			return( NULL );
		}
	}

        /* Get other props.
 	 */
        if( lgrab_ioctl( lg, VIDIOCGWIN, &lg->window) ||
		lgrab_ioctl( lg, VIDIOCGPICT, &lg->picture) ) {
		lgrab_destroy( lg );
		return( NULL );
	}

	/* Set 24 bit mode.
	 */
	lg->picture.depth = 24;
	lg->picture.palette = VIDEO_PALETTE_RGB24;
	if( lgrab_ioctl( lg, VIDIOCSPICT, &lg->picture ) ) {
		lgrab_destroy( lg );
		return( NULL );
	}

	return( lg );
}

#ifdef DEBUG
static void
lgrab_dump_capability( struct video_capability *capability )
{
        printf( "capability->name = \"%s\"\n", capability->name );
        printf( "capability->channels = %d\n", capability->channels );
        printf( "capability->audios = %d\n", capability->audios );
        printf( "capability->maxwidth = %d\n", capability->maxwidth );
        printf( "capability->maxheight = %d\n", capability->maxheight );
        printf( "capability->minwidth = %d\n", capability->maxwidth );
        printf( "capability->minheight = %d\n", capability->maxheight );
	printf( "capability->type = " );
	decode_print_flags( decode_vtype, IM_NUMBER( decode_vtype ),
                capability->type );
	printf( "\n" );
}

static void
lgrab_dump_channel( struct video_channel *channel )
{
	printf( "channel->channel = %d\n", channel->channel );
	printf( "channel->name = \"%s\"\n", channel->name );
	printf( "channel->tuners = %d\n", channel->tuners );

	printf( "channel->flags = " );
	decode_print_flags( decode_ctype, IM_NUMBER( decode_ctype ),
		channel->flags );
	printf( "\n" );
	printf( "channel->type = " );
	decode_print( decode_type, IM_NUMBER( decode_type ),
                channel->type );
	printf( "\n" );
	printf( "channel->norm = %d\n", channel->norm );
}

static void
lgrab_dump_picture( struct video_picture *picture )
{
        printf( "picture->brightness = %d\n", picture->brightness );
        printf( "picture->hue = %d\n", picture->hue );
        printf( "picture->colour = %d\n", picture->colour );
        printf( "picture->contrast = %d\n", picture->contrast );
        printf( "picture->whiteness = %d\n", picture->whiteness );
        printf( "picture->depth = %d\n", picture->depth );
        printf( "picture->palette = " );
	decode_print( decode_palette, IM_NUMBER( decode_palette ),
                picture->palette );
	printf( "\n" );
}

static void 
lgrab_dump( LGrab *lg )
{
        int i;

        printf( "lg->device = \"%s\"\n", lg->device );
        printf( "lg->capture_buffer = %p\n",
                lg->capture_buffer );
        printf( "lg->capture_size = 0x%x\n",
                (unsigned int) lg->capture_size );
        printf( "lg->fd = %d\n", lg->fd );

        printf( "lg->c_channel = %d\n", lg->c_channel );
        printf( "lg->c_width = %d\n", lg->c_width );
        printf( "lg->c_height = %d\n", lg->c_height );
        printf( "lg->c_ngrabs = %d\n", lg->c_ngrabs );

	lgrab_dump_capability( &lg->capability );
        for( i = 0; i < lg->capability.channels; i++ ) 
		lgrab_dump_channel( &lg->channel[i] );
	lgrab_dump_picture( &lg->picture );

        printf( "mbuf->size = 0x%x\n", (unsigned int) lg->mbuf.size );
        printf( "mbuf->frames = %d\n", lg->mbuf.frames );
        printf( "mbuf->offsets = " );
	for( i = 0; i < lg->mbuf.frames; i++ )
		printf( "0x%x ", (unsigned int) lg->mbuf.offsets[i] );
        printf( "\n" );
}
#endif /*DEBUG*/

static int
lgrab_set_capture_size( LGrab *lg, int width, int height )
{
	lg->c_width = width;
	lg->c_height = height;

	lg->window.clipcount = 0;
	lg->window.flags = 0;
	lg->window.x = 0;
	lg->window.y = 0;
	lg->window.width = width;
	lg->window.height = height;
	if( lgrab_ioctl( lg, VIDIOCSWIN, &lg->window ) )
		return( -1 );

	/* Make sure the correct amount of memory is mapped.
	 */
	if( lgrab_ioctl( lg, VIDIOCGMBUF, &lg->mbuf ) )
		return( -1 );

	if( lg->capture_buffer ) {
		munmap( lg->capture_buffer, lg->capture_size );
		lg->capture_buffer = NULL;
	}

	lg->capture_size = lg->mbuf.size;
	if( !(lg->capture_buffer = mmap( 0, lg->capture_size, 
		PROT_READ | PROT_WRITE, MAP_SHARED, lg->fd, 0 )) ) {
		im_error( "lgrab_set_capture_size", 
			"%s", _( "unable to map memory" ) );
		return( -1 );
	}

	return( 0 );
}

static int
lgrab_set_channel( LGrab *lg, int channel )
{
	if( channel < 0 || channel >= lg->capability.channels ) {
		im_error( "lgrab_set_channel", 
			_( "channel not between 0 and %d" ),
			lg->capability.channels - 1 );
		return( -1 );
	}

	if( lgrab_ioctl( lg, VIDIOCSCHAN, &lg->channel[channel] ) )
		return( -1 );
	lg->c_channel = channel;

	return( 0 );
}

static int
lgrab_set_brightness( LGrab *lg, int brightness )
{
	lg->picture.brightness = IM_CLIP( 0, brightness, 65535 );
	if( lgrab_ioctl( lg, VIDIOCSPICT, &lg->picture ) )
		return( -1 );

	return( 0 );
}

static int
lgrab_set_colour( LGrab *lg, int colour )
{
	lg->picture.colour = IM_CLIP( 0, colour, 65535 );
	if( lgrab_ioctl( lg, VIDIOCSPICT, &lg->picture ) )
		return( -1 );

	return( 0 );
}

static int
lgrab_set_contrast( LGrab *lg, int contrast )
{
	lg->picture.contrast = IM_CLIP( 0, contrast, 65535 );
	if( lgrab_ioctl( lg, VIDIOCSPICT, &lg->picture ) )
		return( -1 );

	return( 0 );
}

static int
lgrab_set_hue( LGrab *lg, int hue )
{
	lg->picture.hue = IM_CLIP( 0, hue, 65535 );
	if( lgrab_ioctl( lg, VIDIOCSPICT, &lg->picture ) )
		return( -1 );

	return( 0 );
}

static int
lgrab_set_ngrabs( LGrab *lg, int ngrabs )
{
	lg->c_ngrabs = IM_CLIP( 1, ngrabs, 1000 );

	return( 0 );
}

/* Grab a single frame.
 */
static int
lgrab_capture1( LGrab *lg )
{
	lg->mmap.format = lg->picture.palette;
	lg->mmap.frame = 0;
	lg->mmap.width = lg->c_width;
	lg->mmap.height = lg->c_height;
	if( lgrab_ioctl( lg, VIDIOCMCAPTURE, &lg->mmap ) ||
		lgrab_ioctl( lg, VIDIOCSYNC, &lg->mmap.frame ) )
		return( -1 );

	return( 0 );
}

/* Grab and average many frames.
 */
static int
lgrab_capturen( LGrab *lg )
{
	if( lg->c_ngrabs == 1 ) {
		if( lgrab_capture1( lg ) )
			return( -1 );
	}
	else {
		int i, j;
		int npx = lg->c_width * lg->c_height * 3;
		unsigned int *acc;

		if( !(acc = IM_ARRAY( NULL, npx, unsigned int )) )
			return( -1 );
		memset( acc, 0, npx * sizeof( unsigned int ) );

		for( i = 0; i < lg->c_ngrabs; i++ ) {
			if( lgrab_capture1( lg ) ) {
				FREE( acc );
				return( -1 );
			}

			for( j = 0; j < npx; j++ ) 
				acc[j] += (unsigned char) lg->capture_buffer[j];
		}

		for( j = 0; j < npx; j++ ) {
			int avg = (acc[j] + lg->c_ngrabs / 2) / lg->c_ngrabs;

			lg->capture_buffer[j] = IM_CLIP( 0, avg, 255 );
		}

		FREE( acc );
	}

	return( 0 );
}

static int
lgrab_capture( LGrab *lg, IMAGE *im )
{
	int x, y;
	unsigned char *line;

	if( lgrab_capturen( lg ) )
		return( -1 );

	if( im_outcheck( im ) )
		return( -1 );
        im_initdesc( im, lg->c_width, lg->c_height, 3,
                IM_BBITS_BYTE, IM_BANDFMT_UCHAR,
                IM_CODING_NONE, IM_TYPE_MULTIBAND, 1.0, 1.0, 0, 0 );
        if( im_setupout( im ) )
                return( -1 );
	if( !(line = IM_ARRAY( im, 
		IM_IMAGE_SIZEOF_LINE( im ), unsigned char )) )
                return( -1 );

        for( y = 0; y < lg->c_height; y++ ) {
                unsigned char *p = (unsigned char *) lg->capture_buffer + 
			y * IM_IMAGE_SIZEOF_LINE( im );
                unsigned char *q = line;

		for( x = 0; x < lg->c_width; x++ ) {
			q[0] = p[2];
			q[1] = p[1];
			q[2] = p[0];

			p += 3;
			q += 3;
		}

                if( im_writeline( y, im, line ) )
                        return( -1 );
	}

	return( 0 );
}

int
im_video_v4l1( IMAGE *im, const char *device,
	int channel, int brightness, int colour, int contrast, int hue, 
	int ngrabs )
{
	LGrab *lg;

	if( !(lg = lgrab_new( device )) )
		return( -1 );

	if( lgrab_set_capture_size( lg, 
		lg->capability.maxwidth, lg->capability.maxheight ) ||
		lgrab_set_channel( lg, channel ) ||
		lgrab_set_brightness( lg, brightness ) ||
		lgrab_set_colour( lg, colour ) ||
		lgrab_set_contrast( lg, contrast ) ||
		lgrab_set_hue( lg, hue ) ||
		lgrab_set_ngrabs( lg, ngrabs ) ||
		lgrab_capture( lg, im ) ) {
		lgrab_destroy( lg );
		return( -1 );
	}

#ifdef DEBUG
	printf( "Successful capture with:\n" );
	lgrab_dump( lg );
#endif /*DEBUG*/

	lgrab_destroy( lg );

	return( 0 );
}

#else /*!HAVE_VIDEODEV*/

#include <vips/vips.h>

int
im_video_v4l1( IMAGE *im, const char *device,
	int channel, int brightness, int colour, int contrast, int hue, 
	int ngrabs )
{
	im_error( "im_video_v4l1", 
		"%s", _( "compiled without im_video_v4l1 support" ) );
	return( -1 );
}

#endif /*HAVE_VIDEODEV*/
