/*
 * $Id$
 *
 * dv2sub.c -- utility for extracting information from raw DV file
 * Copyright (C) 2006 Vaclav Ovsik <zito@users.sourceforge.net>
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "config.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <getopt.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "dv.h"

#define MMAP_BLOCK_LEN	    (16 * 1024 * 1024)

static int verbose_flag = 0;
static int mm_flag = 1;

#ifndef MIN
#define MIN(a, b)   ( (a) < (b) ? (a) : (b) )
#endif

#ifndef MAX
#define MAX(a, b)   ( (a) > (b) ? (a) : (b) )
#endif

#define FRAME_SIZE_PAL	144000
#define FRAME_SIZE_NTSC	120000

#define FRAME_SIZE_MIN	MIN(FRAME_SIZE_PAL, FRAME_SIZE_NTSC)
#define FRAME_SIZE_MAX	MAX(FRAME_SIZE_PAL, FRAME_SIZE_NTSC)

typedef int (* callback_t)(int, dv_decoder_t *, unsigned char *, void *);

typedef struct cbent_st
{
    callback_t cb;
    void *data;
} cbent_t;

static int dv_stream(int fd, int pass_through_flag, cbent_t cbent[])
{
    dv_decoder_t *dec;
    if ( (dec = dv_decoder_new(0, 0, 0)) == NULL )
    {
	fprintf(stderr, "dv_decoder_new() returns NULL\n");
	goto err_decoder_new;
    }

    unsigned char *frame;
    if ( (frame = (unsigned char *)malloc(FRAME_SIZE_MAX)) == NULL )
    {
	fprintf(stderr, "malloc() failed: %s\n", strerror(errno));
	goto err_malloc_frame;
    }

    int frameno = 0;
    size_t rest = 0;
    while ( 1 )
    {
	size_t len = rest;
	ssize_t n;
	do
	{
	    n = read(fd, frame + len, FRAME_SIZE_MAX - len);
	    if ( n == (ssize_t)-1 )
	    {
		fprintf(stderr, "read() failed: %s\n", strerror(errno));
		goto err_loop;
	    }
	    len += n;
	}
	while ( n );
	if ( len == 0 )
	    break;
	if ( len < FRAME_SIZE_MIN )
	{
	    fprintf(stderr, "remains only %u bytes, premature EOF?\n",
		    len);
	    goto err_loop;
	}
	if ( dv_parse_header(dec, frame) < 0 )
	{
	    fprintf(stderr, "dv_parse_header() failed\n"
		    "Input stream is corrupted or isn't raw DV stream!\n");
	    goto err_loop;
	}
	dv_parse_packs(dec, frame);
	for(cbent_t *ce = cbent; ce->cb != NULL; ce++)
	{
	    if ( (*(ce->cb))(frameno, dec, frame, ce->data) < 0 )
		goto err_loop;
	}
	size_t frame_size = dv_is_PAL(dec) ? FRAME_SIZE_PAL : FRAME_SIZE_NTSC;
	if ( pass_through_flag )
	{
	    size_t wlen = 0;
	    do
	    {
		ssize_t n = write(1, frame + wlen, frame_size - wlen);
		if ( n == (ssize_t)-1 )
		{
		    fprintf(stderr, "write() failed: %s\n", strerror(errno));
		    goto err_loop;
		}
		wlen += n;
	    }
	    while ( frame_size != wlen );
	}
	rest = FRAME_SIZE_MAX - frame_size;
	memmove(frame, frame + frame_size, rest);
	frameno++;
    }

    for(cbent_t *ce = cbent; ce->cb != NULL; ce++)
    {
	if ( (*(ce->cb))(-1, NULL, NULL, ce->data) < 0 )
	    goto err_loop;
    }

    free(frame);
    dv_decoder_free(dec);
    return 0;

err_loop:
    free(frame);
err_malloc_frame:
    dv_decoder_free(dec);
err_decoder_new:
    return -1;
}

#if HAVE_MMAP
static int dv_stream_mm(int fd, cbent_t cbent[])
{
    dv_decoder_t *dec;
    if ( (dec = dv_decoder_new(0, 0, 0)) == NULL )
    {
	fprintf(stderr, "dv_decoder_new() returns NULL\n");
	goto err_decoder_new;
    }

    size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
    if ( page_size == (size_t)-1 )
    {
	fprintf(stderr, "sysconf(_SC_PAGESIZE) returns -1\n");
	goto err_sysconf;
    }

    off_t flen = lseek(fd, 0, SEEK_END);
    if ( flen == (off_t)-1 )
    {
	fprintf(stderr, "lseek() failed: %s\n", strerror(errno));
	goto err_lseek;
    }

    unsigned char *mmdata = NULL;
    off_t offset = 0;
    int frameno = 0;
    off_t mmoff = 0;
    int mmlen = 0;
    while ( offset < flen )
    {
	if ( flen - offset < FRAME_SIZE_MIN )
	{
	    fprintf(stderr, "remains only %d bytes, premature EOF?\n",
		    (int)(flen - offset));
	    goto err_loop;
	}
	if ( offset + FRAME_SIZE_MAX > mmoff + mmlen )
	{
	    if ( mmdata )
	    {
		if ( munmap(mmdata, mmlen) )
		{
		    fprintf(stderr, "munmap() failed: %s\n", strerror(errno));
		    goto err_unmap;
		}
	    }
	    mmoff = offset - offset % page_size;
	    mmlen = MIN(MMAP_BLOCK_LEN, flen - offset + page_size);
	    if ( (mmdata = (unsigned char *)mmap(NULL, mmlen,
			    PROT_READ, MAP_SHARED, fd, mmoff)) == NULL )
	    {
		fprintf(stderr, "mmap() failed: %s\n", strerror(errno));
		goto err_mmap;
	    }
	}
	unsigned char *frame = mmdata + (int)(offset - mmoff);
	if ( dv_parse_header(dec, frame) < 0 )
	{
	    fprintf(stderr, "dv_parse_header() failed\n"
		    "Input stream is corrupted or isn't raw DV stream!\n");
	    goto err_loop;
	}
	dv_parse_packs(dec, frame);
	for(cbent_t *ce = cbent; ce->cb != NULL; ce++)
	{
	    if ( (*(ce->cb))(frameno, dec, frame, ce->data) < 0 )
		goto err_loop;
	}
	offset += dv_is_PAL(dec) ? FRAME_SIZE_PAL : FRAME_SIZE_NTSC;
	frameno++;
    }

    for(cbent_t *ce = cbent; ce->cb != NULL; ce++)
    {
	if ( (*(ce->cb))(-1, NULL, NULL, ce->data) < 0 )
	    goto err_loop;
    }

    if ( mmdata )
	if ( munmap(mmdata, mmlen) )
	{
	    fprintf(stderr, "munmap() failed: %s\n", strerror(errno));
	    goto err_unmap;
	}
    dv_decoder_free(dec);
    return 0;

err_loop:
    if ( mmdata )
	munmap(mmdata, mmlen);
err_mmap:
err_unmap:
err_lseek:
err_sysconf:
    dv_decoder_free(dec);
err_decoder_new:
    return -1;
}
#endif

static int dv_gensub(int frameno,
	dv_decoder_t *dec,
	unsigned char *frame,
	void *data)
{
    static char last_dtmstr[32];
    static int last_dtmstr_frameno;
    static int last_frameno;

    if ( frameno < 0 )
    {
	char *s = strchr(last_dtmstr, ' ');
	if ( s )
	    *s = '|';
	fprintf((FILE *)data, "{%d}{%d}%s\r\n",
		last_dtmstr_frameno, last_frameno, last_dtmstr);
	return 0;
    }

    char dtmstr[32];
    if ( dv_get_recording_datetime(dec, dtmstr) == 0 )
	dtmstr[0] = 0;
    if ( frameno )
    {
	if ( strcmp(last_dtmstr, dtmstr) )
	{
	    char *s = strchr(last_dtmstr, ' ');
	    if ( s )
		*s = '|';
	    fprintf((FILE *)data, "{%d}{%d}%s\r\n",
		    last_dtmstr_frameno, last_frameno, last_dtmstr);
	    strcpy(last_dtmstr, dtmstr);
	    last_dtmstr_frameno = frameno;
	}
    }
    else
    {
	strcpy(last_dtmstr, dtmstr);
	last_dtmstr_frameno = frameno;
    }
    last_frameno = frameno;
    return 0;
}

static int dv_info(int frameno,
	dv_decoder_t *dec,
	unsigned char *frame,
	void *data)
{
    if ( frameno < 0 )
	return 0;

    char tstpstr[32], dtmstr[32];
    char const *v_sysstr = dv_is_PAL(dec) ? "pal" : "ntsc";
    char const *v_fmtstr = dv_format_normal(dec) > 0 ? "normal" :
	    dv_format_wide(dec) > 0 ? "wide" :
	    dv_format_letterbox(dec) > 0 ? "letterbox" :
	    "unknown";
    int is_prog = dv_is_progressive(dec);
    char const *v_progr = is_prog > 0 ? " progressive" :
	    is_prog == 0 ? " interlaced" :
	    "";
    int a_sampl = dv_get_num_samples(dec);
    int a_ch = dv_get_num_channels(dec);
    int a_freq = dv_get_frequency(dec);
    dv_get_timestamp(dec, tstpstr);
    dv_get_recording_datetime(dec, dtmstr);

    fprintf((FILE *)data, "Frame # %d:\n"
	    "\tvideo: %s %s%s\n"
	    "\taudio: %d channels, %d Hz, %d samples\n"
	    "\ttimestamp: %s  recording date & time: %s\n",
	    frameno,
	    v_sysstr, v_fmtstr, v_progr,
	    a_ch, a_freq, a_sampl,
	    tstpstr, dtmstr);
    return 0;
}

static int dv_stream_wrap(int fd,
	const char *fn,
	int pass_through_flag,
	cbent_t cbent[])
{
    if ( verbose_flag )
    {
	if ( fd )
	    fprintf(stderr, "processing file `%s'...\n", fn);
	else
	    fprintf(stderr, "processing stdin...\n");
    }
    struct stat stat_buf;
    if ( fstat(fd, &stat_buf) )
    {
	if ( fd )
	    fprintf(stderr, "fstat(%d) failed (file `%s'): %s\n",
		    fd, fn, strerror(errno));
	else
	    fprintf(stderr, "fstat(0) failed: %s\n", strerror(errno));
	return -1;
    }
#if HAVE_MMAP
    if ( S_ISREG(stat_buf.st_mode) && mm_flag && !pass_through_flag )
	return dv_stream_mm(fd, cbent);
    else
#endif
	return dv_stream(fd, pass_through_flag, cbent);
}

static char const *progname;

static void usage( void )
{
    puts("Usage: dv2sub [options] [<dvfile>]\n"
"\n"
"Options:\n"
" -i, --info                generate textual information about frames\n"
"     --info-out <fn>       like -i, but write to file <fn>, instead stdout\n"
" -s, --subtitles           generate MicroDVD subtitles with rec. date & time\n"
"     --subtitles-out <fn>  like -s, but write to file <fn>, instead stdout\n"
"     --no-mmap             don't use memory mapped input\n"
" -p, --pass-through        pass input DV stream to stdout (implies --no-mmap)\n"
" -v, --verbose             be verbose\n"
" -h, --help                this short usage listing\n"
" -V, --version             utility version info\n"
"\n"
"Report bugs to " PACKAGE_BUGREPORT
	    );
    exit(1);
}

int main(int argc, char *argv[], char *env[])
{
    progname = argv[0];

    int info_flag = 0;
    int subtitles_flag = 0;
    int pass_through_flag = 0;
    char const *subtitles_filename = NULL;
    char const *info_filename = NULL;
    FILE *info_file = NULL;
    FILE *subtitles_file = NULL;

    if ( argc == 1 )
	usage();

    while ( 1 )
    {
	static struct option long_options[] =
	    {
		{"info", no_argument,			NULL, 'i'},
		{"subtitles", required_argument,	NULL, 's'},
		{"no-mmap", no_argument,		&mm_flag, 0},
		{"info-out", required_argument,		NULL, 0},
		{"subtitles-out", required_argument,	NULL, 0},
		{"pass-through", no_argument,		NULL, 'p'},
		{"verbose", no_argument,		NULL, 'v'},
		{"version", no_argument,	 	NULL, 'V'},
		{"help", no_argument,		 	NULL, 'h'},
		{0, 0, 0, 0}
	    };

	/* `getopt_long' stores the option index here. */
	int option_index = 0;

	int c = getopt_long(argc, argv, "ispvVh",
		long_options, &option_index);

	/* Detect the end of the options. */
	if ( c == -1 )
	    break;

	switch (c)
        {
	    case 0:
		/* If this option set a flag, do nothing else now. */
		if ( long_options[option_index].flag != 0 )
		    break;
		char const *optnam = long_options[option_index].name;
		if ( strcmp(optnam, "info-out") == 0 )
		{
		    info_filename = optarg;
		    break;
		}
		if ( strcmp(optnam, "subtitles-out") == 0 )
		{
		    subtitles_filename = optarg;
		    break;
		}
		break;

	    case 'i':
		info_flag = 1;
		break;

	    case 's':
		subtitles_flag = 1;
		break;

	    case 'p':
		pass_through_flag = 1;
		break;

	    case 'v':
		verbose_flag = 1;
		break;

	    case 'V':
		printf("%s %s\n", progname, PACKAGE_VERSION);
		exit(0);

	    case 'h':
		usage();

	    case '?':
		/* `getopt_long' already printed an error message. */
		usage();
      
	    default:
		abort();
        }
    }

    if ( pass_through_flag
	    + (info_flag && !info_filename)
	    + (subtitles_flag && !subtitles_filename)
	    > 1 )
    {
	fprintf(stderr, "Conflict options for stdout."
		" You make a mistake probably.\n");
	exit(1);
    }

    int fail = 0;

    cbent_t ce[8];
    memset(ce, 0, sizeof(ce));
    int i_ce = 0;

    if ( info_filename )
    {
	if ( strcmp(info_filename, "-") == 0 )
	    info_file = stdout;
	else
	    if ( (info_file = fopen(info_filename, "w")) == NULL )
	    {
		fprintf(stderr, "fopen(\"%s\"...) failed: %s\n",
			info_filename, strerror(errno));
		exit(1);
	    }
	info_flag = 1;
    }
    else
	info_file = stdout;

    if ( info_flag )
    {
	ce[i_ce].cb = &dv_info;
	ce[i_ce].data = info_file;
	i_ce++;
    }

    if ( subtitles_filename )
    {
	if ( strcmp(subtitles_filename, "-") == 0 )
	    subtitles_file = stdout;
	else
	    if ( (subtitles_file = fopen(subtitles_filename, "wb")) == NULL )
	    {
		fprintf(stderr, "fopen(\"%s\"...) failed: %s\n",
			subtitles_filename, strerror(errno));
		exit(1);
	    }
	subtitles_flag = 1;
    }
    else
	subtitles_file = stdout;

    if ( subtitles_flag )
    {
	ce[i_ce].cb = &dv_gensub;
	ce[i_ce].data = subtitles_file;
	i_ce++;
    }

    if ( i_ce )
    {
	if ( optind == argc )
	{
	    fail |= dv_stream_wrap(0, NULL, pass_through_flag, ce);
	}
	else
	{
	    for(int i = optind; i < argc; i++)
	    {
		int fd;
		if ( (fd = open(argv[i], O_RDONLY)) < 0 )
		{
		    fprintf(stderr, "open(\"%s\"...) failed: %s\n",
			argv[i], strerror(errno));
		    continue;
		}
		fail |= dv_stream_wrap(fd, argv[i], pass_through_flag, ce);
		close(fd);
	    }
	}
    }

    if ( info_filename )
    {
	if ( strcmp(info_filename, "-") != 0 )
	{
	    if ( fclose(info_file) )
	    {
		fprintf(stderr, "fclose() (file `%s') failed: %s\n",
			info_filename, strerror(errno));
	    }
	    else
		info_file = NULL;
	}
    }

    if ( subtitles_filename )
    {
	if ( strcmp(subtitles_filename, "-") != 0 )
	{
	    if ( fclose(subtitles_file) )
	    {
		fprintf(stderr, "fclose() (file `%s') failed: %s\n",
			subtitles_filename, strerror(errno));
	    }
	    else
		subtitles_file = NULL;
	}
    }

    if ( verbose_flag )
	fprintf(stderr, "all done\n");

    return fail ? 1 : 0;
}
