// -*-c++-*-

// fixg2sxd - a utility to convert fig to sxd format

// Copyright (C) 2003-2022 Alexander Bürger, acfb@users.sourceforge.net

// 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 2 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, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

#include "zipwrite.h"

#include <cstdlib>
#include <ctime>
#include <iostream>
#include <streambuf>

using namespace std;

// ------------------------------------------------------------------------

class WriterBuf : public std::streambuf
{
public:
    typedef int (*WriteFunc)( void* user, const char* buf, unsigned length );
private:
    WriteFunc writefunc;
    void* user;
    unsigned bufsize;
    char *buf;

public:
    WriterBuf( WriteFunc wf, void* u, unsigned bs = 1)
        : writefunc( wf ),
          user( u ),
          bufsize( bs < 1 ? 1 : bs ),
          buf( new char[ bufsize ] )
        {
            setp( buf, buf+bufsize );
        }

    ~WriterBuf();

protected:
    int overflow( int c );
    int sync();
};

WriterBuf::~WriterBuf()
{
    if( buf ) {
        sync();
        delete[] buf;
    }
}

int WriterBuf::overflow( int c )
{
    sync();
    if( c != EOF ) {
        *pptr() = static_cast<char>( c );
        pbump( 1 );
    }
    return c;
}

int WriterBuf::sync()
{
    int ret = 0;
    if( pptr() > pbase() )
    {
        ret = writefunc( user, buf, pptr() - pbase() );
        setp( buf, buf+bufsize );
    }
    return ret;
}

// ------------------------------------------------------------------------

ostream& write2( ostream& out, unsigned short s )
{
    unsigned char bytes[2] = {
        static_cast<unsigned char>(s&0xff),
        static_cast<unsigned char>((s>>8)&0xff)
    };
    out.write( (const char*)bytes, 2 );
    return out;
}

ostream& write4( ostream& out, unsigned int i )
{
    unsigned char bytes[4] = {
        static_cast<unsigned char>(i&0xff),
        static_cast<unsigned char>((i>>8)&0xff),
        static_cast<unsigned char>((i>>16)&0xff),
        static_cast<unsigned char>((i>>24)&0xff)
    };
    out.write( (const char*)bytes, 4 );
    return out;
}

// ------------------------------------------------------------------------

ZipWriter::~ZipWriter()
{
    Close();

    const int rel_offset_central_dir = zip.tellp();
    // write central directory
    for( fileinfos_t::iterator i=fileinfos.begin(); i!=fileinfos.end(); ++i )
    {
        fileinfo& fi = *i;
        // central file header signature
        write4( zip, 0x02014b50 );
        write2( zip, 20 );
        write2( zip, 20 );
        // general_purpose_bits, crc and size come later
        write2( zip, (1<<3 | 1<<1));
        // compression method
        write2( zip, 8 );
        write2( zip, fi.last_mod_file_time );
        write2( zip, fi.last_mod_file_date );
        write4( zip, fi.crc32 );
        write4( zip, fi.compressed_size );
        write4( zip, fi.uncompressed_size );
        write2( zip, fi.file_name.length() );
        // extra field length, file comment length, disk number start,
        // internal file attributes (each 2 bytes)
        write4( zip, 0 );
        write4( zip, 0 );
        // external file attributes
        write4( zip, 0 );
        write4( zip, fi.rel_offset_local_header );
        zip.write( fi.file_name.data(), fi.file_name.length() );
    }

    const int rel_end_central_dir = zip.tellp();
    const int size_central_dir = rel_end_central_dir - rel_offset_central_dir;

    // write end of central dir
    write4( zip, 0x06054b50 );
    // disk number
    write2( zip, 0 );
    // number of the disk with the start of the central directory
    write2( zip, 0 );
    // n.o. entries in the centr. dir. on this disk
    write2( zip, fileinfos.size() );
    // total number of entries in the central dir.
    write2( zip, fileinfos.size() );
    // size of the central directory (4 bytes)
    write4( zip, size_central_dir );
    // offset of start of central directory with respect to the
    // starting disk number
    write4( zip, rel_offset_central_dir );
    // .ZIP file comment length -- comment is empty (2 bytes)
    write2( zip, 0 );
}

ostream& ZipWriter::GetStream( const char* name )
{
    if( out )
        Close();

    fileinfo fi;
    fi.rel_offset_local_header = zip.tellp();

    time_t now = time(0);
    struct tm* t = localtime(&now);
    fi.last_mod_file_time =
        (t->tm_sec/2)+(32*t->tm_min)+(2048*(uLong)t->tm_hour);
    fi.last_mod_file_date =
        (t->tm_mday)+(32*(t->tm_mon+1))+(512*(t->tm_year-1900));

    // write local file header:

    // local_file_header_signature
    write4( zip, 0x04034b50 );
    write2( zip, 20 );
    write2( zip, (1<<3 | 1<<1));
    // compression method
    write2( zip, 8 );
    write2( zip, fi.last_mod_file_time );
    write2( zip, fi.last_mod_file_date );

    write4( zip, fi.crc32 = 0 );
    write4( zip, fi.compressed_size = 0 );
    write4( zip, fi.uncompressed_size = 0 );

    fi.file_name = name;
    write2( zip, fi.file_name.length() );
    write2( zip, 0 );

    zip.write( fi.file_name.data(), fi.file_name.length() );

    zs.zalloc = 0; zs.zfree = 0; zs.opaque = 0;
    zs.total_in = zs.total_out = 0;
    zs.next_out = (Bytef*)cbuf;
    zs.avail_out = sizeof(cbuf);
    if( deflateInit2( &zs, 9, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL,
                      Z_DEFAULT_STRATEGY ) != Z_OK )
    {
        cerr << "deflateInit2" << endl;
        exit( -1 );
    }
    fi.crc32 = crc32( 0, Z_NULL, 0 );

    fileinfos.push_back( fi );
    out = new ostream( new WriterBuf( writefunc, this, 32768 ) );
    return *out;
}

void ZipWriter::Close()
{
    if( !out )
        return;

    // write everything
    streambuf* sb = out->rdbuf();
    delete out;
    delete sb;
    out = 0;

    // finish zip stream
    int err = Z_OK;
    while( err == Z_OK
           && ( err = deflate( &zs, Z_FINISH )) == Z_OK )
    {
        zip.write( cbuf, sizeof(cbuf) );
        zs.next_out = (Bytef*)cbuf;
        zs.avail_out = sizeof(cbuf);
    }
    if( err == Z_STREAM_END ) {
        zip.write( cbuf, sizeof(cbuf)-zs.avail_out );
    } else {
        cerr << "zip error" << endl;
        exit( -1 );
    }
    if( deflateEnd( &zs ) != Z_OK ) {
        cerr << "deflateEnd" << endl;
        exit( -1 );
    }

    fileinfo& fi = fileinfos.back();
    fi.uncompressed_size = zs.total_in;
    fi.compressed_size = zs.total_out;

    // write crc and sizes
    write4( zip, fi.crc32 );
    write4( zip, fi.uncompressed_size );
    write4( zip, fi.compressed_size );
}

int ZipWriter::Write( const char* buf, unsigned length )
{
    zs.next_in = (Bytef*)buf;
    zs.avail_in = length;
    fileinfos.back().crc32 = crc32(fileinfos.back().crc32,(Bytef*)buf,length);
    int err = Z_OK;
    while( err == Z_OK && zs.avail_in>0 ) {
        if( zs.avail_out == 0 ) {
            //flush, reset cbuf
            zip.write( cbuf, sizeof(cbuf) );
            zs.next_out = (Bytef*)cbuf;
            zs.avail_out = sizeof(cbuf);
        }
        err = deflate( &zs, Z_NO_FLUSH );
    }
    if( err != Z_OK ) {
        cerr << "while 1" << endl;
        exit( -1 );
    }
    return 0;
}

int ZipWriter::writefunc( void* user, const char* buf, unsigned length )
{
    return ((ZipWriter*)user)->Write( buf, length );
}

// ------------------------------------------------------------------------

#ifdef TEST_ZIPWRITE
int main( int argc, char* argv[] )
{
    if( argc < 3 ) {
        cerr << "need 3 args" << endl;
        exit( -1 );
    }

    std::ofstream zipout( argv[1] );
    ZipWriter zw( zipout );
    for( int f = 2; f<argc; ++f ) {
        ifstream file( argv[f], ios::binary );
        if( !file ) {
            cerr << "cannot read '" << argv[f] << "' - skipping" << endl;
            continue;
        }
        zw.GetStream( argv[f] ) << file.rdbuf();
    }
}
#endif // TEST_ZIPWRITE
