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

#include <libmaus2/dazzler/align/AlignmentFileCat.hpp>
#include <libmaus2/geometry/RangeSet.hpp>
#include <libmaus2/dazzler/align/DalignerIndexDecoder.hpp>
#include <libmaus2/dazzler/align/LasFileRange.hpp>
#include <libmaus2/dazzler/align/SimpleOverlapParser.hpp>
#include <libmaus2/util/OutputFileNameTools.hpp>
#include <libmaus2/digest/md5.hpp>

namespace libmaus2
{
	namespace dazzler
	{
		namespace align
		{
			struct LasIntervals
			{
				typedef LasIntervals this_type;
				typedef std::unique_ptr<this_type> unique_ptr_type;
				typedef std::shared_ptr<this_type> shared_ptr_type;

				struct LasIntervalsIndexEntry
				{
					uint64_t from;
					uint64_t to;
					uint64_t index;

					std::string toString() const
					{
						std::ostringstream ostr;
						ostr << "LasIntervalsIndexEntry(from=" << from << ",to=" << to << ",index=" << index << ")";
						return ostr.str();
					}

					LasIntervalsIndexEntry()
					{

					}

					LasIntervalsIndexEntry(uint64_t const rfrom, uint64_t const rto, uint64_t const rindex)
					: from(rfrom), to(rto), index(rindex)
					{

					}

					uint64_t getFrom() const
					{
						return from;
					}

					uint64_t getTo() const
					{
						return to;
					}

					std::ostream & serialise(std::ostream & out) const
					{
						libmaus2::util::NumberSerialisation::serialiseNumber(out,from);
						libmaus2::util::NumberSerialisation::serialiseNumber(out,to);
						libmaus2::util::NumberSerialisation::serialiseNumber(out,index);
						return out;
					}

					std::istream & deserialise(std::istream & in)
					{
						from = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
						to = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
						index = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
						return in;
					}
				};

				struct LasIntervalsEntry
				{
					std::string fn;
					uint64_t mini;
					uint64_t maxi;
					uint64_t i;
					int64_t tspace;

					std::string toString() const
					{
						std::ostringstream ostr;
						ostr
							<< "LasIntervalsEntry("
							<< "fn=" << fn << ","
							<< "mini=" << mini << ","
							<< "maxi=" << maxi << ","
							<< "i=" << i << ","
							<< "tspace=" << tspace << ")";
						return ostr.str();
					}

					LasIntervalsEntry()
					{

					}

					LasIntervalsEntry(
						std::string const & rfn,
						uint64_t const rmini,
						uint64_t const rmaxi,
						uint64_t const ri,
						int64_t const rtspace
					) : fn(rfn), mini(rmini), maxi(rmaxi), i(ri), tspace(rtspace)
					{

					}

					bool empty() const
					{
						return mini >= maxi;
					}

					std::ostream & serialise(std::ostream & out) const
					{
						libmaus2::util::StringSerialisation::serialiseString(out,fn);
						libmaus2::util::NumberSerialisation::serialiseNumber(out,mini);
						libmaus2::util::NumberSerialisation::serialiseNumber(out,maxi);
						libmaus2::util::NumberSerialisation::serialiseNumber(out,i);
						libmaus2::util::NumberSerialisation::serialiseNumber(out,tspace);
						return out;
					}

					std::istream & deserialise(std::istream & in)
					{
						fn = libmaus2::util::StringSerialisation::deserialiseString(in);
						mini = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
						maxi = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
						i = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
						tspace = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
						return in;
					}

					bool operator<(LasIntervalsEntry const & L) const
					{
						return mini < L.mini;
					}

					LasIntervalsIndexEntry getIndexEntry() const
					{
						return LasIntervalsIndexEntry(mini,maxi+1,i);
					}
				};


				struct FileAndInfo
				{
					typedef FileAndInfo this_type;
					typedef std::unique_ptr<this_type> unique_ptr_type;
					typedef std::shared_ptr<this_type> shared_ptr_type;

					int64_t const tspace;
					int64_t fileid;
					// las
					libmaus2::aio::InputStreamInstance::unique_ptr_type pISI;
					DalignerIndexDecoder::unique_ptr_type pindex;
					OverlapParser parser;
					uint64_t rest;
					libmaus2::autoarray::AutoArray<uint8_t> B;

					FileAndInfo(int64_t const rtspace, uint64_t const Bsize = 64*1024)
					: tspace(rtspace), fileid(-1), pISI(), parser(tspace), rest(0), B(Bsize,false)
					{
					}

					void reset()
					{
						pISI.reset();
						pindex.reset();
					}

					bool parseBlock(OverlapParser::split_type const splittype = OverlapParser::overlapparser_do_split)
					{
						uint64_t const toread = std::min(static_cast<uint64_t>(B.size()),rest);
						pISI->read(reinterpret_cast<char *>(B.begin()),toread);
						if ( pISI->gcount() != static_cast<int64_t>(toread) )
						{
							libmaus2::exception::LibMausException lme;
							lme.getStream() << "[E] FileAndInfo::parseBlock: failed to read from file" << std::endl;
							lme.finish();
							throw lme;
						}

						parser.parseBlock(B.begin(),B.begin()+toread,splittype);

						rest -= toread;

						return (toread != 0) || (!parser.isIdle());
					}

					void setup(
						uint64_t const aread,
						int64_t const nfileid,
						std::string const & fn
					)
					{
						rest = 0;
						parser.reset();

						assert ( rest == 0 );
						assert ( parser.isIdle() );

						if ( nfileid != fileid )
						{
							fileid = nfileid;

							{
								DalignerIndexDecoder::unique_ptr_type tindex(new DalignerIndexDecoder(fn));
								pindex = std::move(tindex);
							}

							{
								libmaus2::aio::InputStreamInstance::unique_ptr_type tISI(new libmaus2::aio::InputStreamInstance(fn));
								pISI = std::move(tISI);
							}
						}

						DalignerIndexDecoder & index = *pindex;
						uint64_t const from = index[aread+0];
						uint64_t const to = index[aread+1];
						rest = to - from;

						pISI->clear();
						pISI->seekg(from);
					}
				};

				struct FileAndInfoGet
				{
					typedef FileAndInfoGet this_type;
					typedef std::unique_ptr<this_type> unique_ptr_type;
					typedef std::shared_ptr<this_type> shared_ptr_type;

					FileAndInfo & FAI;
					uint64_t i;

					FileAndInfoGet(FileAndInfo & rFAI) : FAI(rFAI), i(0) {}

					bool getNext(std::pair<uint8_t const *, uint8_t const *> & P)
					{
						while ( true )
						{
							OverlapData & data = FAI.parser.getData();
							if ( i < data.size() )
							{
								P = data.getData(i++);
								return true;
							}
							else
							{
								bool const ok = FAI.parseBlock();

								if ( ok )
								{
									i = 0;
								}
								else
								{
									return false;
								}
							}
						}
					}

					bool peekNext(std::pair<uint8_t const *, uint8_t const *> & P)
					{
						while ( true )
						{
							OverlapData & data = FAI.parser.getData();
							if ( i < data.size() )
							{
								P = data.getData(i);
								return true;
							}
							else
							{
								bool const ok = FAI.parseBlock();

								if ( ok )
								{
									i = 0;
								}
								else
								{
									return false;
								}
							}
						}
					}
				};

				std::vector<std::string> getVin() const
				{
					std::vector < std::string > Vin;
					for ( uint64_t i = 0; i < V.size(); ++i )
						Vin.push_back(V[i].fn);
					return Vin;
				}

				std::vector < LasIntervalsEntry > V;
				uint64_t tspace;
				uint64_t nreads;
				std::vector < LasIntervalsIndexEntry > IV;
				libmaus2::geometry::RangeSet<LasIntervalsIndexEntry>::unique_ptr_type R;
				std::vector < std::string > Vin;

				std::string toString() const
				{
					std::ostringstream ostr;

					ostr << "LasIntervals(\n";
					for ( uint64_t i = 0; i < V.size(); ++i )
						ostr << "\tV[" << i << "]=" << V[i].toString() << "\n";
					ostr << "\ttspace=" << tspace << "\n";
					ostr << "\tnreads=" << nreads << "\n";
					for ( uint64_t i = 0; i < IV.size(); ++i )
						ostr << "\tIV[" << i << "]=" << IV[i].toString() << "\n";
					for ( uint64_t i = 0; i < Vin.size(); ++i )
						ostr << "\tVin[" << i << "]=" << Vin[i] << "\n";
					ostr << ")\n";

					return ostr.str();
				}

				static std::string getName(std::vector < std::string > const & Vin)
				{
					if ( !Vin.size() )
					{
						libmaus2::exception::LibMausException lme;
						lme.getStream() << "[E] LasIntervals::getName(): file name vector is empty" << std::endl;
						lme.finish();
						throw lme;
					}
					else
					{
						std::string dalindex = DalignerIndexDecoder::getDalignerIndexName(Vin[0]);
						std::string shortname = libmaus2::util::OutputFileNameTools::clipOff(dalindex,".idx");

						std::ostringstream digestinstr;
						for ( uint64_t i = 0; i < Vin.size(); ++i )
							digestinstr << Vin[i] << "\n";
						std::string const digestin = digestinstr.str();

						std::string digest;
						bool const mdok = libmaus2::util::MD5::md5(digestin,digest);
						assert ( mdok );

						std::string const longname = shortname + "_" + digest + ".lasintv";

						return longname;
					}
				}

				std::string getName() const
				{
					return getName(Vin);
				}

				void serialise() const
				{
					std::string const fn = getName();
					libmaus2::aio::OutputStreamInstance OSI(fn);
					serialise(OSI);
					OSI.flush();
				}

				std::ostream & serialise(std::ostream & out) const
				{
					libmaus2::util::NumberSerialisation::serialiseNumber(out,tspace);
					libmaus2::util::NumberSerialisation::serialiseNumber(out,nreads);
					libmaus2::util::NumberSerialisation::serialiseNumber(out,IV.size());
					for ( uint64_t i = 0; i < IV.size(); ++i )
						IV[i].serialise(out);
					libmaus2::util::NumberSerialisation::serialiseNumber(out,V.size());
					for ( uint64_t i = 0; i < V.size(); ++i )
						V[i].serialise(out);
					return out;
				}

				std::istream & deserialise(std::istream & in)
				{
					tspace = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
					nreads = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
					IV.resize(libmaus2::util::NumberSerialisation::deserialiseNumber(in));
					for ( uint64_t i = 0; i < IV.size(); ++i )
						IV[i].deserialise(in);
					V.resize(libmaus2::util::NumberSerialisation::deserialiseNumber(in));
					for ( uint64_t i = 0; i < V.size(); ++i )
						V[i].deserialise(in);

					libmaus2::geometry::RangeSet<LasIntervalsIndexEntry>::unique_ptr_type tR(
						new libmaus2::geometry::RangeSet<LasIntervalsIndexEntry>(nreads)
					);
					for ( uint64_t i = 0; i < IV.size(); ++i )
						tR->insert(IV[i]);
					R = std::move(tR);

					Vin = getVin();

					return in;
				}

				std::vector<uint64_t> getIds(uint64_t const from, uint64_t const to) const
				{
					std::vector<LasIntervalsIndexEntry const *> V = R->search(
						LasIntervalsIndexEntry(from,to,0)
					);

					std::vector<uint64_t> R;
					for ( uint64_t i = 0; i < V.size(); ++i )
						R.push_back(V[i]->index);

					std::sort(R.begin(),R.end());

					return R;
				}

				bool openSingle(uint64_t const aread, int64_t const tspace, FileAndInfo::shared_ptr_type & ptr) const
				{
					std::vector<uint64_t> VID = getIds(aread,aread+1);

					if ( ! ptr )
					{
						FileAndInfo::shared_ptr_type tptr(new FileAndInfo(tspace));
						ptr = tptr;
					}

					if ( VID.size() )
					{
						assert ( VID.size() == 1 );

						uint64_t const fileid = VID[0];
						std::string const fn = V[fileid].fn; // Vin[fileid];

						ptr->setup(aread,fileid,fn);
						return true;
					}
					else
					{
						return false;
					}
				}

				bool openSingle(uint64_t const aread, FileAndInfo::shared_ptr_type & ptr) const
				{
					return openSingle(aread,tspace,ptr);
				}

				std::vector < LasFileRange > getFileRanges(uint64_t const from, uint64_t const to) const
				{
					std::vector<uint64_t> const R = getIds(from,to);
					std::vector < LasFileRange > VLFR;

					for ( uint64_t i = 0; i < R.size(); ++i )
					{
						uint64_t const id = R[i];
						libmaus2::dazzler::align::DalignerIndexDecoder index(V[id].fn /* Vin[id] */);

						uint64_t const startoffset = index[from];
						uint64_t const endoffset = index[to];

						if ( endoffset > startoffset )
							VLFR.push_back(LasFileRange(id,startoffset,endoffset));
					}

					#if 0
					{
						libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
						std::cerr << "[V] getFileRange [" << from << "," << to << ")" << " got " << VLFR.size() << std::endl;
						for ( uint64_t i = 0; i < VLFR.size(); ++i )
							std::cerr << "\tVLFR[" << i << "]=" << VLFR[i].toString() << std::endl;
						for ( uint64_t i = 0; i < R.size(); ++i )
						{
							libmaus2::dazzler::align::DalignerIndexDecoder index(V[R[i]].fn /* Vin[id] */);
							std::cerr << "\tR[" << i << "]=" << R[i] << " -> " << V[R[i]].fn << " index.minaread=" << index.minaread << "," << index.minaread+index.n << std::endl;
						}
					}
					#endif

					return VLFR;
				}

				SimpleOverlapParserConcat::unique_ptr_type getFileRangeParser(
					uint64_t const from, uint64_t const to,
					uint64_t const blocksize,
					OverlapParser::split_type const rsplittype = OverlapParser::overlapparser_do_split
				) const
				{
					std::vector < LasFileRange > const V = getFileRanges(from,to);

					SimpleOverlapParserConcat::unique_ptr_type Pparser(
						new SimpleOverlapParserConcat(
							tspace,
							V,
							Vin,
							blocksize,
							rsplittype
						)
					);

					return Pparser;
				}

				libmaus2::dazzler::align::AlignmentFileCat::unique_ptr_type openRange(uint64_t const from, uint64_t const to) const
				{
					std::vector<uint64_t> const R = getIds(from,to);

					libmaus2::dazzler::align::AlignmentFileCat::unique_ptr_type ptr(
						new libmaus2::dazzler::align::AlignmentFileCat(
							Vin,R,from,to
						)
					);

					return ptr;
				}

				int64_t size() const
				{
					if ( IV.size() )
						return IV.back().to;
					else
						return std::numeric_limits<int64_t>::min();
				}

				std::pair<int64_t,int64_t> getInterval() const
				{
					if ( IV.size() )
						return std::pair<int64_t,int64_t>(IV.front().from,IV.back().to);
					else
						return std::pair<int64_t,int64_t>(-1,-1);
				}

				static LasIntervalsEntry getEntry(std::string const & fn, uint64_t const i)
				{
					libmaus2::dazzler::align::OverlapIndexer::constructIndexIf(fn);

					int64_t const mini = libmaus2::dazzler::align::OverlapIndexer::getMinimumARead(fn);
					int64_t const maxi = libmaus2::dazzler::align::OverlapIndexer::getMaximumARead(fn);
					int64_t const tspace = libmaus2::dazzler::align::AlignmentFile::getTSpace(fn);

					if ( mini < 0 )
						return LasIntervalsEntry(fn,0,0,i,tspace);
					else
						return LasIntervalsEntry(fn,mini,maxi,i,tspace);
				}

				static std::vector<LasIntervalsEntry> getEntries(std::vector<std::string> const & Vin)
				{
					std::vector<LasIntervalsEntry> V;
					for ( uint64_t i = 0; i < Vin.size(); ++i )
						V.push_back(getEntry(Vin[i],i));
					return V;
				}

				void checkOrder()
				{
					std::vector<LasIntervalsEntry> VNE;
					for ( uint64_t i = 0; i < V.size(); ++i )
						if ( !V[i].empty() )
							VNE.push_back(V[i]);

					for ( uint64_t i = 1; i < VNE.size(); ++i )
						if ( ! (VNE[i-1].maxi < VNE[i].mini) )
						{
							libmaus2::exception::LibMausException lme;
							lme.getStream() << "[E] LasIntervals order defect" << std::endl;
							lme.getStream() << "[" << VNE[i-1].mini << "," << VNE[i-1].maxi << "] " << VNE[i-1].fn << std::endl;
							lme.getStream() << "[" << VNE[i-0].mini << "," << VNE[i-0].maxi << "] " << VNE[i-0].fn << std::endl;
							lme.finish();
							throw lme;
						}
				}

				void init()
				{
					checkOrder();

					if ( V.size() )
					{
						tspace = V[0].tspace;
						for ( uint64_t i = 1; i < V.size(); ++i )
							if ( static_cast<int64_t>(V[i].tspace) != static_cast<int64_t>(tspace) )
							{
								libmaus2::exception::LibMausException lme;
								lme.getStream() << "[E] LasIntervals: inconsisten tspace" << std::endl;
								lme.finish();
								throw lme;
							}
					}

					for ( uint64_t i = 0; i < V.size(); ++i )
						if ( ! V[i].empty() )
							IV.push_back(V[i].getIndexEntry());

					for ( uint64_t i = 1; i < IV.size(); ++i )
						IV[i-1].to = IV[i].from;

					for ( uint64_t i = 0; i < IV.size(); ++i )
						R->insert(IV[i]);
				}

				LasIntervals(std::vector<std::string> const & Vin, uint64_t const rnreads, std::ostream & /* errOSI */)
				: V(getEntries(Vin)), tspace(0),
				  nreads(rnreads), R(new libmaus2::geometry::RangeSet<LasIntervalsIndexEntry>(nreads)),
				  Vin(getVin())
				{
					init();
				}

				LasIntervals(std::vector < LasIntervalsEntry > const & rV, uint64_t const rnreads, std::ostream & /* errOSI */)
				: V(rV), tspace(0),
				  nreads(rnreads), R(new libmaus2::geometry::RangeSet<LasIntervalsIndexEntry>(nreads)),
				  Vin(getVin())
				{
					init();
				}

				static void createIndex(std::vector<std::string> const & rVin, uint64_t const rnreads, std::ostream & errOSI)
				{
					this_type obj(rVin,rnreads,errOSI);
					obj.serialise();
				}

				static bool createIndexIf(std::vector<std::string> const & rVin, uint64_t const rnreads, std::ostream & errOSI)
				{
					std::string const name = getName(rVin);

					if ( ! libmaus2::util::GetFileSize::fileExists(name) )
					{
						createIndex(rVin,rnreads,errOSI);
						return true;
					}

					for ( uint64_t i = 0; i < rVin.size(); ++i )
					{
						std::string const lasfn = rVin[i];

						if ( libmaus2::util::GetFileSize::isOlder(name,lasfn) )
						{
							createIndex(rVin,rnreads,errOSI);
							return true;
						}
					}

					return false;
				}

				LasIntervals(std::istream & in)
				: V(), nreads(0), IV(), R(), Vin()
				{
					deserialise(in);
				}

				static LasIntervals::unique_ptr_type load(std::string const & fn)
				{
					libmaus2::aio::InputStreamInstance ISI(fn);
					LasIntervals::unique_ptr_type tptr(new LasIntervals(ISI));
					return tptr;
				}

				static LasIntervals::unique_ptr_type load(std::vector<std::string> const & Vin)
				{
					LasIntervals::unique_ptr_type tptr(load(getName(Vin)));
					return tptr;
				}
			};
		}
	}
}
#endif
