/*
 *  Playback demo for Sensoray Model 2253
 *  Copyright (c) 2011 Sensoray Company, Inc.
 *
 */

/* Ensure 64-bit offsets are used for large files */
#define _LARGEFILE64_SOURCE
#define _FILE_OFFSET_BITS 64

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h>             /* getopt_long() */
#include <fcntl.h>              /* low-level i/o */
#include <unistd.h>
#include <errno.h>
#include <malloc.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>          /* for videodev2.h */
#include <linux/videodev2.h>
#include <sys/soundcard.h>
#include <pthread.h>
#include <time.h>
#include <signal.h>
#include "s2253ioctl.h"
#include "s2253.h"
#ifdef USE_MP4
#include "mp4.h"	/* mp4 remuxer */
#endif

//#define USE_JPEG
#ifdef USE_JPEG
#include <jpeglib.h>
#endif


struct buffer {
	void *                  start;
	size_t                  length;
};

static int g_loop = 0;
static int g_jpeg = 0;

#ifdef USE_JPEG
static struct jpeg_decompress_struct jdec = {0};
static struct jpeg_error_mgr jerr = {0};
static unsigned char *jbuf = NULL;

static void get_jpeg_info(FILE *file, int *pwidth, int *pheight)
{
	jdec.err = jpeg_std_error(&jerr);
	//printf("jpeg_create_decompress\n");
	jpeg_create_decompress(&jdec);
	//printf("jpeg_stdio_src\n");
	jpeg_stdio_src(&jdec, file);

	//printf("jpeg_read_header\n");
	jpeg_read_header(&jdec, TRUE);
	
	jdec.out_color_space = JCS_YCbCr;     // set the output colorspace and dither mode
	jdec.dither_mode = JDITHER_NONE;
	
	//printf("jpeg_start_decompress\n");
	jpeg_start_decompress(&jdec);

	//fprintf(stderr, "%d %d %d\n", jdec.output_width, jdec.output_height, jdec.output_components);
	
	*pwidth = jdec.output_width;         // store the resolution
	*pheight = jdec.output_height;
	
	jbuf = malloc(jdec.output_width * jdec.output_components);
}


// returns 0 on success
static int decode_jpeg(FILE *file, void *dest)
{
	int i;
	unsigned char *uyvy = dest;
	//struct timeval t0, t1;
	
	//gettimeofday(&t0, NULL);
	
	
	if (jdec.output_scanline != 0) {
		// reuse jpeg decompressor
		
		//printf("jpeg_read_header\n");
		jpeg_read_header(&jdec, TRUE);
		
		jdec.out_color_space = JCS_YCbCr;     // set the output colorspace and dither mode
		jdec.dither_mode = JDITHER_NONE;

		//printf("jpeg_start_decompress\n");
		jpeg_start_decompress(&jdec);
	}

	//printf("jpeg_read_scanlines\n");
	while (jdec.output_scanline < jdec.output_height) {
		jpeg_read_scanlines(&jdec, &jbuf, 1);
		
		for (i = 0; i < jdec.output_width; i++) {
			*uyvy++ = jbuf[i*3 + 1 + (i&1)];
			*uyvy++ = jbuf[i*3+0];
		}
	}
	
	//printf("jpeg_abort_decompress\n");
	jpeg_abort_decompress(&jdec);  // allows reusing jpeg decompressor
	
	const unsigned char *p = jdec.src->next_input_byte;
	if (jdec.src->bytes_in_buffer >= 2 && p[0] == 0xff && p[1] == 0xd9) {
		// didn't consume end-of-image bytes?
		jdec.src->bytes_in_buffer -= 2;
		jdec.src->next_input_byte += 2;
	}

	//gettimeofday(&t1,NULL);
	//printf("%g %x %x\n", (t1.tv_sec - t0.tv_sec)+1e-6*(t1.tv_usec - t0.tv_usec), a, b);
	
	return 0;
}
#endif

void playback_raw(char *dev_name, int fd, int width, int height, FILE *file, int pixelformat)
{
	struct v4l2_requestbuffers req;
	unsigned int n_buffers, i;
	struct buffer *         buffers         = NULL;
	enum v4l2_buf_type type;
        struct v4l2_format fmt;
	
	//printf("%s: %s %d %d %d %p %x\n", __func__, dev_name, fd, width, height, file, pixelformat);
#ifdef USE_JPEG	
	if (g_jpeg) {
		// read resolution from jpeg file
		get_jpeg_info(file, &width, &height);
	}
#endif

        memset(&fmt, 0, sizeof fmt);
        fmt.type                = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	fmt.fmt.pix.width       = width;
	fmt.fmt.pix.height      = height;
	fmt.fmt.pix.pixelformat = pixelformat;
	switch (pixelformat) {
	case V4L2_PIX_FMT_UYVY:
		fmt.fmt.pix.bytesperline = width * 2;
		fmt.fmt.pix.sizeimage = width * height * 2;
		break;
	case V4L2_PIX_FMT_GREY: // Y8
		fmt.fmt.pix.bytesperline = width;
		fmt.fmt.pix.sizeimage = width * height;
		break;
	case V4L2_PIX_FMT_NV12:
		fmt.fmt.pix.bytesperline = width;
		fmt.fmt.pix.sizeimage = width * height * 3 / 2;
		break;
	default:
		fprintf(stderr, "unsupported pixel format\n");
		exit (EXIT_FAILURE);
	}
	//printf("%s: framesize=%d\n", __func__, fmt.fmt.pix.sizeimage);

	if (-1 == ioctl (fd, VIDIOC_S_FMT, &fmt)) {
                perror ("VIDIOC_S_FMT");
		exit (EXIT_FAILURE);
	}

	memset(&req, 0, sizeof req);

	req.count               = 4;
	req.type                = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	req.memory              = V4L2_MEMORY_MMAP;
	
	if (-1 == ioctl (fd, VIDIOC_REQBUFS, &req)) {
		if (EINVAL == errno) {
			fprintf (stderr, "%s does not support "
				"memory mapping\n", dev_name);
			exit (EXIT_FAILURE);
		} else {
			perror ("VIDIOC_REQBUFS");
			exit (EXIT_FAILURE);
		}
	}

	if (req.count < 2) {
		fprintf (stderr, "Insufficient buffer memory on %s\n",
			dev_name);
		exit (EXIT_FAILURE);
	}

	buffers = calloc (req.count, sizeof (*buffers));

	if (!buffers) {
		fprintf (stderr, "Out of memory\n");
		exit (EXIT_FAILURE);
	}

	for (n_buffers = 0; n_buffers < req.count; ++n_buffers) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof buf);

		buf.type        = V4L2_BUF_TYPE_VIDEO_OUTPUT;
		buf.memory      = V4L2_MEMORY_MMAP;
		buf.index       = n_buffers;

		if (-1 == ioctl (fd, VIDIOC_QUERYBUF, &buf)) {
			perror ("VIDIOC_QUERYBUF");
			exit(EXIT_FAILURE);
		}
		
		//fprintf(stderr, "buf.length = %d\n", buf.length);

		buffers[n_buffers].length = buf.length;
		buffers[n_buffers].start =
			mmap (NULL /* start anywhere */,
			      buf.length,
			      PROT_READ | PROT_WRITE /* required */,
			      MAP_SHARED /* recommended */,
			      fd, buf.m.offset);

		if (MAP_FAILED == buffers[n_buffers].start) {
			perror ("mmap");
			exit(EXIT_FAILURE);
		}
	}
	type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	if (-1 == ioctl (fd, VIDIOC_STREAMON, &type)) {
		perror ("VIDIOC_STREAMON");
		exit(EXIT_FAILURE);
	}
	i = 0;
	while (!feof(file) || (g_loop && fseek(file, 0, SEEK_SET) == 0)) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof buf);
		buf.type        = V4L2_BUF_TYPE_VIDEO_OUTPUT;
		buf.memory      = V4L2_MEMORY_MMAP;
		
		if (i >= n_buffers) {

			if (-1 == ioctl (fd, VIDIOC_DQBUF, &buf)) {
				switch (errno) {
				case EAGAIN:
					continue;

				case EIO:
					/* Could ignore EIO, see spec. */

					/* fall through */

				default:
					perror ("VIDIOC_DQBUF");
					exit(EXIT_FAILURE);
				}
			}

			assert (buf.index < n_buffers);
		} else {
			buf.index = i++;
		}
#ifdef USE_JPEG
		if (g_jpeg) {
			if (decode_jpeg(file, buffers[buf.index].start) != 0)
				break;
			buf.bytesused = fmt.fmt.pix.sizeimage;
		} else 
#endif
		{
			buf.bytesused = fread (buffers[buf.index].start, 1, fmt.fmt.pix.sizeimage, file);
		}
		
		if (-1 == ioctl (fd, VIDIOC_QBUF, &buf)) {
			perror ("VIDIOC_QBUF");
			exit(EXIT_FAILURE);
		}
	}

	type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	if (-1 == ioctl (fd, VIDIOC_STREAMOFF, &type)) {
		perror ("VIDIOC_STREAMOFF");
		exit(EXIT_FAILURE);
	}
	for (i = 0; i < n_buffers; ++i) {
		if (-1 == munmap (buffers[i].start, buffers[i].length)) {
			perror ("munmap");
			exit(EXIT_FAILURE);
		}
	}
}

/* buffer size must be large enough for 1 frame and audio samples */
#define BUFSIZE (512*1024)

static int G_fd = -1;
static int G_paused = 0;

static void
quit_handler(int sig)
{
#ifdef VIDIOC_DECODER_CMD
	struct v4l2_decoder_cmd cmd = {};

	cmd.cmd =  V4L2_DEC_CMD_STOP;
	cmd.flags = V4L2_DEC_CMD_STOP_IMMEDIATELY;
	if (ioctl(G_fd, VIDIOC_DECODER_CMD, &cmd))
		perror("VIDIOC_DECODER_CMD");
	fprintf(stderr, "Stopping\n");
#else
	int buftype = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	if (ioctl(G_fd, VIDIOC_STREAMOFF, &buftype))
		perror("VIDIOC_STREAMOFF");
#endif
	exit(EXIT_FAILURE);
}

static void
pause_handler(int sig)
{
#ifdef VIDIOC_DECODER_CMD
	struct v4l2_decoder_cmd cmd = {};

	G_paused ^= 1;
	cmd.cmd = G_paused ? V4L2_DEC_CMD_PAUSE : V4L2_DEC_CMD_RESUME;
	if (ioctl(G_fd, VIDIOC_DECODER_CMD, &cmd))
		perror("VIDIOC_DECODER_CMD");
	fprintf(stderr, "%s\n", G_paused ? "Pausing" : "Resuming");
#endif
}

#ifdef USE_MP4
static int decode_mp4_file(FILE *file, int dev, char *dev_name, unsigned char *buf)
{
	muxer_handle mux = NULL;
	int rc;
	
	mux = mp4_refrag_create();
	if (!mux) {
		fprintf(stderr, "Failed to allocate mp4");
		exit(EXIT_FAILURE);
	}
	mp4_file_pointer(mux, file);
	rc = mp4_is_fragmented(mux);
	if (rc == 0) {
		/* When using VideoBuf2, there seems to be a stream corruption 
		 * bug when  writing individual frames to the file descriptor.
		 * If we use stdio file buffering through fdopen, the corruption
		 * goes away.
		 */
		FILE *fdev = fdopen(dev, "wb");
		int len;
		fprintf(stderr, "Using mp4 converter\n");
		while (1) {
			mp4_buf_put(mux, buf, BUFSIZE);
			mp4_remux(mux);
			len = mp4_buf_get(mux);
                        if (len <= 0)
                        	break;
			while (len > 0) {
				//rc = write(dev, buf, len);
				rc = fwrite(buf, 1, len, fdev);
                                if (rc >= 0) {
                                	len -= rc;
				} else {
					// we may get interrupted by SIGUSR1 during pause/resume
					if (errno == EINTR) {
						errno = 0;
						continue;
					}
					perror(dev_name);
					exit(EXIT_FAILURE);
				}
			}
		}
		fclose(fdev);
		rc = 0;
	}
	mp4_delete(mux);
	return rc;
}
#endif

static void
usage                           (FILE *                 fp,
                                 int                    argc,
                                 char **                argv)
{
	fprintf (fp,
		"Usage: %s [options] <filename>\n\n"
		"Options:\n"
		"-d | --device name   Video device name [/dev/video2]\n"
		"-h | --help          Print this message\n"
		"-c | --caption       Caption for OSD\n"
		"-p | --pal           Set Standard to PAL (default NTSC)\n"
		"The compressed format is normally auto-detected from file header,\n"
		" but this cannot be done for raw format, so use these options:\n"
		"-u | --uyvy          Force raw UYVY format\n"
		"-n | --nv12          Force raw NV12 format\n"
		"-8 | --y8            Force raw Y8 format\n"
		"-s | --size wxh      Set size of raw frame\n"
		"-l | --latency n     Set latency mode: 0=normal 1=low\n"
		"-L | --loop          Loop video (Raw formats only)\n"
		"-t | --mpegts        Force MPEG-TS format\n"
		"-j | --jpeg          Force JPEG format decoded on host\n"
#ifdef VIDIOC_DECODER_CMD
		"To pause/resume, send SIGUSR1 to this process.\n"
#endif
		"",
		argv[0]);
}



static const char short_options [] = "d:hpc:un8s:l:tL"
#ifdef USE_JPEG
		"j"
#endif
		;

static const struct option
long_options [] = {
	{ "device",	required_argument,	NULL,		'd' },
	{ "help",	no_argument,		NULL,		'h' },
	{ "pal",	no_argument,		NULL,		'p' },
	{ "caption",	required_argument,	NULL,		'c' },
	{ "uyvy",	no_argument,		NULL,		'u' },
	{ "nv12",	no_argument,		NULL,		'n' },
	{ "y8",		no_argument,		NULL,		'8' },
	{ "size",	required_argument,	NULL,		's' },
	{ "latency",	required_argument,	NULL,		'l' },
	{ "mpegts",	no_argument,		NULL,		't' },
	{ "loop",	no_argument,		NULL,		'L' },
#ifdef USE_JPEG
	{ "jpeg",	no_argument,		NULL,		'j' },
#endif
	{ 0, 0, 0, 0 },
};



int
main                            (int                    argc,
                                 char **                argv)
{
	char *dev_name = "/dev/video2";
	int G_pal = 0;
	char *file_name = NULL;
	FILE *file = NULL;
	int fd = -1;
	int dev = -1;
	int rc = 0;
	unsigned char *buf = NULL;
	char *caption = NULL;
	int width = 0;
	int height = 0;
	int pixelformat = 0;
	int latency = -1;
	int mpeg_ts = 0;
	int used = 0;

	signal(SIGINT, quit_handler);
	signal(SIGTERM, quit_handler);
	
	buf = malloc(BUFSIZE);
	if (!buf) {
		fprintf(stderr, "Failed to allocate memory\n");
		exit(EXIT_FAILURE);
	}

	for (;;) {
		int index;
		int c;

		c = getopt_long (argc, argv,
				 short_options, long_options,
				 &index);

		if (-1 == c)
			break;

		switch (c) {
		case 0: /* getopt_long() flag */
			break;
		case 'd':
			dev_name = optarg;
			break;
		case 'h':
			usage (stdout, argc, argv);
			exit (EXIT_SUCCESS);
		case 'p':
			G_pal = 1;
			break;
		case 'c':
			caption = optarg;
			break;
		case 'u':
			pixelformat = V4L2_PIX_FMT_UYVY;
			break;
		case '8':
			pixelformat = V4L2_PIX_FMT_GREY; // Y8
			break;
		case 'n':
			pixelformat = V4L2_PIX_FMT_NV12;
			break;
		case 's': {
			char *p;
			p = strchr(optarg, 'x');
			if (p) {
				*p = '\0';
				width = atoi(optarg);
				height = atoi(p + 1);
			}
			break;
		}
		case 'l':
			latency = !!atoi(optarg);
			break;
		case 'L':
			g_loop = 1;
			break;
		case 't':
			mpeg_ts = 1;
			break;
#ifdef USE_JPEG
		case 'j':
			g_jpeg = 1;
			pixelformat = V4L2_PIX_FMT_UYVY;
			break;
#endif
		default:
			usage (stderr, argc, argv);
			exit (EXIT_FAILURE);
		}
	}
	if (optind >= argc) {
		usage (stderr, argc, argv);
		exit (EXIT_FAILURE);
	}
	file_name = argv[optind];
		

	dev = open(dev_name, O_RDWR);
	if (dev < 0) {
		perror(dev_name);
		return -1;
	}
	G_fd = dev;
	signal(SIGUSR1, pause_handler);
	{
		struct v4l2_capability cap;
		if (-1 == ioctl(dev, VIDIOC_QUERYCAP, &cap)) {
			perror("VIDIOC_QUERYCAP");
			exit (EXIT_FAILURE);
		}
		if ((((cap.capabilities & V4L2_CAP_DEVICE_CAPS) ? 
				cap.device_caps : cap.capabilities)
		    & V4L2_CAP_VIDEO_OUTPUT) == 0) {
			fprintf(stderr, "%s is not a video output device\n",
				dev_name);
			exit (EXIT_FAILURE);
		}
	}
	{
		v4l2_std_id std = G_pal ? V4L2_STD_PAL : V4L2_STD_NTSC;
		// TODO: set "mode" control to "idle" first
		if (-1 == ioctl(dev, VIDIOC_S_STD, &std))
			perror("VIDIOC_S_STD");
	}
	if (caption) {
		struct s2253_clock clk;
		struct timeval tv;
		struct s2253_osd osd;
		memset(&osd, 0, sizeof(osd));
		osd.osdOn = caption[0] ? 1 : 0;
		osd.osdBmp = 1;
		osd.transparent = 1;
		osd.positionTop = 1;
		osd.fraction = 0;
		osd.xOffset = 40;
		osd.yOffset = 40;
		strncpy(osd.line, caption, sizeof(osd.line));
		if (-1 == ioctl (dev, S2253_VIDIOC_OSD, &osd)) {
       			perror ("S2253_VIDIOC_OSD");
       			exit (EXIT_FAILURE);
		}

		/* set the clock on the device to our local time */
		gettimeofday(&tv, NULL);  /* get UTC time */
		tzset();		  /* get timezone and daylight info */
		clk.sec = tv.tv_sec - timezone + daylight * (60*60);
		clk.usec = tv.tv_usec;
		if (-1 == ioctl (dev, S2253_VIDIOC_CLOCK, &clk)) {
       			perror ("S2253_VIDIOC_CLOCK");
       			exit (EXIT_FAILURE);
		}
	}

	if (strcmp(file_name, "-") == 0) {
		fd = 0;
		file_name = "stdin";
		file = stdin;
	} else {
		struct stat st;
		fd = open(file_name, O_RDONLY);
		if (fd < 0) {
			fprintf(stderr, "Failed to open input file: %s\n", file_name);
			exit(EXIT_FAILURE);
		}
		if (fstat(fd, &st) < 0) {
			perror("stat");
			exit(EXIT_FAILURE);
		}
		if (S_ISREG(st.st_mode)) { /* it's a regular file */
			file = fdopen(fd, "r");
			if (!file) {
				fprintf(stderr, "fdopen failed\n");
				exit(EXIT_FAILURE);
			}
			if (st.st_size == 0) {
				fprintf(stderr, "Input file is empty: %s\n", file_name);
				exit(EXIT_FAILURE);
			}
		}
	}
	fprintf(stderr, "Playing %s...\n", file_name);


	{
		struct v4l2_control ctrl = {
			.id = S2253_CID_LOW_LATENCY,
			.value = 0,
		};
		if ((!file && latency != 0) || latency == 1) {
			ctrl.value = 1;
			fprintf(stderr, "Low-latency output mode enabled\n");
		}
		if (-1 == ioctl(dev, VIDIOC_S_CTRL, &ctrl)) {
			perror("VIDIOC_S_CTRL");
			exit(EXIT_FAILURE);
		}
	}
	if (pixelformat) {
		if (!file) {
			fprintf(stderr, "raw playback only works with regular files\n");
			exit(EXIT_FAILURE);
		}
		playback_raw(dev_name, dev, width, height, file, pixelformat);
	} else {
		rc = 1;
#ifdef USE_MP4
		if (file) {
			rc = decode_mp4_file(file, dev, dev_name, buf);
		}
#endif
		if (mpeg_ts) {
			struct v4l2_format fmt;
        		memset(&fmt, 0, sizeof fmt);
        		fmt.type                = V4L2_BUF_TYPE_VIDEO_OUTPUT;
			fmt.fmt.pix.width       = 640;
			fmt.fmt.pix.height      = 480;
			fmt.fmt.pix.bytesperline = 640 * 2;
			fmt.fmt.pix.sizeimage = 640 * 480 * 2;
			fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MPEG;
			if (-1 == ioctl (dev, VIDIOC_S_FMT, &fmt)) {
                		perror ("VIDIOC_S_FMT");
				exit (EXIT_FAILURE);
			}
			if (-1 == ioctl (dev, VIDIOC_STREAMON, &fmt.type)) {
				perror ("VIDIOC_STREAMON");
				exit(EXIT_FAILURE);
			}
		}
		while (rc != 0) {
			if (file) {
				rc = fread(buf + used, 1, BUFSIZE - used, file);
				if (rc == 0 && used == 0 && feof(file)) {
					break; /* EOF */
				}
				if (rc < 0) {
					fprintf(stderr, "%s %s\n", file_name, strerror(ferror(file)));
					exit(EXIT_FAILURE);
				}
			} else {
				rc = read(fd, buf + used, BUFSIZE - used);
				if (rc == 0 && used == 0) {
					break; /* EOF */
				}
				if (rc < 0) {
					perror("read");
					exit(EXIT_FAILURE);
				}
			}
			used += rc;
			rc = write(dev, buf, used);
			if (rc < 0) {
				// we may get interrupted by SIGUSR1 during pause/resume
				if (errno == EINTR) {
					errno = 0;
					continue;
				}
				printf("rc=%d errno=%d\n", rc, errno);
				perror(dev_name);
				exit(EXIT_FAILURE);
			}
			used -= rc;
			memmove(buf, buf + rc, used);
		}
	}
	if (file)
		fclose(file);
	close(fd);

	// This close will block until end-of-stream, unless VIDIOC_STREAMOFF 
	// or VIDIOC_DECODER_CMD+V4L2_DEC_CMD_STOP are called.
	close(dev);
	return 0;
}
