/*
    libmaus2
    Copyright (C) 2009-2020 German Tischler-Höhle
    Copyright (C) 2011-2014 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_PARALLEL_SIMPLETHREADPOOL_HPP)
#define LIBMAUS2_PARALLEL_SIMPLETHREADPOOL_HPP

#include <libmaus2/parallel/SimpleThreadPoolThreadIdTree.hpp>
#include <libmaus2/parallel/SimpleThreadPoolBase.hpp>
#include <libmaus2/parallel/StdSpinLock.hpp>
#include <libmaus2/parallel/StdBasicSpinLock.hpp>
#include <libmaus2/parallel/StdSemaphore.hpp>
#include <libmaus2/parallel/StdTerminatableSynchronousHeap.hpp>
#include <libmaus2/parallel/SimpleThreadPoolThread.hpp>
#include <libmaus2/parallel/SimpleThreadPoolInterface.hpp>
#include <libmaus2/parallel/SimpleThreadWorkPackageDispatcher.hpp>
#include <libmaus2/parallel/SimpleThreadWorkPackageComparator.hpp>
#include <libmaus2/autoarray/AutoArray.hpp>
#include <libmaus2/util/unordered_map.hpp>
#include <atomic>
#include <stack>

#if defined(__linux__)
#include <unistd.h>
#include <sys/syscall.h>
#endif


namespace libmaus2
{
	namespace parallel
	{
		struct SimpleThreadPool : public SimpleThreadPoolInterface, public SimpleThreadPoolBase
		{
			typedef SimpleThreadPool this_type;
			typedef std::unique_ptr<this_type> unique_ptr_type;
			typedef std::shared_ptr<this_type> shared_ptr_type;

			std::atomic<uint64_t> nextDispatcherId;
			std::atomic<uint64_t> nextpackageid;

			// panic mode data
			std::atomic<bool> panicflag;
			typedef libmaus2::parallel::StdBasicSpinLock panicflag_lock_type;
			panicflag_lock_type panicflaglock;
			libmaus2::exception::LibMausException::unique_ptr_type lme;

			// semaphore for notifying about start completion
			libmaus2::parallel::StdSemaphore startsem;

			// threads
			libmaus2::autoarray::AutoArray<StdThread::unique_ptr_type> threads;
			libmaus2::autoarray::AutoArray<SimpleThreadPoolThreadId> AID;

			// package heap
			libmaus2::parallel::StdTerminatableSynchronousHeap<
				libmaus2::parallel::SimpleThreadWorkPackage *,
				libmaus2::parallel::SimpleThreadWorkPackageComparator
			> Q;

			// dispatcher map
			libmaus2::util::unordered_map<uint64_t,SimpleThreadWorkPackageDispatcher *>::type dispatchers;
			libmaus2::parallel::StdMutex dispatcherslock;

			/**
			 * constructors/destructors
			 **/
			SimpleThreadPool(
				uint64_t const rnumthreads
			)
			: nextDispatcherId(0), nextpackageid(0), panicflag(false), threads(rnumthreads), AID(rnumthreads)
			{
				for ( uint64_t i = 0; i < threads.size(); ++i )
				{
					StdThreadCallable::unique_ptr_type cptr(new SimpleThreadPoolThreadCallable(*this,i));
					StdThread::unique_ptr_type tptr(new StdThread(cptr));
					threads[i] = std::move(tptr);
				}

				try
				{
					for ( uint64_t i = 0; i < threads.size(); ++i )
					{
						threads[i]->start();
						AID[i] = SimpleThreadPoolThreadId(threads[i]->get_id(),threads[i].get(),this,i);
					}

					std::sort(AID.begin(),AID.end());
					SimpleThreadPoolThreadIdTree::addGlobal(AID.begin(),AID.end());

					#if 0
					std::cerr << "global tree ";
					SimpleThreadPoolThreadIdTree::printGlobal(std::cerr);
					std::cerr << std::endl;
					#endif

					for ( uint64_t i = 0; i < threads.size(); ++i )
						// wait until another thread is running
						startsem.wait();
				}
				catch(...)
				{
					Q.terminate();
					for ( uint64_t i = 0; i < threads.size(); ++i )
						try
						{
							threads[i]->tryJoin();
						}
						catch(...)
						{

						}
					throw;
				}
			}

			~SimpleThreadPool()
			{
				if ( panicflag )
					globalPanicCount -= 1;

				try
				{
					SimpleThreadPoolThreadIdTree::removeGlobal(AID.begin(),AID.end());
				}
				catch(...)
				{

				}

				#if 0
				internalJoin();

				for ( uint64_t i = 0; i < threads.size(); ++i )
					threads[i].reset();
				#endif
			}

			/**
			 * wait for end
			 **/
			void internalJoin()
			{
				for ( uint64_t i = 0; i < threads.size(); ++i )
					threads[i]->tryJoin();
			}

			void join()
			{
				internalJoin();

				if ( lme.get() )
					throw *lme;
			}

			/**
			 * info
			 **/
			uint64_t getNumThreads() const
			{
				return threads.size();
			}

			/**
                         * state printing
                         **/
                        void printPendingHistogram(std::ostream & out)
			{
				std::vector<libmaus2::parallel::SimpleThreadWorkPackage *> pending =
					Q.pending();
				std::map<char const *, uint64_t> hist;
				for ( uint64_t i = 0; i < pending.size(); ++i )
					hist[pending[i]->getPackageName()]++;
				for ( std::map<char const *, uint64_t>::const_iterator ita = hist.begin();
					ita != hist.end(); ++ita )
				{
					out << "P\t" << ita->first << "\t" << ita->second << "\n";
				}
			}

                        void printPending(std::ostream & out)
			{
				std::vector<libmaus2::parallel::SimpleThreadWorkPackage *> pending =
					Q.pending();
				for ( uint64_t i = 0; i < pending.size(); ++i )
					out << pending[i]->getPackageName() << " " << pending[i]->subid << std::endl;
			}

			void printRunningHistogram(std::ostream & out)
			{
				std::vector<libmaus2::parallel::SimpleThreadWorkPackage *> running;
				for ( uint64_t i = 0; i < threads.size(); ++i )
				{
					libmaus2::parallel::SimpleThreadWorkPackage * pack =
						dynamic_cast<SimpleThreadPoolThreadCallable &>(threads[i]->getCallable()).getCurrentPackage();
					if ( pack )
						running.push_back(pack);
				}
				std::map<char const *, uint64_t> hist;
				for ( uint64_t i = 0; i < running.size(); ++i )
					hist[running[i]->getPackageName()]++;
				for ( std::map<char const *, uint64_t>::const_iterator ita = hist.begin();
					ita != hist.end(); ++ita )
				{
					out << "R\t" << ita->first << "\t" << ita->second << "\n";
				}
			}

			void printStateHistogram(std::ostream & out)
			{
				printPendingHistogram(out);
				printRunningHistogram(out);
			}

			/**
			 * panic mode handling
			 **/
			std::string getPanicMessage()
			{
                        	panicflag_lock_type::scope_lock_type lpanicflaglock(panicflaglock);
                        	if ( lme )
                        		return lme->what();
				else
					return std::string();
			}

                        void panic(libmaus2::exception::LibMausException const & ex)
                        {
                        	panicflag_lock_type::scope_lock_type lpanicflaglock(panicflaglock);
                        	panicflag = true;
                        	globalPanicCount += 1;

                        	Q.terminate();

                        	if ( ! lme.get() )
                        	{
                        		libmaus2::exception::LibMausException::unique_ptr_type tex(ex.uclone());
                        		lme = std::move(tex);
				}
                        }

                        void panic(std::exception const & ex)
                        {
                        	panicflag_lock_type::scope_lock_type lpanicflaglock(panicflaglock);
                        	panicflag = true;
                        	globalPanicCount += 1;

                        	Q.terminate();

                        	if ( ! lme.get() )
                        	{
                        		libmaus2::exception::LibMausException::unique_ptr_type tlme(
                        			new libmaus2::exception::LibMausException
                        		);
                        		lme = std::move(tlme);
                        		lme->getStream() << ex.what();
                        		lme->finish();
                        	}
                        }

                        bool isInPanicMode()
                        {
                        	panicflag_lock_type::scope_lock_type lpanicflaglock(panicflaglock);
				return panicflag;
                        }

                        /**
                         * thread start callback
                         **/
			void notifyThreadStart()
			{
				startsem.post();
			}

			/**
			 * queue handling
			 **/
			void enque(SimpleThreadWorkPackage * P)
			{
				P->packageid = nextpackageid++;
				Q.enque(P);
			}

			void terminate()
			{
				Q.terminate();
			}

			SimpleThreadWorkPackage * getPackage()
			{
				return Q.deque();
			}

			/**
			 * dispatcher handling
			 **/
			SimpleThreadWorkPackageDispatcher * getDispatcher(libmaus2::parallel::SimpleThreadWorkPackage * P)
			{
				SimpleThreadWorkPackageDispatcher * R;
				{
					libmaus2::parallel::StdMutex::scope_lock_type slock(dispatcherslock);
					libmaus2::util::unordered_map<uint64_t,SimpleThreadWorkPackageDispatcher *>::type::iterator it =
						dispatchers.find(P->dispatcherid);
					assert ( it != dispatchers.end() );
					R = it->second;
				}
				return R;
			}

			void registerDispatcher(uint64_t const id, SimpleThreadWorkPackageDispatcher * D)
			{
				libmaus2::parallel::StdMutex::scope_lock_type slock(dispatcherslock);
				dispatchers[id] = D;
			}

			void removeDispatcher(uint64_t const id)
			{
				libmaus2::parallel::StdMutex::scope_lock_type slock(dispatcherslock);
				libmaus2::util::unordered_map<uint64_t,SimpleThreadWorkPackageDispatcher *>::type::iterator it =
					dispatchers.find(id);
				assert ( it != dispatchers.end() );
				dispatchers.erase(id);
			}

			uint64_t getNextDispatcherId()
			{
				libmaus2::parallel::StdMutex::scope_lock_type slock(dispatcherslock);
				while ( dispatchers.find(nextDispatcherId) != dispatchers.end() )
					++nextDispatcherId;
				return nextDispatcherId++;
			}
		};
	}
}
#endif
