/**
    bambam
    Copyright (C) 2019 German Tischler

    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 <biobambam2/UpdateNumericalIndex.hpp>
#include <libmaus2/lz/BgzfInflateBase.hpp>
#include <libmaus2/bambam/BamHeader.hpp>
#include <libmaus2/bambam/SamDecoder.hpp>
#include <libmaus2/util/ArgInfo.hpp>
#include <libmaus2/util/LineBuffer.hpp>
#include <libmaus2/util/Concat.hpp>
#include <libmaus2/bambam/BamBlockWriterBaseFactory.hpp>
#include <libmaus2/lz/BgzfDeflate.hpp>
#include <libmaus2/aio/InputOutputStreamInstance.hpp>

#include <biobambam2/BamBamConfig.hpp>
#include <biobambam2/Licensing.hpp>
#include <biobambam2/ReadHeader.hpp>

int getDefaultLevel()
{
	return -1;
}

int getDefaultAppend()
{
	return 0;
}

void printHelpMessage(libmaus2::util::ArgInfo const & /* arginfo */)
{
	std::cerr << ::biobambam2::Licensing::license();
	std::cerr << std::endl;
	std::cerr << "Key=Value pairs:" << std::endl;
	std::cerr << std::endl;

	std::vector< std::pair<std::string,std::string> > V;

	V.push_back ( std::pair<std::string,std::string> ( "level=<["+::biobambam2::Licensing::formatNumber(getDefaultLevel())+"]>", libmaus2::bambam::BamBlockWriterBaseFactory::getBamOutputLevelHelpText() ) );
	V.push_back ( std::pair<std::string,std::string> ( "append=<["+::biobambam2::Licensing::formatNumber(getDefaultAppend())+"]>", "append given text to header instead of replacing header by it (default: 0/replace)" ) );

	::biobambam2::Licensing::printMap(std::cerr,V);

	std::cerr << std::endl;
}


int bamreheader(libmaus2::util::ArgInfo const & arginfo)
{
	uint64_t const numrest = arginfo.getNumRestArgs();
	int const level = libmaus2::bambam::BamBlockWriterBaseFactory::checkCompressionLevel(arginfo.getValue("level",getDefaultLevel()));
	bool const append = arginfo.getValue("append",getDefaultAppend()) != 0;

	if ( ! numrest )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "[E] usage: " << arginfo.progname << " <header.sam> [<in.bam>]" << std::endl;
		lme.finish();
		throw lme;
	}
	std::string const headerfn = arginfo.getUnparsedRestArg(0);
	std::string const headertext = libmaus2::util::GetFileSize::readFileAsStream(headerfn);
	biobambam2::ReHeaderUpdate RH(headertext,append);

	// only header given on command line, read BAM file to be modified from standard input channel
	if ( numrest == 1 )
	{
		std::pair<uint64_t,std::string> const compdata = readHeader(std::cin,RH,level);
		std::cout.write(compdata.second.c_str(),compdata.second.size());
		libmaus2::util::Concat::concat(std::cin,std::cout);
	}
	// header and BAM files provided on command line, modify files in place
	else if ( numrest > 1 )
	{
		for ( uint64_t i = 1; i < numrest; ++i )
		{
			std::string const bamfn = arginfo.getUnparsedRestArg(i);
			uint64_t const infs = libmaus2::util::GetFileSize::getFileSize(bamfn);

			libmaus2::aio::InputOutputStreamInstance::unique_ptr_type IOSI(new libmaus2::aio::InputOutputStreamInstance(bamfn,std::ios::in | std::ios::out));

			IOSI->exceptions(std::ios::badbit|std::ios::failbit);

			std::pair<uint64_t,std::string> compdata = readHeader(*IOSI,RH,level);

			uint64_t const inheadersize = compdata.first;
			std::string const & outheader = compdata.second;
			uint64_t const outheadersize = outheader.size();
			uint64_t const tocopy = infs - inheadersize;
			uint64_t const outfs = outheadersize + tocopy;

			if ( inheadersize == outheadersize )
			{
				IOSI->clear();
				IOSI->seekp(0);
				IOSI->write(outheader.c_str(),outheader.size());
				IOSI->flush();
			}
			else
			{
				uint64_t const copyblocksize = 1024*1024;
				libmaus2::autoarray::AutoArray<char> A(copyblocksize,false);

				uint64_t const numblocks = (tocopy + copyblocksize - 1)/copyblocksize;

				if ( outheadersize > inheadersize )
				{
					uint64_t copyinptr = infs;
					uint64_t copyoutptr = infs + (outheadersize - inheadersize);

					for ( uint64_t b = 0; b < numblocks; ++b )
					{
						uint64_t const istart = b * copyblocksize;
						uint64_t const iend   = std::min(istart+copyblocksize,tocopy);
						uint64_t const isize  = (iend-istart);

						copyinptr -= isize;
						copyoutptr -= isize;

						IOSI->clear();
						IOSI->seekg(copyinptr);
						IOSI->read(A.begin(),isize);

						if ( (!*IOSI) || (IOSI->gcount() != static_cast<int64_t>(isize)) )
						{
							libmaus2::exception::LibMausException lme;
							lme.getStream() << "[E] failed to read interval [" << copyinptr << "," << copyinptr+isize << ") from file " << bamfn << std::endl;
							lme.finish();
							throw lme;
						}

						IOSI->clear();
						IOSI->seekp(copyoutptr);
						IOSI->write(A.begin(),isize);

						if ( (!*IOSI) || (IOSI->gcount() != static_cast<int64_t>(isize)) )
						{
							libmaus2::exception::LibMausException lme;
							lme.getStream() << "[E] failed to write interval [" << copyoutptr << "," << copyoutptr+isize << ") to file " << bamfn << std::endl;
							lme.finish();
							throw lme;
						}
					}

					IOSI->clear();
					IOSI->seekp(0);
					IOSI->write(outheader.c_str(),outheader.size());
					IOSI->flush();
				}
				else // if ( outheadersize > inheadersize )
				{
					assert ( outheadersize < inheadersize );

					uint64_t copyinptr = inheadersize;
					uint64_t copyoutptr = outheadersize;

					for ( uint64_t b = 0; b < numblocks; ++b )
					{
						uint64_t const istart = b * copyblocksize;
						uint64_t const iend   = std::min(istart+copyblocksize,tocopy);
						uint64_t const isize  = (iend-istart);

						IOSI->clear();
						IOSI->seekg(copyinptr);
						IOSI->read(A.begin(),isize);

						if ( (!*IOSI) || (IOSI->gcount() != static_cast<int64_t>(isize)) )
						{
							libmaus2::exception::LibMausException lme;
							lme.getStream() << "[E] failed to read interval [" << copyinptr << "," << copyinptr+isize << ") from file " << bamfn << std::endl;
							lme.finish();
							throw lme;
						}

						IOSI->clear();
						IOSI->seekp(copyoutptr);
						IOSI->write(A.begin(),isize);

						if ( (!*IOSI) || (IOSI->gcount() != static_cast<int64_t>(isize)) )
						{
							libmaus2::exception::LibMausException lme;
							lme.getStream() << "[E] failed to write interval [" << copyoutptr << "," << copyoutptr+isize << ") to file " << bamfn << std::endl;
							lme.finish();
							throw lme;
						}

						copyinptr += isize;
						copyoutptr += isize;
					}

					IOSI->clear();
					IOSI->seekp(0);
					IOSI->write(outheader.c_str(),outheader.size());
					IOSI->flush();
				}
			}

			IOSI.reset();

			if ( outfs < infs )
			{
				std::cerr << "[V] truncating " << bamfn << " to size " << outfs << std::endl;

				while ( ::truncate(bamfn.c_str(),outfs) != 0 )
				{
					int const error = errno;

					switch ( error )
					{
						case EINTR:
						case EAGAIN:
							break;
						default:
						{
							libmaus2::exception::LibMausException lme;
							lme.getStream() << "[E] failed to truncate file " << bamfn << " to size " << outfs << ": " << strerror(errno) << std::endl;
							lme.finish();
							throw lme;
						}
					}
				}
			}

			biobambam2::updateNumericalIndex(bamfn,compdata,&std::cerr);
		}
	}

	return EXIT_SUCCESS;
}

int main(int argc, char * argv[])
{
	try
	{
		libmaus2::util::ArgInfo const arginfo(argc,argv);

		for ( uint64_t i = 0; i < arginfo.restargs.size(); ++i )
			if (
				arginfo.restargs[i] == "-v"
				||
				arginfo.restargs[i] == "--version"
			)
			{
				std::cerr << ::biobambam2::Licensing::license();
				return EXIT_SUCCESS;
			}
			else if (
				arginfo.restargs[i] == "-h"
				||
				arginfo.restargs[i] == "--help"
			)
			{
				printHelpMessage(arginfo);
				return EXIT_SUCCESS;
			}

		return bamreheader(arginfo);
	}
	catch(std::exception const & ex)
	{
		std::cerr << ex.what() << std::endl;
		return EXIT_FAILURE;
	}
}
