/*
    libmaus2
    Copyright (C) 2020 German Tischler-Höhle

    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/parallel/threadpool/bgzf/BgzfCompressContext.hpp>
#include <libmaus2/lz/BgzfConstants.hpp>

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

#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
static struct libdeflate_compressor * allocateCompressor(int level)
{
	if ( level < 0 )
		level = 6;

	auto * lcompressor = libdeflate_alloc_compressor(level);

	if ( ! lcompressor )
		throw libmaus2::exception::LibMausException("[E] BgzfCompressContext: libdeflate_alloc_compressor failed");

	return lcompressor;
}
#else
std::shared_ptr<zlib_z_stream> allocateStream(int level)
{
	if ( level < 0 )
		level = Z_DEFAULT_COMPRESSION;

	std::shared_ptr<z_stream> stream(new z_stream);
	std::memset(stream.get(),0,sizeof(z_stream));
	stream->zalloc = nullptr;
	stream->zfree = nullptr;
	stream->opaque = nullptr;
	stream->avail_in = 0;
	stream->next_in = nullptr;
        int const r = deflateInit2(stream.get(), level, Z_DEFLATED, -15 /* window size */, 8 /* mem level, gzip default */, Z_DEFAULT_STRATEGY);

	if ( r != Z_OK )
		throw libmaus2::exception::LibMausException("[E] BgzfCompressContext: deflateInit2 failed");

	return stream;
}
#endif

std::size_t libmaus2::parallel::threadpool::bgzf::BgzfCompressContext::computeBound()
{
	std::size_t const maxpayload = libmaus2::lz::BgzfConstants::getBgzfMaxPayLoad();

	std::size_t low = 0;
	std::size_t high = maxpayload;

	assert ( compressBound(0) < maxpayload );

	while ( high-low > 1 )
	{
		std::size_t const m = (low+high)/2;

		assert ( m > low );
		assert ( m < high );

		std::size_t const b = compressBound(m);

		// bound too large?
		if ( b > maxpayload )
			high = m;
		else
			low = m;
	}

	return 0;
}

libmaus2::parallel::threadpool::bgzf::BgzfCompressContext::BgzfCompressContext(int level)
:
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	compressor(
		allocateCompressor(level),
		[](auto p) {libdeflate_free_compressor(p);}

	)
	#else
	stream(allocateStream(level))
	#endif
	,
	bound(computeBound())
{
}

libmaus2::parallel::threadpool::bgzf::BgzfCompressContext::~BgzfCompressContext()
{
	#if !defined(LIBMAUS2_HAVE_LIBDEFLATE)
	deflateEnd(stream.get());
	#endif
}

std::size_t libmaus2::parallel::threadpool::bgzf::BgzfCompressContext::compressBound(std::size_t const s)
{
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	return libdeflate_deflate_compress_bound(compressor.get(),s);
	#else
	return deflateBound(stream.get(),s);
	#endif
}

std::size_t libmaus2::parallel::threadpool::bgzf::BgzfCompressContext::compress(
	char * in,
	size_t n_in,
	char * out,
	size_t n_out
)
{
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	return libdeflate_deflate_compress(compressor.get(),in,n_in,out,n_out);
	#else
	assert ( stream );
	if ( deflateReset(stream.get()) != Z_OK )
		throw libmaus2::exception::LibMausException("[E] BgzfCompressContext::decompress: deflateReset failed");

	stream->avail_in = n_in;
	stream->next_in = reinterpret_cast<Bytef*>(in);
	stream->avail_out = n_out;
	stream->next_out = reinterpret_cast<Bytef*>(out);

	int const r = deflate(stream.get(),Z_FINISH);

	bool const r_ok = (r == Z_STREAM_END);

	// return 0 on failure
	if ( !r_ok )
		return 0;

	return n_out - stream->avail_out;
	#endif
}

uint32_t libmaus2::parallel::threadpool::bgzf::BgzfCompressContext::crc32(
	char const * in,
	size_t const n_in
)
{
	#if defined(LIBMAUS2_HAVE_LIBDEFLATE)
	return libdeflate_crc32(0 /* init value */, in, n_in);
	#else
	uint32_t lcrc = ::crc32(0,0,0);
	lcrc = ::crc32(lcrc,reinterpret_cast<Bytef const *>(in),n_in);
	return lcrc;
	#endif
}
