/*
 * Copyright (C) 2007 Loren Merritt <lorenm@u.washington.edu>
 *
 * 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 St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#define _LARGEFILE_SOURCE
#define _FILE_OFFSET_BITS 64

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#define _GNU_SOURCE
#include <getopt.h>

#include "src/common.h"
#include "muxers.h"

typedef struct {
    int decode;
    int progress;
    int seek;
    int frame_total;
    char *infile;
    char *outfile;
    FILE *bsf;
    y8_mux_t *y8;
} cli_opt_t;

static int Parse( int argc, char **argv, xlsc_param_t *param, cli_opt_t *opt );
static int Decode( xlsc_param_t *param, cli_opt_t *opt );
static int Encode( xlsc_param_t *param, cli_opt_t *opt );

int main( int argc, char **argv )
{
    xlsc_param_t param;
    cli_opt_t opt;
    int ret;

    xlsc_param_default( &param );

    if( Parse( argc, argv, &param, &opt ) < 0 )
        return -1;

    if( opt.decode )
    {
        ret = Decode( &param, &opt );
        y8_out_close( opt.y8 );
        fclose( opt.bsf );
    }
    else
    {
        ret = Encode( &param, &opt );
        y8_in_close( opt.y8 );
        fclose( opt.bsf );
    }
    return ret;
}

static int Parse( int argc, char **argv, xlsc_param_t *param, cli_opt_t *opt )
{
    memset( opt, 0, sizeof(cli_opt_t) );
    opt->progress = 1;

    for( ;; )
    {
        int error = 0;
        int long_opts_index = -1;

#define OPT_FRAMES 256
#define OPT_SEEK 257
#define OPT_QUIET 258
#define OPT_RESOLUTION 259

        static struct option long_opts[] =
        {
            { "deadzone",  required_argument, NULL, 0 },
            { "dump-file", required_argument, NULL, 0 },
            { "keyint",    required_argument, NULL, 'I' },
            { "lambda",    required_argument, NULL, 0 },
            { "lookahead", required_argument, NULL, 'l' },
            { "lookahead-lambda",required_argument, NULL, 0 },
            { "mbd",       required_argument, NULL, 'm' },
            { "qp",        required_argument, NULL, 'q' },
            { "no-umv",    no_argument,       NULL, 0 },
            { "verbose",   required_argument, NULL, 'v' },

            { "decode",    no_argument,       NULL, 'd' },
            { "encode",    no_argument,       NULL, 'e' },
            { "input",     required_argument, NULL, 'i' },
            { "output",    required_argument, NULL, 'o' },
            { "frames",    required_argument, NULL, OPT_FRAMES },
            { "seek",      required_argument, NULL, OPT_SEEK },
            { "resolution",required_argument, NULL, OPT_RESOLUTION },
            { "quiet",     no_argument,       NULL, OPT_QUIET },
            { 0, 0, 0, 0 }
        };

        int c = getopt_long( argc, argv, "dei:I:l:m:o:q:v",
                             long_opts, &long_opts_index);

        if( c == -1 )
        {
            break;
        }

        switch( c )
        {
            case 'd':
                opt->decode = 1;
                break;
            case 'e':
                opt->decode = 0;
                break;
            case 'i':
                opt->infile = optarg;
                break;
            case 'o':
                opt->outfile = optarg;
                break;
            case OPT_FRAMES:
                opt->frame_total = atoi( optarg );
                if( opt->frame_total < 0 )
                    error = 1;
                break;
            case OPT_SEEK:
                opt->seek = atoi( optarg );
                if( opt->seek < 0 )
                    error = 1;
                break;
            case OPT_RESOLUTION:
                if( !optarg || sscanf( optarg, "%ux%u", &param->width, &param->height ) != 2 )
                    error = 1;
                break;
            case OPT_QUIET:
                opt->progress = 0;
                break;
            default:
            {
                int i;
                if( long_opts_index < 0 )
                {
                    for( i = 0; long_opts[i].name; i++ )
                        if( long_opts[i].val == c )
                        {
                            long_opts_index = i;
                            break;
                        }
                    if( long_opts_index < 0 )
                    {
                        /* getopt_long already printed an error message */
                        return -1;
                    }
                }

                error |= xlsc_param_parse( param, long_opts[long_opts_index].name, optarg );
            }
        }

        if( error )
        {
            const char *name = long_opts_index > 0 ? long_opts[long_opts_index].name : argv[optind-2];
            fprintf( stderr, "xlsc [error]: invalid argument: %s = %s\n", name, optarg );
            return -1;
        }
    }

    if( !opt->infile || !opt->outfile )
    {
        if( !opt->infile && !opt->outfile )
            fprintf( stderr, "xlsc [error]: --input and --output are required\n" );
        else
            fprintf( stderr, "xlsc [error]: --%sput is required\n", opt->infile?"out":"in" );
        return -1;
    }

    if( opt->decode )
    {
        opt->bsf = fopen( opt->infile, "rb" );
        opt->y8 = NULL; // delay until we know the resolution
        if( !opt->bsf )
        {
            fprintf( stderr, "xlsc [error]: can't read %s\n", opt->infile );
            return -1;
        }
    }
    else
    {
        if( !param->width || !param->height )
        {
            char *p;
            for( p = opt->infile; *p; p++ )
            {
                if( *p >= '0' && *p <= '9'
                    && sscanf( p, "%ux%u", &param->width, &param->height ) == 2 )
                    break;
            }
        }
        if( !param->width || !param->height )
        {
            fprintf( stderr, "xlsc [error]: can't find resolution\n" );
            return -1;
        }

        opt->y8 = y8_in_open( opt->infile, param->width, param->height );
        if( !opt->y8 )
        {
            fprintf( stderr, "xlsc [error]: can't read %s\n", opt->infile );
            return -1;
        }

        opt->bsf = fopen( opt->outfile, "wb" );
        if( !opt->bsf )
        {
            fprintf( stderr, "xlsc [error]: can't write %s\n", opt->outfile );
            return -1;
        }
    }

    return 0;
}


static uint64_t mdate()
{
    struct timeval tv_date;
    gettimeofday( &tv_date, NULL );
    return (int64_t)tv_date.tv_sec * 1000000 + tv_date.tv_usec;
}

static void progress_update( const char *name, int frame, int frame_total, int force )
{
    static uint64_t tstart = 0;
    static int progress = 0;
    if( tstart == 0 )
        tstart = mdate();
    else if( (frame_total ? frame * 1000 / frame_total > progress
                          : frame % 10 == 0)
             || force )
    {
        int64_t elapsed = mdate() - tstart;
        double fps = elapsed > 0 ? frame * 1e6 / elapsed : 0;
        if( frame_total > 0 )
        {
            int eta = elapsed * (frame_total - frame) / (frame * 1e6);
            fprintf( stderr, "encoded frames: %d/%d (%.1f%%), %.2f fps, eta %d:%02d:%02d  \r",
                     frame, frame_total, 100.*frame/frame_total, fps,
                     eta/3600, (eta/60)%60, eta%60 );
            progress++;
        }
        else
            fprintf( stderr, "%s %d, %.2f fps   \r", name, frame, fps );
        fflush( stderr ); // needed in windows
    }
}

static int Decode( xlsc_param_t *param, cli_opt_t *opt )
{
    xlsc_t *h;
    xlsc_picture_t pic;
    int frame;
    int frame_total = opt->frame_total;
    uint8_t *data = NULL;
    int size = 0;

    if( bsf_in_read_frame( opt->bsf, &data, &size ) < 0 ) // header
    {
        fprintf( stderr, "xlsc [error]: can't read header\n" );
        return -1;
    }

    if( ( h = xlsc_decoder_open( param, data, size ) ) == NULL )
    {
        fprintf( stderr, "xlsc [error]: xlsc_decoder_open failed\n" );
        return -1;
    }

    opt->y8 = y8_out_open( opt->outfile, param->width, param->height );
    if( !opt->y8 )
    {
        fprintf( stderr, "xlsc [error]: can't write %s\n", opt->outfile );
    }

    for( frame = 0; frame < frame_total || frame_total == 0; frame++ )
    {
        if( opt->progress )
            progress_update( "decoded:", frame, frame_total, 0 );

        if( bsf_in_read_frame( opt->bsf, &data, &size ) < 0 )
            break;

        xlsc_decoder_decode( h, data, size, &pic );
        if( y8_out_write_frame( opt->y8, &pic ) < 0 )
        {
            fprintf( stderr, "xlsc [error]: can't write %s\n", opt->outfile );
            return -1;
        }
    }

    if( opt->progress )
    {
        progress_update( "decoded:", frame, frame_total, 1 );
        fprintf( stderr, "\n" );
    }

    xlsc_decoder_close( h );

    return 0;
}

static int Encode( xlsc_param_t *param, cli_opt_t *opt )
{
    xlsc_t *h;
    xlsc_picture_t pic;
    int frame, frame_total;
    uint8_t *data;
    int size;

    frame_total = y8_in_get_frame_total( opt->y8 );
    frame_total -= opt->seek;
    if( ( frame_total == 0 || opt->frame_total < frame_total )
        && opt->frame_total > 0 )
        frame_total = opt->frame_total;

    if( ( h = xlsc_encoder_open( param, &data, &size ) ) == NULL )
    {
        fprintf( stderr, "xlsc [error]: xlsc_encoder_open failed\n" );
        return -1;
    }

    if( bsf_out_write_frame( opt->bsf, data, size ) < 0 ) // header
    {
        fprintf( stderr, "xlsc [error]: can't write %s\n", opt->outfile );
        return -1;
    }

    pic.plane = malloc( param->width * param->height );

    for( frame = 0; frame < frame_total || frame_total == 0; frame++ )
    {
        if( opt->progress )
            progress_update( "encoded:", frame, frame_total, 0 );

        if( y8_in_read_frame( opt->y8, &pic, frame + opt->seek ) < 0 )
            break;

        pic.type = XLSC_FRAME_AUTO;
        pic.qp = 0;

        xlsc_encoder_encode( h, &data, &size, &pic );
        if( size > 0 && bsf_out_write_frame( opt->bsf, data, size ) < 0 )
        {
            fprintf( stderr, "xlsc [error]: can't write %s\n", opt->outfile );
            return -1;
        }
    }

    // flush delayed frames
    for( ; frame++; )
    {
        xlsc_encoder_encode( h, &data, &size, NULL );
        if( size == 0 ) // FIXME fails if encode delay > total number of frames
            break;
        if( bsf_out_write_frame( opt->bsf, data, size ) < 0 )
        {
            fprintf( stderr, "xlsc [error]: can't write %s\n", opt->outfile );
            return -1;
        }
    }

    if( opt->progress )
    {
        progress_update( "encoded:", frame, frame_total, 1 );
        fprintf( stderr, "\n" );
    }

    free( pic.plane );
    xlsc_encoder_close( h );

    return 0;
}
