/*
    libmaus2
    Copyright (C) 2009-2015 German Tischler
    Copyright (C) 2011-2015 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_SCRAMINPUTCONTAINER_HPP)
#define LIBMAUS2_BAMBAM_SCRAMINPUTCONTAINER_HPP

#include <libmaus2/aio/InputStream.hpp>
#include <libmaus2/aio/InputStreamFactoryContainer.hpp>
#include <libmaus2/bambam/Scram.h>
#include <libmaus2/lz/BufferedGzipStream.hpp>
#include <libmaus2/aio/InputStreamInstance.hpp>

namespace libmaus2
{
	namespace bambam
	{
		struct ScramInputContainer
		{
			static std::map<void *, std::shared_ptr<scram_cram_io_input_t> > Mcontrol;
			static std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type> Mstream;
			static std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type> Mcompstream;
			static libmaus2::parallel::StdMutex Mlock;

			static scram_cram_io_input_t * allocate(char const * filename, int const decompress)
			{
				libmaus2::parallel::ScopeStdMutex Llock(Mlock);
				std::shared_ptr<scram_cram_io_input_t> sptr;
				libmaus2::aio::InputStreamInstance::shared_ptr_type sstr;
				libmaus2::aio::InputStreamInstance::shared_ptr_type gstr;

				/* allocate data structure */
				try
				{
					std::shared_ptr<scram_cram_io_input_t> tptr(new scram_cram_io_input_t);
					sptr = tptr;
				}
				catch(std::exception const & ex)
				{
					std::cerr << ex.what() << std::endl;
					return nullptr;
				}

				/* store pointer to data structure in Mcontrol */
				try
				{
					Mcontrol[sptr.get()] = std::shared_ptr<scram_cram_io_input_t>();
				}
				catch(std::exception const & ex)
				{
					std::cerr << ex.what() << std::endl;
					return nullptr;
				}

				{
					std::map<void *, std::shared_ptr<scram_cram_io_input_t> >::iterator it = Mcontrol.find(sptr.get());
					assert (it != Mcontrol.end());
					it->second = sptr;
				}

				/* open file via factory */
				try
				{
					libmaus2::aio::InputStreamInstance::shared_ptr_type tstr(new libmaus2::aio::InputStreamInstance(filename));
					sstr = tstr;
				}
				catch(std::exception const & ex)
				{
					std::cerr << "Failed to open " << filename << ":\n" << ex.what() << std::endl;

					std::map<void *, std::shared_ptr<scram_cram_io_input_t> >::iterator it = Mcontrol.find(sptr.get());
					assert ( it != Mcontrol.end() );
					Mcontrol.erase(it);

					return nullptr;
				}

				try
				{
					Mstream[sptr.get()] = libmaus2::aio::InputStreamInstance::shared_ptr_type();
				}
				catch(...)
				{
					std::map<void *, std::shared_ptr<scram_cram_io_input_t> >::iterator it = Mcontrol.find(sptr.get());
					assert ( it != Mcontrol.end() );
					Mcontrol.erase(it);

					return nullptr;
				}

				{
					std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type>::iterator it = Mstream.find(sptr.get());
					assert ( it != Mstream.end() );
					it->second = sstr;
				}

				if ( decompress )
				{
					try
					{
						std::shared_ptr<std::istream> tptr(new libmaus2::lz::BufferedGzipStream(*sstr));
						libmaus2::aio::InputStream::unique_ptr_type iptr(new libmaus2::aio::InputStream(tptr));
						libmaus2::aio::InputStreamInstance::shared_ptr_type ttptr(new libmaus2::aio::InputStreamInstance(iptr));
						gstr = ttptr;
					}
					catch(...)
					{
						std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type>::iterator its = Mstream.find(sptr.get());
						assert ( its != Mstream.end() );
						Mstream.erase(its);

						std::map<void *, std::shared_ptr<scram_cram_io_input_t> >::iterator itc = Mcontrol.find(sptr.get());
						assert ( itc != Mcontrol.end() );
						Mcontrol.erase(itc);

						return nullptr;
					}

					try
					{
						Mcompstream[sptr.get()] = libmaus2::aio::InputStreamInstance::shared_ptr_type();
					}
					catch(...)
					{
						std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type>::iterator its = Mstream.find(sptr.get());
						assert ( its != Mstream.end() );
						Mstream.erase(its);

						std::map<void *, std::shared_ptr<scram_cram_io_input_t> >::iterator itc = Mcontrol.find(sptr.get());
						assert ( itc != Mcontrol.end() );
						Mcontrol.erase(itc);

						return nullptr;
					}

					{
						std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type>::iterator its = Mcompstream.find(sptr.get());
						assert ( its != Mcompstream.end() );
						its->second = gstr;
					}
				}

				memset(sptr.get(),0,sizeof(scram_cram_io_input_t));
				sptr->user_data = decompress ? gstr.get() : sstr.get();
				sptr->fread_callback = call_fread;
				sptr->fseek_callback = call_fseek;
				sptr->ftell_callback = call_ftell;

				return sptr.get();
			}

			static scram_cram_io_input_t * deallocate(scram_cram_io_input_t * obj)
			{
				libmaus2::parallel::ScopeStdMutex Llock(Mlock);

				if ( obj )
				{
					{
						std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type>::iterator its = Mcompstream.find(obj);
						if ( its != Mcompstream.end() )
							Mcompstream.erase(its);
					}
					{
						std::map<void *, libmaus2::aio::InputStreamInstance::shared_ptr_type>::iterator its = Mstream.find(obj);
						if ( its != Mstream.end() )
							Mstream.erase(its);
					}
					{
						std::map<void *, std::shared_ptr<scram_cram_io_input_t> >::iterator itc = Mcontrol.find(obj);
						assert ( itc != Mcontrol.end() );
						Mcontrol.erase(itc);
					}
				}
				return nullptr;
			}

			static size_t call_fread(void * ptr, size_t size, size_t nmemb, void *stream)
			{
				std::istream * pistr = (std::istream *)stream;
				char * out = reinterpret_cast<char *>(ptr);
				uint64_t bytestoread = size*nmemb;
				uint64_t r = 0, rr = 0;

				do
				{
					pistr->read(out,bytestoread);
					rr = pistr->gcount();
					r += rr;
					bytestoread -= rr;
					out += rr;
				} while ( bytestoread && rr );

				return (r / size);
			}

			static int call_fseek(void * fd, libmaus2::off_t offset, int whence)
			{
				std::istream * pistr = reinterpret_cast<std::istream *>(fd);
				pistr->clear();

				if ( whence == libmaus2::posix::PosixFunctions::get_SEEK_SET() )
				{
					pistr->seekg(offset,std::ios::beg);
				}
				else if ( whence == libmaus2::posix::PosixFunctions::get_SEEK_CUR() )
				{
					pistr->seekg(offset,std::ios::cur);
				}
				else if ( whence == libmaus2::posix::PosixFunctions::get_SEEK_END() )
				{
					pistr->seekg(offset,std::ios::end);
				}
				else
				{
					return -1;
				}

				if ( pistr->fail() )
				{
					pistr->clear();
					return -1;
				}
				else
				{
					return 0;
				}
			}

			static libmaus2::off_t call_ftell(void * fd)
			{
				std::istream * pistr = reinterpret_cast<std::istream *>(fd);
				return pistr->tellg();
			}
		};
	}
}
#endif
