#include "base/run_set.h"
#include "base/sequence_generator.h"
#include <chrono>
#include <random>
#include <stdexcept>

//----------------------------------------------------------------

namespace {
	// - The maximum generated value is not greater than (begin+size)
	// - The generated values are not aligned to the step, if the begin
	//   value is not aligned.
	class forward_sequence_generator: public base::sequence_generator {
	public:
		forward_sequence_generator(uint64_t begin,
					   uint64_t size,
					   uint64_t step)
			: begin_(begin),
			  step_(step),
			  current_(begin),
			  rounds_(0)
		{
			verify_parameters(size, step);
			end_ = begin + (size / step) * step;
		}

		forward_sequence_generator()
			: begin_(0), end_(1), step_(1),
			  current_(0), rounds_(0) {
		}

		uint64_t next() {
			uint64_t r = current_;
			current_ += step_;
			if (current_ >= end_) {
				current_ = begin_;
				++rounds_;
			}
			return r;
		}

		void reset(uint64_t begin, uint64_t size, uint64_t step = 1) {
			verify_parameters(size, step);

			begin_ = begin;
			end_ = begin + (size / step) * step;
			step_ = step;
			current_ = begin;
			rounds_ = 0;
		}

		static void verify_parameters(uint64_t size, uint64_t step) {
			if (!size || !step)
				throw std::runtime_error("size or step must be non-zero");

			if (size < step)
				throw std::runtime_error("size must be greater than the step");
		}

		uint64_t get_rounds() {
			return rounds_;
		}

	private:
		uint64_t begin_;
		uint64_t end_;
		uint64_t step_;
		uint64_t current_;
		uint64_t rounds_;
	};

	// - The maximum generated value is not greater than (begin+size)
	// - The generated values are not aligned to the step, if the begin
	//   value is not aligned.
	class random_sequence_generator: public base::sequence_generator {
	public:
		// TODO: load random seeds
		random_sequence_generator(uint64_t begin,
					  uint64_t size,
					  uint64_t step,
					  unsigned seq_nr = 1)
			: begin_(begin),
			  step_(step),
			  max_forward_steps_(seq_nr),
			  rand_seed_(std::chrono::high_resolution_clock::now().time_since_epoch().count()),
			  results_engine_(rand_seed_),
			  steps_engine_(rand_seed_),
			  nr_generated_(0)
		{
			if (!size || !step || !seq_nr)
				throw std::runtime_error("size, step, or forward steps must be non-zero");

			if (size < step)
				throw std::runtime_error("size must be greater than the step");

			nr_steps_ = size / step;

			if (!max_forward_steps_ || max_forward_steps_ > nr_steps_)
				throw std::runtime_error("invalid number of forward steps");

			results_distr_ = std::uniform_int_distribution<uint64_t>(0,
					nr_steps_ - 1);

			if (max_forward_steps_ > 1) {
				steps_distr_ = std::uniform_int_distribution<uint64_t>(1,
						max_forward_steps_);
				reset_forward_generator();
			}
		}

		uint64_t next() {
			// FIXME: eliminate if-else
			uint64_t step_idx = (max_forward_steps_ > 1) ?
				next_forward_step() : next_random_step();

			return begin_ + step_idx * step_;
		}

	private:
		void reset_forward_generator() {
			uint64_t begin = peek_random_step();

			unsigned seq_nr = steps_distr_(steps_engine_);
			base::run_set<uint64_t>::const_iterator it = rand_map_.upper_bound(begin);
			if (it != rand_map_.end())
				seq_nr = std::min<uint64_t>(seq_nr, *it->begin_ - begin);
			else
				seq_nr = std::min<uint64_t>(seq_nr, nr_steps_ - begin);

			forward_gen_.reset(begin, seq_nr);
		}

		uint64_t next_forward_step() {
			uint64_t step_idx = forward_gen_.next();
			consume_random_map(step_idx);

			if (forward_gen_.get_rounds())
				reset_forward_generator();

			return step_idx;
		}

		uint64_t next_random_step() {
			uint64_t step_idx = peek_random_step();
			consume_random_map(step_idx);

			return step_idx;
		}

		void consume_random_map(uint64_t step_idx) {
			rand_map_.add(step_idx);

			++nr_generated_;

			// wrap-around
			if (nr_generated_ == nr_steps_) {
				rand_map_.clear();
				nr_generated_ = 0;
			}
		}

		// TODO: improve the complexity of generating unique sequence
		uint64_t peek_random_step() {
			uint64_t step_idx;

			bool found = true;
			while (found) {
				step_idx = results_distr_(results_engine_);
				found = rand_map_.member(step_idx);
			}

			return step_idx;
		}

		uint64_t begin_;
		uint64_t nr_steps_;
		uint64_t step_;
		unsigned max_forward_steps_;
		uint64_t rand_seed_;

		std::mt19937_64 results_engine_;
		std::mt19937_64 steps_engine_;
		std::uniform_int_distribution<uint64_t> results_distr_;
		std::uniform_int_distribution<uint64_t> steps_distr_;
		base::run_set<uint64_t> rand_map_;
		uint64_t nr_generated_;
		forward_sequence_generator forward_gen_;
	};
}

//----------------------------------------------------------------

base::sequence_generator::ptr
base::create_forward_sequence_generator(uint64_t begin,
					uint64_t size,
					uint64_t step)
{
	return sequence_generator::ptr(
			new forward_sequence_generator(begin, size, step));
}

base::sequence_generator::ptr
base::create_random_sequence_generator(uint64_t begin,
				       uint64_t size,
				       uint64_t step,
				       unsigned seq_nr)
{
	return sequence_generator::ptr(
			new random_sequence_generator(begin, size, step, seq_nr));
}

//----------------------------------------------------------------
