/**
    biobambam
    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 <biobambam2/BamBamConfig.hpp>
#include <biobambam2/Licensing.hpp>
#include <biobambam2/DepthInterval.hpp>

#include <iomanip>

#include <config.h>

#include <libmaus2/util/TempFileRemovalContainer.hpp>
#include <libmaus2/bambam/BamMultiAlignmentDecoderFactory.hpp>

#include <libmaus2/util/NumberSerialisation.hpp>
#include <libmaus2/util/ArgParser.hpp>

struct IntervalHandler
{
	int64_t const mindepth;
	int64_t const maxdepth;
	bool const binary;

	biobambam2::DepthInterval PD;
	bool PD_valid;

	libmaus2::bambam::BamHeader const & header;

	int64_t nextrefid;

	IntervalHandler(
		int64_t const rmindepth,
		int64_t const rmaxdepth,
		bool const rbinary,
		libmaus2::bambam::BamHeader const & rheader
	) : mindepth(rmindepth), maxdepth(rmaxdepth), binary(rbinary), PD_valid(false), header(rheader), nextrefid(0)
	{

	}

	void handleRaw(biobambam2::DepthInterval const & D)
	{
		if ( static_cast<int64_t>(D.depth) >= mindepth && static_cast<int64_t>(D.depth) <= maxdepth )
		{
			if ( binary )
				D.serialise(std::cout);
			else
			std::cout << D << "\n";
		}
	}

	void handle(biobambam2::DepthInterval const & D)
	{
		while ( nextrefid < static_cast<int64_t>(D.refid) )
		{
			biobambam2::DepthInterval const D(nextrefid,0,header.getRefIDLength(nextrefid),0);

			handleRaw(D);

			nextrefid += 1;
		}

		if ( ! PD_valid )
		{
			PD = D;
			PD_valid = true;
		}
		else if ( D.refid == PD.refid && D.depth == PD.depth )
		{
			assert ( D.from == PD.to );
			PD.to = D.to;
		}
		else
		{
			handleRaw(PD);

			PD = D;
		}

		nextrefid = D.refid+1;
	}

	void finish()
	{
		if ( PD_valid )
		{
			handleRaw(PD);
		}

		while ( nextrefid < static_cast<int64_t>(header.getNumRef()) )
		{
			biobambam2::DepthInterval const D(nextrefid,0,header.getRefIDLength(nextrefid),0);

			handleRaw(D);

			nextrefid += 1;
		}
	}
};


void bamdepth(libmaus2::util::ArgParser const & arg)
{
	libmaus2::bambam::BamAlignmentDecoderWrapper::unique_ptr_type decwrapper(
		libmaus2::bambam::BamMultiAlignmentDecoderFactory::construct(arg,true /* put rank */));
	::libmaus2::bambam::BamAlignmentDecoder * ppdec = &(decwrapper->getDecoder());
	::libmaus2::bambam::BamAlignmentDecoder & dec = *ppdec;
	::libmaus2::bambam::BamHeader const & header = dec.getHeader();
	libmaus2::bambam::BamAlignment const & algn = dec.getAlignment();
	uint64_t const mindepth = arg.getParsedArgOrDefault<uint64_t>("mindepth",0);
	uint64_t const maxdepth = arg.getParsedArgOrDefault<uint64_t>("maxdepth",std::numeric_limits<int64_t>::max());
	bool const binary = arg.getParsedArgOrDefault<int>("binary",0);
	int const verbose = arg.getParsedArgOrDefault<int>("verbose",1);

	libmaus2::util::FiniteSizeHeap<uint64_t> FSH(0);

	uint64_t prevstart = 0;
	uint64_t depth = 0;
	int64_t prevrefid = -1;

	IntervalHandler IH(mindepth,maxdepth,binary,header);

	while ( dec.readAlignment() )
	{
		if ( algn.isMapped() )
		{
			if ( verbose && (algn.getRefID() != prevrefid) )
				std::cerr << "[V] " << dec.getHeader().getRefIDName(algn.getRefID()) << std::endl;

			libmaus2::math::IntegerInterval<int64_t> const I = algn.getReferenceInterval();

			int64_t const i_from = I.from;
			int64_t const i_to = i_from + I.diameter();

			// std::cerr << "[" << i_from << "," << i_to << ")" << std::endl;

			// stack is not empty and top element is finished
			while (
				(!FSH.empty())
				&&
				(
					algn.getRefID() != prevrefid
					||
					static_cast<int64_t>(FSH.top()) <= i_from
				)
			)
			{
				uint64_t const end = FSH.pop();

				if ( end > prevstart )
				{
					biobambam2::DepthInterval const D(prevrefid,prevstart,end,depth);
					IH.handle(D);
				}

				depth -= 1;
				prevstart = end;
			}

			// we have reached the end of a refid
			if ( algn.getRefID() != prevrefid && prevrefid >= 0 )
			{
				assert ( depth == 0 );

				// length of ref id
				int64_t const len = dec.getHeader().getRefIDLength(prevrefid);

				// if there is a depth 0 stretch in the end
				if ( len > static_cast<int64_t>(prevstart) )
				{
					biobambam2::DepthInterval const D(prevrefid,prevstart,len,depth);
					IH.handle(D);
				}

				prevstart = 0;
			}

			if ( i_from > static_cast<int64_t>(prevstart) )
			{
				biobambam2::DepthInterval const D(algn.getRefID(),prevstart,i_from,depth);
				IH.handle(D);
			}

			depth += 1;
			prevstart = i_from;

			FSH.pushBump(i_to);

			prevrefid = algn.getRefID();
		}
	}

	while (
		(!FSH.empty())
	)
	{
		uint64_t const end = FSH.pop();

		if ( end > prevstart )
		{
			biobambam2::DepthInterval const D(prevrefid,prevstart,end,depth);
			IH.handle(D);
		}

		depth -= 1;
		prevstart = end;
	}

	if ( prevrefid >= 0 )
	{
		assert ( depth == 0 );

		int64_t const len = dec.getHeader().getRefIDLength(prevrefid);

		if ( len > static_cast<int64_t>(prevstart) )
		{
			biobambam2::DepthInterval const D(prevrefid,prevstart,len,depth);
			IH.handle(D);
		}

		prevstart = 0;
	}

	IH.finish();
}

int main(int argc, char * argv[])
{
	try
	{
		std::vector<libmaus2::util::ArgParser::ArgumentDefinition> Vformat = libmaus2::bambam::BamAlignmentDecoderInfo::getArgumentDefinitions();
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("","mindepth",true));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("","maxdepth",true));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("","binary",true));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("v","verbose",true));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("T","",true));

		::libmaus2::util::ArgParser arg(argc,argv,Vformat);

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