/*
    libmaus2
    Copyright (C) 2009-2013 German Tischler
    Copyright (C) 2011-2013 Genome Research Limited

    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/>.
*/
#include <libmaus2/lz/BgzfDeflateHeaderFunctions.hpp>

#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
#include <libdeflate.h>
#endif

uint8_t const * libmaus2::lz::BgzfDeflateHeaderFunctions::fillHeaderFooter(
        #if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
        libmaus2::lz::ZlibInterface * zintf,
        #endif
        uint8_t const * const pa,
        uint8_t * const outbuf,
        unsigned int const payloadsize,
        unsigned int const uncompsize
)
{
        // set size of compressed block in header
        unsigned int const blocksize = getBgzfHeaderSize()/*header*/+getBgzfFooterSize()/*footer*/+payloadsize-1;
        assert ( blocksize < getBgzfMaxBlockSize() );
        outbuf[16] = (blocksize >> 0) & 0xFF;
        outbuf[17] = (blocksize >> 8) & 0xFF;

        uint8_t * footptr = outbuf + getBgzfHeaderSize() + payloadsize;

        // compute crc of uncompressed data
        #if defined(LIBMAUS2_HAVE_LIBDEFLATE)
        uint32_t const crc = libdeflate_crc32(0,pa,uncompsize);
        #else
        uint32_t crc = zintf->z_crc32(0,0,0);
        crc = zintf->z_crc32(crc, reinterpret_cast<Bytef const *>(pa), uncompsize);
        #endif

        // crc
        *(footptr++) = (crc >> 0)  & 0xFF;
        *(footptr++) = (crc >> 8)  & 0xFF;
        *(footptr++) = (crc >> 16) & 0xFF;
        *(footptr++) = (crc >> 24) & 0xFF;
        // uncompressed size
        *(footptr++) = (uncompsize >> 0) & 0xFF;
        *(footptr++) = (uncompsize >> 8) & 0xFF;
        *(footptr++) = (uncompsize >> 16) & 0xFF;
        *(footptr++) = (uncompsize >> 24) & 0xFF;

        return footptr;
}

struct LocalDeflateInfo
{
        libmaus2::lz::ZlibInterface::unique_ptr_type zintf;

        LocalDeflateInfo(int const level)
        : zintf(libmaus2::lz::ZlibInterface::construct())
        {
                libmaus2::lz::BgzfDeflateHeaderFunctions::deflateinitz(zintf.get(),level);
        }
        ~LocalDeflateInfo()
        {
                libmaus2::lz::BgzfDeflateHeaderFunctions::deflatedestroyz(zintf.get());
        }
};

// maximum space used for compressed version of a block of size blocksize
uint64_t libmaus2::lz::BgzfDeflateHeaderFunctions::getFlushBound(unsigned int const blocksize, int const level)
{
        if ( level >= Z_DEFAULT_COMPRESSION && level <= Z_BEST_COMPRESSION )
        {
                LocalDeflateInfo LDI(level);
                return LDI.zintf->z_deflateBound(blocksize);
        }
        else
        {
                return std::max(blocksize,getBgzfMaxBlockSize()) << 1;
        }
}

uint64_t libmaus2::lz::BgzfDeflateHeaderFunctions::getReqBufSpace(int const level)
{
        return getBgzfHeaderSize() + getBgzfFooterSize() + getFlushBound(getBgzfMaxBlockSize(),level);
}

void libmaus2::lz::BgzfDeflateHeaderFunctions::deflateinitz(libmaus2::lz::ZlibInterface * p, int const level)
{
        p->eraseContext();

        p->setZAlloc(Z_NULL);
        p->setZFree(Z_NULL);
        p->setOpaque(Z_NULL);
        int ret = p->z_deflateInit2(level, Z_DEFLATED, -15 /* window size */,
                8 /* mem level, gzip default */, Z_DEFAULT_STRATEGY);
        if ( ret != Z_OK )
        {
                ::libmaus2::exception::LibMausException se;
                se.getStream() << "deflateInit2 failed." << std::endl;
                se.finish();
                throw se;
        }
}

void libmaus2::lz::BgzfDeflateHeaderFunctions::deflatedestroyz(libmaus2::lz::ZlibInterface * p)
{
        p->z_deflateEnd();
}


uint64_t libmaus2::lz::BgzfDeflateHeaderFunctions::getReqBufSpaceTwo(int const level)
{
        uint64_t const halfblocksize = (getBgzfMaxBlockSize()+1)/2;
        uint64_t const singleblocksize = getBgzfHeaderSize() + getBgzfFooterSize() + getFlushBound(halfblocksize,level);
        uint64_t const doubleblocksize = 2 * singleblocksize;
        return doubleblocksize;
}

void libmaus2::lz::BgzfDeflateHeaderFunctions::setupHeader(uint8_t * const outbuf)
{
        outbuf[0] = ::libmaus2::lz::GzipHeader::ID1;
        outbuf[1] = ::libmaus2::lz::GzipHeader::ID2;
        outbuf[2] = 8; // CM
        outbuf[3] = 4; // FLG, extra data
        outbuf[4] = outbuf[5] = outbuf[6] = outbuf[7] = 0; // MTIME
        outbuf[8] = 0; // XFL
        outbuf[9] = 255; // undefined OS
        outbuf[10] = 6; outbuf[11] = 0; // xlen=6
        outbuf[12] = 'B';
        outbuf[13] = 'C';
        outbuf[14] = 2; outbuf[15] = 0; // length of field
        outbuf[16] = outbuf[17] = 0; // block length, to be filled later
}

uint64_t libmaus2::lz::BgzfDeflateHeaderFunctions::getOutBufSizeTwo(int const level)
{
        return std::max(static_cast<uint64_t>(getBgzfMaxBlockSize()),getReqBufSpaceTwo(level));
}
