/*
    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_BAMBAM_BAMRAWDECODER_HPP)
#define LIBMAUS2_BAMBAM_BAMRAWDECODER_HPP

#include <libmaus2/bambam/BamHeader.hpp>
#include <libmaus2/bambam/DecoderBase.hpp>
#include <libmaus2/lz/BgzfInflate.hpp>
#include <libmaus2/util/GetObject.hpp>
#include <sstream>

namespace libmaus2
{
	namespace bambam
	{
		struct BamRawDecoderBase
		{
			struct RawInterval
			{
				std::pair<uint64_t,uint64_t> start;
				std::pair<uint64_t,uint64_t> end;

				RawInterval() : start(), end() {}
				RawInterval(
					std::pair<uint64_t,uint64_t> const & rstart,
					std::pair<uint64_t,uint64_t> const & rend
				) : start(rstart), end(rend)
				{

				}

				bool operator==(RawInterval const & RHS) const
				{
					return
						start == RHS.start
						&&
						end == RHS.end;
				}

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

		template<bool _throw_on_eof>
		struct BamRawDecoderTemplate : public BamRawDecoderBase
		{
			static bool const throw_on_eof = _throw_on_eof;
			typedef BamRawDecoderTemplate<throw_on_eof> this_type;
			typedef std::unique_ptr<this_type> unique_ptr_type;
			typedef std::shared_ptr<this_type> shared_ptr_type;

			private:
			libmaus2::aio::InputStreamInstance::unique_ptr_type Pistr;
			std::istream & istr;
			libmaus2::lz::BgzfInflate<std::istream> infl;
			libmaus2::lz::BgzfInflateInfo previnfo;
			bool eof;
			uint64_t blockskip;

			::libmaus2::bambam::BamHeaderParserState parsestate;
			bool needheader;
			bool haveheader;

			std::pair<uint64_t,uint64_t> P;

			libmaus2::autoarray::AutoArray<unsigned char> U;
			unsigned char const * ua;
			unsigned char const * uc;
			unsigned char const * ue;

			::libmaus2::bambam::BamHeader header;

			libmaus2::autoarray::AutoArray<unsigned char> Astall;
			uint64_t Astallused;
			std::pair<uint64_t,uint64_t> AstallP;

			void init(uint64_t const sp)
			{
				haveheader = false;

				if ( sp )
				{
					istr.clear();
					istr.seekg(sp);
					istr.clear();
				}
			}

			public:
			BamRawDecoderTemplate(
				std::istream & ristr,
				bool const rneedheader = true,
				std::pair<uint64_t,uint64_t> const offset = std::pair<uint64_t,uint64_t>(0,0)
			)
			: Pistr(), istr(ristr), infl(istr), eof(false), blockskip(offset.second), needheader(rneedheader), P(offset), U(infl.getBgzfMaxBlockSize()), ua(nullptr), uc(nullptr), ue(nullptr), Astallused(0)
			{
				init(offset.first);
			}

			BamRawDecoderTemplate(
				std::string const & fn, bool const rneedheader = true,
				std::pair<uint64_t,uint64_t> const offset = std::pair<uint64_t,uint64_t>(0,0)
			)
			: Pistr(new libmaus2::aio::InputStreamInstance(fn)), istr(*Pistr), infl(istr), eof(false), blockskip(offset.second), needheader(rneedheader), P(offset), U(infl.getBgzfMaxBlockSize()), ua(nullptr), uc(nullptr), ue(nullptr), Astallused(0)
			{
				init(offset.first);
			}


			std::pair<
				std::pair<uint8_t const *,uint64_t>,
				RawInterval
			>
				getPos()
			{
				while ( true )
				{
					if ( uc == ue )
					{
						if ( eof )
							return std::pair<
								std::pair<uint8_t const *,uint64_t>,
								RawInterval
							>
							(
								std::pair<uint8_t const *,uint64_t>(nullptr,0),
								RawInterval()
							);

						P.first += previnfo.compressed;

						libmaus2::lz::BgzfInflateInfo const info = infl.readAndInfo(
							reinterpret_cast<char *>(U.begin()),
							U.size()
						);

						previnfo = info;

						if ( info.streameof )
						{
							if ( info.uncompressed )
							{
								if ( throw_on_eof )
								{
									libmaus2::exception::LibMausException lme;
									lme.getStream() << "[E] BamRawDecoder: EOF block with data" << std::endl;
									lme.finish();
									throw lme;
								}
							}

							if ( needheader && !haveheader )
							{
								if ( throw_on_eof )
								{
									libmaus2::exception::LibMausException lme;
									lme.getStream() << "[E] BamRawDecoder: EOF while reading header" << std::endl;
									lme.finish();
									throw lme;
								}
							}

							if ( Astallused )
							{
								if ( throw_on_eof )
								{
									libmaus2::exception::LibMausException lme;
									lme.getStream() << "[E] BamRawDecoder: EOF while reading data" << std::endl;
									lme.finish();
									throw lme;
								}
							}

							eof = true;

							return std::pair<
								std::pair<uint8_t const *,uint64_t>,
								RawInterval
							>
							(
								std::pair<uint8_t const *,uint64_t>(nullptr,0),
								RawInterval()
							);
						}
						else
						{
							ua = U.begin();
							uc = ua;

							assert ( blockskip <= info.uncompressed );
							uc += blockskip;
							blockskip = 0;

							ue = ua + info.uncompressed;
						}
					}
					else if ( needheader && !haveheader )
					{
						::libmaus2::util::GetObject<uint8_t const *> G(uc);
						std::pair<bool,uint64_t> const P = parsestate.parseHeader(G,ue-uc);

						// header complete?
						if ( P.first )
						{
							header.init(parsestate);
							haveheader = true;
							uc += P.second;
						}
						else
						{
							uc = ue;
						}
					}
					else if ( Astallused )
					{
						while ( uc < ue && Astallused < sizeof(uint32_t) )
							Astall.push(Astallused,*(uc++));

						if ( Astallused >= sizeof(uint32_t) )
						{
							// decode BAM block size
							uint64_t const blocksize = libmaus2::bambam::DecoderBase::getLEInteger(
								reinterpret_cast<uint8_t const *>(Astall.begin()), sizeof(uint32_t)
							);

							// how many bytes do we need in the stall buffer
							uint64_t const avneeded = sizeof(uint32_t) + blocksize;

							// add missing
							if ( uc < ue && Astallused < avneeded )
							{
								uint64_t const av = ue-uc;
								uint64_t const needed = avneeded - Astallused;
								uint64_t const topush = std::min(av,needed);
								Astall.push(Astallused,uc,uc+topush);
								uc += topush;
							}

							if ( Astallused == avneeded )
							{
								Astallused = 0;

								return std::pair<
									std::pair<uint8_t const *,uint64_t>,
									RawInterval
								>
								(
									std::pair<uint8_t const *,uint64_t>(
										reinterpret_cast<uint8_t const *>(Astall.begin()+sizeof(uint32_t)),
										blocksize
									)
									,
									RawInterval(
										AstallP,
										std::pair<uint64_t,uint64_t>(P.first,uc-ua)
									)
								);
							}
							else
							{
								assert ( uc == ue );
							}
						}
					}
					else
					{
						assert ( ! Astallused );

						uint64_t blocksize;

						if (
							(ue - uc) >= static_cast< ::std::ptrdiff_t>(sizeof(uint32_t))
							&&
							(ue - uc) >= static_cast< ::std::ptrdiff_t>((sizeof(uint32_t) + (blocksize=libmaus2::bambam::DecoderBase::getLEInteger(reinterpret_cast<uint8_t const *>(uc),sizeof(uint32_t)))))
						)
						{
							unsigned char const * up = uc;
							uc += sizeof(uint32_t) + blocksize;

							return std::pair<
								std::pair<uint8_t const *,uint64_t>,
								RawInterval
							>
							(
								std::pair<uint8_t const *,uint64_t>(
									reinterpret_cast<uint8_t const *>(up + sizeof(uint32_t))
									,
									blocksize
								)
								,
								RawInterval(
									std::pair<uint64_t,uint64_t>(P.first,up-ua),
									std::pair<uint64_t,uint64_t>(P.first,uc-ua)
								)
							);
						}
						else
						{
							AstallP.first  = P.first;
							AstallP.second = uc-ua;
							Astall.push(Astallused,*uc++);
						}
					}
				}
			}

			std::pair<uint8_t const *,uint64_t> get()
			{
				return getPos().first;
			}

			libmaus2::bambam::BamHeader const & getHeader() const
			{
				if ( haveheader )
					return header;
				else
				{
					libmaus2::exception::LibMausException lme;
					lme.getStream() << "[E] BamRawDecoder::getHeader(): header not present" << std::endl;
					lme.finish();
					throw lme;
				}
			}
		};

		typedef BamRawDecoderTemplate<true> BamRawDecoder;
		typedef BamRawDecoderTemplate<false> BamRawDecoderNoThrowEOF;
	}
}
#endif
