/*
    libmaus2
    Copyright (C) 2019 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/>.
*/
#if ! defined(LIBMAUS2_GTF_INTERVALLIST_HPP)
#define LIBMAUS2_GTF_INTERVALLIST_HPP

#include <libmaus2/gtf/Exon.hpp>
#include <libmaus2/gtf/ExonInterval.hpp>
#include <libmaus2/util/FiniteSizeHeap.hpp>

namespace libmaus2
{
	namespace gtf
	{
		struct IntervalList
		{
			typedef IntervalList this_type;
			typedef std::unique_ptr<this_type> unique_ptr_type;

			struct IntervalWithId
			{
				std::pair<uint64_t,uint64_t> Q;
				uint64_t start;
				uint64_t end;

				IntervalWithId(
					std::pair<uint64_t,uint64_t> const rQ,
					uint64_t const rstart,
					uint64_t const rend
				) : Q(rQ), start(rstart), end(rend) {}

				IntervalWithId() : Q(), start(0), end(0) {}
				IntervalWithId(uint64_t const Qf) : Q(Qf,0), start(0), end(0) {}

				std::string toString() const
				{
					std::ostringstream ostr;
					ostr << "IntervalWithId([" << Q.first << "," << Q.second << ")," << start << "," << end << ")";
					return ostr.str();
				}
			};


			struct IntervalWithIdPosComparator
			{
				bool operator()(IntervalWithId const & A, IntervalWithId const & B) const
				{
					return A.Q.first < B.Q.first;
				}
			};

			uint64_t getId(libmaus2::gtf::ExonInterval const & E) const
			{
				IntervalWithId const * it = std::lower_bound(
					V.begin(),
					V.begin()+V_o,
					IntervalWithId(E.from),
					IntervalWithIdPosComparator()
				);

				if ( it != V.begin() + V_o && it->Q.first == E.from )
					return it - V.begin();
				else
				{
					assert ( it != V.begin() );
					it -= 1;
					assert ( E.from > it->Q.first );
					assert ( E.from < it->Q.second );

					return it-V.begin();
				}
			}

			IntervalWithId const & get(uint64_t const id) const
			{
				return V.at(id);
			}

			libmaus2::autoarray::AutoArray<uint64_t> Aid;
			uint64_t Aid_o;

			libmaus2::autoarray::AutoArray<IntervalWithId> V;
			uint64_t V_o;

			// intervals unique to a single exon
			libmaus2::autoarray::AutoArray<libmaus2::gtf::ExonInterval> AU;
			uint64_t AU_o;

			libmaus2::autoarray::AutoArray< std::atomic<uint64_t> >::unique_ptr_type AC;

			libmaus2::util::FiniteSizeHeap< std::pair<uint64_t,uint64_t> > FSHcopy;

			IntervalList()
			: Aid_o(0), V_o(0), AU_o(0), FSHcopy(0)
			{

			}

			std::ostream & printUnique(std::ostream & out) const
			{
				for ( uint64_t i = 0; i < AU_o; ++i )
					out << AU[i] << "\n";
				return out;
			}

			static bool intersect(std::pair<uint64_t,uint64_t> const & P, std::pair<uint64_t,uint64_t> const & Q)
			{
				if ( Q.first < P.first )
					return intersect(Q,P);

				assert ( P.first <= Q.first );

				return P.second > Q.first;
			}

			struct QueryContext
			{
				libmaus2::autoarray::AutoArray<uint64_t> A;
				libmaus2::autoarray::AutoArray<libmaus2::gtf::Exon> E;
				libmaus2::autoarray::AutoArray<libmaus2::gtf::ExonInterval> EI;
			};

			std::pair<uint64_t,uint64_t> query(std::pair<uint64_t,uint64_t> const & Q, QueryContext & context, libmaus2::gtf::Exon const * E, uint64_t const chr) const
			{
				if ( ! V_o )
					return std::pair<uint64_t,uint64_t>(0,0);

				// search for intervals
				IntervalWithId const * lp =
					std::lower_bound(
						V.begin(),
						V.begin()+V_o,
						IntervalWithId(Q.first),
						IntervalWithIdPosComparator()
					);

				// adjust
				while ( lp != V.begin() && intersect(lp[-1].Q,Q) )
					--lp;

				assert (
					lp == V.begin() || !intersect(lp[-1].Q,Q)
				);

				uint64_t o = 0;
				uint64_t EI_o = 0;

				libmaus2::math::IntegerInterval<int64_t> const IQ(Q.first,static_cast<int64_t>(Q.second)-1);

				// while interval overlap
				for ( ; (lp < V.begin() + V_o) && intersect(lp->Q,Q); ++lp )
				{
					// interval covered by any exon
					if ( lp->end != lp->start )
					{
						// how many exons cover this interval?
						// uint64_t const freq = lp->end - lp->start;

						for ( uint64_t i = lp->start; i < lp->end; ++i )
							context.A.push(o,Aid[i]);

						libmaus2::math::IntegerInterval<int64_t> const IL(lp->Q.first,static_cast<int64_t>(lp->Q.second)-1);
						libmaus2::math::IntegerInterval<int64_t> const IC = IQ.intersection(IL);
						assert ( ! IC.isEmpty() );
						libmaus2::gtf::ExonInterval const UI(chr,IC.from,IC.from + IC.diameter());
						context.EI.push(EI_o,UI);

						// std::cerr << "found " << lp->Q.first << "," << lp->Q.second << " for " << Q.first << "," << Q.second << std::endl;
					}
				}

				assert ( lp == V.begin() + V_o || lp->Q.first >= Q.second );

				std::sort(context.A.begin(),context.A.begin()+o);
				o = std::unique(context.A.begin(),context.A.begin()+o) - context.A.begin();

				uint64_t oe = 0;
				for ( uint64_t i = 0; i < o; ++i )
				{
					libmaus2::gtf::Exon const & exon = E[context.A[i]];
					context.E.push(oe,exon);
					// std::cerr << "found " << exon.getFrom() << "," << exon.getTo() << " for " << Q.first << "," << Q.second << std::endl;
				}

				return std::pair<uint64_t,uint64_t>(oe,EI_o);
			}

			/**
			 * validate that every IntervalWithId stored for Q contains id
			 **/
			void validate(std::pair<uint64_t,uint64_t> const & Q, uint64_t const id) const
			{
				IntervalWithId const * lp =
					std::lower_bound(
						V.begin(),
						V.begin()+V_o,
						IntervalWithId(Q.first),
						IntervalWithIdPosComparator()
					);

				IntervalWithId const * hp = lp;
				while ( hp != V.begin() + V_o && hp->Q.second < Q.second )
					++hp;

				assert ( lp != V.begin() + V_o );
				assert ( hp != V.begin() + V_o );
				assert ( lp->Q.first == Q.first );
				assert ( hp->Q.second == Q.second );

				hp += 1;

				for ( IntervalWithId const * ip = lp; ip < hp; ++ip )
				{
					bool found = false;
					for ( uint64_t i = ip->start; (!found) && (i < ip->end); ++i )
						if ( Aid[i] == id )
							found = true;

					if ( ! found )
					{
						std::cerr << "validate " << Q.first << "," << Q.second << " id=" << id << std::endl;

						std::cerr << "lp->Q.first=" << lp->Q.first << std::endl;
						std::cerr << "hp[-1].Q.second=" << hp[-1].Q.second << std::endl;

						std::cerr << "ip->Q=[" << ip->Q.first << "," << ip->Q.second << ")" << std::endl;
						for ( uint64_t i = ip->start; (i < ip->end); ++i )
							std::cerr << "\tid\t" << Aid[i] << std::endl;
					}

					assert ( found );
				}
			}

			/**
			 * push an interval Q with ids in FSH
			 **/
			void push(std::pair<uint64_t,uint64_t> Q, libmaus2::util::FiniteSizeHeap< std::pair<uint64_t,uint64_t> > const & FSH, uint64_t const chr)
			{
				assert ( Q.second > Q.first );

				IntervalWithId I;
				I.Q = Q;
				I.start = Aid_o;

				FSHcopy.copyFrom(FSH);
				while ( !FSHcopy.empty() )
					Aid.push(Aid_o,FSHcopy.pop().second);

				I.end = Aid_o;

				// if interval is only covered by a single exon
				if ( I.end - I.start == 1 )
					AU.push(AU_o,libmaus2::gtf::ExonInterval(chr,Q.first,Q.second));

				assert ( V_o == 0 || I.Q.first >= V[V_o-1].Q.second );

				V.push(V_o,I);
			}

			/**
			 * construct by sorted list of exons
			 *
			 * @param LAP64 exons sorted by coordinate
			 * @param LOP64 length of array LAP64
			 * @param chr chromosome id
			 **/
			static unique_ptr_type construct(
				libmaus2::gtf::Exon const * LAP64,
				uint64_t const LOP64,
				uint64_t const chr
			)
			{
				libmaus2::util::FiniteSizeHeap< std::pair<uint64_t,uint64_t> > FSH(0);
				uint64_t prevstart = 0;

				IntervalList::unique_ptr_type pIL(new IntervalList);

				for ( uint64_t j = 0; j < LOP64; ++j )
				{
					libmaus2::gtf::Exon const & exon = LAP64[j];
					std::pair<uint64_t,uint64_t> const P = exon.getPair();

					while ( !FSH.empty() && FSH.top().first <= P.first )
					{
						// push interval and active exon ids
						if ( FSH.top().first > prevstart )
						{
							std::pair<uint64_t,uint64_t> Q(prevstart,FSH.top().first);
							pIL->push(Q,FSH,chr);
						}

						prevstart = FSH.top().first;
						FSH.pop();
					}

					// push interval and active exon ids
					if ( P.first > prevstart )
					{
						std::pair<uint64_t,uint64_t> Q(prevstart,P.first);
						pIL->push(Q,FSH,chr);
					}

					prevstart = P.first;
					FSH.pushBump(std::pair<uint64_t,uint64_t>(P.second,j));
				}

				while ( !FSH.empty() )
				{
					// push interval and active exon ids
					if ( FSH.top().first > prevstart )
					{
						std::pair<uint64_t,uint64_t> Q(prevstart,FSH.top().first);
						pIL->push(Q,FSH,chr);
					}

					prevstart = FSH.top().first;
					FSH.pop();
				}

				for ( uint64_t j = 0; j < LOP64; ++j )
					pIL->validate(LAP64[j].getPair(),j);

				libmaus2::autoarray::AutoArray< std::atomic<uint64_t> >::unique_ptr_type TAC(
					new libmaus2::autoarray::AutoArray< std::atomic<uint64_t> >(pIL->V_o)
				);
				pIL->AC = std::move(TAC);

				return pIL;
			}
		};
	}
}
#endif
