/*
    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_AIO_OUTPUTSTREAMFACTORYCONTAINER_HPP)
#define LIBMAUS2_AIO_OUTPUTSTREAMFACTORYCONTAINER_HPP

#include <libmaus2/aio/OutputStreamFactory.hpp>
#include <libmaus2/aio/PosixFdOutputStreamFactory.hpp>
#include <libmaus2/aio/MemoryOutputStreamFactory.hpp>
#include <libmaus2/aio/OFStreamOutputStreamFactory.hpp>
#include <libmaus2/aio/FileRemoval.hpp>
#include <libmaus2/aio/SecrecyOutputStreamFactory.hpp>
#include <libmaus2/aio/URLObject.hpp>
#include <cctype>
#include <experimental/filesystem>

namespace libmaus2
{
	namespace aio
	{
		struct OutputStreamFactoryContainer
		{
			private:
			static std::map<std::string,libmaus2::aio::OutputStreamFactory::shared_ptr_type> factories;

			static std::map<std::string,libmaus2::aio::OutputStreamFactory::shared_ptr_type> setupFactories()
			{
				std::map<std::string,libmaus2::aio::OutputStreamFactory::shared_ptr_type> tfactories;

				#if defined(LIBMAUS2_AIO_POSIXFDOUTPUTSTREAM_SUPPORTED)
				libmaus2::aio::PosixFdOutputStreamFactory::shared_ptr_type tfilefact(new libmaus2::aio::PosixFdOutputStreamFactory);
				tfactories["file"] = tfilefact;
				#endif

				libmaus2::aio::OFStreamOutputStreamFactory::shared_ptr_type tofstreamfact(new libmaus2::aio::OFStreamOutputStreamFactory);
				tfactories["stdcxx"] = tofstreamfact;
				#if !defined(LIBMAUS2_AIO_POSIXFDOUTPUTSTREAM_SUPPORTED)
				tfactories["file"] = tofstreamfact;
				#endif

				libmaus2::aio::MemoryOutputStreamFactory::shared_ptr_type tmemfact(new libmaus2::aio::MemoryOutputStreamFactory);
				tfactories["mem"] = tmemfact;

				#if defined(LIBMAUS2_HAVE_LIBSECRECY)
				libmaus2::aio::SecrecyOutputStreamFactory::shared_ptr_type tsecfact(new libmaus2::aio::SecrecyOutputStreamFactory);
				tfactories["libsecrecy"] = tsecfact;
				#endif

				return tfactories;
			}

			static libmaus2::aio::OutputStreamFactory::shared_ptr_type haveFactoryForProtocol(std::string const & url)
			{
				libmaus2::aio::URLObject const U(url);
				libmaus2::aio::OutputStreamFactory::shared_ptr_type ptr;

				if ( U.protocol.size() )
				{
					auto const it = factories.find(U.protocol);
					if ( it != factories.end() )
						ptr = it->second;
				}

				return ptr;
			}

			static libmaus2::aio::OutputStreamFactory::shared_ptr_type getFactory(std::string const & url)
			{
				libmaus2::aio::OutputStreamFactory::shared_ptr_type ptr = haveFactoryForProtocol(url);
				return ptr ? ptr : factories.find("file")->second;
			}

			static void copy(std::string const & from, std::string const & to);

			public:
			static libmaus2::aio::OutputStream::unique_ptr_type constructUnique(std::string const & url)
			{
				libmaus2::aio::OutputStreamFactory::shared_ptr_type factory = getFactory(url);

				if ( haveFactoryForProtocol(url) )
				{
					libmaus2::aio::URLObject const U(url);
					libmaus2::aio::OutputStream::unique_ptr_type tptr(factory->constructUnique(U.path,U.parameters));
					return tptr;
				}
				else
				{
					libmaus2::aio::OutputStream::unique_ptr_type tptr(factory->constructUnique(url));
					return tptr;
				}
			}

			static std::string getInnerURLPart(std::string const & url)
			{
				if ( haveFactoryForProtocol(url) )
				{
					uint64_t col = url.size();
					for ( uint64_t i = 0; i < url.size() && col == url.size(); ++i )
						if ( url[i] == ':' )
							col = i;

					std::string const protocol = url.substr(0,col);

					return url.substr(protocol.size()+1);
				}
				else
				{
					return url;
				}
			}

			static libmaus2::aio::OutputStream::shared_ptr_type constructShared(std::string const & url)
			{
				libmaus2::aio::OutputStreamFactory::shared_ptr_type factory = getFactory(url);

				if ( haveFactoryForProtocol(url) )
				{
					libmaus2::aio::URLObject const U(url);
					libmaus2::aio::OutputStream::shared_ptr_type tptr(factory->constructShared(U.path,U.parameters));
					return tptr;
				}
				else
				{
					libmaus2::aio::OutputStream::shared_ptr_type tptr(factory->constructShared(url));
					return tptr;
				}
			}

			static bool tryOpen(std::string const & url)
			{
				try
				{
					libmaus2::aio::OutputStream::shared_ptr_type tptr(constructShared(url));
					return true;
				}
				catch(...)
				{
					return false;
				}
			}

			static void rename(std::string const & from, std::string const & to)
			{
				libmaus2::aio::OutputStreamFactory::shared_ptr_type fromfact = getFactory(from);
				libmaus2::aio::OutputStreamFactory::shared_ptr_type tofact   = getFactory(to);

				if ( fromfact.get() == tofact.get() )
				{
					fromfact->rename(getInnerURLPart(from),getInnerURLPart(to));
				}
				else
				{
					copy(from,to);
					::libmaus2::aio::FileRemoval::removeFile(from);
				}
			}

			static void mkdir(std::string const & name, uint64_t const mode)
			{
				libmaus2::aio::OutputStreamFactory::shared_ptr_type fact = getFactory(name);

				if ( haveFactoryForProtocol(name) )
				{
					uint64_t col = name.size();
					for ( uint64_t i = 0; i < name.size() && col == name.size(); ++i )
						if ( name[i] == ':' )
							col = i;

					std::string const protocol = name.substr(0,col);
					std::string const dn = name.substr(protocol.size()+1);
					fact->mkdir(dn,mode);
				}
				else
				{
					fact->mkdir(name,mode);
				}
			}

			static void addHandler(std::string const & protocol, libmaus2::aio::OutputStreamFactory::shared_ptr_type factory)
			{
				factories[protocol] = factory;
			}

			static std::vector<std::string> getDirectoryPath(std::string f, char const sep = '/')
			{
				std::vector<std::string> V;

				// if path is not empty and does not begin with separator
				if ( ! f.size() || f[0] != sep )
				{
					// while path is not empty
					while ( f.size() )
					{
						// scan back for separator
						int64_t z = f.size() - 1;

						while ( z >= 0 && f[z] != sep )
							--z;

						// if none was found
						if ( z < 0 )
						{
							// push dot
							V.push_back(".");
							// set empty string
							f = std::string();
						}
						else
						{
							// get front part up to separator
							std::string const front = f.substr(0,z);

							// push front part
							V.push_back(front);

							// use front part as new f
							f = front;
						}
					}
				}
				// if path begins with separator
				else
				{
					assert ( f.size() && f[0] == sep );

					// while path is not empty
					while ( f.size() )
					{
						// scan back for separator
						uint64_t z = f.size();
						while ( f[z-1] != sep )
							--z;
						assert ( f[z-1] == sep );

						// get front part up to last separator (excluding the separator)
						std::string const front = f.substr(0,z-1);

						// if path is not empty
						if ( front.size() )
							V.push_back(front);
						// else put root
						else
							V.push_back(std::string("/"));

						f = front;
					}

				}

				// reverse, put shortest path first and longest last
				std::reverse(V.begin(),V.end());

				return V;
			}

			/**
			 * create directories
			 **/
			static void mkdirp(std::string const & name, uint64_t const mode, char const sep = '/')
			{
				uint64_t slashback = 0;
				while ( slashback < name.size() && name[name.size()-slashback-1] == sep )
					++slashback;

				std::string const sname = name.substr(0,name.size()-slashback) + "/a";

				std::vector<std::string> const V = getDirectoryPath(sname,sep);

				for ( uint64_t i = 0; i < V.size(); ++i )
					// check whether path already exists
					if ( ! std::experimental::filesystem::exists(V[i]) )
						// if not, then create it as a directory
						mkdir(V[i],mode);
			}

			static void makeDirectoryPath(std::string const & f, uint64_t const mode, char const sep = '/')
			{
				std::vector<std::string> const V = getDirectoryPath(f,sep);
				mkdirp(V.back(),mode,sep);
			}
		};
	}
}
#endif
