///////////////////////////////////////////////////////////////////////////////
//
//  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 Ullrich Pollaehne.
//  Portions created by Kona Blend are Copyright (C) 2008.
//  Portions created by David Byron are Copyright (C) 2010.
//  Portions created by Robert Kausch are Copyright (C) 2022.
//  All Rights Reserved.
//
//  Contributors:
//      Kona Blend, kona8lend@@gmail.com
//      Ullrich Pollaehne, u.pollaehne@@gmail.com
//      David Byron, dbyron@dbyron.com
//      Robert Kausch, robert.kausch@freac.org
//
///////////////////////////////////////////////////////////////////////////////

#include "util/impl.h"

namespace mp4v2 { namespace util {

///////////////////////////////////////////////////////////////////////////////
///
/// Chapter utility program class.
///
/// This class provides an implementation for a QuickTime/Nero chapter utility which
/// allows to add, delete, convert export or import QuickTime and Nero chapters
/// in MP4 container files.
///
///
/// @see Utility
///
///////////////////////////////////////////////////////////////////////////////
class ChapterUtility : public Utility
{
private:
    static const double CHAPTERTIMESCALE; //!< the timescale used for chapter tracks (1000)

    enum FileLongCode {
        LC_CHPT_ANY = _LC_MAX,
        LC_CHPT_QT,
        LC_CHPT_NERO,
        LC_CHPT_NATIVE,
        LC_CHPT_COMMON,
        LC_CHP_LIST,
        LC_CHP_CONVERT,
        LC_CHP_EVERY,
        LC_CHP_EXPORT,
        LC_CHP_IMPORT,
        LC_CHP_REMOVE
    };

    enum ChapterFormat {
        CHPT_FMT_NATIVE,
        CHPT_FMT_COMMON
    };

    enum FormatState {
        FMT_STATE_INITIAL,
        FMT_STATE_TIME_LINE,
        FMT_STATE_TITLE_LINE,
        FMT_STATE_FINISH
    };

public:
    ChapterUtility( int, char** );

protected:
    // delegates implementation
    bool utility_option( int, bool& );
    bool utility_job( JobContext& );

private:
    bool actionList    ( JobContext& ); 
    bool actionConvert ( JobContext& );
    bool actionEvery   ( JobContext& );
    bool actionExport  ( JobContext& );
    bool actionImport  ( JobContext& );
    bool actionRemove  ( JobContext& );

private:
    Group  _actionGroup;
    Group  _parmGroup;

    bool        (ChapterUtility::*_action)( JobContext& );
    void        fixQtScale(MP4FileHandle );
    MP4TrackId  getReferencingTrack( MP4FileHandle, bool& );
    string      getChapterTypeName( MP4ChapterType ) const;
    bool        parseChapterFile( const string&, vector<MP4Chapter_t>&, Timecode::Format& );
    bool        readChapterFile( const string&, char**, File::Size& );
    MP4Duration convertFrameToMillis( MP4Duration, uint32_t );

    MP4ChapterType _ChapterType;
    ChapterFormat  _ChapterFormat;
    uint32_t       _ChaptersEvery;
    string         _ChapterFile;
};

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

const double ChapterUtility::CHAPTERTIMESCALE = 1000.0;

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

ChapterUtility::ChapterUtility( int argc, char** argv )
    : Utility        ( "mp4chaps", argc, argv )
    , _actionGroup   ( "ACTIONS" )
    , _parmGroup     ( "ACTION PARAMETERS" )
    , _action        ( NULL )
    , _ChapterType   ( MP4ChapterTypeAny )
    , _ChapterFormat ( CHPT_FMT_NATIVE )
    , _ChaptersEvery ( 0 )
{
    // add standard options which make sense for this utility
    _group.add( STD_OPTIMIZE );
    _group.add( STD_DRYRUN );
    _group.add( STD_KEEPGOING );
    _group.add( STD_OVERWRITE );
    _group.add( STD_FORCE );
    _group.add( STD_QUIET );
    _group.add( STD_DEBUG );
    _group.add( STD_VERBOSE );
    _group.add( STD_HELP );
    _group.add( STD_VERSION );
    _group.add( STD_VERSIONX );

    _parmGroup.add( 'A', false, "chapter-any",   false, LC_CHPT_ANY,    "act on any chapter type (default)" );
    _parmGroup.add( 'Q', false, "chapter-qt",    false, LC_CHPT_QT,     "act on QuickTime chapters" );
    _parmGroup.add( 'N', false, "chapter-nero",  false, LC_CHPT_NERO,   "act on Nero chapters" );
    _parmGroup.add(  0 , false, "format-native", false, LC_CHPT_NATIVE, "export chapters in native format (default)" );
    _parmGroup.add( 'C', false, "format-common", false, LC_CHPT_COMMON, "export chapters in common format" );
    _groups.push_back( &_parmGroup );

    _actionGroup.add( 'l', false, "list",    false, LC_CHP_LIST,    "list available chapters" );
    _actionGroup.add( 'c', false, "convert", false, LC_CHP_CONVERT, "convert available chapters" );
    _actionGroup.add( 'e', true,  "every",   true,  LC_CHP_EVERY,   "create chapters every NUM seconds", "NUM" );
    _actionGroup.add( 'x', false, "export",  false, LC_CHP_EXPORT,  "export chapters to mp4file.chapters.txt", "TXT" );
    _actionGroup.add( 'i', false, "import",  false, LC_CHP_IMPORT,  "import chapters from mp4file.chapters.txt", "TXT" );
    _actionGroup.add( 'r', false, "remove",  false, LC_CHP_REMOVE,  "remove all chapters" );
    _groups.push_back( &_actionGroup );

    _usage = "[OPTION]... ACTION [ACTION PARAMETERS] mp4file...";
    _description =
        // 79-cols, inclusive, max desired width
        // |----------------------------------------------------------------------------|
        "\nFor each mp4 file specified, perform the specified ACTION. An action must be"
        "\nspecified. Some options are not applicable to some actions.";
}

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

/** Action for listing chapters from <b>job.file</b>
 *
 *  
 *  @param job the job to process
 *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
 */
bool
ChapterUtility::actionList( JobContext& job )
{
    job.fileHandle = MP4Read( job.file.c_str() );
    if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
    {
        return herrf( "unable to open for read: %s\n", job.file.c_str() );
    }

    MP4Chapter_t * chapters = 0;
    uint32_t chapterCount = 0;

    // get the list of chapters
    MP4ChapterType chtp = MP4GetChapters(job.fileHandle, &chapters, &chapterCount, _ChapterType);
    if (0 == chapterCount)
    {
        verbose1f( "File \"%s\" does not contain chapters of type %s\n", job.file.c_str(),
                   getChapterTypeName( _ChapterType ).c_str() );
        return SUCCESS;
    }

    // start output (more or less like mp4box does)
    ostringstream report;
    report << getChapterTypeName( chtp ) << ' ' << "Chapters of " << '"' << job.file << '"' << endl;

    Timecode duration(0, CHAPTERTIMESCALE);
    duration.setFormat( Timecode::DECIMAL );
    for (uint32_t i = 0; i < chapterCount; ++i)
    {
        // print the infos
        report << '\t' << "Chapter #" << setw( 3 ) << setfill( '0' ) << i+1
               << " - " << duration.svalue << " - " << '"' << chapters[i].title << '"' << endl;

        // add the duration of this chapter to the sum (is the start time of the next chapter)
        duration += Timecode(chapters[i].duration, CHAPTERTIMESCALE);
    }

    verbose1f( "%s", report.str().c_str() );

    // free up the memory
    MP4Free(chapters);

    return SUCCESS;
}

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

/** Action for converting chapters in <b>job.file</b>
 *
 *  
 *  @param job the job to process
 *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
 */
bool
ChapterUtility::actionConvert( JobContext& job )
{
    MP4ChapterType sourceType;

    switch( _ChapterType )
    {
    case MP4ChapterTypeNero:
        sourceType = MP4ChapterTypeQt;
        break;
    case MP4ChapterTypeQt:
        sourceType = MP4ChapterTypeNero;
        break;
    default:
        return herrf( "invalid chapter type \"%s\" define the chapter type to convert to\n",
                      getChapterTypeName( _ChapterType ).c_str() );
    }

    ostringstream oss;
    oss << "converting chapters in file " << '"' << job.file << '"'
        << " from " << getChapterTypeName( sourceType ) << " to " << getChapterTypeName( _ChapterType ) << endl;

    verbose1f( "%s", oss.str().c_str() );
    if( dryrunAbort() )
    {
        return SUCCESS;
    }

    job.fileHandle = MP4Modify( job.file.c_str() );
    if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
    {
        return herrf( "unable to open for write: %s\n", job.file.c_str() );
    }

    MP4ChapterType chtp = MP4ConvertChapters( job.fileHandle, _ChapterType );
    if( MP4ChapterTypeNone == chtp )
    {
        return herrf( "File %s does not contain chapters of type %s\n", job.file.c_str(),
                      getChapterTypeName( sourceType ).c_str() );
    }

    fixQtScale( job.fileHandle );
    job.optimizeApplicable = true;

    return SUCCESS;
}

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

/** Action for setting chapters every n second in <b>job.file</b>
 *
 *  
 *  @param job the job to process
 *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
 */
bool
ChapterUtility::actionEvery( JobContext& job )
{
    ostringstream oss;
    oss << "Setting " << getChapterTypeName( _ChapterType ) << " chapters every "
        << _ChaptersEvery << " seconds in file " << '"' << job.file << '"' << endl;

    verbose1f( "%s", oss.str().c_str() );
    if( dryrunAbort() )
    {
        return SUCCESS;
    }

    job.fileHandle = MP4Modify( job.file.c_str() );
    if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
    {
        return herrf( "unable to open for write: %s\n", job.file.c_str() );
    }

    bool isVideoTrack = false;
    MP4TrackId refTrackId = getReferencingTrack( job.fileHandle, isVideoTrack );
    if( !MP4_IS_VALID_TRACK_ID(refTrackId) )
    {
        return herrf( "unable to find a video or audio track in file %s\n", job.file.c_str() );
    }

    Timecode refTrackDuration( MP4GetTrackDuration( job.fileHandle, refTrackId ), MP4GetTrackTimeScale( job.fileHandle, refTrackId ) );
    refTrackDuration.setScale( CHAPTERTIMESCALE );

    Timecode chapterDuration( _ChaptersEvery * 1000, CHAPTERTIMESCALE );
    chapterDuration.setFormat( Timecode::DECIMAL );
    vector<MP4Chapter_t> chapters;

    do
    {
        MP4Chapter_t chap;
        chap.duration = refTrackDuration.duration > chapterDuration.duration ? chapterDuration.duration : refTrackDuration.duration;
        sprintf(chap.title, "Chapter %lu", (unsigned long)chapters.size()+1);

        chapters.push_back( chap );
        refTrackDuration -= chapterDuration;
    }
    while( refTrackDuration.duration > 0 );

    if( 0 < chapters.size() )
    {
        MP4SetChapters(job.fileHandle, &chapters[0], (uint32_t)chapters.size(), _ChapterType);
    }

    fixQtScale( job.fileHandle );
    job.optimizeApplicable = true;

    return SUCCESS;
}

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

/** Action for exporting chapters from the <b>job.file</b>
 *
 *  
 *  @param job the job to process
 *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
 */
bool
ChapterUtility::actionExport( JobContext& job )
{
    job.fileHandle = MP4Read( job.file.c_str() );
    if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
    {
        return herrf( "unable to open for read: %s\n", job.file.c_str() );
    }

    // get the list of chapters
    MP4Chapter_t*  chapters = 0;
    uint32_t       chapterCount = 0;
    MP4ChapterType chtp = MP4GetChapters( job.fileHandle, &chapters, &chapterCount, _ChapterType );
    if (0 == chapterCount)
    {
        return herrf( "File \"%s\" does not contain chapters of type %s\n", job.file.c_str(),
                      getChapterTypeName( chtp ).c_str() );
    }

    // build the filename
    string outName = job.file;
    if( _ChapterFile.empty() )
    {
        FileSystem::pathnameStripExtension( outName );
        outName.append( ".chapters.txt" );
    }
    else
    {
        outName = _ChapterFile;
    }

    ostringstream oss;
    oss << "Exporting " << chapterCount << " " << getChapterTypeName( chtp );
    oss << " chapters from file " << '"' << job.file << '"' << " into chapter file " << '"' << outName << '"' << endl;

    verbose1f( "%s", oss.str().c_str() );
    if( dryrunAbort() )
    {
        // free up the memory
        MP4Free(chapters);

        return SUCCESS;
    }

    // open the file
    File out( outName, File::MODE_CREATE );
    if( openFileForWriting( out ) )
    {
        // free up the memory
        MP4Free(chapters);

        return FAILURE;
    }

    // set up format
#if defined( _WIN32 )
    static const char* LINEND = "\r\n";
#else
    static const char* LINEND = "\n";
#endif
    File::Size nout;
    bool failure = SUCCESS;
    int width = 2;
    if( _ChapterFormat == CHPT_FMT_COMMON && (chapterCount / 100) >= 1 )
        width = 3;

    // write additional information
    if( _ChapterFormat == CHPT_FMT_NATIVE ) {
        ostringstream oss;
        // write metadata
        const MP4Tags* tags = MP4TagsAlloc();
        MP4TagsFetch( tags, job.fileHandle );

        if( tags->albumArtist && (!tags->artist || !strequal(tags->albumArtist, tags->artist)) )
            oss << "## album-artist: " << tags->albumArtist << LINEND;

        if( tags->artist )
            oss << "## artist: " << tags->artist << LINEND;

        if( tags->album && (!tags->name || !strequal(tags->album, tags->name)) )
            oss << "## album: " << tags->album << LINEND;

        if( tags->name )
            oss << "## title: " << tags->name << LINEND;

        if( oss.tellp() > 0 )
            oss << "##" << LINEND;

        MP4TagsFree( tags );

        // write total duration
        Timecode movieDuration( MP4GetDuration( job.fileHandle ),
                                MP4GetTimeScale( job.fileHandle ) );
        movieDuration.setScale( CHAPTERTIMESCALE );
        movieDuration.setFormat( Timecode::DECIMAL );

        oss << "## total-duration: " << movieDuration.svalue << LINEND
            << "##" << LINEND;

        string str = oss.str();
        if( out.write( str.c_str(), str.size(), nout ) )
            failure = herrf( "write to %s failed: %s\n", outName.c_str(), sys::getLastErrorStr() );
    }

    // write the chapters
    Timecode duration( 0, CHAPTERTIMESCALE );
    duration.setFormat( Timecode::DECIMAL );
    for( uint32_t i = 0; i < chapterCount; ++i )
    {
        // print the infos
        ostringstream oss;
        switch( _ChapterFormat )
        {
            case CHPT_FMT_COMMON:
                oss << "CHAPTER" << setw( width ) << setfill( '0' ) << i+1 <<     '=' << duration.svalue << LINEND
                    << "CHAPTER" << setw( width ) << setfill( '0' ) << i+1 << "NAME=" << chapters[i].title << LINEND;
                break;
            case CHPT_FMT_NATIVE:
            default:
                oss << duration.svalue << ' ' << chapters[i].title << LINEND;
        }

        string str = oss.str();
        if( out.write( str.c_str(), str.size(), nout ) )
        {
            failure = herrf( "write to %s failed: %s\n", outName.c_str(), sys::getLastErrorStr() );
            break;
        }

        // add the duration of this chapter to the sum (the start time of the next chapter)
        duration += Timecode(chapters[i].duration, CHAPTERTIMESCALE);
    }
    out.close();
    if( failure )
    {
        verbose1f( "removing file %s\n", outName.c_str() );
        ::remove( outName.c_str() );
    }

    // free up the memory
    MP4Free(chapters);

    return SUCCESS;
}

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

/** Action for importing chapters into the <b>job.file</b>
 *
 *  
 *  @param job the job to process
 *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
 */
bool
ChapterUtility::actionImport( JobContext& job )
{
    vector<MP4Chapter_t> chapters;
    Timecode::Format format;

    // create the chapter file name
    string inName = job.file;
    if( _ChapterFile.empty() )
    {
        FileSystem::pathnameStripExtension( inName );
        inName.append( ".chapters.txt" );
    }
    else
    {
        inName = _ChapterFile;
    }

    if( parseChapterFile( inName, chapters, format ) )
    {
        return FAILURE;
    }

    ostringstream oss;
    oss << "Importing " << chapters.size() << " " << getChapterTypeName( _ChapterType );
    oss << " chapters from file " << inName << " into file " << '"' << job.file << '"' << endl;

    verbose1f( "%s", oss.str().c_str() );
    if( dryrunAbort() )
    {
        return SUCCESS;
    }

    if( 0 == chapters.size() )
    {
        return herrf( "No chapters found in file %s\n", inName.c_str() );
    }

    job.fileHandle = MP4Modify( job.file.c_str() );
    if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
    {
        return herrf( "unable to open for write: %s\n", job.file.c_str() );
    }

    bool isVideoTrack = false;
    MP4TrackId refTrackId = getReferencingTrack( job.fileHandle, isVideoTrack );
    if( !MP4_IS_VALID_TRACK_ID(refTrackId) )
    {
        return herrf( "unable to find a video or audio track in file %s\n", job.file.c_str() );
    }
    if( Timecode::FRAME == format && !isVideoTrack )
    {
        // we need a video track for this
        return herrf( "unable to find a video track in file %s but chapter file contains frame timestamps\n", job.file.c_str() );
    }

    // get duration and recalculate scale
    Timecode refTrackDuration( MP4GetTrackDuration( job.fileHandle, refTrackId ),
                               MP4GetTrackTimeScale( job.fileHandle, refTrackId ) );
    refTrackDuration.setScale( CHAPTERTIMESCALE );

    // check for chapters starting after duration of reftrack
    for( vector<MP4Chapter_t>::iterator it = chapters.begin(); it != chapters.end(); )
    {
        Timecode curr( (*it).duration, CHAPTERTIMESCALE );
        if( refTrackDuration <= curr )
        {
            hwarnf( "Chapter '%s' start: %s, playlength of file: %s, chapter cannot be set\n",
                    (*it).title, curr.svalue.c_str(), refTrackDuration.svalue.c_str() );
            it = chapters.erase( it );
        }
        else
        {
            ++it;
        }
    }
    if( 0 == chapters.size() )
    {
        return SUCCESS;
    }

    // convert start time into duration
	uint32_t framerate = static_cast<uint32_t>( CHAPTERTIMESCALE );
    if( Timecode::FRAME == format )
    {
        // get the framerate
        MP4SampleId sampleCount = MP4GetTrackNumberOfSamples( job.fileHandle, refTrackId );
        Timecode tmpcd( refTrackDuration.svalue, CHAPTERTIMESCALE );
		framerate = static_cast<uint32_t>( std::ceil( ((double)sampleCount / (double)tmpcd.duration) * CHAPTERTIMESCALE ) );
    }

    for( vector<MP4Chapter_t>::iterator it = chapters.begin(); it != chapters.end(); ++it )
    {
        MP4Duration currDur = (*it).duration;
        MP4Duration nextDur =  chapters.end() == it+1 ? refTrackDuration.duration : (*(it+1)).duration;

        if( Timecode::FRAME == format )
        {
            // convert from frame nr to milliseconds
            currDur = convertFrameToMillis( (*it).duration, framerate );

            if( chapters.end() != it+1 )
            {
                nextDur = convertFrameToMillis( (*(it+1)).duration, framerate );
            }
        }

        (*it).duration = nextDur - currDur;
    }

    // now set the chapters
    MP4SetChapters( job.fileHandle, &chapters[0], (uint32_t)chapters.size(), _ChapterType );

    fixQtScale( job.fileHandle );
    job.optimizeApplicable = true;

    return SUCCESS;
}

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

/** Action for removing chapters from the <b>job.file</b>
 *
 *  
 *  @param job the job to process
 *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
 */
bool
ChapterUtility::actionRemove( JobContext& job )
{
    ostringstream oss;
    oss << "Deleting " << getChapterTypeName( _ChapterType ) << " chapters from file " << '"' << job.file << '"' << endl;

    verbose1f( "%s", oss.str().c_str() );
    if( dryrunAbort() )
    {
        return SUCCESS;
    }

    job.fileHandle = MP4Modify( job.file.c_str() );
    if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
    {
        return herrf( "unable to open for write: %s\n", job.file.c_str() );
    }

    MP4ChapterType chtp = MP4DeleteChapters( job.fileHandle, _ChapterType );
    if( MP4ChapterTypeNone == chtp )
    {
        return FAILURE;
    }

    fixQtScale( job.fileHandle );
    job.optimizeApplicable = true;

    return SUCCESS;
}

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

/** process positional argument
 *
 *  @see Utility::utility_job( JobContext& )
 */
bool
ChapterUtility::utility_job( JobContext& job )
{
    if( !_action )
    {
        return herrf( "no action specified\n" );
    }

    return (this->*_action)( job );
}

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

/** process command-line option
 *
 *  @see Utility::utility_option( int, bool& )
 */
bool
ChapterUtility::utility_option( int code, bool& handled )
{
    handled = true;

    switch( code ) {
        case 'A':
        case LC_CHPT_ANY:
            _ChapterType = MP4ChapterTypeAny;
            break;

        case 'Q':
        case LC_CHPT_QT:
            _ChapterType = MP4ChapterTypeQt;
            break;

        case 'N':
        case LC_CHPT_NERO:
            _ChapterType = MP4ChapterTypeNero;
            break;

        case LC_CHPT_NATIVE:
            _ChapterFormat = CHPT_FMT_NATIVE;
            break;

        case 'C':
        case LC_CHPT_COMMON:
            _ChapterFormat = CHPT_FMT_COMMON;
            break;

        case 'l':
        case LC_CHP_LIST:
            _action = &ChapterUtility::actionList;
            break;

        case 'e':
        case LC_CHP_EVERY:
        {
            istringstream iss( prog::optarg );
            iss >> _ChaptersEvery;
            if( iss.rdstate() != ios::eofbit )
            {
                return herrf( "invalid number of seconds: %s\n", prog::optarg );
            }
            _action = &ChapterUtility::actionEvery;
            break;
        }

        case 'x':
            _action = &ChapterUtility::actionExport;
            break;

        case LC_CHP_EXPORT:
            _action = &ChapterUtility::actionExport;
            /* currently not supported since the chapters of n input files would be written to one chapter file
            _ChapterFile = prog::optarg;
            if( _ChapterFile.empty() )
            {
                return herrf( "invalid TXT file: empty-string\n" );
            }
            */
            break;

        case 'i':
            _action = &ChapterUtility::actionImport;
            break;

        case LC_CHP_IMPORT:
            _action = &ChapterUtility::actionImport;
            /* currently not supported since the chapters of n input files would be read from one chapter file
            _ChapterFile = prog::optarg;
            if( _ChapterFile.empty() )
            {
                return herrf( "invalid TXT file: empty-string\n" );
            }
            */
            break;

        case 'c':
        case LC_CHP_CONVERT:
            _action = &ChapterUtility::actionConvert;
            break;

        case 'r':
        case LC_CHP_REMOVE:
            _action = &ChapterUtility::actionRemove;
            break;

        default:
            handled = false;
            break;
    }

    return SUCCESS;
}

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

/** Fix a QuickTime/iPod issue with long audio files.
 *
 *  This function checks if the <b>file</b> is a long audio file (more than
 *  about 6 1/2 hours) and modifies the timescale if necessary to allow
 *  playback of the file in QuickTime player and on some iPod models.
 *
 *  @param file the opened MP4 file
 */
void
ChapterUtility::fixQtScale(MP4FileHandle file)
{
    // get around a QuickTime/iPod issue with storing the number of samples in a signed 32Bit value
    if( INT_MAX < MP4GetDuration(file))
    {
        bool isVideoTrack = false;
        if( MP4_IS_VALID_TRACK_ID(getReferencingTrack( file, isVideoTrack )) & isVideoTrack )
        {
            // if it is a video, everything is different
            return;
        }

        // timescale too high, lower it
        MP4ChangeMovieTimeScale(file, 1000);
    }
}

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

/** Finds a suitable track that can reference a chapter track.
 *
 *  This function returns the first video or audio track that is found
 *  in the <b>file</b>.
 *  This track ca be used to reference the QuickTime chapter track.
 *
 *  @param file the opened MP4 file
 *  @param isVideoTrack receives true if the found track is video, false otherwise
 *  @return the <b>MP4TrackId</b> of the found track
 */
MP4TrackId
ChapterUtility::getReferencingTrack( MP4FileHandle file, bool& isVideoTrack )
{
    isVideoTrack = false;

    uint32_t trackCount = MP4GetNumberOfTracks( file );
    if( 0 == trackCount )
    {
        return MP4_INVALID_TRACK_ID;
    }

    MP4TrackId refTrackId = MP4_INVALID_TRACK_ID;
    for( uint32_t i = 0; i < trackCount; ++i )
    {
        MP4TrackId    id = MP4FindTrackId( file, i );
        const char* type = MP4GetTrackType( file, id );
        if( MP4_IS_VIDEO_TRACK_TYPE( type ) )
        {
            refTrackId = id;
            isVideoTrack = true;
            break;
        }
        else if( MP4_IS_AUDIO_TRACK_TYPE( type ) )
        {
            refTrackId = id;
            break;
        }
    }

    return refTrackId;
}

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

/** Return a human readable representation of a <b>MP4ChapterType</b>.
 *
 *  @param chapterType the chapter type
 *  @return a string representing the chapter type
 */
string
ChapterUtility::getChapterTypeName( MP4ChapterType chapterType) const
{
    switch( chapterType )
    {
    case MP4ChapterTypeQt:
        return string( "QuickTime" );
        break;

    case MP4ChapterTypeNero:
        return string( "Nero" );
        break;

    case MP4ChapterTypeAny:
        return string( "QuickTime and Nero" );
        break;

    default:
        return string( "Unknown" );
    }
}

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

/** Read a file into a buffer.
 *
 *  This function reads the file named by <b>filename</b> into a buffer allocated
 *  by malloc and returns the pointer to this buffer in <b>buffer</b> and the size
 *  of this buffer in <b>fileSize</b>.
 *
 *  @param filename  the name of the file.
 *  @param buffer    receives a pointer to the created buffer
 *  @param fileSize  reference to a <b>io::StdioFile::Size</b> that receives the size of the file
 *  @return true if there was an error, false otherwise
 */
bool
ChapterUtility::readChapterFile( const string& filename, char** buffer, File::Size& fileSize )
{
    // open the file
    File in( filename, File::MODE_READ );
    File::Size nin;
    if( in.open() ) {
        return herrf( "opening chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() );
    }

    // get the file size
    fileSize = in.size;
    if( 0 >= fileSize )
    {
        in.close();
        return herrf( "getting size of chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() );
    }

    // allocate a buffer for the file and read the content
    char* inBuf = static_cast<char*>( malloc( fileSize+1 ) );
    if( in.read( inBuf, fileSize, nin ) )
    {
        in.close();
        free(inBuf);
        return herrf( "reading chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() );
    }
    in.close();
    inBuf[fileSize] = 0;

    *buffer = inBuf;

    return SUCCESS;
}

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

/** Read and parse a chapter file.
 *
 *  This function reads and parses a chapter file and returns a vector of
 *  <b>MP4Chapter_t</b> elements.
 *
 *  @param filename the name of the file.
 *  @param vector   receives a vector of chapters
 *  @param format   receives the <b>Timecode::Format</b> of the timestamps
 *  @return true if there was an error, false otherwise
 */
bool
ChapterUtility::parseChapterFile( const string& filename, vector<MP4Chapter_t>& chapters, Timecode::Format& format )
{
    // get the content
    char * inBuf;
    File::Size fileSize;
    if( readChapterFile( filename, &inBuf, fileSize ) )
    {
        return FAILURE;
    }

    // separate the text lines
    char* pos = inBuf;
    while (pos < inBuf + fileSize)
    {
        if (*pos == '\n' || *pos == '\r')
        {
            *pos = 0;
            if (pos > inBuf)
            {
                // remove trailing whitespace
                char* tmp = pos-1;
                while ((*tmp == ' ' || *tmp == '\t') && tmp > inBuf)
                {
                    *tmp = 0;
                    tmp--;
                }
            }
        }
        pos++;
    }
    pos = inBuf;

    // check for a BOM
    const unsigned char* uPos = reinterpret_cast<unsigned char*>( pos );
    if( 0xEF == *uPos && 0xBB == *(uPos+1) && 0xBF == *(uPos+2) )
    {
        // UTF-8 (we do not need the BOM)
        pos += 3;
    }
    else if(   ( 0xFE == *uPos && 0xFF == *(uPos+1) )   // UTF-16 big endian
            || ( 0xFF == *uPos && 0xFE == *(uPos+1) ) ) // UTF-16 little endian
    {
        return herrf( "chapter file '%s' has UTF-16 encoding which is not supported (only UTF-8 is allowed)\n",
                      filename.c_str() );
    }
    else if(   ( 0x0 == *uPos && 0x0 == *(uPos+1) && 0xFE == *(uPos+2) && 0xFF == *(uPos+3) )   // UTF-32 big endian
            || ( 0xFF == *uPos && *(uPos+1) == 0xFE && *(uPos+2) == 0x0 && 0x0 == *(uPos+3) ) ) // UTF-32 little endian
    {
        return herrf( "chapter file '%s' has UTF-32 encoding which is not supported (only UTF-8 is allowed)\n",
                      filename.c_str() );
    }

    // parse the lines
    bool failure = false;
    uint32_t currentChapter = 0;
    FormatState formatState = FMT_STATE_INITIAL;
    char* titleStart = 0;
    uint32_t titleLen = 0;
    char* timeStart = 0;
    while( pos < inBuf + fileSize )
    {
        if( 0 == *pos || ' ' == *pos || '\t' == *pos )
        {
            // uninteresting chars
            pos++;
            continue;
        }
        else if( '#' == *pos )
        {
            // comment line
            pos += strlen( pos );
            continue;
        }
        else if( isdigit( *pos ) )
        {
            // mp4chaps native format: hh:mm:ss.sss <title>

            timeStart = pos;

            // read the title if there is one
            titleStart = strchr( timeStart, ' ' );
            if( NULL == titleStart )
            {
                titleStart = strchr( timeStart, '\t' );
            }

            if( NULL != titleStart )
            {
                *titleStart = 0;
                pos = ++titleStart;

                while( ' ' == *titleStart || '\t' == *titleStart )
                {
                    titleStart++;
                }

                titleLen = (uint32_t)strlen( titleStart );

                // advance to the end of the line
                pos = titleStart + 1 + titleLen;
            }
            else
            {
                // advance to the end of the line
                pos += strlen( pos );
            }

            formatState = FMT_STATE_FINISH;
        }
        else if( 0 == strncasecmp( pos, "CHAPTER", 7 ) )
        {
            // common format: CHAPTERxx=hh:mm:ss.sss\nCHAPTERxxNAME=<title>

            char* equalsPos = strchr( pos+7, '=' );
            if( NULL == equalsPos )
            {
                herrf( "Unable to parse line \"%s\"\n", pos );
                failure = true;
                break;
            }

            *equalsPos = 0;

            char* tlwr = pos;
            while( equalsPos != tlwr )
            {
                *tlwr = tolower( *tlwr );
                tlwr++;
            }

            if( NULL != strstr( pos, "name" ) )
            {
                // mark the chapter title
                uint32_t chNr = 0;
                sscanf( pos, "chapter%uname", &chNr );
                if( chNr != currentChapter )
                {
                    // different chapter number => different chapter definition pair
                    if( FMT_STATE_INITIAL != formatState )
                    {
                        herrf( "Chapter lines are not consecutive before line \"%s\"\n", pos );
                        failure = true;
                        break;
                    }

                    currentChapter = chNr;
                }
                formatState = FMT_STATE_TIME_LINE == formatState ? FMT_STATE_FINISH
                                                                 : FMT_STATE_TITLE_LINE;

                titleStart = equalsPos + 1;
                titleLen = (uint32_t)strlen( titleStart );

                // advance to the end of the line
                pos = titleStart + titleLen;
            }
            else
            {
                // mark the chapter start time
                uint32_t chNr = 0;
                sscanf( pos, "chapter%u", &chNr );
                if( chNr != currentChapter )
                {
                    // different chapter number => different chapter definition pair
                    if( FMT_STATE_INITIAL != formatState )
                    {
                        herrf( "Chapter lines are not consecutive at line \"%s\"\n", pos );
                        failure = true;
                        break;
                    }

                    currentChapter = chNr;
                }
                formatState = FMT_STATE_TITLE_LINE == formatState ? FMT_STATE_FINISH 
                                                                  : FMT_STATE_TIME_LINE;

                timeStart = equalsPos + 1;

                // advance to the end of the line
                pos = timeStart + strlen( timeStart );
            }
        }

        if( FMT_STATE_FINISH == formatState )
        {
            // now we have title and start time
            MP4Chapter_t chap;

            strncpy( chap.title, titleStart, min( titleLen, (uint32_t)MP4V2_CHAPTER_TITLE_MAX ) );
            chap.title[titleLen] = 0;

            Timecode tc( 0, CHAPTERTIMESCALE );
            string tm( timeStart );
            if( tc.parse( tm ) )
            {
                herrf( "Unable to parse time code from \"%s\"\n", tm.c_str() );
                failure = true;
                break;
            }
            chap.duration = tc.duration;
            format = tc.format;

            // ad the chapter to the list
            chapters.push_back( chap );

            // re-initialize
            formatState = FMT_STATE_INITIAL;
            titleStart = timeStart = NULL;
            titleLen = 0;
        }
    }
    free( inBuf );
    if( failure )
    {
        return failure;
    }

    return SUCCESS;
}

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

/** Convert from frame to millisecond timestamp.
 *
 *  This function converts a timestamp from hh:mm:ss:ff to hh:mm:ss.sss
 *
 *  @param duration  the timestamp in hours:minutes:seconds:frames.
 *  @param framerate the frames per second
 *  @return the timestamp in milliseconds
 */
MP4Duration
ChapterUtility::convertFrameToMillis( MP4Duration duration, uint32_t framerate )
{
    Timecode tc( duration, CHAPTERTIMESCALE );
    if( framerate < tc.subseconds )
    {
        uint64_t seconds = tc.subseconds / framerate;
        tc.setSeconds( tc.seconds + seconds );
        tc.setSubseconds( (tc.subseconds - (seconds * framerate)) * framerate );
    }
    else
    {
        tc.setSubseconds( tc.subseconds * framerate );
    }

    return tc.duration;
}

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

}} // namespace mp4v2::util

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

extern "C"
int main( int argc, char** argv )
{
    mp4v2::util::ChapterUtility util( argc, argv );
    return util.process();
}
