#include <bu/sio.h>
#include <bu/optparser.h>
#include <bu/file.h>
#include <bu/streamstack.h>
#include <bu/strfilter.h>
#include <bu/taf.h>
#include <bu/autoconfig.h>

#ifdef BU_HAS_DEFLATE
#include <bu/deflate.h>
#endif
#ifdef BU_HAS_BZIP2
#include <bu/bzip2.h>
#endif
#ifdef BU_HAS_LZMA
#include <bu/lzma.h>
#endif

#include <bu/base64.h>
#include <bu/hex.h>

Bu::String cescape( const Bu::String &sIn )
{
    Bu::String sOut;
    for( Bu::String::const_iterator i = sIn.begin(); i; i++ )
    {
        sOut += Bu::String("\\x%1").arg( (int)*i, Bu::Fmt(2).hex() );
    }
    return sOut;
}

using namespace Bu;

class Options : public OptParser
{
public:
    Options( int argc, char *argv[] ) :
        sClass("Datafiles")
    {
        addHelpBanner("bin2cpp - convert files into executable-embeddable C++ code.\n");
        addHelpBanner("Each file in the input is loaded, filtered according to your options, and written as stack allocated, static variables in a generated class.  You can then access the files as though they were on disk through that class.");
        addHelpBanner("\nUsage: bin2cpp [options] [input1] [input2] [...] [inputN]");
        addHelpBanner(  "   Or: bin2cpp -s <taf spec file>\n");
        addOption( sClass, 'c', "Class name [default=\"Datafiles\"]");
        addOption( sOutBase, 'o', "Output base filename [defaults to classname]");
        addOption( sOutDir, 'd', "Output directory [defaults to current dir]");
        addOption( slot(this, &Options::addFilter), 'f', "Add filter: "
#ifdef BU_HAS_DEFLATE
            "deflate, "
#endif
#ifdef BU_HAS_BZIP2
            "bzip2, "
#endif
#ifdef BU_HAS_LZMA
            "lzma, "
#endif
            "base64, hex");
        addOption( sSpecFile, 's', "Use the specified spec file instead of providing options on the command line.  If you use this option all others are ignored.");
        setNonOption( slot(this, &Options::addInput) );
        addHelpOption();

        parse( argc, argv );

        if( !sOutBase.isSet() )
            sOutBase = sClass.toLower();
        if( sOutDir.isSet() )
            sOutDir += "/";
    }

    virtual ~Options()
    {
    }

    int addFilter( Bu::StrArray aArgs )
    {
        slFilter.append( aArgs[1] );
        return 1;
    }

    int addInput( Bu::StrArray aArgs )
    {
        slInput.append( aArgs[0] );
        return 0;
    }

    Bu::String sClass;
    Bu::String sOutBase;
    Bu::String sOutDir;
    Bu::StringList slInput;
    Bu::StringList slFilter;
    Bu::String sSpecFile;
};

int main( int argc, char *argv[] )
{
    Options opt( argc, argv );

    if( !opt.sSpecFile.isEmpty() )
    {
        Bu::File fTaf( opt.sSpecFile, Bu::File::Read );
        Bu::TafReader rTaf( fTaf );
        Bu::TafGroup *pRoot = rTaf.readGroup();

        if( pRoot == NULL || pRoot->getName() != "bin2cpp" )
        {
            sio << "Specfied spec file does not appear to be a bin2cpp taf "
                "specifications file." << sio.nl;
            return 5;
        }

        opt.sOutBase = opt.sClass = pRoot->getProperty("class");
        if( pRoot->hasProperty("output") )
            opt.sOutBase = pRoot->getProperty("output");
        opt.sOutDir = pRoot->getProperty("dir", ".") + "/";

        delete pRoot;
    }

    File fHdrOut( opt.sOutDir + opt.sOutBase + ".h", File::WriteNew );
    File fSrcOut( opt.sOutDir + opt.sOutBase + ".cpp", File::WriteNew );

    Bu::Hash<char, bool> hFilters;

    Formatter fHdr( fHdrOut );
    Formatter fSrc( fSrcOut );
    fHdr << "#ifndef BIN2CPP_" << opt.sClass.toUpper() << "_H" << fHdr.nl
        << "#define BIN2CPP_" << opt.sClass.toUpper() << "_H" << fHdr.nl << fHdr.nl
        << "#include <bu/string.h>" << fHdr.nl
        << "#include <bu/streamstack.h>" << fHdr.nl
        << fHdr.nl
        << "class " << opt.sClass << fHdr.nl
        << "{" << fHdr.nl
        << "public:" << fHdr.nl
        << "\tclass File { public: int iSize; const char *data; const char *flt; };" << fHdr.nl << fHdr.nl
        << "\tstatic const File &getFile( const Bu::String &sName );" << fHdr.nl
        << "\tstatic Bu::StreamStack *open( const Bu::String &sName );" << fHdr.nl
        << "\tstatic Bu::StreamStack *openRaw( const Bu::String &sName );" << fHdr.nl
        << "\tstatic Bu::String getString( const Bu::String &sName );" << fHdr.nl
        << "\tstatic Bu::String getStringRaw( const Bu::String &sName );" << fHdr.nl
        << fHdr.nl;
    fHdr << "public:" << fHdr.nl
        << "\tstatic const File aFile[];" << fHdr.nl
        << "};" << fHdr.nl << fHdr.nl;
    fHdr << "#endif";

    fSrc << "#include \"" << opt.sOutBase << ".h\"" << fSrc.nl;
#ifdef BU_HAS_DEFLATE
    fSrc << "#include <bu/deflate.h>" << fSrc.nl;
#endif
#ifdef BU_HAS_BZIP2
    fSrc << "#include <bu/bzip2.h>" << fSrc.nl;
#endif
#ifdef BU_HAS_LZMA
    fSrc << "#include <bu/lzma.h>" << fSrc.nl;
#endif
    fSrc << "#include <bu/base64.h>" << fSrc.nl;
    fSrc << "#include <bu/hex.h>" << fSrc.nl;
    fSrc << "#include <bu/strfilter.h>" << fSrc.nl;
    fSrc << "#include <bu/staticmembuf.h>" << fSrc.nl;
    fSrc << fSrc.nl;
    fSrc << "const " << opt.sClass << "::File " << opt.sClass << "::aFile[] = {" << fSrc.nl;

    for( Bu::StringList::iterator i = opt.slInput.begin(); i; i++ )
    {
        File fIn( *i, File::Read );
        Bu::String sDat;
        char buf[1024];
        while( !fIn.isEos() )
        {
            sDat.append( buf, fIn.read( buf, 1024 ) );
        }

        Bu::String sFltDesc;
        for( Bu::StringList::iterator f = opt.slFilter.begin(); f; f++ )
        {
            if( false ) { } // should optomize out
#ifdef BU_HAS_DEFLATE
            else if( *f == "deflate" )
            {
                sDat = encodeStr<Deflate>( sDat );
                sFltDesc.prepend("d");
                hFilters.insert('d', true );
            }
#endif
#ifdef BU_HAS_BZIP2
            else if( *f == "bzip2" )
            {
                sDat = encodeStr<BZip2>( sDat );
                sFltDesc.prepend("b");
                hFilters.insert('b', true );
            }
#endif
#ifdef BU_HAS_LZMA
            else if( *f == "lzma" )
            {
                sDat = encodeStr<Lzma>( sDat );
                sFltDesc.prepend("l");
                hFilters.insert('l', true );
            }
#endif
            else if( *f == "base64" )
            {
                sDat = encodeStr<Base64>( sDat );
                sFltDesc.prepend("6");
                hFilters.insert('6', true );
            }
            else if( *f == "hex" )
            {
                sDat = encodeStr<Hex>( sDat );
                sFltDesc.prepend("h");
                hFilters.insert('h', true );
            }
            else
            {
                sio << "No known filter named " << *f << sio.nl;
                return 1;
            }
        }
        
        fSrc << "  {" << sDat.getSize() << ", \"";

		for( Bu::String::iterator j = sDat.begin(); j; j++ )
		{
			fSrc << "\\x" << Fmt::hex() << (unsigned char)*j;
		}
		fSrc << "\", \"" << sFltDesc << "\"}," << fSrc.nl;
    }
    fSrc << "};" << fSrc.nl << fSrc.nl;

    fSrc << "const " << opt.sClass << "::File &" << opt.sClass << "::getFile( const Bu::String &sName )"
        << fSrc.nl
        << "{" << fSrc.nl
        << "\tswitch( Bu::__calcHashCode( sName ) )" << fSrc.nl
        << "\t{" << fSrc.nl;

    int idx = 0;
    {
        typedef Bu::List<Bu::StringList::iterator> IterList;
        typedef Bu::Hash<uint32_t, IterList> IterHashList;
        typedef Bu::Hash<Bu::String, int> IdxHash;

        IdxHash hIdx;
        IterHashList hCodes;
        for( Bu::StringList::iterator i = opt.slInput.begin(); i; i++ )
        {
            uint32_t uc = Bu::__calcHashCode( *i );
            if( !hCodes.has( uc ) )
            {
                hCodes.insert( uc, IterList() );
            }
            hCodes.get( uc ).append( i );
            hIdx.insert( *i, idx++ );
        }
        for( IterHashList::iterator r = hCodes.begin(); r; r++ )
        {
            fSrc << "\t\tcase " << r.getKey() << "UL:" << fSrc.nl;
            for( IterList::iterator i = r.getValue().begin(); i; i++ )
            {
                fSrc << "\t\t\tif( sName == \"" << cescape(*(*i)) << "\" ) // "
                    << *(*i) << fSrc.nl
                    << "\t\t\t\treturn aFile[" << hIdx.get(*(*i)) << "];" << fSrc.nl;
            }
            fSrc << "\t\t\tbreak;" << fSrc.nl;
        }
    }
    fSrc << "\t}" << fSrc.nl
        << "\tthrow Bu::ExceptionBase(\"No file matching \\\"%s\\\" found.\", sName.getStr() );" << fSrc.nl
        << "}" << fSrc.nl << fSrc.nl;
    
    fSrc << "Bu::StreamStack *" << opt.sClass << "::open( const Bu::String &sName )" << fSrc.nl
        << "{" << fSrc.nl
        << "\tconst File &f = getFile( sName );" << fSrc.nl
        << "\tBu::StreamStack *s = new Bu::StreamStack( new Bu::StaticMemBuf( f.data, f.iSize ) );" << fSrc.nl;

    if( !hFilters.isEmpty() )
    {
        fSrc << "\tfor( const char *t = f.flt; *t; t++ )" << fSrc.nl
            << "\t{" << fSrc.nl
            << "\t\tswitch( *t )" << fSrc.nl
            << "\t\t{" << fSrc.nl;
        if( hFilters.has('d') )
            fSrc << "\t\t\tcase 'd': s->pushFilter<Bu::Deflate>(); break;" << fSrc.nl;
        if( hFilters.has('b') )
            fSrc << "\t\t\tcase 'b': s->pushFilter<Bu::BZip2>(); break;" << fSrc.nl;
        if( hFilters.has('l') )
            fSrc << "\t\t\tcase 'l': s->pushFilter<Bu::Lzma>(); break;" << fSrc.nl;
        if( hFilters.has('6') )
            fSrc << "\t\t\tcase '6': s->pushFilter<Bu::Base64>(); break;" << fSrc.nl;
        if( hFilters.has('h') )
            fSrc << "\t\t\tcase 'h': s->pushFilter<Bu::Hex>(); break;" << fSrc.nl;
        fSrc << "\t\t}" << fSrc.nl
            << "\t}" << fSrc.nl;
    }
    fSrc << "\treturn s;" << fSrc.nl
        << "}" << fSrc.nl << fSrc.nl;

    fSrc << "Bu::StreamStack *" << opt.sClass << "::openRaw( const Bu::String &sName )" << fSrc.nl
        << "{" << fSrc.nl
        << "\tconst File &f = getFile( sName );" << fSrc.nl
        << "\treturn new Bu::StreamStack( new Bu::StaticMemBuf( f.data, f.iSize ) );" << fSrc.nl
        << "}" << fSrc.nl << fSrc.nl;

    fSrc << "Bu::String " << opt.sClass << "::getString( const Bu::String &sName )" << fSrc.nl
        << "{" << fSrc.nl
        << "\tBu::StreamStack *ss = open( sName );" << fSrc.nl
        << "\tBu::String s;" << fSrc.nl
        << "\tchar buf[1024];" << fSrc.nl
        << "\twhile( !ss->isEos() )" << fSrc.nl
        << "\t{" << fSrc.nl
        << "\t\ts.append( buf, ss->read( buf, 1024 ) );" << fSrc.nl
        << "\t}" << fSrc.nl
        << "\tdelete ss;" << fSrc.nl
        << "\treturn s;" << fSrc.nl
        << "}" << fSrc.nl << fSrc.nl;
    
    fSrc << "Bu::String " << opt.sClass << "::getStringRaw( const Bu::String &sName )" << fSrc.nl
        << "{" << fSrc.nl
        << "\tconst File &f = getFile( sName );" << fSrc.nl
        << "\treturn Bu::String( f.data, f.iSize );" << fSrc.nl
        << "}" << fSrc.nl << fSrc.nl;

    return 0;
}

