/**
    biobambam2
    Copyright (C) 2020 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/BamBamConfig.hpp>
#include <biobambam2/Licensing.hpp>
#include <biobambam2/DepthInterval.hpp>
#include <biobambam2/DepthIntervalGetter.hpp>

#include <iomanip>
#include <regex>

#include <config.h>

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

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

#include <libmaus2/util/TempFileRemovalContainer.hpp>
#include <libmaus2/bambam/BamMultiAlignmentDecoderFactory.hpp>
#include <libmaus2/gtf/BAMGTFMap.hpp>
#include <libmaus2/util/NumberSerialisation.hpp>
#include <libmaus2/gtf/ExonInfo.hpp>
#include <libmaus2/gtf/IntervalListSet.hpp>
#include <libmaus2/gtf/RangeSets.hpp>
#include <libmaus2/aio/SerialisedPeeker.hpp>
#include <libmaus2/avl/AVLSet.hpp>
#include <libmaus2/parallel/NumCpus.hpp>
#include <libmaus2/fastx/GCContent.hpp>
#include <libmaus2/util/MemUsage.hpp>
#include <libmaus2/sorting/SortingBufferedOutputFile.hpp>
#include <libmaus2/fastx/FastABPGenerator.hpp>
#include <libmaus2/fastx/FastaBPDecoder.hpp>



std::map<std::pair<uint64_t,uint64_t>,uint64_t> GPGC;
libmaus2::parallel::StdMutex GPGClock;

std::map<uint64_t,uint64_t> GP;
libmaus2::parallel::StdMutex GPlock;

std::map<uint64_t,uint64_t> GKGC;
libmaus2::parallel::StdMutex GKGClock;
std::map<uint64_t,uint64_t> GKS;
libmaus2::parallel::StdMutex GKSlock;

double const thresfrac = 0.999;
double const Pfrac = 0.999;

static uint64_t const print[] = { 1, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
static uint64_t const print_n = sizeof(print)/sizeof(print[0]);

static void handleFile(
	std::string const & tmpfn,
	uint64_t const numsamples,
	libmaus2::bambam::BamHeader const & header,
	std::ostream & outstr,
	std::ostream & errstr,
	std::string const & bpfn
)
{
	uint64_t refid;

	// get refid we are handling
	{
		libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(tmpfn);
		biobambam2::DepthInterval DI;

		if ( ! SP.peekNext(DI) )
			return;

		refid = DI.refid;
	}

	libmaus2::aio::InputStreamInstance bpISI(bpfn);
	libmaus2::fastx::FastaBPDecoder bpDEC(bpISI);

	std::string faseqname = bpDEC.getSequenceName(bpISI,refid);

	assert ( faseqname == header.getRefIDName(refid) );

	libmaus2::autoarray::AutoArray<char> const AS = bpDEC.decodeSequence(bpISI,refid);

	typedef libmaus2::sorting::SerialisingSortingBufferedOutputFile<biobambam2::DepthInterval,biobambam2::DepthIntervalDepthOrder>
		depth_sorter_type;

	// compute length of target regions
	uint64_t l = 0;
	{
		libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(tmpfn);
		biobambam2::DepthInterval DI;
		libmaus2::fastx::GCContent GC(AS.begin(),AS.end(),200);

		std::map < uint64_t, biobambam2::DepthInterval > MDI;
		std::map < uint64_t, std::string > MFN;
		std::map < uint64_t, libmaus2::aio::OutputStreamInstance::shared_ptr_type > MOSI;

		while ( SP.getNext(DI) )
		{
			l += DI.to-DI.from;

			for ( uint64_t p = DI.from; p < DI.to; ++p )
			{
				assert ( p < GC.e-GC.a );

				while ( GC.midPos() < p )
					GC.nextPos();

				// get GC content for window and quantify it
				double const gcfrac = GC.getFrac();
				double const gcperc = gcfrac * 100.0;
				double const gcquant = 5.0;
				double const gc = std::floor((gcperc / gcquant)+0.5) * gcquant;
				uint64_t const ugc = static_cast<uint64_t>(gc);

				if ( MDI.find(ugc) == MDI.end() )
				{
					std::ostringstream fnostr;
					fnostr << tmpfn << "_gcsplit_" << ugc;
					std::string const fn = fnostr.str();
					libmaus2::util::TempFileRemovalContainer::addTempFile(fn);

					libmaus2::aio::OutputStreamInstance::shared_ptr_type sptr(
						new libmaus2::aio::OutputStreamInstance(fn)
					);

					MDI[ugc] = biobambam2::DepthInterval(DI.refid,0,0,0);
					MFN[ugc] = fn;
					MOSI[ugc] = sptr;
				}

				std::map < uint64_t, biobambam2::DepthInterval >::iterator it_MDI = MDI.find(ugc);
				std::map < uint64_t, libmaus2::aio::OutputStreamInstance::shared_ptr_type >::iterator it_MOSI = MOSI.find(ugc);
				assert ( it_MDI != MDI.end() );
				assert ( it_MOSI != MOSI.end() );

				// current interval
				biobambam2::DepthInterval & LDI = it_MDI->second;

				// extend previous interval?
				if ( LDI.depth == DI.depth && LDI.to == p )
					LDI.to = p+1;
				else
				{
					if ( LDI.to > LDI.from )
						LDI.serialise(*(it_MOSI->second));

					LDI = biobambam2::DepthInterval(DI.refid,p,p+1,DI.depth);
				}
			}
		}

		// flush out last intervals
		for ( std::map < uint64_t, biobambam2::DepthInterval >::const_iterator it = MDI.begin(); it != MDI.end(); ++it )
		{
			assert ( it->second.to > it->second.from );
			uint64_t const ugc = it->first;

			std::map < uint64_t, libmaus2::aio::OutputStreamInstance::shared_ptr_type >::iterator it_MOSI = MOSI.find(ugc);
			assert ( it_MOSI != MOSI.end() );

			it->second.serialise(*(it_MOSI->second));
		}

		// flush output streams
		for ( std::map < uint64_t, libmaus2::aio::OutputStreamInstance::shared_ptr_type >::iterator it = MOSI.begin(); it != MOSI.end(); ++it )
			it->second->flush();

		// close files
		MOSI.clear();

		// sort files by depth
		for ( std::map < uint64_t, std::string >::const_iterator it = MFN.begin(); it != MFN.end(); ++it )
		{
			uint64_t const ugc = it->first;
			std::string const fn = it->second;
			depth_sorter_type::sort(fn,1024*1024 /* blocksize */);

			// compute total length
			uint64_t l = 0;
			{
				libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(fn);
				biobambam2::DepthInterval DI;
				while ( SP.getNext(DI) )
					l += DI.to-DI.from;
			}

			{
				uint64_t ld = l;
				libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(fn);
				biobambam2::DepthInterval DI;

				uint64_t print_o = 0;
				while ( SP.peekNext(DI) )
				{
					uint64_t const depth = DI.depth;

					// get length for this depth
					uint64_t s = 0;
					while ( SP.peekNext(DI) && DI.depth == depth )
					{
						SP.getNext(DI);
						s += DI.to-DI.from;
					}

					errstr << "[PGC]\t" << refid << "\t" << header.getRefIDName(refid) << "\t" << ugc << "\t" << depth << "\t" << s << "/" << l << "\t" << static_cast<double>(s)/l << "\n";

					{
						libmaus2::parallel::StdMutex::scope_lock_type slock(GPGClock);
						GPGC[std::make_pair(ugc,depth)] += s;
					}

					while ( print_o < print_n && depth >= print[print_o] )
					{
						errstr << "[HGC]\t" << refid << "\t" << header.getRefIDName(refid) << "\t" << ugc << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << "\n";
						print_o += 1;
					}

					ld -= s;
				}

				while ( print_o < print_n )
				{
					errstr << "[HGC]\t" << refid << "\t" << header.getRefIDName(refid) << "\t" << ugc << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << "\n";
					print_o += 1;
				}
			}

			libmaus2::aio::FileRemoval::removeFile(fn);
		}
	}

	std::string const tmptmp = tmpfn + ".tmp";
	libmaus2::util::TempFileRemovalContainer::addTempFile(tmptmp);

	typedef depth_sorter_type::merger_ptr_type depth_merger_ptr_type;

	depth_merger_ptr_type dmerger;

	{
		libmaus2::sorting::SerialisingSortingBufferedOutputFile<biobambam2::DepthInterval,biobambam2::DepthIntervalDepthOrder> SSBOF(
			tmptmp,1024*1024
		);

		libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(tmpfn);
		biobambam2::DepthInterval DI;
		while ( SP.getNext(DI) )
			SSBOF.put(DI);

		depth_merger_ptr_type tmerger(SSBOF.getMerger());
		dmerger = std::move(tmerger);
	}


	libmaus2::fastx::GCContent GC(AS.begin(),AS.end(),200);

	static uint64_t const print_max = print[print_n-1];

	int64_t thresdepth = -1;

	std::vector< std::pair<uint64_t,uint64_t> > DM;
	{
		biobambam2::DepthInterval D;

		uint64_t s = 0;
		uint64_t pd = std::numeric_limits<uint64_t>::max();
		uint64_t ssum = 0;
		int64_t lrefid = -1;

		while ( dmerger->getNext(D) )
		{
			uint64_t d = D.depth;
			lrefid = D.refid;

			if ( d != pd )
			{
				if ( s )
				{
					{
						libmaus2::parallel::StdMutex::scope_lock_type slock(GPlock);
						GP[pd] += s;
					}

					double const prefrac = static_cast<double>(ssum)/l;
					ssum += s;
					double const frac = static_cast<double>(ssum)/l;

					if ( prefrac <= Pfrac )
						errstr << "[P]\t" << lrefid << "\t" << header.getRefIDName(lrefid) << "\t" << pd << "\t" << s << "/" << l << "\t" << static_cast<double>(s)/l << "\t" << frac << std::endl;

					if ( thresdepth < 0 && frac >= thresfrac )
						thresdepth = pd;
				}

				s = 0;
				pd = d;
			}

			if ( d > print_max )
				d = print_max;

			uint64_t const ll = D.to-D.from;

			s += ll;

			while ( !(d < DM.size()) )
				DM.push_back(std::pair<uint64_t,uint64_t>());

			assert ( d < DM.size() );

			DM[d].first = d;
			DM[d].second += ll;
		}

		if ( s )
		{
			{
				libmaus2::parallel::StdMutex::scope_lock_type slock(GPlock);
				GP[pd] += s;
			}

			double const prefrac = static_cast<double>(ssum)/l;
			ssum += s;
			double const frac = static_cast<double>(ssum)/l;

			if ( prefrac <= Pfrac )
				errstr << "[P]\t" << lrefid << "\t" << header.getRefIDName(lrefid) << "\t" << pd << "\t" << s << "/" << l << "\t" << static_cast<double>(s)/l << "\t" << frac << std::endl;

			if ( thresdepth < 0 && static_cast<double>(ssum)/l >= thresfrac )
				thresdepth = pd;
		}
	}

	// errstr << "[V thresdepth]\t" << thresdepth << std::endl;

	dmerger.reset();
	libmaus2::aio::FileRemoval::removeFile(tmptmp);


	{
		libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(tmpfn);
		biobambam2::DepthInterval DI;
		std::map<uint64_t,uint64_t> KM;

		// number of windows featuring quantified GC content
		std::map<uint64_t,uint64_t> KGC;
		// sum of depth for quantified GC content
		std::map<uint64_t,uint64_t> KS;

		while ( SP.getNext(DI) )
		{
			uint64_t const d = DI.depth;

			if ( d <= thresdepth )
			{
				for ( uint64_t p = DI.from; p < DI.to; ++p )
				{
					assert ( p < GC.e-GC.a );

					while ( GC.midPos() < p )
						GC.nextPos();

					double const gcfrac = GC.getFrac();
					double const gcperc = gcfrac * 100.0;
					double const gcquant = 5.0;
					double const gc = std::floor((gcperc / gcquant)+0.5) * gcquant;
					uint64_t const ugc = static_cast<uint64_t>(gc);

					// update GC counter for (quantified) content
					KGC[ugc] += 1;
					KS[ugc] += d;

					uint64_t const udquant = 5;
					double const dquant = udquant;
					double const dquant2 = dquant/2.0;

					uint64_t const dq = std::floor((d + dquant2)/dquant) * udquant;

					uint64_t const key = (dq << 7) | ugc;

					KM[key] += 1;

					// std::cerr << "p=" << p << " midpos=" << GC.midPos() << " d=" << d << " GC=" << GC.getFrac() << std::endl;
				}
			}
		}

		uint64_t kgsum = 0;
		for ( std::map<uint64_t,uint64_t>::const_iterator it = KGC.begin(); it != KGC.end(); ++it )
		{
			kgsum += it->second;

			libmaus2::parallel::StdMutex::scope_lock_type slock(GKGClock);
			GKGC[it->first] += it->second;
		}

		for ( std::map<uint64_t,uint64_t>::const_iterator it = KS.begin(); it != KS.end(); ++it )
		{
			libmaus2::parallel::StdMutex::scope_lock_type slock(GKSlock);
			GKS[it->first] += it->second;
		}

		double const dkgsum = kgsum;
		uint64_t KGCmax = 0;
		for ( std::map<uint64_t,uint64_t>::const_iterator it = KGC.begin(); it != KGC.end(); ++it )
		{
			errstr << "[KGC]\t" << refid << "\t" << header.getRefIDName(refid) << "\t" << it->first << "\t" << it->second << "\t" << (it->second/dkgsum) << std::endl;

			KGCmax = std::max(KGCmax,it->second);
		}

		std::map<uint64_t,double> KGCMEAN;
		for ( std::map<uint64_t,uint64_t>::const_iterator it = KGC.begin(); it != KGC.end(); ++it )
		{
			std::map<uint64_t,uint64_t>::const_iterator sit = KS.find(it->first);
			assert ( sit != KS.end() );

			double const mean = static_cast<double>(sit->second) / it->second;

			KGCMEAN[it->first] = mean;

			errstr << "[KGCMEAN]\t" << refid << "\t" << header.getRefIDName(refid) << "\t" << it->first << "\t" << mean << std::endl;
		}

		#if 0
		std::map<uint64_t,double> KGCM;
		for ( std::map<uint64_t,uint64_t>::const_iterator it = KGC.begin(); it != KGC.end(); ++it )
		{
			assert ( it->second );
			double const frac = static_cast<double>(KGCmax) / static_cast<double>(it->second);
			KGCM[it->first] = frac;

			errstr << "[KGCM]\t" << refid << "\t" << it->first << "\t" << frac << std::endl;
		}

		uint64_t prevd = std::numeric_limits<uint64_t>::max();
		for ( std::map<uint64_t,uint64_t>::const_iterator it = KM.begin(); it != KM.end(); ++it )
		{
			uint64_t const key = it->first;
			uint64_t const d = key >> 7;
			uint64_t const gcperc = key&(127);

			if ( d != prevd && prevd !=  std::numeric_limits<uint64_t>::max() )
			{
				errstr << "[HM]\t" << refid << std::endl;
			}

			std::map<uint64_t,double>::const_iterator kgcmit = KGCM.find(gcperc);
			assert ( kgcmit != KGCM.end() );

			errstr << "[HM]\t" << refid << "\t" << d << "\t" << gcperc << "\t" << it->second << "\t" << (it->second * kgcmit->second) << std::endl;

			prevd = d;
		}
		#endif
	}

	#if 0
	std::map<uint64_t,uint64_t> DM;

	{
		libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(tmpfn);
		biobambam2::DepthInterval DI;
		while ( SP.getNext(DI) )
		{
			uint64_t const ll = DI.to-DI.from;
			l += ll;
			DM[DI.depth] += ll;
		}
	}
	#endif

	libmaus2::aio::SerialisedPeeker<biobambam2::DepthInterval> SP(tmpfn);
	biobambam2::DepthInterval DI;

	if ( ! SP.peekNext(DI) )
		return;



	uint64_t ld = l;
	uint64_t print_o = 0;
	for ( 	uint64_t print_i = 0; print_i < DM.size() && print_o < print_n; ++print_i )
	{
		std::vector < std::pair<uint64_t,uint64_t> >::const_iterator it = DM.begin()+print_i;

		while ( print_o < print_n && it->first >= print[print_o] )
		{
			errstr << "[H]\t" << refid << "\t" << header.getRefIDName(refid) << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << std::endl;
			// std::cerr << "[H]\t" << refid << "\t" << header.getRefIDName(refid) << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << std::endl;
			print_o += 1;
		}

		ld -= it->second;
	}

	while ( print_o < print_n )
	{
		errstr << "[H]\t" << refid << "\t" << header.getRefIDName(refid) << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << std::endl;
		// std::cerr << "[H]\t" << refid << "\t" << header.getRefIDName(refid) << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << std::endl;
		print_o += 1;
	}

	uint64_t const windowsize = (2*l + numsamples - 1) / numsamples;
	uint64_t const shift = (windowsize+1) / 2;

	errstr << "[V] using windowsize=" << windowsize << " shift=" << shift << " for refid=" << refid << " of length " << l << std::endl;
	// std::cerr << "[V] using windowsize=" << windowsize << " shift=" << shift << " for refid=" << refid << " of length " << l << std::endl;

	while ( SP.peekNext(DI) )
	{

		biobambam2::DepthIntervalGetter DIG(SP);
		uint64_t v;
		uint64_t p;

		std::deque<uint64_t> D;
		#if 0
		std::map<uint64_t,uint64_t> M;
		#endif
		typedef libmaus2::avl::AVLSet< std::pair<uint64_t,uint64_t> > avl_tree_type;
		avl_tree_type AVL;
		uint64_t avlsub = 0;

		std::deque<uint64_t> P;
		while ( D.size() < windowsize && DIG.getNext(v,p) )
		{
			D.push_back(v);
			P.push_back(p);
			#if 0
			M[v]++;
			#endif
			AVL.insert(std::pair<uint64_t,uint64_t>(v,avlsub++));
		}

		bool running = D.size();
		uint64_t sum = std::accumulate(D.begin(),D.end(),0ull);

		for ( uint64_t pos = 0; running; ++pos )
		{
			#if 0
			std::vector < uint64_t > V(D.begin(),D.end());
			std::sort(V.begin(),V.end());
			#endif

			uint64_t const med =
				AVL.size()
				?
				AVL[AVL.size()/2]->first
				:
				0;

			double const avg = static_cast<double>(sum) / D.size();

			// output
			// - ref id
			// - window index
			// - minimum depth in window
			// - average depth in window
			// - median depth in window
			// - maximum depth in window
			outstr << DIG.refid << "\t" << pos << "\t" << AVL.begin()->first << "\t" << avg << "\t" << med << "\t" << AVL.rbegin()->first << "\n";
			// std::cerr << DIG.refid << "\t" << pos << "\t" << AVL.begin()->first << "\t" << avg << "\t" << med << "\t" << AVL.rbegin()->first << "\n";

			uint64_t up = shift;

			running = false;
			uint64_t p;
			while ( up && DIG.getNext(v,p) )
			{
				up -= 1;
				running = true;

				// remove front element
				uint64_t const todel = D.front();
				D.pop_front();
				P.pop_front();

				// look for element in avl tree
				avl_tree_type::const_iterator const avl_it = AVL.lower_bound(std::make_pair(todel,0));
				assert ( avl_it != AVL.end() && avl_it->first == todel );
				AVL.erase(avl_it);

				sum -= todel;

				#if 0
				M[todel] -= 1;
				if ( M[todel] == 0 )
					M.erase(todel);
				#endif

				// push new element
				D.push_back(v);
				P.push_back(p);
				sum += v;
				#if 0
				M[v] += 1;
				#endif
				AVL.insert(std::pair<uint64_t,uint64_t>(v,avlsub++));
			}
		}

		outstr << std::flush;
	}
}

struct DepthIntervalHandler
{
	virtual ~DepthIntervalHandler() {}
	virtual void operator()(biobambam2::DepthInterval const & D) = 0;
};

struct ExonHandler : public DepthIntervalHandler
{
	std::string const & tmpfn;
	uint64_t const numsamples;
	libmaus2::aio::OutputStreamInstance::unique_ptr_type pOSI;

	int64_t previd;

	libmaus2::bambam::BamHeader const & header;

	libmaus2::gtf::BAMGTFMap const & bamgtfmap;
	libmaus2::gtf::RangeSets const & RS;

	libmaus2::autoarray::AutoArray < libmaus2::gtf::Exon const * > query_R;
	libmaus2::autoarray::AutoArray<libmaus2::geometry::RangeSet<libmaus2::gtf::Exon>::search_q_element> query_todo;

	libmaus2::gtf::GTFData const & gtfdata;

	bool const cds;

	std::ostream & outstr;
	std::ostream & errstr;

	std::string const & bpfn;

	ExonHandler(
		std::string const & rtmpfn,
		uint64_t const rnumsamples,
		libmaus2::bambam::BamHeader const & rheader,
		libmaus2::gtf::BAMGTFMap const & rbamgtfmap,
		libmaus2::gtf::RangeSets const & rRS,
		libmaus2::gtf::GTFData const & rgtfdata,
		bool const rcds,
		std::ostream & routstr,
		std::ostream & rerrstr,
		std::string const & rbpfn
	) : tmpfn(rtmpfn), numsamples(rnumsamples), pOSI(), previd(-1), header(rheader), bamgtfmap(rbamgtfmap), RS(rRS), gtfdata(rgtfdata), cds(rcds), outstr(routstr), errstr(rerrstr), bpfn(rbpfn)
	{
	}

	void flush()
	{
		if ( pOSI )
		{
			pOSI->flush();
			pOSI.reset();

			handleFile(tmpfn,numsamples,header,outstr,errstr,bpfn);
		}
	}

	void operator()(biobambam2::DepthInterval const & D)
	{
		if ( static_cast<int64_t>(D.refid) != previd )
			flush();

		int64_t const gtfid = bamgtfmap.Vmap[D.refid];

		if ( gtfid >= 0 )
		{
			libmaus2::geometry::RangeSet<libmaus2::gtf::Exon> const & LRS = *(RS.VRS.at(gtfid));
			uint64_t const o = LRS.search(libmaus2::gtf::Exon(D.from,D.to),query_R,query_todo);

			#if 0
			std::cerr << "query_R.size()=" << query_R.size() << std::endl;
			std::cerr << "query_todo.size()=" << query_todo.size() << std::endl;
			#endif

			libmaus2::math::IntegerInterval<int64_t> ID(D.from,static_cast<int64_t>(D.to)-1);

			std::vector < libmaus2::math::IntegerInterval<int64_t> > VIC;

			for ( uint64_t i = 0; i < o; ++i )
			{
				libmaus2::gtf::Exon const & E = *(query_R[i]);
				libmaus2::math::IntegerInterval<int64_t> const IE = E.getInterval();
				libmaus2::math::IntegerInterval<int64_t> const IC = ID.intersection(IE);

				if (
					!IC.isEmpty()
				)
				{
					// CDS_start, CDS_end
					if ( cds )
					{
						for ( uint64_t z = E.CDS_start; z < E.CDS_end; ++z )
						{
							libmaus2::gtf::CDS const & C = gtfdata.ACDS[z];

							int64_t const ifrom = C.getFrom();
							int64_t const ito = C.getTo();
							libmaus2::math::IntegerInterval<int64_t> ICDS(ifrom,ito);

							libmaus2::math::IntegerInterval<int64_t> const ICC = ICDS.intersection(ID);
							if ( !ICC.isEmpty() )
								VIC.push_back(ICC);
						}
					}
					else
					{
						VIC.push_back(IC);
					}
				}
			}

			VIC = libmaus2::math::IntegerInterval<int64_t>::mergeTouchingOrOverlapping(VIC);

			for ( uint64_t i = 0; i < VIC.size(); ++i )
			{
				libmaus2::math::IntegerInterval<int64_t> const & IC = VIC[i];

				uint64_t const from = IC.from;
				uint64_t const to = from + IC.diameter();

				biobambam2::DepthInterval DIC = D;
				DIC.from = from;
				DIC.to = to;

				// errstr << DIC.print(header) << "\n";

				if ( ! pOSI )
				{
					libmaus2::aio::OutputStreamInstance::unique_ptr_type tOSI(new libmaus2::aio::OutputStreamInstance(tmpfn));
					pOSI = std::move(tOSI);
					previd = DIC.refid;
				}

				DIC.serialise(*pOSI);
			}
		}
	}
};

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

	biobambam2::DepthInterval PD;
	bool PD_valid;

	libmaus2::bambam::BamHeader const & header;

	DepthIntervalHandler & DIH;

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

	}

	void handleRaw(biobambam2::DepthInterval const & D)
	{
		if ( static_cast<int64_t>(D.depth) >= mindepth && static_cast<int64_t>(D.depth) <= maxdepth )
			DIH(D);
	}

	void handle(biobambam2::DepthInterval const & D)
	{
		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;
		}
	}

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

static uint64_t getDefaultNumThreads()
{
	return libmaus2::parallel::NumCpus::getNumLogicalProcessors();
}

void bamexondepth(libmaus2::util::ArgParser const & arg)
{
	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);
	uint64_t const numsamples = arg.getParsedArgOrDefault<int>("numsamples",1024);
	std::string const tmpfilenamebase = arg.uniqueArgPresent("T") ? arg["T"] : libmaus2::util::ArgInfo::getDefaultTmpFileName(arg.progname);
	bool const cds = arg.uniqueArgPresent("cds");
	uint64_t numthreads = arg.getParsedArgOrDefault<uint64_t>("threads",1);
	std::string const seq = arg.uniqueArgPresent("sequence") ? arg["sequence"] : std::string();

	if ( numthreads == 0 )
		numthreads = getDefaultNumThreads();

	std::string const annofn = arg[0];
	std::string const reffn = arg[1];

	std::string const bpfn = reffn + ".fabp";
	std::string const fastatmp = tmpfilenamebase + "_fatmp";

	if ( ! libmaus2::util::GetFileSize::fileExists(bpfn) )
	{
		libmaus2::aio::OutputStreamInstance::unique_ptr_type ptr(
			new libmaus2::aio::OutputStreamInstance(fastatmp)
		);

		libmaus2::aio::InputStreamInstance ISI(reffn);

		libmaus2::fastx::FastABPGenerator::fastAToFastaBP(ISI,*ptr,&std::cerr);

		ptr->flush();
		ptr.reset();

		libmaus2::aio::OutputStreamFactoryContainer::rename(
			fastatmp,bpfn
		);
	}

	libmaus2::gtf::GTFData::unique_ptr_type pGTF(libmaus2::gtf::GTFData::obtain(annofn,verbose));
	libmaus2::gtf::GTFData const & gtfdata = *pGTF;

	libmaus2::bambam::BamHeader::shared_ptr_type sheader(
		libmaus2::bambam::BamMultiAlignmentDecoderFactory::constructHeader(arg));

	libmaus2::gtf::BAMGTFMap const bamgtfmap(*sheader,gtfdata);
	libmaus2::gtf::RangeSets const RS(gtfdata,bamgtfmap,*sheader);

	std::map<uint64_t,double> M_F_COVERED;
	std::map<uint64_t,uint64_t> M_ANY_COVERED;
	std::map<uint64_t,uint64_t> M_CNT;
	std::map<uint64_t,std::string> M_out;
	std::map<uint64_t,std::string> M_err;
	libmaus2::parallel::StdMutex M_lock;

	typedef std::pair<uint64_t,uint64_t> up;
	std::vector<up> VUP;

	std::regex R(
		seq.size()
		?
		seq
		:
		std::string(".*")
	);

	for ( uint64_t zz = 0; zz < sheader->getNumRef(); ++zz )
	{
		std::string const name = sheader->getRefIDName(zz);

		if ( std::regex_match(name,R) )
		{
			uint64_t const size = sheader->getRefIDLength(zz);
			VUP.push_back(std::make_pair(size,zz));
		}
	}
	std::sort(VUP.begin(),VUP.end(),std::greater<up>());

	std::atomic<uint64_t> gfailed(0);

	#if defined(_OPENMP)
	#pragma omp parallel for num_threads(numthreads) schedule(dynamic,1)
	#endif
	for ( uint64_t zzz = 0; zzz < VUP.size(); ++zzz )
	{
		try
		{
			uint64_t const zz = VUP[zzz].second;

			{
			libmaus2::parallel::StdMutex::scope_lock_type M_s_lock(M_lock);
			M_F_COVERED[zz] = 0;
			M_ANY_COVERED[zz] = 0;
			M_CNT[zz] = 0;
			}

			libmaus2::util::ArgParser argcopy = arg;
			argcopy.eraseArg("range");
			argcopy.replaceArg("ranges",sheader->getRefIDName(zz));

			{
				libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
				std::cerr << "[V] processing " << zz << "(" << sheader->getRefIDName(zz) << ")" << " of length " << sheader->getRefIDLength(zz) << std::endl;
			}

			libmaus2::bambam::BamAlignmentDecoderWrapper::unique_ptr_type decwrapper(
				libmaus2::bambam::BamMultiAlignmentDecoderFactory::construct(argcopy));
			::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();

			std::ostringstream tmpfnstr;
			tmpfnstr << tmpfilenamebase << "_difile_" << zz;

			std::string const tmpfn = tmpfnstr.str();
			libmaus2::util::TempFileRemovalContainer::addTempFile(tmpfn);

			std::ostringstream outstr;
			std::ostringstream errstr;
			ExonHandler H(tmpfn,numsamples,header,bamgtfmap,RS,gtfdata,cds,outstr,errstr,bpfn);

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

			uint64_t prevstart = 0;
			uint64_t depth = 0;
			int64_t prevrefid = zz;

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

			uint64_t c = 0;

			while ( dec.readAlignment() )
			{
				if ( algn.isMapped() )
				{
					libmaus2::math::IntegerInterval<int64_t> const I = algn.getReferenceInterval();

					std::vector<libmaus2::math::IntegerInterval<int64_t> > VIC;

					int64_t const gtfid = bamgtfmap.Vmap[algn.getRefID()];

					if ( gtfid >= 0 )
					{
						libmaus2::geometry::RangeSet<libmaus2::gtf::Exon> const & LRS = *(RS.VRS.at(gtfid));
						uint64_t const o = LRS.search(libmaus2::gtf::Exon(I.from,I.from + I.diameter()),H.query_R,H.query_todo);

						for ( uint64_t i = 0; i < o; ++i )
						{
							libmaus2::gtf::Exon const & E = *(H.query_R[i]);
							libmaus2::math::IntegerInterval<int64_t> const IE = E.getInterval();
							libmaus2::math::IntegerInterval<int64_t> const IC = IE.intersection(I);

							if ( !IC.isEmpty() )
								VIC.push_back(IC);
						}
					}

					VIC = libmaus2::math::IntegerInterval<int64_t>::mergeTouchingOrOverlapping(VIC);

					uint64_t b_covered = 0;
					for ( uint64_t i = 0; i < VIC.size(); ++i )
						b_covered += VIC[i].diameter();

					double const f_covered = I.diameter() ? static_cast<double>(b_covered) / I.diameter() : 1.0;
					uint64_t const any_covered = VIC.size() ? 1 : 0;

					{
					libmaus2::parallel::StdMutex::scope_lock_type M_s_lock(M_lock);
					M_F_COVERED[algn.getRefID()] += f_covered;
					M_ANY_COVERED[algn.getRefID()] += any_covered;
					M_CNT[algn.getRefID()] += 1;
					}

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

					// 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();
				}

				c += 1;
				if ( verbose && (c % (128*1024)) == 0 )
				{
					libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
					std::cerr << "[V] " << c << "\t" << algn.getRefID() << "(" << sheader->getRefIDName(algn.getRefID()) << ")" << ":" << algn.getPos() << "/" << sheader->getRefIDLength(algn.getRefID()) << "\t" << libmaus2::util::MemUsage() << std::endl;
				}
			}

			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();

			H.flush();

			{
			libmaus2::parallel::StdMutex::scope_lock_type M_s_lock(M_lock);
			M_out[zz] = outstr.str();
			M_err[zz] = errstr.str();
			}

			libmaus2::aio::FileRemoval::removeFile(tmpfn);

			{
				libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
				std::cerr << "[V] processed " << zz << "(" << sheader->getRefIDName(zz) << ")" << " of length " << sheader->getRefIDLength(zz) << std::endl;
			}
		}
		catch(std::exception const & ex)
		{
			libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
			std::cerr << ex.what() << std::endl;
			gfailed = 1;
		}
	}

	if ( gfailed )
	{
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "[E] parallel loop failed" << std::endl;
		lme.finish();
		throw lme;
	}

	double gf = 0;
	uint64_t gany = 0;
	uint64_t gcnt = 0;

	for ( std::map<uint64_t,uint64_t>::const_iterator it = M_CNT.begin(); it != M_CNT.end(); ++it )
	{
		uint64_t const refid = it->first;
		uint64_t const cnt = it->second;

		std::map<uint64_t,double>::const_iterator f_it = M_F_COVERED.find(refid);
		std::map<uint64_t,uint64_t>::const_iterator any_it = M_ANY_COVERED.find(refid);
		std::map<uint64_t,std::string>::const_iterator out_it = M_out.find(refid);
		std::map<uint64_t,std::string>::const_iterator err_it = M_err.find(refid);

		assert ( f_it != M_F_COVERED.end() );
		assert ( any_it != M_ANY_COVERED.end() );
		assert ( out_it != M_out.end() );
		assert ( err_it != M_err.end() );

		double const f = f_it->second;
		uint64_t const any = any_it->second;

		std::cout << out_it->second;
		std::cerr << err_it->second;
		std::cerr << "[ON_TARGET]\t" << refid << "\t" << sheader->getRefIDName(refid) << "\t"
			<< any << "/" << cnt << "\t" << (cnt ? static_cast<double>(any)/cnt : 1) << "\t"
			<< f << "/" << cnt << "\t" << (cnt ? static_cast<double>(f)/cnt : 1) << "\n";

		gf += f;
		gany += any;
		gcnt += cnt;
	}

	std::cerr << "[SUM_ON_TARGET]\t"
		<< gany << "/" << gcnt << "\t" << (gcnt ? static_cast<double>(gany)/gcnt : 1) << "\t"
		<< gf << "/" << gcnt << "\t" << (gcnt ? static_cast<double>(gf)/gcnt : 1) << "\n";

	uint64_t GPsum = 0;
	for ( std::map<uint64_t,uint64_t>::const_iterator it = GP.begin(); it != GP.end(); ++it )
		GPsum += it->second;

	uint64_t lGPsum = 0;
	for ( std::map<uint64_t,uint64_t>::const_iterator it = GP.begin(); it != GP.end(); ++it )
	{
		double const prefrac = lGPsum / static_cast<double>(GPsum);
		lGPsum += it->second;
		double const frac = lGPsum / static_cast<double>(GPsum);

		if ( prefrac <= Pfrac )
			std::cerr << "[SUM_P]\t" << it->first << "\t" << it->second << "/" << GPsum << "\t" << static_cast<double>(it->second)/GPsum << "\t" << frac << std::endl;
	}

	uint64_t gkgsum = 0;
	for ( std::map<uint64_t,uint64_t>::const_iterator it = GKGC.begin(); it != GKGC.end(); ++it )
		gkgsum += it->second;

	double const dgkgsum = gkgsum;
	for ( std::map<uint64_t,uint64_t>::const_iterator it = GKGC.begin(); it != GKGC.end(); ++it )
		std::cerr << "[SUM_KGC]\t" << it->first << "\t" << it->second << "\t" << (it->second/dgkgsum) << std::endl;

	for ( std::map<uint64_t,uint64_t>::const_iterator it = GKGC.begin(); it != GKGC.end(); ++it )
	{
		std::map<uint64_t,uint64_t>::const_iterator sit = GKS.find(it->first);
		assert ( sit != GKS.end() );

		double const mean = static_cast<double>(sit->second) / it->second;

		std::cerr << "[SUM_KGCMEAN]\t" << it->first << "\t" << mean << std::endl;
	}

	uint64_t print_o = 0;
	uint64_t ld = GPsum;
	for ( std::map<uint64_t,uint64_t>::const_iterator it = GP.begin(); it != GP.end(); ++it )
	{
		while ( print_o < print_n && it->first >= print[print_o] )
		{
			std::cerr << "[SUM_H]\t" << ">=" << print[print_o] << "\t" << ld << "/" << GPsum << "\t" << static_cast<double>(ld)/GPsum << std::endl;
			print_o += 1;
		}

		ld -= it->second;
	}

	while ( print_o < print_n )
	{
		std::cerr << "[SUM_H]\t" << ">=" << print[print_o] << "\t" << ld << "/" << GPsum << "\t" << static_cast<double>(ld)/GPsum << std::endl;
		print_o += 1;
	}


	{
		std::map<std::pair<uint64_t,uint64_t>,uint64_t>::const_iterator it = GPGC.begin();

		while ( it != GPGC.end() )
		{
			uint64_t const ugc = it->first.first;

			std::map<std::pair<uint64_t,uint64_t>,uint64_t>::const_iterator low_it = it;
			std::map<std::pair<uint64_t,uint64_t>,uint64_t>::const_iterator high_it = it;

			uint64_t l = 0;
			while ( high_it != GPGC.end() && high_it->first.first == ugc )
			{
				l += high_it->second;
				high_it++;
			}

			uint64_t print_o = 0;
			uint64_t ld = l;
			for (
				std::map<std::pair<uint64_t,uint64_t>,uint64_t>::const_iterator cur_it = low_it;
				cur_it != high_it ; ++cur_it )
			{
				uint64_t const depth = cur_it->first.second;
				uint64_t const cnt = cur_it->second;

				std::cerr << "[SUM_PGC]\t" << ugc << "\t" << depth << "\t" << cnt << "/" << l << "\t" << static_cast<double>(cnt)/l << std::endl;

				while ( print_o < print_n && print[print_o] <= depth )
				{
					std::cerr << "[SUM_HGC]\t" << ugc << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << std::endl;
					print_o += 1;
				}

				ld -= cnt;
			}

			while ( print_o < print_n )
			{
				std::cerr << "[SUM_HGC]\t" << ugc << "\t>=" << print[print_o] << "\t" << ld << "/" << l << "\t" << static_cast<double>(ld)/l << std::endl;
				print_o += 1;
			}

			it = high_it;
		}
	}
}

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));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("","cds",false));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("","numsamples",true));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("t","threads",true));
		Vformat.push_back(libmaus2::util::ArgParser::ArgumentDefinition("s","sequence",true));

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

		if ( arg.size() < 2 )
		{
			std::cerr << "[E] usage: " << arg.progname << " <anno.gtf> <ref.fa>" << std::endl;
			return EXIT_FAILURE;
		}

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