// ライセンス: GPL2

#define _DEBUG
#include "jddebug.h"

#include "miscgzip.h"
#include "miscmsg.h"

#include <zlib.h>


// バッファサイズ
enum{
    DEFLATE_OUTSIZE = 1024 * 1024, // deflate時の出力

    INFLATE_INSIZE = 256* 1024,  // inflate時の入力
    INFLATE_OUTSIZE = 1024 * 1024  // inflate時の出力
};


// gzipヘッダのフラグ
enum
{
    FTEXT = 1,
    FHCRC = 2,
    FEXTRA = 4,
    FNAME = 8,
    FCOMMENT = 16
};


//
// gzip ヘッダ ( RFC1952 )
//
class GZIPHDR
{
    char m_ID[ 2 ]; 
    char m_CM;      
    char m_FLG;
    time_t m_MTIME;
    char m_XSL;
    char m_OS;

    std::string m_filename;

public:

    GZIPHDR( const std::string& filename, const time_t mtime )
    {
        m_ID[ 0 ] = 0x1f; m_ID[ 1 ] = 0x8b; // gzip ヘッダ
        m_CM = 0x08;    // deflate 使用
        m_FLG = FNAME;  // 名前指定
        m_MTIME = mtime; // modification time
        m_XSL = 0; // 拡張フラグ
        m_OS = 3;  // UNIX

        m_filename = filename;
    }

    void set_currenttime(){ m_MTIME = time( NULL ); }

    //
    // ヘッダのファイルへの書き出し
    //
    int operator >> ( FILE* fout ){

        const size_t hdrsize = 10; // m_OSメンバまで書き込み
        if( fwrite( this, 1, hdrsize, fout ) != hdrsize ) throw "failed to write gzip header";

        // FNAME
        const size_t filename_size = m_filename.size() + 1; // +1 は'\0' の分
        if( fwrite( m_filename.c_str(), 1, filename_size, fout ) != filename_size ) throw "failed to write gzip header";

        return hdrsize + filename_size;
    }

    //
    // ヘッダのバッファからの読み込み
    //
    // buf : 入力バッファ
    // size : バッファサイズ
    //
    // 戻り値 : ヘッダのサイズ
    //
    const size_t read_header( const char* buf, const size_t size )
    {
#ifdef _DEBUG
        std::cout << "GZIPHDR::read_header size = " << size << std::endl;
#endif
        size_t pos = 0;

        // gzipヘッダチェック
        if( buf[ 0 ] != m_ID[ 0 ]
            || buf[ 1 ] != m_ID[ 1 ] ) throw "this format is not gzip";
        pos += 2;

        // deflateか
        if( buf[ pos++ ] != m_CM ) throw "CM of this file is not deflate";

        // FLG
        m_FLG = ( ( unsigned char* )buf )[ pos++ ];

        // MTIME
        memcpy( &m_MTIME, buf + pos, sizeof( time_t ) );
        pos += sizeof( time_t );

        // XSL
        m_XSL = ( ( unsigned char* )buf )[ pos++ ];

        // OS
        m_OS = ( ( unsigned char* )buf )[ pos++ ];

        // XLEN + 拡張フィールド
        if( m_FLG & FEXTRA ){
            unsigned short lngext;
            memcpy( &lngext, buf + pos, sizeof( unsigned short ) );
#ifdef _DEBUG
            std::cout << "FEXTRA lng = " << lngext << std::endl;
#endif
            pos += sizeof( unsigned short ) + lngext;
        }

        // NAME
        if( m_FLG & FNAME ){
            int i = 0;
            char tmp_str[ 256 ];
            while( pos < size && i < 256  && buf[ pos ] != '\0' ) tmp_str[ i++ ] = buf[ pos++ ];
            tmp_str[ i ] = buf[ pos++ ];
            if( tmp_str[ i ] != '\0' ) throw "FNAME of this file is broken";
            m_filename = tmp_str;
        }

        // COMMENT
        if( m_FLG & FCOMMENT ){
            int i = 0;
            char tmp_str[ 256 ];
            while( pos < size && i < 256  && buf[ pos ] != '\0' ) tmp_str[ i++ ] = buf[ pos++ ];
            tmp_str[ i ] = buf[ pos++ ];
            if( tmp_str[ i ] != '\0' ) throw "COMMENT of this file is broken";
#ifdef _DEBUG
            std::cout << "COMMENT = " << tmp_str << std::endl;
#endif
        }

        // CRC16
        if( m_FLG & FHCRC ){
            pos += 2;
#ifdef _DEBUG
            std::cout << "CRC16\n";
#endif
        }

        if( pos >= size ) throw "buffer over flow in gunzip";

#ifdef _DEBUG
        std::cout << "flag = " << std::hex << ( unsigned int )m_FLG << std::endl
                  << "MTIME = " << m_MTIME << std::endl
                  << "XSL = " << m_XSL << std::endl
                  << "OS = " << ( unsigned int )m_OS << std::endl
                  << "NAME = " << m_filename << std::endl
                  << std::dec << "header size = " << pos << std::endl;
#endif
        return pos;
    }
};


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


//
// gzip のヘッダチェック
//
// 戻り値 : ヘッダサイズ(エラーなら-1)
//
const size_t MISC::check_gzipheader( const char* buf, const size_t size )
{
    size_t ret = -1;
    
    try{
        GZIPHDR header( "", 0 );
        ret = header.read_header( buf, size );
    }
    catch( const std::string& err ){ MISC::ERRMSG( err ); }
    catch( const char* err ){ MISC::ERRMSG( err ); }

    return ret;
}

 
//
// gzip作成
//
// filename : オリジナルのファイル名
// gzipfile : 出力する gz ファイル
// inbuf :  オリジナルデータ
// inbufsize : オリジナルデータサイズ
// mtime : 更新時刻(0なら現在の時刻が入る)
//
// 戻り値 : 出力サイズ(ヘッダやcrcを除く)
//
const size_t MISC::gzip( const std::string& filename, const std::string& gzipfile,
           const char* inbuf, const size_t inbufsize, const time_t mtime )
{
#ifdef _DEBUG
    std::cout << "MISC::gzip : gzipfile = " << gzipfile << " bufsize = " << inbufsize
              << " mtime = " << mtime << std::endl;
#endif

    size_t outsize_total = 0;
    FILE* fout = NULL;
    z_stream zs;
    memset( &zs, 0, sizeof( z_stream ) );

    try{

        fout = fopen( gzipfile.c_str(), "wb");
        if( ! fout ) throw "failed to open " + gzipfile;

        // GZIPヘッダ出力
        GZIPHDR header( filename, mtime );
        if( ! mtime ) header.set_currenttime();
        header >> fout;

        // zlib 初期化
        zs.zalloc = Z_NULL;
        zs.zfree = Z_NULL;
        zs.opaque = Z_NULL;
        if( deflateInit2( &zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
                          -15, // zlibヘッダとcrcを出力しない
                          8, Z_DEFAULT_STRATEGY ) != Z_OK ) throw "deflateInit failed";

        zs.next_in = ( Bytef* )inbuf;
        zs.avail_in = inbufsize;

        Bytef outbuf[ DEFLATE_OUTSIZE ];
        zs.next_out = outbuf;
        zs.avail_out = DEFLATE_OUTSIZE;

        // 圧縮中
        for(;;){

            int status = deflate( &zs, Z_FINISH );

            if( status == Z_STREAM_END ) break;
            else if( status != Z_OK ) throw "deflate failed";

            // 出力バッファを使い切ったら保存
            if( ! zs.avail_out ){

                const size_t outsize = DEFLATE_OUTSIZE;
                if( fwrite( outbuf, 1, outsize, fout ) != outsize ) throw "failed to write : " + gzipfile;
                outsize_total += outsize;

                zs.next_out = outbuf;
                zs.avail_out = DEFLATE_OUTSIZE;
#ifdef _DEBUG
                std::cout << "deflate " << outsize << std::endl;
#endif
            }
        }

        // 出力バッファの残りを保存
        const size_t outsize = DEFLATE_OUTSIZE - zs.avail_out;
        if( outsize ){
            if( fwrite( outbuf, 1, outsize, fout ) != outsize ) throw "failed to write : " + gzipfile;
            outsize_total += outsize;
#ifdef _DEBUG
            std::cout << "deflate " << outsize << std::endl;
#endif
        }

        // CRC32と元のファイルサイズを出力
        uLong crcval = crc32( 0, Z_NULL, 0 );
        crcval = crc32(  crcval, ( Bytef* )inbuf, inbufsize );
        if( fwrite( &crcval, 1, sizeof( uLong ), fout ) != sizeof( uLong ) ) throw "failed to write : " + gzipfile;

        uLong filesize = inbufsize;
        if( fwrite( &filesize, 1, sizeof( uLong ), fout ) != sizeof( uLong ) ) throw "failed to write : " + gzipfile;

#ifdef _DEBUG
        std::cout << "filename = " << filename << std::endl
                  << "crc = " << std::hex << crcval << std::dec << std::endl
                  << "inputsize = " << filesize << std::endl
                  << "outputsize = " << outsize_total << std::endl
                  << "rate = " << (double)outsize_total/filesize*100 << std::endl;
#endif
    }
    catch( const std::string& err ){ MISC::ERRMSG( err ); }
    catch( const char* err ){ MISC::ERRMSG( err ); }

    if( fout ){
#ifdef _DEBUG
        std::cout << "fclose\n";
#endif
        fclose( fout );
    }

    // 後始末
    if( zs.zalloc ){
#ifdef _DEBUG
        std::cout << "deflateEnd\n";
#endif
        if( deflateEnd( &zs ) != Z_OK ) MISC::ERRMSG( "deflateEnd failed" );
    }

    return outsize_total;
}


//
// gzip解凍
//
// gzipfile : 入力する gz ファイル
// str : 解凍した文字列
//
// 戻り値 : オリジナルのファイルサイズ
//
const size_t MISC::gunzip( const std::string& gzipfile, std::string& str )
{
#ifdef _DEBUG
    std::cout << "MISC::gunzip : gzipfile = " << gzipfile << std::endl;
#endif

    str.clear();

    size_t outsize_total = 0;
    FILE* fin = NULL;
    z_stream zs;
    memset( &zs, 0, sizeof( z_stream ) );

    try{

        fin = fopen( gzipfile.c_str(), "rb");
        if( ! fin ) throw "failed to open " + gzipfile;

        // zlib 初期化
        zs.zalloc = Z_NULL;
        zs.zfree = Z_NULL;
        zs.opaque = Z_NULL;
        if ( inflateInit2( &zs,
                           -MAX_WBITS // raw inflateモード
//                           15 + 32 // デフォルトの15に+32する( windowBits = 47 )と自動でヘッダ認識
                 ) != Z_OK ) throw "inflateInit2 failed";

        char outbuf[ INFLATE_OUTSIZE ];
        zs.next_out = ( Bytef* )outbuf;
        zs.avail_out = INFLATE_OUTSIZE;
            
        // 解凍中
        bool check_header = true;
        for(;;){

            Bytef inbuf[ INFLATE_INSIZE ];
            size_t bufsize = fread( inbuf, 1, INFLATE_INSIZE, fin );

            // GZIPヘッダ解析
            size_t offset = 0;
            if( check_header ){
                GZIPHDR header( "", 0 );
                offset = header.read_header( ( char* )inbuf, bufsize );
                check_header = false;
            }

            zs.next_in = inbuf + offset;
            zs.avail_in = bufsize;

            int status = inflate( &zs, Z_NO_FLUSH );

            if( status == Z_STREAM_END ) break;
            else if( status != Z_OK ) throw "inflate failed";

            // 出力バッファを使い切った
            if( ! zs.avail_out ){

                const size_t outsize = INFLATE_OUTSIZE;
                outbuf[ outsize ] = '\0';
                str += outbuf;
                outsize_total += outsize;

                zs.next_out = ( Bytef* )outbuf;
                zs.avail_out = INFLATE_OUTSIZE;

#ifdef _DEBUG
                std::cout << "inflate " << outsize << std::endl;
#endif
            }
        }

        // 出力バッファの残り
        const size_t outsize = INFLATE_OUTSIZE - zs.avail_out;
        if( outsize ){

            outbuf[ outsize ] = '\0';
            str += outbuf;
            outsize_total += outsize;

#ifdef _DEBUG
                std::cout << "inflate " << outsize << std::endl;
#endif
        }

#ifdef _DEBUG
        std::cout << "outputsize = " << outsize_total << std::endl;
#endif
    }
    catch( const std::string& err ){ MISC::ERRMSG( err ); }
    catch( const char* err ){ MISC::ERRMSG( err ); }

    if( fin ){
#ifdef _DEBUG
        std::cout << "fclose\n";
#endif
        fclose( fin );
    }

    // 後始末
    if( zs.zalloc ){
#ifdef _DEBUG
        std::cout << "inflateEnd\n";
#endif
        if( inflateEnd( &zs ) != Z_OK ) MISC::ERRMSG( "inflateEnd failed" );
    }

    return outsize_total;
}

