/*
    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/BgzfDeflateZStreamBase.hpp>

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

struct LocalLibDeflateCompressor
{
	libdeflate_compressor * compressor;

	LocalLibDeflateCompressor(int const level)
	{
		compressor = libdeflate_alloc_compressor(level);
		if ( ! compressor )
		{
			libmaus2::exception::LibMausException lme;
			lme.getStream() << "[E] LocalLibDeflateCompressor: failed to allocate compressor for level " << level << std::endl;
			lme.finish();
			throw lme;
		}
	}

	~LocalLibDeflateCompressor()
	{
		libdeflate_free_compressor(compressor);
	}

	uint64_t getBound(uint64_t const n)
	{
		return libdeflate_deflate_compress_bound(compressor,n);
	}
};
#endif

void libmaus2::lz::BgzfDeflateZStreamBase::deflatedestroy()
{
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	if ( compressor )
	{
		libdeflate_free_compressor((libdeflate_compressor *)compressor);
		compressor = nullptr;
	}
	#endif

	if ( zintf )
	{
		deflatedestroyz(zintf.get());
		zintf.reset();
	}
}

void libmaus2::lz::BgzfDeflateZStreamBase::deflateinit(int const rlevel)
{
	level = rlevel;

	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	if ( level >= 1 && level <= 12 )
	{
		compressor = libdeflate_alloc_compressor(level);
		if ( ! compressor )
		{
			libmaus2::exception::LibMausException lme;
			lme.getStream() << "[E] BgzfDeflateZStreamBase::deflateInit: failed to allocate compressor for level " << level << std::endl;
			lme.finish();
			throw lme;
		}

		deflbound = computeDeflateBound(level);
	}
	else
	#endif
		if ( level >= Z_DEFAULT_COMPRESSION && level <= Z_BEST_COMPRESSION )
	{
		auto tptr(libmaus2::lz::ZlibInterface::construct());
		zintf = std::move(tptr);

		deflateinitz(zintf.get(),level);

		// search for number of bytes that will never produce more compressed space than we have
		unsigned int bound = getBgzfMaxBlockSize();

		while ( zintf->z_deflateBound(bound) > (getBgzfMaxBlockSize()-(getBgzfHeaderSize()+getBgzfFooterSize())) )
			--bound;

		deflbound = bound;
	}
	else
	{
		::libmaus2::exception::LibMausException se;
		se.getStream() << "BgzfDeflateZStreamBase::deflateinit(): unknown/unsupported compression level " << level << std::endl;
		se.finish();
		throw se;
	}
}

void libmaus2::lz::BgzfDeflateZStreamBase::resetz()
{
	assert ( zintf );
	if ( zintf->z_deflateReset() != Z_OK )
	{
		::libmaus2::exception::LibMausException se;
		se.getStream() << "deflateReset failed." << std::endl;
		se.finish();
		throw se;
	}
}

void libmaus2::lz::BgzfDeflateZStreamBase::deflatereinit(int const rlevel)
{
	deflatedestroy();
	deflateinit(rlevel);
}

// compress block of length len from input pa to output outbuf
// returns the number of compressed bytes produced
uint64_t libmaus2::lz::BgzfDeflateZStreamBase::compressBlock(uint8_t * pa, uint64_t const len, uint8_t * outbuf)
{
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	if ( level >= 1 && level <= 12 )
	{
		size_t const r = libdeflate_deflate_compress(reinterpret_cast<libdeflate_compressor *>(compressor),pa,len,outbuf+getBgzfHeaderSize(),getBgzfMaxPayLoad());

		if ( r == 0 )
		{
			libmaus2::exception::LibMausException se;
			se.getStream() << "libdeflate_deflate_compress() failed." << std::endl;
			se.finish(false /* do not translate stack trace */);
			throw se;
		}
		else
		{
			return r;
		}
	}
	else
	#endif
		if ( level >= Z_DEFAULT_COMPRESSION && level <= Z_BEST_COMPRESSION )
	{
		// reset zlib object
		resetz();

		// maximum number of output bytes
		zintf->setAvailOut(getBgzfMaxPayLoad());
		// next compressed output byte
		zintf->setNextOut(reinterpret_cast<Bytef *>(outbuf) + getBgzfHeaderSize());
		// number of bytes to be compressed
		zintf->setAvailIn(len);
		// data to be compressed
		zintf->setNextIn(reinterpret_cast<Bytef *>(pa));

		// call deflate
		if ( zintf->z_deflate(Z_FINISH) != Z_STREAM_END )
		{
			libmaus2::exception::LibMausException se;
			se.getStream() << "deflate() failed." << std::endl;
			se.finish(false /* do not translate stack trace */);
			throw se;
		}

		return getBgzfMaxPayLoad() - zintf->getAvailOut();
	}
	else
	{
		::libmaus2::exception::LibMausException se;
		se.getStream() << "BgzfDeflateZStreamBase::compressBlock(): unknown/unsupported compression level " << level << std::endl;
		se.finish();
		throw se;
	}
}

libmaus2::lz::BgzfDeflateZStreamBaseFlushInfo libmaus2::lz::BgzfDeflateZStreamBase::flushBound(
	BgzfDeflateInputBufferBase & in,
	BgzfDeflateOutputBufferBase & out,
	bool const fullflush
)
{
	// full flush, compress block in two halves
	if ( fullflush )
	{
		uint64_t const toflush = in.pc-in.pa;
		uint64_t const flush0 = (toflush+1)/2;
		uint64_t const flush1 = toflush-flush0;

		/* compress first half of data */
		uint64_t const payload0 = compressBlock(in.pa,flush0,out.outbuf.begin());
		fillHeaderFooter(
			#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
			zintf.get(),
			#endif
			in.pa,out.outbuf.begin(),payload0,flush0
		);

		/* compress second half of data */
		setupHeader(out.outbuf.begin()+getBgzfHeaderSize()+payload0+getBgzfFooterSize());
		uint64_t const payload1 = compressBlock(in.pa+flush0,flush1,out.outbuf.begin()+getBgzfHeaderSize()+payload0+getBgzfFooterSize());
		fillHeaderFooter(
			#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
			zintf.get(),
			#endif
			in.pa+flush0,out.outbuf.begin()+getBgzfHeaderSize()+payload0+getBgzfFooterSize(),payload1,flush1
		);

		assert ( 2*getBgzfHeaderSize()+2*getBgzfFooterSize()+payload0+payload1 <= out.outbuf.size() );

		in.pc = in.pa;

		return
			BgzfDeflateZStreamBaseFlushInfo(
				flush0,
				getBgzfHeaderSize()+getBgzfFooterSize()+payload0,
				flush1,
				getBgzfHeaderSize()+getBgzfFooterSize()+payload1
			);
	}
	else
	{
		unsigned int const toflush = std::min(static_cast<unsigned int>(in.pc-in.pa),deflbound);
		unsigned int const unflushed = (in.pc-in.pa)-toflush;

		/*
		 * write out compressed data
		 */
		uint64_t const payloadsize = compressBlock(in.pa,toflush,out.outbuf.begin());
		fillHeaderFooter(
			#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
			zintf.get(),
			#endif
			in.pa,out.outbuf.begin(),payloadsize,toflush
		);

		#if 0
		/*
		 * copy rest of uncompressed data to front of buffer
		 */
		if ( unflushed )
			memmove(in.pa,in.pc-unflushed,unflushed);

		// set new output pointer
		in.pc = in.pa + unflushed;
		#endif

		/* number number of bytes in output buffer */
		// return getBgzfHeaderSize()+getBgzfFooterSize()+payloadsize;
		return BgzfDeflateZStreamBaseFlushInfo(
			toflush,
			getBgzfHeaderSize()+getBgzfFooterSize()+payloadsize,
			in.pa, // moveto
			in.pc-unflushed, // movefrom
			unflushed // movesize
		);
	}
}


uint64_t libmaus2::lz::BgzfDeflateZStreamBase::computeDeflateBound(int const rlevel)
{
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	if ( rlevel >= 1 && rlevel <= 12 )
	{
		LocalLibDeflateCompressor LLDC(rlevel);

		// search for number of bytes that will never produce more compressed space than we have
		unsigned int bound = getBgzfMaxBlockSize();

		while (
			LLDC.getBound(bound) >
			(getBgzfMaxBlockSize()-(getBgzfHeaderSize()+getBgzfFooterSize()))
		)
			--bound;

		return bound;
	}
	else
	#endif
		if ( rlevel >= Z_DEFAULT_COMPRESSION && rlevel <= Z_BEST_COMPRESSION )
	{
		libmaus2::lz::ZlibInterface::unique_ptr_type zintf(libmaus2::lz::ZlibInterface::construct());
		deflateinitz(zintf.get(),rlevel);

		// search for number of bytes that will never produce more compressed space than we have
		unsigned int bound = getBgzfMaxBlockSize();

		while (
			zintf->z_deflateBound(bound) >
			(getBgzfMaxBlockSize()-(getBgzfHeaderSize()+getBgzfFooterSize()))
		)
			--bound;

		return bound;
	}
	else
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "[E] BgzfDeflateZStreamBase::computeDeflateBound: level " << rlevel << " not supported" << std::endl;
		lme.finish();
		throw lme;
	}
}

libmaus2::lz::BgzfDeflateZStreamBase::BgzfDeflateZStreamBase(int const rlevel)
:
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	compressor(nullptr),
	#endif
	zintf()
{
	deflateinit(rlevel);
}

libmaus2::lz::BgzfDeflateZStreamBase::~BgzfDeflateZStreamBase()
{
	deflatedestroy();
}

// flush input buffer into output buffer
libmaus2::lz::BgzfDeflateZStreamBaseFlushInfo libmaus2::lz::BgzfDeflateZStreamBase::flush(
	BgzfDeflateInputBufferBase & in,
	BgzfDeflateOutputBufferBase & out,
	bool const fullflush
)
{
	try
	{
		uint64_t const uncompressedsize = in.pc-in.pa;
		uint64_t const payloadsize = compressBlock(in.pa,uncompressedsize,out.outbuf.begin());
		fillHeaderFooter(
			#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
			zintf.get(),
			#endif
			in.pa,out.outbuf.begin(),payloadsize,uncompressedsize
		);

		in.pc = in.pa;

		return BgzfDeflateZStreamBaseFlushInfo(uncompressedsize,getBgzfHeaderSize()+getBgzfFooterSize()+payloadsize);
		// return getBgzfHeaderSize()+getBgzfFooterSize()+payloadsize;
	}
	catch(...)
	{
		return flushBound(in,out,fullflush);
	}
}

libmaus2::lz::BgzfDeflateZStreamBaseFlushInfo libmaus2::lz::BgzfDeflateZStreamBase::flush(uint8_t * const pa, uint8_t * const pe, BgzfDeflateOutputBufferBase & out)
{
	if ( pe-pa > static_cast<ptrdiff_t>(getBgzfMaxBlockSize()) )
	{
		::libmaus2::exception::LibMausException se;
		se.getStream() << "BgzfDeflateZStreamBase()::flush: block is too big for BGZF" << std::endl;
		se.finish();
		throw se;
	}

	try
	{
		uint64_t const uncompressedsize = pe-pa;
		uint64_t const payloadsize = compressBlock(pa,uncompressedsize,out.outbuf.begin());
		fillHeaderFooter(
			#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
			zintf.get(),
			#endif
			pa,out.outbuf.begin(),payloadsize,uncompressedsize
		);
		return BgzfDeflateZStreamBaseFlushInfo(uncompressedsize,getBgzfHeaderSize()+getBgzfFooterSize()+payloadsize);
	}
	catch(...)
	{
		uint64_t const toflush = pe-pa;
		uint64_t const flush0 = (toflush+1)/2;
		uint64_t const flush1 = toflush-flush0;

		/* compress first half of data */
		uint64_t const payload0 = compressBlock(pa,flush0,out.outbuf.begin());
		fillHeaderFooter(
			#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
			zintf.get(),
			#endif
			pa,out.outbuf.begin(),payload0,flush0
		);

		/* compress second half of data */
		setupHeader(out.outbuf.begin()+getBgzfHeaderSize()+payload0+getBgzfFooterSize());
		uint64_t const payload1 = compressBlock(pa+flush0,flush1,out.outbuf.begin()+getBgzfHeaderSize()+payload0+getBgzfFooterSize());
		fillHeaderFooter(
			#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
			zintf.get(),
			#endif
			pa+flush0,out.outbuf.begin()+getBgzfHeaderSize()+payload0+getBgzfFooterSize(),payload1,flush1
		);

		assert ( 2*getBgzfHeaderSize()+2*getBgzfFooterSize()+payload0+payload1 <= out.outbuf.size() );

		return
			BgzfDeflateZStreamBaseFlushInfo(
				flush0,
				getBgzfHeaderSize()+getBgzfFooterSize()+payload0,
				flush1,
				getBgzfHeaderSize()+getBgzfFooterSize()+payload1
			);
	}
}
