// Copyright 2020 Embedded Artistry LLC
// SPDX-License-Identifier: Licensed for use by MBARI and its subsidiaries
// Question: can we open source this?

#ifndef TIMER_MANAGER_HPP_
#define TIMER_MANAGER_HPP_

#ifdef __GNUG__
	#ifndef __clang__
		// There is a (presumed) false-positive generated with static allocation
		// and timers here - only GCC detects it, and the warning is so unhelpful
		// that it cannot even report WHERE the dereference might occur... just
		// in the struct declaration!
		#pragma GCC diagnostic ignored "-Wnull-dereference"
	#endif
#endif

#include <cassert>
#include <Timer_Interface.hpp>
#include <etl/list.h>
#include <etl/vector.h>
#include <functional>
#include <limits>
#include <list>
#include <vector>

/** Timer Manager Class
 *
 * The Timer Manager takes a hardware timer and uses it to produce a
 * number of software timers.
 *
 * An alternative structure accomplishing this task is the `callback_timer`
 * provided by ETL in `callback_timer.h`, which uses a `tick()` function:
 *		https://www.etlcpp.com/callback_timer.html
 *
 * @tparam TMaxTimers The maximum number of software timers that can be created.
 *	Size 0 indicates dynamic memory will be used. All other sizes will enable
 *	static memory allocation.
 * @tparam TTimeUnit The time-keeping units. The default `Timer::timer_period_t` is inherited
 *	from the Timer base class. This can be overriden to provide a specific timer resolution
 *	(e.g. 1ms instead of 1us). TTimeUnit is limited by the granularity that the Timer can support.
 * @tparam TTimeoutCallback The storage type for the callback function.
 * @tparam TTimerDevice The type of timer device that this class will manage.
 *	The default Timer type is the base class that framework timers derive from.
 *
 * @ingroup FrameworkHwPlatform
 */
template<const size_t TMaxTimers = 0, typename TTimeUnit = Timer::period_t,
		 typename TTimeoutCallback = std::function<void()>, typename TTimerDevice = Timer>
class TimerManager
{
  public:
	/// @brief Consumers interact with the software timer through the TimerHandle class
	class TimerHandle;

	/// @brief The function prototype for the Dispatcher.
	/// The dispacher uses inplace_function regardless of static or dynamic memory allocation.
	using DispatcherFunc = std::function<void(const TTimeoutCallback&)>;

  private:
	/// @brief Alias for the time unit representation (e.g., uint32_t, uint64_t)
	using TimeRep_t = typename TTimeUnit::rep;

	/// The timer handle can call private TimerManager functions
	friend class TimerManager::TimerHandle;

	/** Timer Delay Information
	 *
	 * The delayInfo struct is used to track information for a given time request.
	 */
	struct delayInfo
	{
		delayInfo() noexcept :
			config(Timer::Config::oneshot), current_time(0), target_time(0), cb(),
			wait_in_progress(false)
		{
			// empty body
		}

		delayInfo(const delayInfo& rhs) noexcept :
			config(rhs.config), current_time(rhs.current_time), target_time(rhs.target_time),
			cb(rhs.cb), wait_in_progress(rhs.wait_in_progress)
		{
			// empty body
		}

		delayInfo(delayInfo&& rhs) noexcept :
			config(std::move(rhs.config)), target_time(std::move(rhs.target_time)),
			cb(std::move(rhs.cb)), wait_in_progress(std::move(rhs.wait_in_progress))
		{
			rhs.wait_in_progress = false;
		}

		/// @brief The requested timer configuration
		Timer::Config config;

		/// @brief The adjusted delay time.
		/// This field allows allowing for delay adjustments due to other timer requests
		TimeRep_t current_time;

		/// @brief The original delay time
		/// This value is stored particularly for periodic timers
		TimeRep_t target_time;

		/// @brief The timer callback function.
		/// When the timer expires, this callback is called
		TTimeoutCallback cb;

		/// @brief Indicates whether the timer is currently running
		bool wait_in_progress;
	};

	/** Type definition for the underlying timer queue.
	 *
	 * This type is used to manage timers which have been allocated.
	 *
	 * The type changes depending on whether static or dynamic memory allocation is being used
	 * (dynamic memory is indicated by `TMaxTimers == 0`)
	 */
	using TTimerQueueType = typename std::conditional<(TMaxTimers == 0), std::list<delayInfo>,
													  etl::list<delayInfo, TMaxTimers>>::type;

	/// @brief Convenience alias for the queue handle type.
	/// A list iterator is used because iterators remain valid even with insertion/deletions
	using TQueueHandle = typename TTimerQueueType::iterator;

	/** Convenience type for the queue of scheduled timers.
	 *
	 * This queue is used to manage the list of currently scheduled timers.
	 *
	 * The size of the scheduled queue will be <= the size of the allocated queue.
	 *
	 * The type changes depending on whether static or dynamic memory allocation is being used
	 * (dynamic memory is indicated by `TMaxTimers == 0`)
	 */
	using TScheduledQueueType =
		typename std::conditional<(TMaxTimers == 0), std::vector<TQueueHandle>,
								  etl::vector<TQueueHandle, TMaxTimers>>::type;

	/** Comparator for sorting scheduled timers
	 *
	 * This comparison operator is used when inserting new timers into the scheduled list.
	 * Timers must be sorted so that they execute in proper timeout order.
	 */
	struct scheduleQueueCompare
	{
		bool operator()(const TQueueHandle& lhs, const TQueueHandle& rhs) noexcept
		{
			return rhs->current_time < lhs->current_time;
		}
	};

  public:
	/** Create a TimerManager without a dispatcher.
	 *
	 * Creating a TimerManager without a dispatcher will result in the manager invoking the
	 * functions.
	 *
	 * When no dispatcher is used, timer callbacks are called directly by the TimerManager.
	 *
	 * @param timer The timer hardware instance which the TimerManager will manage.
	 */
	explicit TimerManager(TTimerDevice& timer) noexcept : timer_hw_(timer)
	{
	}

	void start()
	{
		auto callback = std::bind(&TimerManager::TimerInterruptHandler, this);
		timer_hw_.registerCallback(callback);
		timer_hw_.config(Timer::Config::periodic);
	}

	/** Destroy the TimerManager
	 *
	 * On destruction, the TimerManager stops the underlying timer hardware and unregisters
	 * the TimerManager's timer interrupt callback.
	 */
	~TimerManager() noexcept
	{
		timer_hw_.stop();
		timer_hw_.registerCallback(nullptr);
	}

	/// Deleted copy constructor
	TimerManager(const TimerManager&) = delete;

	/// Deleted copy assignment operator
	const TimerManager& operator=(const TimerManager&) = delete;

	/// Deleted move constructor
	TimerManager(TimerManager&&) = delete;

	/// Deleted move assignment operator
	TimerManager& operator=(TimerManager&&) = delete;

	/** Allocate a new software timer
	 *
	 * Allocates a new timer in the TimerManager and returns a handle to the user.
	 *
	 * @returns A handle to the allocated software timer. The handle is used by the consumer
	 * 	to configure the timer. The TimerHandle's lifetime controls the timer's lifetime - once
	 *	the TimerHandle leaves scope, the timer will be automatically unregistered.
	 */
	TimerHandle allocate() noexcept
	{
		TimerHandle handle(this);

		if constexpr(TMaxTimers > 0) // NOLINT
		{
			assert(timer_list_.size() < timer_list_.max_size());
		}

		timer_list_.push_back(delayInfo());

		// We emplaced our element at the back, so decrement 1 from end() to get the iterator
		auto h = timer_list_.end();
		handle.handle_ = --h;

		return handle;
	}

  private:
	/** TimerManager Default Dispatch Function.
	 *
	 * If no external dispatcher is provided, TimerManager has one which
	 * just executes the operation by default.
	 *
	 * @param op The callback function object to execute.
	 */
	void TimerManagerDispatch(const TTimeoutCallback& op) noexcept
	{
		op();
	}

	/** TimerManager's Timer Interrupt Handler
	 *
	 * TimerManager registers its own interrupt with the Timer hardware driver.
	 * When the timer interrupt fires, we account for the time that has elapsed,
	 * clear all scheduled timers, and restart the timer hardware with the next
	 * timeout request.
	 */
	void TimerInterruptHandler() noexcept
	{
		// This logic currently depends on the following behavior on the STM32:
		// Timeout value is read from the current period
		// Additional ticks beyond the interrupt are read via count, for more accuracy
		// If we are in "one-shot" mode and not repeating mode, these ticks won't be present.
		// We then decrement period + count, then set new period.
		// There is going to be jitter, that's ok for our purposes. Higher-sensitivity
		// needs will have to be relegated to hardware timeouts

		auto count = timer_hw_.period() + timer_hw_.count();
		adjustScheduledTimerCount(count);
		clearExpiredTimers();
		startNextTimer();
	}

	/**
	 *
	 */
	void addToScheduledQueue(TQueueHandle handle) noexcept
	{
		// If we are already waiting, we need to update the existing request, but
		// we don't add to the heap
		if(!handle->wait_in_progress)
		{
			if constexpr(TMaxTimers > 0)
			{
				assert(scheduled_queue_.size() < scheduled_queue_.capacity());
			}

			scheduled_queue_.push_back(handle);

			handle->wait_in_progress = true;
		}

		sortScheduledQueue();
	}

	void popScheduledQueueFront() noexcept
	{
		// Move to end
		std::pop_heap(scheduled_queue_.begin(), scheduled_queue_.end(), scheduleQueueCompare());
		// and remove the last element
		scheduled_queue_.pop_back();
	}

	void adjustScheduledTimerCount(TTimeUnit time_base) noexcept
	{
		for(auto& s: scheduled_queue_)
		{
			if(time_base.count() <= s->current_time)
			{
				s->current_time -= time_base.count();
			}
			else
			{
				s->current_time = 0;
			}
		}
	}

	void startNextTimer() noexcept
	{
		if(scheduled_queue_.empty())
		{
			// No more work, need to stop the timer.
			stopRunningTimer();
		}
		else
		{
			timer_hw_.restart(scheduled_queue_[0]->current_time);
		}
	}

	auto stopRunningTimer() noexcept
	{
		timer_hw_.stop();
		auto time_base = timer_hw_.count();

		return time_base;
	}

	void schedule(TQueueHandle handle, TTimeUnit delay, const TTimeoutCallback& func,
				  Timer::Config config) noexcept
	{
		auto time_base = stopRunningTimer();

		adjustScheduledTimerCount(time_base);

		handle->config = config;
		handle->target_time = delay.count();
		handle->current_time = delay.count();
		handle->cb = func;

		addToScheduledQueue(handle);
		clearExpiredTimers();
		startNextTimer();
	}

	void schedule(TQueueHandle handle, TTimeUnit delay, TTimeoutCallback&& func,
				  Timer::Config config) noexcept
	{
		auto time_base = stopRunningTimer();

		adjustScheduledTimerCount(time_base);

		handle->config = config;
		handle->target_time = delay.count();
		handle->current_time = delay.count();
		handle->cb = std::move(func);

		addToScheduledQueue(handle);
		clearExpiredTimers();
		startNextTimer();
	}

	/// Returns false if no wait was scheduled or if the callback was already called
	/// Returns true if cancelled successfully
	bool cancel(TQueueHandle handle) noexcept
	{
		bool canceled = false;

		if(handle->wait_in_progress)
		{
			handle->wait_in_progress = false;
			// Check if the timer is currently running
			if(handle == scheduled_queue_.front())
			{
				// Stop the timer and update the counts for the structures
				auto count = stopRunningTimer();
				adjustScheduledTimerCount(count);

				// Remove the element from the heap and start the next timer
				popScheduledQueueFront();
				if(!scheduled_queue_.empty())
				{
					startNextTimer();
				}
			}
			else
			{
				// Remove from list
				auto it = std::find(scheduled_queue_.begin(), scheduled_queue_.end(), handle);
				assert(it != scheduled_queue_.end());
				scheduled_queue_.erase(it);
			}
			canceled = true;
		}

		return canceled;
	}

	void deleteTimer(TQueueHandle handle) noexcept
	{
		timer_list_.erase(handle);
	}

	void sortScheduledQueue() noexcept
	{
		std::make_heap(scheduled_queue_.begin(), scheduled_queue_.end(), scheduleQueueCompare());
	}

	void clearExpiredTimers() noexcept
	{
		int expired_count = 0;

		for(auto& entry: scheduled_queue_)
		{
			if(entry->wait_in_progress && entry->current_time == 0)
			{
				auto callback = entry->cb;

				// Oneshot timers are removed - periodic timers stay in the list
				if(entry->config == Timer::Config::periodic)
				{
					entry->current_time = entry->target_time;
				}
				else
				{
					entry->wait_in_progress = false;
					expired_count++;
				}

				if(callback)
				{
					callback();
				}
			}
			else
			{
				break;
			}
		}

		while(expired_count > 0)
		{
			popScheduledQueueFront();
			expired_count--;
		}
	}

  private:
	TTimerQueueType timer_list_{};
	TScheduledQueueType scheduled_queue_{};
	TTimerDevice& timer_hw_;
};

/** Handle to a software timer
 *
 * TimerHandle represents a handle to an event registration. TimerHandle are created
 * during the TimerManager::allocate() process.
 *
 * The handle is used to schedule delays:
 *	@code
 *	auto h = tm.allocate();
 *	h.asyncDelay(std::chrono::milliseconds(1), cb_called);
 *  @endcode
 *
 * The lifetime of the TimerHandle controls the lifetime of the software timer.
 * If the handle leaves scope, the timer will be automatically unregistered.
 *
 * Template parameters simply match those in the original TimerManager declaration, since
 * TimerHandle is a class member.
 */
template<const size_t TMaxTimers, typename TTimeUnit, typename TTimeoutCallback,
		 typename TTimerDevice>
class TimerManager<TMaxTimers, TTimeUnit, TTimeoutCallback, TTimerDevice>::TimerHandle final :
	public Timer
{
	friend class TimerManager<TMaxTimers, TTimeUnit, TTimeoutCallback, TTimerDevice>;

  public:
	/// Default constructor which creates an invalid object
	TimerHandle() noexcept : mgr_(nullptr)
	{
	}

	/// Destroying the TimerHandle also deletes it from the TimerManager's timer allocation queue
	~TimerHandle() noexcept
	{
		stop();
	}

	/// @brief Move Constructor
	TimerHandle(TimerHandle&& rhs) noexcept : mgr_(rhs.mgr_), handle_(rhs.handle_)
	{
		rhs.mgr_ = nullptr;
		rhs.handle_ = mgr_->timer_list_.end();
	}

	/// @brief Move assignment operator
	TimerHandle& operator=(TimerHandle&& rhs) noexcept
	{
		std::swap(mgr_, rhs.mgr_);
		std::swap(handle_, rhs.handle_);

		return *this;
	}

	/// Delete the copy constructor
	TimerHandle(const TimerHandle&) = delete;

	/// Delete the copy assignment operator
	const TimerHandle& operator=(const TimerHandle&) = delete;

	void start() noexcept final
	{
		state_ = Timer::State::armed;
		mgr_->schedule(handle_, period_, callback_, config_);
	}

	/** Cancel a software timer
	 *
	 * Request cancellation for a software timer. The cancellation request is best-effort,
	 * and the timer may expire before the request is completed.
	 *
	 * @pre The TimerHandle is valid
	 * @post The timer is cancelled, or has expired before cancellation was carried out.
	 *
	 * @returns True if the timer was cancelled, false if the timer expired before cancellation.
	 */
	void stop() noexcept final
	{
		state_ = Timer::State::stopped;
		if(valid())
		{
			mgr_->cancel(handle_);
		}
	}

	void registerCallback(const cb_t& cb) noexcept final
	{
		callback_ = cb;
	}

	void registerCallback(cb_t&& cb) noexcept final
	{
		callback_ = std::move(cb);
	}

	/** Check if the TimerHandle is valid.
	 *
	 * @returns true if the TimerHandle is valid, false otherwise
	 */
	bool valid() const noexcept
	{
		return (mgr_ != nullptr && handle_ != mgr_->timer_list_.end());
	}

	/** Operator bool checks validity.
	 *
	 * Operator bool() is used to check that a TimerHandle is valid.
	 *
	 * @returns true if the TimerHandle is valid, false otherwise
	 */
	operator bool() const noexcept
	{
		return valid();
	}

	[[nodiscard]] virtual period_t count() const noexcept final
	{
		// this is unsupported, is there a way to get it?
		if(valid())
		{
			return period_t(handle_->current_time);
		}
		else
		{
			return period_t(0);
		}
	}

  private:
	/// Private constructor, used by TimerManager::allocate() to create new TimerHandle instances
	explicit TimerHandle(TimerManager* mgr) noexcept : mgr_(mgr)
	{
	}

  private:
	/// @brief The owning TimerManager
	TimerManager* mgr_;
	TTimeoutCallback callback_;

	/// @brief The handle for the delayInfo structure which corresponds with this timer
	TQueueHandle handle_;
};

#endif // TIMER_MANAGER_HPP_
