/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

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 3 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, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
//////////////////////////////////////////////////////////////////////////////
//
//  plDatMerger - Command line utility app that takes multiple dat files
//                and merges them into one
//
//////////////////////////////////////////////////////////////////////////////

#include "hsTypes.h"
#include "hsStream.h"
#include "hsUtils.h"
#include "hsTimer.h"
#include "../plFile/hsFiles.h"
#include "plRawResManager.h"
#include "plRawPageAccessor.h"
#include "../plResMgr/plRegistryDiskSource.h"
#include "../plResMgr/plRegistryDiskMergedSourceData.h"
#include "../plResMgr/plRegistryHelpers.h"
#include "../plResMgr/plRegistry.h"
#include "../plResMgr/plRegistryNode.h"
#include "../plResMgr/plResMgrSettings.h"
#include "../plResMgr/plPageInfo.h"
#include "../plAgeDescription/plAgeDescription.h"
#include "../plFile/hsFiles.h"
#include "../plFile/plFileUtils.h"
#include "../pnKeyedObject/plKey.h"

#include <stdio.h>
#include <stdlib.h>

//// Tiny String Filter Class ////////////////////////////////////////////////

class plStrFilter
{
    protected:

        hsBool  fWildCardSuffix, fInvert;
        char    *fStr;

        plStrFilter *fChapFilter, *fPageFilter;


        hsBool  IPass( const char *string )
        {
            if( fWildCardSuffix )
            {
                if( strnicmp( fStr, string, strlen( fStr ) ) == 0 )
                    return true;
            }
            else
            {
                if( stricmp( fStr, string ) == 0 )
                    return true;
            }
            return false;
        }

    public:
        plStrFilter() { fWildCardSuffix = fInvert = false; fStr = nil; fChapFilter = fPageFilter = nil; }

        plStrFilter( const char *initLine )
        {
            if( initLine[ 0 ] == '-' )
            {
                fInvert = true;
                initLine++;
            }
            else if( initLine[ 0 ] == '+' )
            {
                initLine++;
                fInvert = false;
            }
            else
                fInvert = false;

            fWildCardSuffix = false;
            fStr = hsStrcpy( initLine );

            fChapFilter = fPageFilter = nil;
            char *comma = strchr( fStr, ',' );
            if( comma != nil )
            {
                char *next = comma + 1;
                *comma = 0;
                comma = strchr( next, ',' );
                if( comma != nil )
                {
                    fPageFilter = new plStrFilter( comma + 1 );
                    *comma = 0;
                }

                fChapFilter = new plStrFilter( next );
            }

            if( fStr[ strlen( fStr ) - 1 ] == '*' )
            {
                fWildCardSuffix = true;
                fStr[ strlen( fStr ) - 1 ] = 0;
            }
        }

        ~plStrFilter()
        {
            delete [] fStr;
            delete fChapFilter;
            delete fPageFilter;
        }

        hsBool  Pass( const char *string )
        {
            hsBool ret = IPass( string );

            if( fInvert )
                ret = !ret;

            return ret;
        }

        hsBool  Pass( const plPageInfo &page )
        {
            hsBool  ret = IPass( page.GetAge() ) &&
                            fChapFilter->IPass( page.GetChapter() ) &&
                                fPageFilter->IPass( page.GetPage() );
            if( fInvert )
                ret = !ret;

            return ret;
        }

        static hsBool   Passes( const char *string, hsTArray<plStrFilter *> &filters )
        {
            UInt32  i;


            for( i = 0; i < filters.GetCount(); i++ )
            {
                if( !filters[ i ]->Pass( string ) )
                    return false;
            }

            return true;
        }

        static hsBool   Passes( const plPageInfo &page, hsTArray<plStrFilter *> &filters )
        {
            UInt32  i;


            for( i = 0; i < filters.GetCount(); i++ )
            {
                if( !filters[ i ]->Pass( page ) )
                    return false;
            }

            return true;
        }
};

//// Globals /////////////////////////////////////////////////////////////////

plRawResManager *gResManager = nil;

char        gDatDirectory[ kFolderIterator_MaxPath ] = ".";
char        gDestFileName[ kFolderIterator_MaxPath ];

hsTArray<plStrFilter *> fPageFilters;
hsTArray<plStrFilter *> *fCurrFilterList = nil;

hsTArray<plRegistryPageNode *>  fSourcePages;

plRegistryDiskMergedSourceData *gDestMergeData = nil;


//// PrintHelp ///////////////////////////////////////////////////////////////

int PrintHelp( void )
{
    puts( "" );
    puts( "Usage:\tplDatMerger [oldDir] [newDir] [patchDir] |-na| |-np| |-fp| |-lAgeName| |-anewAgeDir|" );
    puts( "Where:" );
    puts( "\toldDir is the directory containing the old data files" );
    puts( "\tnewDir is the directory containing the new data files" );
    puts( "\tpatchDir is the directory where the patch files will go" );
    puts( "\t                   WARNING: This should point to a 'patches'" );
    puts( "\t                   subdir under 'newDir'; don't use anything else" );
    puts( "\t                   unless you REALLY know what you're doing." );
    puts( "\t-na is a flag that keeps the builder from updating the" );
    puts( "\t                   version numbers in the age files (for generating" );
    puts( "\t                   previous patch versions, for example." );
    puts( "\t-np is a flag that keeps the builder from actually creating the" );
    puts( "\t                   patch files. Usually helpful when you want to" );
    puts( "\t                   only update version numbers in the dat files" );
    puts( "\t                   and the age files. -na and -np are mutually" );
    puts( "\t                   exclusive." );
    puts( "\t-fp forces writing of entire objects instead of just difference" );
    puts( "\t                   buffers, for debugging purposes." );
    puts( "\t-l limits processing to the single age given. Don't put a space between the l." );
    puts( "\t                   and the age name." );
    puts( "\t-a specifies a different directory to put the modified age files in. If not" );
    puts( "\t                   specified, age files are overwritten in the newDir." );
    puts( "" );

    return -1;
}

hsBool  ReadConfig( const char *filename )
{
    hsUNIXStream    config;


    if( !config.Open( filename, "rt" ) )
        return false;

    char    line[ 512 ];
    int     lineNum = 1;

    while( config.ReadLn( line, sizeof( line ) ) )
    {
        // Switch based on command
        if( stricmp( line, "[pageFilters]" ) == 0 )
            fCurrFilterList = &fPageFilters;
        else if( fCurrFilterList != nil )
        {
            fCurrFilterList->Append( new plStrFilter( line ) );
        }
        else
        {

            char *tok = strtok( line, " \t=" );
            if( tok != nil )
            {
                if( stricmp( tok, "datDir" ) == 0 )
                {
                    tok = strtok( nil, " \t=" );
                    if( tok != nil )
                        strcpy( gDatDirectory, tok );
                    else
                    {
                        printf( "Parse error in init file, line %d", lineNum );
                        return false;
                    }
                }
                else if( stricmp( tok, "destFile" ) == 0 )
                {
                    tok = strtok( nil, "\n\r" );
                    if( tok == nil )
                    {
                        printf( "Parse error in init file, line %d", lineNum );
                        return false;
                    }
                    strcpy( gDestFileName, tok );
                }
                else
                {
                    printf( "Parse error in init file, line %d", lineNum );
                }
            }
        }
        lineNum++;
    }

    config.Close();
    return true;
}

//// Our Main Page Iterator //////////////////////////////////////////////////

class plPageStuffer : public plRegistryPageIterator
{
    public:

        virtual hsBool  EatPage( plRegistryPageNode *page )
        {
            const plPageInfo &info = page->GetPageInfo();
            if( plStrFilter::Passes( info, fPageFilters ) )
                fSourcePages.Append( page );
            return true;
        }
};

//// IShutdown ///////////////////////////////////////////////////////////////

void    IShutdown( int retCode )
{
    UInt32  i;

    for( i = 0; i < fPageFilters.GetCount(); i++ )
        delete fPageFilters[ i ];

    delete gDestMergeData;

    hsgResMgr::Shutdown();

    if( retCode == 0 )
        printf( "Finished!\n" );
    else
        exit( retCode );
}

//// main ////////////////////////////////////////////////////////////////////

int main( int argc, char *argv[] )
{
    puts( "-----------------------------------------------------" );
    puts( "plDatMerger - Plasma 2 dat file merging utility" );
    puts( "-----------------------------------------------------" );

    if( argc < 1 || argc > 8 )
        return PrintHelp();

    // Read our config
    ReadConfig( argv[ 1 ] );

    plResMgrSettings::Get().SetFilterNewerPageVersions( false );
    plResMgrSettings::Get().SetFilterOlderPageVersions( false );

    // Init our special resMgr
    puts( "Initializing resManager..." );
    gResManager = new plRawResManager;
    hsgResMgr::Init( gResManager );

    // Load the registry in to work with
    printf( "Loading registry from directory \"%s\"...\n", gDatDirectory );
    gResManager->AddSource( new plRegistryDiskSource( gDatDirectory ) );

    // Iterate and collect pages to merge
    printf( "Collecting pages...\n" );
    plPageStuffer   pageIter;
    gResManager->IterateAllPages( &pageIter );

    if( fSourcePages.GetCount() == 0 )
    {
        puts( "ERROR: No source pages found to merge!" );
        IShutdown( -1 );
    }


    // Create a merged data source to represent our dest page
    printf( "Merging %d pages to file(s) %s...\n", fSourcePages.GetCount(), gDestFileName );

    gDestMergeData = new plRegistryDiskMergedSourceData( gDestFileName );
    gDestMergeData->SetNumEntries( fSourcePages.GetCount() );

    // Open the dest merged streams and write out our initial, incorrect, entry table so we can get positions right
    hsStream *destIdxStream = gDestMergeData->WriteEntries( true );
    hsStream *destDatStream = gDestMergeData->OpenData( (UInt32)-1, "wb" );

    UInt32 i, bytesRead;
    static UInt8    scratchBuffer[ 1024 * 64 ];     // 32k in size
    for( i = 0; i < fSourcePages.GetCount(); i++ )
    {
        printf( "  Merging %s>%s...\n", fSourcePages[ i ]->GetPageInfo().GetAge(), fSourcePages[ i ]->GetPageInfo().GetPage() );

        // For each page, we open the source streams, read the ENTIRE thing in, front to back, and append it
        // to the dest stream. We then update the entry in the mergeData to reflect our info
        plMSDEntry &entry = gDestMergeData->GetEntry( i );

        entry.fIdxOffset = destIdxStream->GetPosition();
        entry.fDatOffset = destDatStream->GetPosition();

        /// Actually transfer the data
        plRegistrySource *srcSource = fSourcePages[ i ]->GetSource();

        // Idx first
        hsStream *srcStream = srcSource->OpenIndexStream( fSourcePages[ i ] );
        UInt32 size = srcStream->GetEOF();
        do
        {
            bytesRead = srcStream->Read( size > sizeof( scratchBuffer ) ? sizeof( scratchBuffer ) : size, scratchBuffer );
            if( bytesRead > 0 )
                destIdxStream->Write( bytesRead, scratchBuffer );
            size -= bytesRead;
        } while( size > 0 && bytesRead > 0 );
        srcSource->CloseIndexStream( fSourcePages[ i ] );

        // Now dat
        srcStream = srcSource->OpenDataStream( fSourcePages[ i ] );
        size = srcStream->GetEOF();
        do
        {
            bytesRead = srcStream->Read( size > sizeof( scratchBuffer ) ? sizeof( scratchBuffer ) : size, scratchBuffer );
            if( bytesRead > 0 )
                destDatStream->Write( bytesRead, scratchBuffer );
            size -= bytesRead;
        } while( size > 0 && bytesRead > 0 );
        srcSource->CloseDataStream( fSourcePages[ i ] );

        // Update lengths
        entry.fIdxLength = destIdxStream->GetPosition() - entry.fIdxOffset;
        entry.fDatLength = destDatStream->GetPosition() - entry.fDatOffset; 
    }

    printf( "Closing destination files...\n" );
    destIdxStream->Close();
    destDatStream->Close();

    // Re-write the entry table, now that it's correct
    printf( "Updating merged table...\n" );
    gDestMergeData->WriteEntries( false );

    puts( "Shutting down..." );
    IShutdown( 0 );
    return 0;
}