/*
    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(SIMPLEHASHSET_HPP)
#define SIMPLEHASHSET_HPP

#include <libmaus2/exception/LibMausException.hpp>
#include <libmaus2/autoarray/AutoArray.hpp>
#include <libmaus2/hashing/hash.hpp>
#include <libmaus2/math/primes16.hpp>

#if defined(_OPENMP)
#include <omp.h>
#endif

namespace libmaus2
{
	namespace util
	{
		template<typename _key_type>
		struct SimpleHashSetConstants
		{
			typedef _key_type key_type;

			static key_type const unused()
			{
				return std::numeric_limits<key_type>::max();
			}
		};

		template<typename _key_type>
		struct SimpleHashSetHashFunction
		{
			typedef _key_type key_type;

			static uint64_t hash(key_type const & v)
			{
				return libmaus2::hashing::EvaHash::hash642(
					reinterpret_cast<uint64_t const *>(&v),
					1
				);
			}
		};

		template<typename _key_type, typename _hash_function = SimpleHashSetHashFunction<_key_type> >
		struct SimpleHashSet : public SimpleHashSetConstants<_key_type>
		{
			typedef _key_type key_type;
			typedef _hash_function hash_function;

			typedef SimpleHashSetConstants<key_type> base_type;
			typedef SimpleHashSet<key_type,hash_function> this_type;
			typedef std::unique_ptr<this_type> unique_ptr_type;

			protected:
			unsigned int slog;
			uint64_t hashsize;
			uint64_t hashmask;
			std::atomic<uint64_t> fill;

			// hash array
			::libmaus2::autoarray::AutoArray< std::atomic<key_type> > H;

			::libmaus2::parallel::StdMutex elock;

			public:
			typedef std::atomic<key_type> const * const_iterator;
			typedef std::atomic<key_type> * iterator;

			const_iterator begin() const { return H.begin(); }
			const_iterator end() const { return H.begin()+hashsize; }
			iterator begin() { return H.begin(); }
			iterator end() { return H.begin()+hashsize; }

			uint64_t getTableSize() const
			{
				return H.size();
			}

			unique_ptr_type extend() const
			{
				unique_ptr_type O(new this_type(slog+1));
				for ( uint64_t i = 0; i < hashsize; ++i )
					if ( H[i].load() != base_type::unused() )
						O->insert ( H[i] );
				return O;
			}

			unique_ptr_type extendEmpty() const
			{
				unique_ptr_type O(new this_type(slog+1));
				return O;
			}

			void assign(this_type & from)
			{
				slog = from.slog;
				hashsize = from.hashsize;
				hashmask = from.hashmask;
				fill.store(from.fill.load());
				H = from.H;
			}

			static void copy(
				this_type const & from,
				this_type & to,
				uint64_t const blockid,
				uint64_t const numblocks
				)
			{
				uint64_t const blocksize = (from.hashsize+numblocks-1) / numblocks;
				uint64_t const idlow = blockid*blocksize;
				uint64_t const idhigh = std::min(idlow+blocksize,from.hashsize);

				for ( uint64_t i = idlow; i < idhigh; ++i )
					if ( from.H[i] != base_type::unused() )
						to.insert(from.H[i]);
			}

			template<typename key_iterator, typename index_iterator>
			void clear(key_iterator K, index_iterator I, uint64_t const n)
			{
				for ( uint64_t i = 0; i < n; ++i )
					I[i] = index(K[i]);
				for ( uint64_t i = 0; i < n; ++i )
					H[I[i]] = base_type::unused();
				fill -= n;
			}

			void extendInternal(unsigned int const logadd = 1)
			{
				if ( ((1ull<<logadd)*hashsize+fill <= H.size()) )
				{
					iterator p = H.end();
					for ( uint64_t i = 0; i < hashsize; ++i )
						if ( H[i].load() != base_type::unused() )
							(--p)->store(H[i].load());

					slog += logadd;
					hashsize = (1ull<<slog);
					hashmask = hashsize-1;

					while ( p != H.end() )
						insert(*(p++));
				}
				else
				{
					unique_ptr_type O(new this_type(slog+logadd));
					for ( uint64_t i = 0; i < hashsize; ++i )
						if ( H[i].load() != base_type::unused() )
							O->insert ( H[i].load() );

					slog = O->slog;
					hashsize = O->hashsize;
					hashmask = O->hashmask;
					fill.store(O->fill.load());
					H = O->H;
				}
			}

			SimpleHashSet(unsigned int const rslog)
			: slog(rslog), hashsize(1ull << slog), hashmask(hashsize-1), fill(0), H(hashsize,false)
			{
				for ( uint64_t i = 0; i < H.size(); ++i )
					H[i].store(base_type::unused());
			}

			void setup(unsigned int const rslog)
			{
				H.release();
				slog = rslog;
				hashsize = 1ull<<slog;
				hashmask = hashsize-1;
				fill = 0;
				H = ::libmaus2::autoarray::AutoArray<key_type>(hashsize,false);
				std::fill(H.begin(),H.end(),base_type::unused());
			}


			uint64_t hash(key_type const & v) const
			{
				return hash_function::hash(v) & hashmask;
			}

			inline uint64_t displace(uint64_t const p, uint64_t const v) const
			{
				return (p + primes16[v&0xFFFFu]) & hashmask;
			}

			uint64_t size() const
			{
				return fill;
			}

			double loadFactor() const
			{
				return static_cast<double>(fill) / hashsize;
			}

			// insert value and return count after insertion
			void insert(key_type const v)
			{
				uint64_t const p0 = hash(v);
				uint64_t p = p0;

				// uint64_t loopcnt = 0;

				do
				{
					// position in use?
					if ( H[p] != base_type::unused() )
					{
						// value already present
						if ( H[p] == v )
						{
							return;
						}
						// in use but by other value (collision)
						else
						{
							p = displace(p,v);
						}
					}
					// position is not currently in use, try to get it
					else
					{
						key_type lunused = base_type::unused();
						bool const ok = H[p].compare_exchange_strong(lunused, v);

						assert ( H[p] != base_type::unused() );

						// got it
						if ( ok )
						{
							fill += 1;
							return;
						}
						// someone else snapped position p before we got it
						else
						{
							p = displace(p,v);
						}
					}
				} while ( p != p0 );

				::libmaus2::exception::LibMausException se;
				se.getStream() << "SimpleHashSet::insert(): unable to insert, table is full." << std::endl;
				se.finish();
				throw se;
			}

			void insertExtend(key_type const k, double const loadthres = 0.7, unsigned int const logadd = 1)
			{
				while ( (loadFactor() >= loadthres) || (fill == hashsize) )
					extendInternal(logadd);

				insert(k);
			}

			// returns true if value v is contained
			bool contains(key_type const v) const
			{
				uint64_t const p0 = hash(v);
				uint64_t p = p0;

				do
				{
					// position in use?
					if ( H[p] == base_type::unused() )
					{
						return false;
					}
					// correct value stored
					else if ( H[p] == v )
					{
						return true;
					}
					else
					{
						p = displace(p,v);
					}
				} while ( p != p0 );

				return false;
			}

			// returns true if value v is contained
			uint64_t index(key_type const v) const
			{
				uint64_t const p0 = hash(v);
				uint64_t p = p0;

				do
				{
					// correct value stored
					if ( H[p] == v )
					{
						return p;
					}
					// position in use?
					if ( H[p] == base_type::unused() )
					{
						libmaus2::exception::LibMausException lme;
						lme.getStream() << "SimpleHashSet::index: key " << v << " is not contained" << std::endl;
						lme.finish();
						throw lme;
					}
					else
					{
						p = displace(p,v);
					}
				} while ( p != p0 );

				libmaus2::exception::LibMausException lme;
				lme.getStream() << "SimpleHashSet::index: key " << v << " is not contained" << std::endl;
				lme.finish();
				throw lme;
			}
		};

		template<typename _key_type>
		struct ExtendingSimpleHashSet : public SimpleHashSet<_key_type>
		{
			typedef _key_type key_type;

			typedef SimpleHashSet<key_type> base_type;
			typedef ExtendingSimpleHashSet<key_type> this_type;
			typedef std::unique_ptr<this_type> unique_ptr_type;
			typedef std::shared_ptr<this_type> shared_ptr_type;

			typename ::libmaus2::util::SimpleHashSet<key_type>::unique_ptr_type tmpSet;

			ExtendingSimpleHashSet(unsigned int const rslog) : base_type(rslog) {}

			void checkExtend(
				double const critload = 0.8
				)
			{
				if ( base_type::loadFactor() >= critload )
				{
					#if ! defined(_OPENMP)

					base_type::extendInternal();

					#else

					uint64_t const threadid = omp_get_thread_num();

					#pragma omp barrier

					if ( threadid == 0 )
						// produce empty hash map
						tmpSet = std::move(base_type::extendEmpty());

					#pragma omp barrier

					// fill empty hash map
					typedef ::libmaus2::parallel::StdMutex cnt_lock_type;
					cnt_lock_type cntlock;
					int64_t cnt = 0;

					while ( cnt < omp_get_num_threads() )
					{
						int64_t id;

						{
							cnt_lock_type::scope_lock_type scntlock(cntlock);
							if ( cnt < omp_get_num_threads() )
								id = cnt++;
							else
								id = omp_get_num_threads();
						}

						if ( id < omp_get_num_threads() )
							base_type::copy(*this,*tmpSet,id,omp_get_num_threads());
					}

					#pragma omp barrier

					if ( threadid == 0 )
					{
						// copy new hash map
						base_type::assign(*tmpSet);
						// std::cerr << "New hash map size " << base_type::getTableSize() << std::endl;
					}

					#pragma omp barrier

					#endif
				}
			}

		};
	}
}
#endif
