/*
    libmaus2
    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/>.
*/
#if ! defined(LIBMAUS2_BAMBAM_SCRAMDECODER_HPP)
#define LIBMAUS2_BAMBAM_SCRAMDECODER_HPP

#include <libmaus2/LibMausConfig.hpp>
#include <iostream>
#include <cstdlib>

#include <libmaus2/bambam/Scram.h>
#include <libmaus2/bambam/BamAlignmentDecoder.hpp>
#include <libmaus2/bambam/AlignmentValidity.hpp>

namespace libmaus2
{
	namespace bambam
	{
		/**
		 * scram decoder class; alignment decoder based on io_lib
		 **/
		struct ScramDecoder : public libmaus2::bambam::BamAlignmentDecoder
		{
			//! this type
			typedef ScramDecoder this_type;
			//! unique pointer type
			typedef std::unique_ptr<this_type> unique_ptr_type;
			//! shared pointer type
			typedef std::shared_ptr<this_type> shared_ptr_type;

			private:
			//! scram decoder object
			std::shared_ptr<libmaus2_bambam_ScramDecoder> dec;
			// libmaus2_bambam_ScramDecoder * dec;

			//! bam header object
			::libmaus2::bambam::BamHeader bamheader;

			/**
			 * allocate scram decoder object; throws exception on failure
			 *
			 * @param filename input filename, - for stdin
			 * @param mode file mode r (SAM), rb (BAM) or rc (CRAM)
			 * @param reference reference file name (empty string for none)
			 * @return decoder object
			 **/
			std::shared_ptr<libmaus2_bambam_ScramDecoder> allocateDecoder(
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					filename
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					mode
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					reference
					#endif
			)
			{
				#if defined(LIBMAUS2_HAVE_IO_LIB)
				libmaus2_bambam_ScramDecoder * dec = libmaus2_bambam_ScramDecoder_New(filename.c_str(),mode.c_str(),reference.size() ? reference.c_str() : 0);

				if ( ! dec )
				{
					::libmaus2::exception::LibMausException se;
					se.getStream() << "ScramDecoder: failed to open file " << filename << " in mode " << mode << std::endl;
					se.finish();
					throw se;
				}

				std::shared_ptr<libmaus2_bambam_ScramDecoder> sptr(
					dec,[](auto p){libmaus2_bambam_ScramDecoder_Delete(p);}
				);

				return sptr;
				#else
				::libmaus2::exception::LibMausException se;
				se.getStream() << "ScramDecoder: no support for io_lib compiled" << std::endl;
				se.finish();
				throw se;
				#endif
			}

			/**
			 * allocate scram decoder object with range; throws exception on failure
			 *
			 * @param filename input filename, - for stdin
			 * @param mode file mode r (SAM), rb (BAM) or rc (CRAM)
			 * @param reference reference file name (empty string for none)
			 * @return decoder object
			 **/
			std::shared_ptr<libmaus2_bambam_ScramDecoder> allocateDecoder(
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					filename
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					mode
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					reference
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					ref
					#endif
					,
				int64_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					start
					#endif
					,
				int64_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					end
					#endif
			)
			{
				#if defined(LIBMAUS2_HAVE_IO_LIB)
				if ( mode != "rc" )
				{
					::libmaus2::exception::LibMausException se;
					se.getStream() << "ScramDecoder: failed to open file " << filename << " in mode " << mode
						<< " with range (" << ref << "," << start << "," << end << "), "
						<< "ranges are only supported for CRAM input" << std::endl;
					se.finish();
					throw se;
				}

				libmaus2_bambam_ScramDecoder * dec = libmaus2_bambam_ScramDecoder_New_Range(
					filename.c_str(),mode.c_str(),reference.size() ? reference.c_str() : 0,
					ref.c_str(),
					start,end
				);

				if ( ! dec )
				{
					::libmaus2::exception::LibMausException se;
					se.getStream() << "ScramDecoder: failed to open file " << filename << " in mode " << mode
						<< " with range (" << ref << "," << start << "," << end << ")"
						<< std::endl;
					se.finish();
					throw se;
				}

				std::shared_ptr<libmaus2_bambam_ScramDecoder> sptr(
					dec,[](auto p){libmaus2_bambam_ScramDecoder_Delete(p);}
				);

				return sptr;
				#else
				::libmaus2::exception::LibMausException se;
				se.getStream() << "ScramDecoder: no support for io_lib compiled" << std::endl;
				se.finish();
				throw se;
				#endif
			}

			/**
			 * allocate cram decoder object via callbacks
			 *
			 * @param filename input filename, - for stdin
			 * @param mode file mode r (SAM), rb (BAM) or rc (CRAM)
			 * @param reference reference file name (empty string for none)
			 * @return decoder object
			 **/
			std::shared_ptr<libmaus2_bambam_ScramDecoder> allocateDecoder(
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					filename
					#endif
					,
				scram_cram_io_allocate_read_input_t
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					callback_allocate_function
					#endif
					,
				scram_cram_io_deallocate_read_input_t
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					callback_deallocate_function
					#endif
					,
				size_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					bufsize
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					rreferencefilename
					#endif
					,
				bool
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					loadindex
					#endif
			)
			{
				#if defined(LIBMAUS2_HAVE_IO_LIB)
				libmaus2_bambam_ScramDecoder * dec = nullptr;

				if ( loadindex )
				{
					dec = libmaus2_bambam_ScramDecoder_New_Cram_Input_Callback_With_Index(
						filename.c_str(),
						callback_allocate_function,
						callback_deallocate_function,
						bufsize,
						rreferencefilename.size() ? rreferencefilename.c_str() : 0
					);
				}
				else
				{
					dec = libmaus2_bambam_ScramDecoder_New_Cram_Input_Callback(
						filename.c_str(),
						callback_allocate_function,
						callback_deallocate_function,
						bufsize,
						rreferencefilename.size() ? rreferencefilename.c_str() : 0
					);
				}

				if ( ! dec )
				{
					::libmaus2::exception::LibMausException se;
					se.getStream() << "ScramDecoder: failed to open file " << filename << " in CRAM read mode via callback." << std::endl;
					se.finish();
					throw se;
				}

				std::shared_ptr<libmaus2_bambam_ScramDecoder> sptr(
					dec,[](auto p){libmaus2_bambam_ScramDecoder_Delete(p);}
				);

				return sptr;
				#else
				::libmaus2::exception::LibMausException se;
				se.getStream() << "ScramDecoder: no support for io_lib compiled" << std::endl;
				se.finish();
				throw se;
				#endif
			}

			/**
			 * allocate cram decoder object via callbacks and range
			 *
			 * @param filename input filename, - for stdin
			 * @param reference reference file name (empty string for none)
			 * @return decoder object
			 **/
			std::shared_ptr<libmaus2_bambam_ScramDecoder> allocateDecoder(
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					filename
					#endif
					,
				scram_cram_io_allocate_read_input_t
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					callback_allocate_function
					#endif
					,
				scram_cram_io_deallocate_read_input_t
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					callback_deallocate_function
					#endif
					,
				size_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					bufsize
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					rreferencefilename
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					ref
					#endif
					,
				int64_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					start
					#endif
					,
				int64_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					end
					#endif
			)
			{
				#if defined(LIBMAUS2_HAVE_IO_LIB)
				libmaus2_bambam_ScramDecoder * dec = libmaus2_bambam_ScramDecoder_New_Cram_Input_Callback_Range(
					filename.c_str(),
					callback_allocate_function,
					callback_deallocate_function,
					bufsize,
					rreferencefilename.size() ? rreferencefilename.c_str() : 0,
					ref.size() ? ref.c_str() : 0,
					start,end
				);

				if ( ! dec )
				{
					::libmaus2::exception::LibMausException se;
					se.getStream() << "ScramDecoder: failed to open file " << filename << " in CRAM read mode via callback with range." << std::endl;
					se.finish();
					throw se;
				}

				std::shared_ptr<libmaus2_bambam_ScramDecoder> sptr(
					dec,[](auto p){libmaus2_bambam_ScramDecoder_Delete(p);}
				);

				return sptr;
				#else
				::libmaus2::exception::LibMausException se;
				se.getStream() << "ScramDecoder: no support for io_lib compiled" << std::endl;
				se.finish();
				throw se;
				#endif
			}

			bool readAlignmentInternal(
				bool const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					delayPutRank
					#endif
					= false
			)
			{
				#if defined(LIBMAUS2_HAVE_IO_LIB)
				int const r = libmaus2_bambam_ScramDecoder_Decode(dec.get());

				// std::cerr << "got code " << r << std::endl;

				if ( r == -2 )
				{
					::libmaus2::exception::LibMausException se;
					se.getStream() << "ScramDecoder::readAlignment(): failed to read alignment without reaching EOF" << std::endl;
					se.finish();
					throw se;
				}
				else if ( r == -1 )
				{
					return false;
				}

				if ( dec->blocksize > alignment.D.size() )
					alignment.D = ::libmaus2::bambam::BamAlignment::D_array_type(dec->blocksize,false);

				memcpy(alignment.D.begin(),dec->buffer,dec->blocksize);
				alignment.blocksize = dec->blocksize;

				if ( validate )
				{
					BamAlignmentValidityResult const validity(alignment.valid(bamheader));
					if ( validity.valid != ::libmaus2::bambam::libmaus2_bambam_alignment_validity_ok )
					{
						::libmaus2::exception::LibMausException se;
						se.getStream() << "Invalid alignment: " << validity << std::endl;
						se.finish();
						throw se;
					}
				}

				if ( ! delayPutRank )
					putRank();

				return true;
				#else
				::libmaus2::exception::LibMausException se;
				se.getStream() << "ScramDecoder: no support for io_lib compiled" << std::endl;
				se.finish();
				throw se;
				#endif
			}

			public:
			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param mode file mode r (SAM), rb (BAM) or rc (CRAM)
			 * @param reference reference file name (empty string for none)
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoder(std::string const & filename, std::string const & mode, std::string const & reference, bool const rputrank = false)
			:
				libmaus2::bambam::BamAlignmentDecoder(rputrank),
				dec(allocateDecoder(filename,mode,reference)),
				bamheader(std::string(dec->header,dec->header+dec->headerlen))
			{
			}

			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param mode file mode r (SAM), rb (BAM) or rc (CRAM)
			 * @param reference reference file name (empty string for none)
			 * @param ref name of reference sequence for range
			 * @param start range start
			 * @param end range end
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoder(std::string const & filename, std::string const & mode, std::string const & reference,
				std::string const & ref,
				int64_t const start,
				int64_t const end,
				bool const rputrank = false)
			:
				libmaus2::bambam::BamAlignmentDecoder(rputrank),
				dec(allocateDecoder(filename,mode,reference,ref,start,end)),
				bamheader(std::string(dec->header,dec->header+dec->headerlen))
			{
			}

			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param callback_allocate_function stream allocation callback
			 * @param callback_deallocate_function stream deallocation callback
			 * @param bufsize input buffer size
			 * @param reference reference file name (empty string for none)
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoder(
				std::string const & filename,
			        scram_cram_io_allocate_read_input_t   callback_allocate_function,
			        scram_cram_io_deallocate_read_input_t callback_deallocate_function,
			        size_t const bufsize,
			 	std::string const & reference,
				bool const rputrank = false,
				bool const loadindex = false
			)
			:
				libmaus2::bambam::BamAlignmentDecoder(rputrank),
				dec(allocateDecoder(filename,callback_allocate_function,callback_deallocate_function,bufsize,reference,loadindex)),
				bamheader(std::string(dec->header,dec->header+dec->headerlen))
			{
			}
			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param callback_allocate_function stream allocation callback
			 * @param callback_deallocate_function stream deallocation callback
			 * @param bufsize input buffer size
			 * @param reference reference file name (empty string for none)
			 * @param ref name of reference sequence for range
			 * @param start range start
			 * @param end range end
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoder(
				std::string const & filename,
			        scram_cram_io_allocate_read_input_t   callback_allocate_function,
			        scram_cram_io_deallocate_read_input_t callback_deallocate_function,
			        size_t const bufsize,
			 	std::string const & reference,
				std::string const & ref,
				int64_t const start,
				int64_t const end,
				bool const rputrank = false)
			:
				libmaus2::bambam::BamAlignmentDecoder(rputrank),
				dec(allocateDecoder(filename,callback_allocate_function,callback_deallocate_function,bufsize,reference,ref,start,end)),
				bamheader(std::string(dec->header,dec->header+dec->headerlen))
			{
			}

			/**
			 * destructor
			 **/
			~ScramDecoder()
			{
			}

			/**
			 * @return BAM header
			 **/
			libmaus2::bambam::BamHeader const & getHeader() const
			{
				return bamheader;
			}

			/**
			 * set decoding range (only works for indexed CRAM)
			 **/
			void setRange(
				std::string const &
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					ref
					#endif
					,
				int64_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					beg
					#endif
					,
				int64_t const
					#if defined(LIBMAUS2_HAVE_IO_LIB)
					end
					#endif
			)
			{
				#if defined(LIBMAUS2_HAVE_IO_LIB)
				libmaus2::autoarray::AutoArray<char> A(ref.size()+1);
				A[ref.size()] = 0;
				std::copy(ref.begin(),ref.end(),A.begin());

				int const r = libmaus2_bambam_ScramDecoder_Set_Range(dec.get(),A.begin(),beg,end);

				if ( r != 0 )
				{
					libmaus2::exception::LibMausException lme;
					lme.getStream() << "[E] ScramDecoder::setRange(" << ref << "," << beg << "," << end << ") failed" << std::endl;
					lme.finish();
					throw lme;
				}
				#else
				::libmaus2::exception::LibMausException se;
				se.getStream() << "ScramDecoder: no support for io_lib compiled" << std::endl;
				se.finish();
				throw se;
				#endif
			}
		};

		/**
		 * class wrapping a ScramDecoder object
		 **/
		struct ScramDecoderWrapper : public libmaus2::bambam::BamAlignmentDecoderWrapper
		{
			//! wrapped object
			ScramDecoder scramdec;

			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param mode file mode r (SAM), rb (BAM) or rc (CRAM)
			 * @param reference reference file name (empty string for none)
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoderWrapper(
				std::string const & filename,
				std::string const & mode,
				std::string const & reference,
				bool const rputrank = false
			)
			: scramdec(filename,mode,reference,rputrank)
			{

			}
			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param mode file mode r (SAM), rb (BAM) or rc (CRAM)
			 * @param reference reference file name (empty string for none)
			 * @param ref name of reference sequence for range
			 * @param start range start
			 * @param end range end
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoderWrapper(
				std::string const & filename,
				std::string const & mode,
				std::string const & reference,
				std::string const & ref,
				int64_t const start,
				int64_t const end,
				bool const rputrank = false
			)
			: scramdec(filename,mode,reference,ref,start,end,rputrank)
			{

			}
			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param callback_allocate_function stream allocation callback
			 * @param callback_deallocate_function stream deallocation callback
			 * @param reference reference file name (empty string for none)
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoderWrapper(
				std::string const & filename,
			        scram_cram_io_allocate_read_input_t   callback_allocate_function,
			        scram_cram_io_deallocate_read_input_t callback_deallocate_function,
			        size_t const bufsize,
			 	std::string const & reference,
				bool const rputrank = false,
				bool const rLoadCramIndex = false)
			: scramdec(filename,callback_allocate_function,callback_deallocate_function,bufsize,reference,rputrank,rLoadCramIndex)
			{

			}
			/**
			 * constructor
			 *
			 * @param filename input filename, - for stdin
			 * @param callback_allocate_function stream allocation callback
			 * @param callback_deallocate_function stream deallocation callback
			 * @param bufsize input buffer size
			 * @param reference reference file name (empty string for none)
			 * @param ref name of reference sequence for range
			 * @param start range start
			 * @param end range end
			 * @param rputrank put rank (line number) on alignments
			 **/
			ScramDecoderWrapper(
				std::string const & filename,
			        scram_cram_io_allocate_read_input_t   callback_allocate_function,
			        scram_cram_io_deallocate_read_input_t callback_deallocate_function,
			        size_t const bufsize,
			 	std::string const & reference,
				std::string const & ref,
				int64_t const start,
				int64_t const end,
				bool const rputrank = false)
			: scramdec(filename,callback_allocate_function,callback_deallocate_function,bufsize,reference,ref,start,end,rputrank)
			{

			}

			libmaus2::bambam::BamAlignmentDecoder & getDecoder()
			{
				return scramdec;
			}

			void setRange(
				std::string const & ref,
				int64_t const beg,
				int64_t const end
			)
			{
				scramdec.setRange(ref,beg,end);
			}
		};
	}
}
#endif
