/*
 *  libzvbi test
 *
 *  Copyright (C) 2000, 2001, 2002 Michael H. Schimek
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston,
 *  MA 02110-1301, USA.
 */

/* $Id: capture.c,v 1.39 2008-08-19 10:06:52 mschimek Exp $ */

/* For libzvbi version 0.2.x / 0.3.x. */

#undef NDEBUG

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <locale.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#ifdef HAVE_GETOPT_LONG
#include <getopt.h>
#endif

#include "src/version.h"
#if 2 == VBI_VERSION_MINOR
#  include "src/hamm.h"
#  define SCANNING(sp) ((sp)->scanning)
#elif 3 == VBI_VERSION_MINOR
#  include "src/vbi.h"
#  include "src/zvbi.h"
#  include "src/misc.h"
#  define SCANNING(sp)							\
	(0 != ((sp)->videostd_set & VBI_VIDEOSTD_SET_525_60) ? 525 : 625)
#else
#  error VBI_VERSION_MINOR == ?
#endif

#include "sliced.h"

#undef _
#define _(x) x /* TODO */

#define PROGRAM_NAME "zvbi-capture"

static const char *		option_out_file_name;
static enum file_format		option_out_file_format;
static unsigned int		option_out_ts_pid;

static int			option_read_not_pull;
static int			option_strict;
static int			option_dump_wss;
static int			option_dump_sliced;
static int			option_cc_test;
static int			option_cc_test_test;
static vbi_bool		option_raw_output;
static vbi_bool		option_sliced_output;
static unsigned int		option_sim_flags;

static struct stream *		cst;
static struct stream *		wst;
static const char **		sim_cc_streams;
static unsigned int		n_sim_cc_streams;

struct frame {
	vbi_sliced		sliced[50];
	unsigned int		n_lines;

	uint8_t *		raw;

	double			sample_time;
	int64_t			stream_time;
};

static struct frame		frame_buffers[5];
static unsigned int		next_frame;
static unsigned int		n_frames_buffered;
static unsigned int		sliced_output_count;
static unsigned int		raw_output_count;

/*
 *  Dump
 */

extern int vbi_printable (int);

static void
decode_wss_625			(const uint8_t *	buf)
{
	static const char *formats[] = {
		"Full format 4:3, 576 lines",
		"Letterbox 14:9 centre, 504 lines",
		"Letterbox 14:9 top, 504 lines",
		"Letterbox 16:9 centre, 430 lines",
		"Letterbox 16:9 top, 430 lines",
		"Letterbox > 16:9 centre",
		"Full format 14:9 centre, 576 lines",
		"Anamorphic 16:9, 576 lines"
	};
	static const char *subtitles[] = {
		"none",
		"in active image area",
		"out of active image area",
		"?"
	};
	int g1 = buf[0] & 15;
	int parity;

	if (option_dump_wss) {
		parity = g1;
		parity ^= parity >> 2;
		parity ^= parity >> 1;
		g1 &= 7;

		printf ("WSS PAL: ");
		if (!(parity & 1))
			printf ("<parity error> ");
		printf ("%s; %s mode; %s colour coding;\n"
			"  %s helper; reserved b7=%d; %s\n"
			"  open subtitles: %s; %scopyright %s; copying %s\n",
			formats[g1],
			(buf[0] & 0x10) ? "film" : "camera",
			(buf[0] & 0x20) ? "MA/CP" : "standard",
			(buf[0] & 0x40) ? "modulated" : "no",
			!!(buf[0] & 0x80),
			(buf[1] & 0x01) ? "have TTX subtitles; " : "",
			subtitles[(buf[1] >> 1) & 3],
			(buf[1] & 0x08) ? "surround sound; " : "",
			(buf[1] & 0x10) ? "asserted" : "unknown",
			(buf[1] & 0x20) ? "restricted" : "not restricted");
	}
}

static void
decode_wss_cpr1204		(const uint8_t *	buf)
{
	if (option_dump_wss) {
		const int poly = (1 << 6) + (1 << 1) + 1;
		int g = (buf[0] << 12) + (buf[1] << 4) + buf[2];
		int j, crc;

		crc = g | (((1 << 6) - 1) << (14 + 6));

		for (j = 14 + 6 - 1; j >= 0; j--) {
			if (crc & ((1 << 6) << j))
				crc ^= poly << j;
		}

		fprintf (stderr, "WSS CPR >> g=%08x crc=%08x\n", g, crc);
	}
}

static void
decode_sliced			(const vbi_sliced *	s,
				 unsigned int		n_lines,
				 double			sample_time,
				 int64_t		stream_time)
{
	if (option_dump_sliced) {
		const vbi_sliced *q = s;
		unsigned int i;

		printf ("Frame %f %010" PRId64 "\n",
			sample_time, stream_time);

		for (i = 0; i < n_lines; q++, i++) {
			unsigned int j;

			printf ("%08x %3d  ", q->id, q->line);

			for (j = 0; j < sizeof(q->data); j++) {
				printf ("%02x ", 0xFF & q->data[j]);
			}

			putchar (' ');

			for (j = 0; j < sizeof(q->data); j++) {
				char c = _vbi_to_ascii (q->data[j]);
				putchar (c);
			}

			putchar ('\n');
		}
	}

	for (; n_lines > 0; s++, n_lines--) {
		if (s->id == 0) {
			continue;
		} else if (s->id & (VBI_SLICED_VPS |
				    VBI_SLICED_TELETEXT_B |
				    VBI_SLICED_CAPTION_525 |
				    VBI_SLICED_CAPTION_625)) {
			/* Nothing to do.
			   Use the 'decode' tool instead. */
		} else if (s->id & VBI_SLICED_WSS_625) {
			decode_wss_625 (s->data);
		} else if (s->id & VBI_SLICED_WSS_CPR1204) {
			decode_wss_cpr1204 (s->data);
		} else {
			fprintf (stderr, "Oops. Unhandled VBI service %08x\n",
				 s->id);
		}
	}
}

/* Purpose of this function is to record raw and sliced VBI data
   around three kinds of events for remote examination:
   - Frames without data,
   - Lines which contain 0x00 0x00 instead of 0x80 0x80 bytes,
   - Lines where the transmitted bytes have wrong parity.
   The raw VBI data may be required to understand what happened,
   and due to its volume we cannot record it unconditionally. */
static vbi_bool
cc_test				(const vbi_sliced *	sliced,
				 unsigned int		n_lines)
{
	static const unsigned int max_error_count[3] = { 5, 5, 5 };
	static unsigned int error_count[3];
	static unsigned int frame_count;
	unsigned int error_set;
	unsigned int i;

	error_set = 0;

	if (option_cc_test_test && 0 == rand() % 300)
		n_lines = 0;

	if (0 == n_lines) {
		error_msg ("No data on this frame...");
		if (error_count[0] < max_error_count[0]) {
			++error_count[0];
			error_set |= 1 << 0;
		}
	} else {
		for (i = 0; i < n_lines; ++i) {
			if (sliced[i].id & (VBI_SLICED_CAPTION_525 |
					    VBI_SLICED_CAPTION_625)) {
				int b1, b2;
				int c1, c2;

				b1 = sliced[i].data[0];
				b2 = sliced[i].data[1];

				if (option_cc_test_test
				    && 0 == rand() % 300) {
					b1 = 0;
					b2 = 0;
				}

				if (0x00 == b1 && 0x00 == b2) {
					error_msg ("Null bytes...");
					if (error_count[1]
					    < max_error_count[1]) {
						++error_count[1];
						error_set |= 1 << 1;
					}
				}

				c1 = vbi_unpar8 (b1);
				c2 = vbi_unpar8 (b2);

				if (option_cc_test_test
				    && 0 == rand() % 300) {
					c2 = -1;
				}

				if ((c1 | c2) < 0) {
					error_msg ("Parity error...");
					if (error_count[2]
					    < max_error_count[2]) {
						++error_count[2];
						error_set |= 1 << 2;
					}
					break;
				}
			}
		}
	}

	if (0 != error_set) {
		unsigned int n_errors = 0;
		unsigned int n_kinds = 0;

		for (i = 0; i < N_ELEMENTS (error_count); ++i) {
			n_errors += 5 - error_count[i];
			n_kinds += (error_count[i] < 5);
		}

		if (0 == n_kinds) {
			error_msg ("Done.");
			return FALSE; /* terminate loop */
		}

		if (0 == frame_count % (5 * 30)) {
			error_msg ("Waiting for %u errors of %u kinds...",
				   n_errors, n_kinds);
		}
	}

	++frame_count;
 
	if (option_raw_output != (0 != error_set)) {
		option_sliced_output = (0 != error_set);

		raw_output_count = N_ELEMENTS (frame_buffers) * 2;

		if (sliced_output_count < raw_output_count)
			sliced_output_count = raw_output_count;
	}

	return TRUE; /* success, continue loop */
}

static vbi_bool
decode_frame			(const vbi_sliced *	sliced,
				 unsigned int		n_lines,
				 const uint8_t *	raw,
				 const vbi_sampling_par *sp,
				 double			sample_time,
				 int64_t		stream_time)
{
	if (option_dump_sliced || option_dump_wss)
		decode_sliced (sliced, n_lines,
			       sample_time, stream_time);

	if (option_cc_test) {
		struct frame *f;
		unsigned int size;

		f = frame_buffers + next_frame;

		if (n_frames_buffered >= N_ELEMENTS (frame_buffers)) {
			write_stream_sliced (wst,
					     (sliced_output_count > 0) ?
					     f->sliced : NULL,
					     f->n_lines,
					     (raw_output_count > 0) ?
					     f->raw : NULL,
					     sp,
					     f->sample_time,
					     f->stream_time);

			if (sliced_output_count > 0)
				--sliced_output_count;
			if (raw_output_count > 0)
				--raw_output_count;

			n_frames_buffered = N_ELEMENTS (frame_buffers) - 1;
		}

		assert (n_lines <= N_ELEMENTS (f->sliced));

		memcpy (f->sliced, sliced, n_lines * sizeof (*sliced));
		f->n_lines = n_lines;

		size = (sp->count[0] + sp->count[1]) * sp->bytes_per_line;
		memcpy (f->raw, raw, size);

		f->sample_time = sample_time;
		f->stream_time = stream_time;

		if (++next_frame >= N_ELEMENTS (frame_buffers))
			next_frame = 0;

		++n_frames_buffered;	

		if (!cc_test (sliced, n_lines))
			return FALSE;

	} else if (option_raw_output || option_sliced_output) {
		write_stream_sliced (wst,
				     option_sliced_output ? sliced : NULL,
				     n_lines,
				     option_raw_output ? raw : NULL,
				     sp,
				     sample_time,
				     stream_time);
	}

	return TRUE;
}

static void
init_frame_buffers		(const vbi_sampling_par *sp)
{
	unsigned int size;
	unsigned int i;

	size = (sp->count[0] + sp->count[1]) * sp->bytes_per_line;

	for (i = 0; i < N_ELEMENTS (frame_buffers); ++i) {
		frame_buffers[i].raw = malloc (size);
		if (NULL == frame_buffers[i].raw)
			no_mem_exit ();
	}
}

static void
usage				(FILE *			fp)
{
	fprintf (fp, _("\
%s %s -- VBI capture tool\n\n\
Copyright (C) 2000-2007 Michael H. Schimek\n\
This program is licensed under GPLv2 or later. NO WARRANTIES.\n\n\
Usage: %s [options] > sliced VBI data\n\
-h | --help | --usage  Print this message and exit\n\
-q | --quiet           Suppress progress and error messages\n\
-v | --verbose         Increase verbosity\n\
-V | --version         Print the program version and exit\n\
Device options:\n\
-c | --sim-cc file     Simulate a VBI device and load this Closed Caption\n\
                       test stream into the simulation\n\
-d | --device file     Capture from this device (default %s)\n\
                       V4L/V4L2: /dev/vbi, /dev/vbi0, /dev/vbi1, ...\n\
                       Linux DVB: /dev/dvb/adapter0/demux0, ...\n\
		       *BSD bktr driver: /dev/vbi, /dev/vbi0, ...\n\
-i | --pid pid         Capture the stream with this PID from a Linux\n\
                       DVB device\n\
-m | --sim-laced       Simulate a VBI device capturing interlaced raw\n\
                       VBI data\n\
-n | --ntsc            Video standard hint for V4L interface and\n\
                       simulated VBI device (default PAL/SECAM)\n\
-p | --pal | --secam   Video standard hint for V4L interface\n\
-s | --sim             Simulate a VBI device\n\
-u | --sim-unsync      Simulate a VBI device with wrong/unknown field\n\
                       parity\n\
-w | --sim-noise       Simulate a VBI device with noisy signal\n\
-x | --proxy           Capture through the VBI proxy daemon\n\
Output options:\n\
-j | --dump            Sliced VBI data (text)\n\
-l | --sliced          Sliced VBI data (binary)\n\
-o | --output name     Write the VBI data to this file instead of\n\
                       standard output\n"
/* later */ /*
"-r | --raw             Raw VBI data (binary)\n"
*/
"-P | --pes             DVB PES stream\n\
-T | --ts pid          DVB TS stream\n\
"),
		 PROGRAM_NAME, VERSION, program_invocation_name,
		 option_dev_name);
}

static const char short_options[] = "c:d:hi:jlmno:pqr:suvwxPT:V";

#ifdef HAVE_GETOPT_LONG
static const struct option
long_options[] = {
	{ "sim-cc",	required_argument,	NULL,		'c' },
	{ "device",	required_argument,	NULL,		'd' },
	{ "help",	no_argument,		NULL,		'h' },
	{ "usage",	no_argument,		NULL,		'h' },
	{ "pid",	required_argument,	NULL,		'i' },
	{ "dump",	no_argument,		NULL,		'j' },
	{ "sliced",	no_argument,		NULL,		'l' },
	{ "sim-laced",	no_argument,		NULL,		'm' },
	{ "ntsc",	no_argument,		NULL,		'n' },
	{ "output",	required_argument,	NULL,		'o' },
	{ "pal",	no_argument,		NULL,		'p' },
	{ "secam",	no_argument,		NULL,		'p' },
	{ "quiet",	no_argument,		NULL,		'q' },
	{ "raw",	optional_argument,	NULL,		'r' },
	{ "sim",	no_argument,		NULL,		's' },
	{ "sim-unsync",	no_argument,		NULL,		'u' },
	{ "verbose",	no_argument,		NULL,		'v' },
	{ "sim-noise",  optional_argument,	NULL,		'w' },
	{ "proxy",	no_argument,		NULL,		'x' },
	{ "pes",	no_argument,		NULL,		'P' },
	{ "ts",		required_argument,	NULL,		'T' },
	{ "version",	no_argument,		NULL,		'V' },
	{ "loose",	no_argument,	&option_strict,		0 },
	{ "strict",	no_argument,	&option_strict,		2 },
	{ "cc-test",	no_argument,	&option_cc_test,	TRUE },
	{ "cc-test-test", no_argument,	&option_cc_test_test,	TRUE },
	{ "dump-wss",	no_argument,	&option_dump_wss,	TRUE },
	{ "read",	no_argument,	&option_read_not_pull,	TRUE },
	{ "pull",	no_argument,	&option_read_not_pull,	FALSE },
	{ NULL, 0, 0, 0 }
};
#else
#define getopt_long(ac, av, s, l, i) getopt(ac, av, s)
#endif

static int			option_index;

static char *
load_string			(const char *		name)
{
	FILE *fp;
	char *buffer;
	size_t buffer_size;
	size_t done;

	buffer = NULL;
	buffer_size = 0;
	done = 0;

	fp = fopen (name, "r");
	if (NULL == fp) {
		exit (EXIT_FAILURE);
	}

	for (;;) {
		char *new_buffer;
		size_t new_size;
		size_t space;
		size_t actual;

		new_size = 16384;
		if (buffer_size > 0)
			new_size = buffer_size * 2;

		new_buffer = realloc (buffer, new_size);
		if (NULL == new_buffer) {
			free (buffer);
			exit (EXIT_FAILURE);
		}

		buffer = new_buffer;
		buffer_size = new_size;

		space = buffer_size - done - 1;
		actual = fread (buffer + done, 1, space, fp);
		if ((size_t) -1 == actual) {
			exit (EXIT_FAILURE);
		}
		done += actual;
		if (actual < space)
			break;
	}

	buffer[done] = 0;

	fclose (fp);

	return buffer;
}

static void
sim_load_caption		(void)
{
	unsigned int i;

	for (i = 0; i < n_sim_cc_streams; ++i) {
		char *buffer;
		vbi_bool success;

		fprintf (stderr, "Loading '%s'.\n", sim_cc_streams[i]);

		buffer = load_string (sim_cc_streams[i]);
		success = capture_stream_sim_load_caption
			(cst, buffer, /* append */ (i > 0));
		assert (success);
	}
}

int
main				(int			argc,
				 char **		argv)
{
	unsigned int interfaces;
	unsigned int scanning;
	unsigned int services;
	vbi_bool sim_interlaced;
	vbi_bool sim_synchronous;

	init_helpers (argc, argv);

	scanning = 625;

	sim_interlaced = FALSE;
	sim_synchronous = TRUE;

	option_strict = 1;
	option_dump_wss = FALSE;
	option_read_not_pull = FALSE;

	interfaces = (INTERFACE_V4L2 |
		      INTERFACE_V4L |
		      INTERFACE_BKTR);

	for (;;) {
		int c;

		c = getopt_long (argc, argv, short_options,
				 long_options, &option_index);
		if (-1 == c)
			break;

		switch (c) {
		case 0: /* getopt_long() flag */
			if (option_dump_wss) {
				option_raw_output = FALSE;
				option_sliced_output = FALSE;
			}
			break;

		case 'c':
		{
			const char **pp;

			pp = realloc (sim_cc_streams,
				      (n_sim_cc_streams + 1)
				      * sizeof (*pp));
			assert (NULL != pp);
			sim_cc_streams = pp;
			sim_cc_streams[n_sim_cc_streams++] = optarg;
			interfaces = INTERFACE_SIM;
			break;
		}

		case 'd':
			parse_option_dev_name ();
			break;

		case 'h':
			usage (stdout);
			exit (EXIT_SUCCESS);

		case 'i':
			parse_option_dvb_pid ();
			interfaces = INTERFACE_DVB;
			break;

		case 'j':
			option_dump_sliced = TRUE;
			option_raw_output = FALSE;
			option_sliced_output = FALSE;
			break;

		case 'l':
			option_sliced_output = TRUE;
			option_dump_sliced = FALSE;
			option_dump_wss = FALSE;
			if (FILE_FORMAT_XML != option_out_file_format)
				option_out_file_format = FILE_FORMAT_SLICED;
			break;

		case 'm':
			sim_interlaced = TRUE;
			interfaces = INTERFACE_SIM;
			break;

		case 'n':
			scanning = 525;
			break;

		case 'o':
			assert (NULL != optarg);
			option_out_file_name = optarg;
			break;

		case 'p':
			scanning = 625;
			break;

		case 'q':
			parse_option_quiet ();
			break;

#if 0
		case 'r':
			/* Optional optarg: line numbers
			   (not implemented yet). */
			option_raw_output = TRUE;
			option_dump_sliced = FALSE;
			option_dump_wss = FALSE;
			option_out_file_format = FILE_FORMAT_XML;
			break;
#endif
		case 's':
			interfaces = INTERFACE_SIM;
			break;

		case 'u':
			sim_synchronous = FALSE;
			interfaces = INTERFACE_SIM;
			break;

		case 'v':
			parse_option_verbose ();
			break;

		case 'w':
			/* Optional optarg: noise parameters
			   (not implemented yet). */
			interfaces = INTERFACE_SIM;
			option_sim_flags |= _VBI_RAW_NOISE_2;
			break;

		case 'x':
			interfaces &= ~(INTERFACE_SIM | INTERFACE_DVB);
			interfaces |= INTERFACE_PROXY;
			break;

		case 'P':
			option_sliced_output = TRUE;
			option_dump_sliced = FALSE;
			option_dump_wss = FALSE;
			option_out_file_format = FILE_FORMAT_DVB_PES;
			break;

		case 'T':
			option_sliced_output = TRUE;
			option_dump_sliced = FALSE;
			option_dump_wss = FALSE;
			option_out_ts_pid = parse_option_ts ();
			option_out_file_format = FILE_FORMAT_DVB_TS;
			break;

		case 'V':
			printf (PROGRAM_NAME " " VERSION "\n");
			exit (EXIT_SUCCESS);

		default:
			usage (stderr);
			exit (EXIT_FAILURE);
		}
	}

	if (!(option_sliced_output
	      || option_raw_output
	      || option_dump_sliced
	      || option_dump_wss)) {
		error_msg (_("Give one of the -j, -l, -P or -T options\n"
			     "to enable output, or -h for help."));
		exit (EXIT_FAILURE);
	}

	services = (VBI_SLICED_VBI_525 |
		    VBI_SLICED_VBI_625 |
		    VBI_SLICED_TELETEXT_B |
		    VBI_SLICED_CAPTION_525 |
		    VBI_SLICED_CAPTION_625 |
		    VBI_SLICED_VPS |
		    VBI_SLICED_VPS_F2 |
		    VBI_SLICED_WSS_625 |
		    VBI_SLICED_WSS_CPR1204);

	switch (option_out_file_format) {
	case FILE_FORMAT_DVB_PES:
	case FILE_FORMAT_DVB_TS:
		/* Other formats cannot be encoded. */
		services &= (VBI_SLICED_TELETEXT_B |
			     VBI_SLICED_CAPTION_625 |
			     VBI_SLICED_VPS |
			     VBI_SLICED_WSS_625);
		break;

	default:
		break;
	}

	cst = capture_stream_new (interfaces,
				  option_dev_name,
				  scanning,
				  services,
				  /* n_buffers (V4L2 mmap) */ 5,
				  option_dvb_pid,
				  sim_interlaced,
				  sim_synchronous,
				  option_raw_output,
				  option_read_not_pull,
				  option_strict,
				  decode_frame);

	if (interfaces & INTERFACE_SIM) {
		sim_load_caption ();

		capture_stream_sim_set_flags (cst, option_sim_flags);

		if (0 != option_sim_flags)
			capture_stream_sim_decode_raw (cst, TRUE);
	}

	if (option_cc_test_test)
		option_cc_test = TRUE;

	if (option_cc_test) {
		option_raw_output = FALSE;
		option_sliced_output = TRUE;
		option_dump_sliced = FALSE;
		option_dump_wss = FALSE;
		option_out_file_format = FILE_FORMAT_SLICED;
		sliced_output_count = 2 * 60 * 30;
	}

	if (option_raw_output || option_sliced_output) {
		vbi_sampling_par sp;

		capture_stream_get_sampling_par (cst, &sp);

		if (option_cc_test)
			init_frame_buffers (&sp);

		wst = write_stream_new (option_out_file_name,
					option_out_file_format,
					option_out_ts_pid,
					SCANNING (&sp));
	}

	stream_loop (cst);

	stream_delete (wst);
	wst = NULL;

	stream_delete (cst);
	cst = NULL;

	exit(EXIT_SUCCESS);	
}
