///////////////////////////////////////////////////////////////////////////////
//
//  The contents of this file are subject to the Mozilla Public License
//  Version 1.1 (the "License"); you may not use this file except in
//  compliance with the License. You may obtain a copy of the License at
//  http://www.mozilla.org/MPL/
//
//  Software distributed under the License is distributed on an "AS IS"
//  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//  License for the specific language governing rights and limitations
//  under the License.
// 
//  The Original Code is MP4v2.
// 
//  The Initial Developer of the Original Code is Kona Blend.
//  Portions created by Kona Blend are Copyright (C) 2008.
//  All Rights Reserved.
//
//  Contributors:
//      Kona Blend, kona8lend@@gmail.com
//
///////////////////////////////////////////////////////////////////////////////

#include "libutil/impl.h"

namespace mp4v2 { namespace util {

///////////////////////////////////////////////////////////////////////////////

Utility::Utility( const string& name_, int argc_, char** argv_ )
    : _longOptions      ( NULL )
    , _name             ( name_ )
    , _argc             ( argc_ )
    , _argv             ( argv_ )
    , _optimize         ( false )
    , _dryrun           ( false )
    , _keepgoing        ( false )
    , _overwrite        ( false )
    , _force            ( false )
    , _debug            ( 0 )
    , _verbosity        ( 1 )
    , _jobCount         ( 0 )
    , _jobTotal         ( 0 )
    , _debugImplicits   ( false )
    , _group            ( "OPTIONS" )

,STD_OPTIMIZE( 'z', false, "optimize", false, LC_NONE, "optimize mp4 file after modification" )
,STD_DRYRUN( 'y', false, "dryrun", false, LC_NONE, "do not actually create or modify any files" )
,STD_KEEPGOING( 'k', false, "keepgoing", false, LC_NONE, "continue batch processing even after errors" )
,STD_OVERWRITE( 'o', false, "overwrite", false, LC_NONE, "overwrite existing files when creating" )
,STD_FORCE( 'f', false, "force", false, LC_NONE, "force overwrite even if file is read-only" )
,STD_QUIET( 'q', false, "quiet", false, LC_NONE, "equivalent to --verbose 0" )
,STD_DEBUG( 'd', false, "debug", true, LC_DEBUG, "increase debug or long-option to set NUM", "NUM",
    // 79-cols, inclusive, max desired width
    // |----------------------------------------------------------------------------|
    "\nDEBUG LEVELS (for raw mp4 file I/O)"
    "\n  0  supressed"
    "\n  1  add warnings and errors (default)"
    "\n  2  add table details"
    "\n  3  add implicits"
    "\n  4  everything" )
,STD_VERBOSE( 'v', false, "verbose", true, LC_VERBOSE, "increase verbosity or long-option to set NUM", "NUM",
    // 79-cols, inclusive, max desired width
    // |----------------------------------------------------------------------------|
    "\nVERBOSE LEVELS"
    "\n  0  warnings and errors"
    "\n  1  normal informative messages (default)"
    "\n  2  more informative messages"
    "\n  3  everything" )
,STD_HELP( 'h', false, "help", false, LC_HELP, "print brief help or long-option for extended help" )
,STD_VERSION( 0, false, "version", false, LC_VERSION, "print version information and exit" )
,STD_VERSIONX( 0, false, "versionx", false, LC_VERSIONX, "print extended version information", "ARG", "", true )

{
    debugUpdate( 1 );

    _usage = "<UNDEFINED>";
    _description = "<UNDEFINED>";
    _groups.push_back( &_group );
}

///////////////////////////////////////////////////////////////////////////////

Utility::~Utility()
{
    delete[] _longOptions;
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::batch( int argi )
{
    _jobCount = 0;
    _jobTotal = _argc - argi;

    // nothing to be done
    if( !_jobTotal )
        return SUCCESS;

    bool batchResult = FAILURE;
    for( int i = argi; i < _argc; i++ ) {
        bool subResult = FAILURE;
        try {
            if( !job( _argv[i] )) {
                batchResult = SUCCESS;
                subResult = SUCCESS;
            }
        }
        catch( Exception* x ) {
            mp4v2::impl::log.errorf(*x);
            delete x;
        }
 
        if( !_keepgoing && subResult == FAILURE )
            return FAILURE;
    }
    
    return batchResult;
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::debugUpdate( uint32_t debug )
{
    MP4LogLevel level;

    _debug = debug;
    verbose2f( "debug level: %u\n", _debug );

    switch( _debug ) {
        case 0:
            level = MP4_LOG_NONE;
            _debugImplicits = false;
            break;

        case 1:
            level = MP4_LOG_ERROR;
            _debugImplicits = false;
            break;

        case 2:
            level = MP4_LOG_VERBOSE2;
            _debugImplicits = false;
            break;

        case 3:
            level = MP4_LOG_VERBOSE2;
            _debugImplicits = true;
            break;

        case 4:
        default:
            level = MP4_LOG_VERBOSE4;
            _debugImplicits = true;
            break;
    }

    MP4LogSetLevel(level);
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::dryrunAbort()
{
    if( !_dryrun )
        return false;

    verbose2f( "skipping: dry-run mode enabled\n" );
    return true;
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::errf( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );
    vfprintf( stderr, format, ap );
    va_end( ap );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::formatGroups()
{
    // determine longest long-option [+space +argname]
    int longMax = 0;
    list<Group*>::reverse_iterator ie = _groups.rend();
    for( list<Group*>::reverse_iterator it = _groups.rbegin(); it != ie; ++it ) {
        Group& group = **it;
        const Group::List::const_iterator ieo = group.options.end();
        for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ++ito ) {
            const Option& option = **ito;
            if( option.hidden )
                continue;

            int len = (int)option.lname.length();
            if( option.lhasarg )
                len += 1 + (int)option.argname.length();
            if( len > longMax )
                longMax = len;
        }
    }

    // format help output (no line-wrapping yet)
    ostringstream oss;

    int groupCount = 0;
    int optionCount = 0;
    ie = _groups.rend();
    for( list<Group*>::reverse_iterator it = _groups.rbegin(); it != ie; ++it, groupCount++ ) {
        if( groupCount )
            oss << '\n';
        Group& group = **it;
        oss << '\n' << group.name;
        const Group::List::const_iterator ieo = group.options.end();
        for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ++ito, optionCount++ ) {
            const Option& option = **ito;
            if( option.hidden )
                continue;

            oss << "\n ";

            if( option.scode == 0 )
                oss << "    --";
            else
                oss << '-' << option.scode << ", --";

            if( option.lhasarg ) {
                oss << option.lname << ' ' << option.argname;
                oss << setw( longMax - option.lname.length() - 1 - option.argname.length() ) << "";
            }
            else {
                oss << setw( longMax ) << left << option.lname;
            }

            oss << "  ";

            const string::size_type imax = option.descr.length();
            for( string::size_type i = 0; i < imax; i++ )
                oss << option.descr[i];
        }
    }

    _help = oss.str();

    // allocate and populate C-style options
    delete[] _longOptions;
    _longOptions = new prog::Option[optionCount + 1];

    // fill EOL marker
    _longOptions[optionCount].name = NULL;
    _longOptions[optionCount].type = prog::Option::NO_ARG;
    _longOptions[optionCount].flag = 0;
    _longOptions[optionCount].val  = 0;

    _shortOptions.clear();

    int optionIndex = 0;
    ie = _groups.rend();
    for( list<Group*>::reverse_iterator it = _groups.rbegin(); it != ie; ++it ) {
        Group& group = **it;
        const Group::List::const_iterator ieo = group.options.end();
        for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ++ito, optionIndex++ ) {
            const Option& a = **ito;
            prog::Option& b = _longOptions[optionIndex];

            b.name = const_cast<char*>(a.lname.c_str());
            b.type = a.lhasarg ? prog::Option::REQUIRED_ARG : prog::Option::NO_ARG;
            b.flag = 0;
            b.val  = (a.lcode == LC_NONE) ? a.scode : a.lcode;

            if( a.scode != 0 ) {
                _shortOptions += a.scode;
                if( a.shasarg )
                    _shortOptions += ':';
            }
        }
    }
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::job( const string& arg )
{
    verbose2f( "job begin: %s\n", arg.c_str() );

    // perform job
    JobContext job( arg );
    bool result = FAILURE;
    try {
        result = utility_job( job );
    }
    catch( Exception* x ) {
        mp4v2::impl::log.errorf(*x);
        delete x;
    }

    // close file handle flagged with job
    if( job.fileHandle != MP4_INVALID_FILE_HANDLE ) {
        verbose2f( "closing %s\n", job.file.c_str() );
        MP4Close( job.fileHandle );

        // invoke optimize if flagged
        if( _optimize && job.optimizeApplicable ) {
            verbose1f( "optimizing %s\n", job.file.c_str() );
            if( !MP4Optimize( job.file.c_str(), NULL ))
                hwarnf( "optimize failed: %s\n", job.file.c_str() );
        }
    }

    // free data flagged with job
    list<void*>::iterator ie = job.tofree.end();
    for( list<void*>::iterator it = job.tofree.begin(); it != ie; ++it )
        free( *it );


    verbose2f( "job end\n" );
    _jobCount++;
    return result;
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::herrf( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );

    if( _keepgoing ) {
        fprintf( stdout, "WARNING: " );
        vfprintf( stdout, format, ap );
    }
    else {
        fprintf( stderr, "ERROR: " );
        vfprintf( stderr, format, ap );
    }

    va_end( ap );
    return FAILURE;
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::hwarnf( const char* format, ... )
{
    fprintf( stdout, "WARNING: " );
    va_list ap;
    va_start( ap, format );
    vfprintf( stdout, format, ap );
    va_end( ap );
    return FAILURE;
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::outf( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );
    vfprintf( stdout, format, ap );
    va_end( ap );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::printHelp( bool extended, bool toerr )
{
    ostringstream oss;
    oss << "Usage: " << _name << " " << _usage << '\n' << _description << '\n' << _help;

    if( extended ) {
        const list<Group*>::reverse_iterator ie = _groups.rend();
        for( list<Group*>::reverse_iterator it = _groups.rbegin(); it != ie; ++it ) {
            Group& group = **it;
            const Group::List::const_iterator ieo = group.options.end();
            for( Group::List::const_iterator ito = group.options.begin(); ito != ieo; ++ito ) {
                const Option& option = **ito;
                if( option.help.empty() )
                    continue;

                oss << '\n' << option.help;
            }
        }
    }

    if( toerr )
        errf( "%s\n\n", oss.str().c_str() );
    else
        outf( "%s\n\n", oss.str().c_str() );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::printUsage( bool toerr )
{
    ostringstream oss;
    oss <<   "Usage: " << _name << " " << _usage
        << "\nTry -h for brief help or --help for extended help";
 
    if( toerr )
        errf( "%s\n", oss.str().c_str() );
    else
        outf( "%s\n", oss.str().c_str() );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::printVersion( bool extended )
{
    ostringstream oss;
    oss << left;

    if( extended ) {
        oss <<         setw(13) << "utility:" << _name
            << '\n' << setw(13) << "product:" << MP4V2_PROJECT_name
            << '\n' << setw(13) << "version:" << MP4V2_PROJECT_version
            << '\n' << setw(13) << "build date:" << MP4V2_PROJECT_build
            << '\n'
            << '\n' << setw(18) << "repository URL:" << MP4V2_PROJECT_repo_url;
    }
    else {
        oss << _name << " - " << MP4V2_PROJECT_name_formal;
    }

    outf( "%s\n", oss.str().c_str() );
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::process()
{
    bool rv = true;

    try {
        rv = process_impl();
    }
    catch( Exception* x ) {
        _keepgoing = false;
        mp4v2::impl::log.errorf(*x);
        delete x;
    }

    return rv;
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::process_impl()
{
    formatGroups();

    // populate code lookup set
    set<int> codes;
    const Group::List::const_iterator ie = _group.options.end();
    for( Group::List::const_iterator it = _group.options.begin(); it != ie; ++it ) {
        const Option& option = **it;
        if( option.scode != 0 )
            codes.insert( option.scode );
        if( option.lcode != LC_NONE )
            codes.insert( option.lcode );
    }

    for( ;; ) {
        const int code = prog::getOption( _argc, _argv, _shortOptions.c_str(), _longOptions, NULL );
        if( code == -1 )
            break;

        bool handled = false;
        if( utility_option( code, handled ))
            return FAILURE;
        if( handled )
            continue;

        if( codes.find( code ) == codes.end() )
            continue;

        switch( code ) {
            case 'z':
                _optimize = true;
                break;

            case 'y':
                _dryrun = true;
                break;

            case 'k':
                _keepgoing = true;
                break;

            case 'o':
                _overwrite = true;
                break;

            case 'f':
                _overwrite = true;
                _force = true;
                break;

            case 'q':
                _verbosity = 0;
                debugUpdate( 0 );
                break;

            case 'v':
                _verbosity++;
                break;

            case 'd':
                debugUpdate( _debug + 1 );
                break;

            case 'h':
                printHelp( false, false );
                return SUCCESS;

            case LC_DEBUG:
                debugUpdate( std::strtoul( prog::optarg, NULL, 0 ) );
                break;

            case LC_VERBOSE:
            {
                const uint32_t level = std::strtoul( prog::optarg, NULL, 0 );
                _verbosity = ( level < 4 ) ? level : 3;
                break;
            }

            case LC_HELP:
                printHelp( true, false );
                return SUCCESS;

            case LC_VERSION:
                printVersion( false );
                return SUCCESS;

            case LC_VERSIONX:
                printVersion( true );
                return SUCCESS;

            default:
                printUsage( true );
                return FAILURE;
        }
    }

    if( !(prog::optind < _argc) ) {
        printUsage( true );
        return FAILURE;
    }

    const bool result = batch( prog::optind );
    verbose2f( "exit code %d\n", result );
    return result;
}

///////////////////////////////////////////////////////////////////////////////

bool
Utility::openFileForWriting( io::File& file )
{
    // simple case is file does not exist
    if( !io::FileSystem::exists( file.name )) {
        if( file.open() )
            return herrf( "unable to open %s for write: %s\n", file.name.c_str(), sys::getLastErrorStr() );
        return SUCCESS;
    }

    // fail if overwrite is not enabled
    if( !_overwrite )
        return herrf( "file already exists: %s\n", file.name.c_str() );

    // only overwrite if it is a file
    if( !io::FileSystem::isFile( file.name ))
        return herrf( "cannot overwrite non-file: %s\n", file.name.c_str() );

    // first attemp to re-open/truncate so as to keep any file perms
    if( !file.open() )
        return SUCCESS;

    // fail if force is not enabled
    if( !_force )
        return herrf( "unable to overwrite file: %s\n", file.name.c_str() );

    // first attempt to open, truncating file
    if( !file.open() )
        return SUCCESS;

    // nuke file
    if( ::remove( file.name.c_str() ))
        return herrf( "unable to remove %s: %s\n", file.name.c_str(), sys::getLastErrorStr() );

    // final effort
    if( !file.open() )
        return SUCCESS;

    return herrf( "unable to open %s for write: %s\n", file.name.c_str(), sys::getLastErrorStr() );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::verbose( uint32_t level, const char* format, va_list ap )
{
    if( level > _verbosity )
        return;
    vfprintf( stdout, format, ap );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::verbose1f( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );
    verbose( 1, format, ap );
    va_end( ap );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::verbose2f( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );
    verbose( 2, format, ap );
    va_end( ap );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::verbose3f( const char* format, ... )
{
    va_list ap;
    va_start( ap, format );
    verbose( 3, format, ap );
    va_end( ap );
}

///////////////////////////////////////////////////////////////////////////////

const bool Utility::SUCCESS = false;
const bool Utility::FAILURE = true;

///////////////////////////////////////////////////////////////////////////////

Utility::Group::Group( const string& name_ )
    : name    ( name_ )
    , options ( _options )
{
}

///////////////////////////////////////////////////////////////////////////////

Utility::Group::~Group()
{
    const List::iterator ie = _optionsDelete.end();
    for( List::iterator it = _optionsDelete.begin(); it != ie; ++it )
        delete *it;
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::Group::add( const Option& option )
{
    _options.push_back( &option );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::Group::add(
    char          scode,
    bool          shasarg,
    const string& lname,
    bool          lhasarg,
    uint32_t      lcode,
    const string& descr,
    const string& argname,
    const string& help,
    bool          hidden )
{
    Option* o = new Option( scode, shasarg, lname, lhasarg, lcode, descr, argname, help, hidden );
    _options.push_back( o );
    _optionsDelete.push_back( o );
}

///////////////////////////////////////////////////////////////////////////////

void
Utility::Group::add( 
    const string& lname,
    bool          lhasarg,
    uint32_t      lcode,
    const string& descr,
    const string& argname,
    const string& help,
    bool          hidden )
{
    add( 0, false, lname, lhasarg, lcode, descr, argname, help, hidden );
}

///////////////////////////////////////////////////////////////////////////////

Utility::Option::Option(
    char          scode_,
    bool          shasarg_,
    const string& lname_,
    bool          lhasarg_,
    uint32_t      lcode_,
    const string& descr_,
    const string& argname_,
    const string& help_,
    bool          hidden_ )
        : scode   ( scode_ )
        , shasarg ( shasarg_ )
        , lname   ( lname_ )
        , lhasarg ( lhasarg_ )
        , lcode   ( lcode_ )
        , descr   ( descr_ )
        , argname ( argname_ )
        , help    ( help_ )
        , hidden  ( hidden_ )
{
}

///////////////////////////////////////////////////////////////////////////////

Utility::JobContext::JobContext( const string& file_ )
    : file               ( file_ )
    , fileHandle         ( MP4_INVALID_FILE_HANDLE )
    , optimizeApplicable ( false )
{
}

///////////////////////////////////////////////////////////////////////////////

}} // namespace mp4v2::util
