/* Copyright (C) 2024 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. 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 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. 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 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef INCLUDED_FUTURE
#define INCLUDED_FUTURE

#include "ps/FutureForward.h"

#include <atomic>
#include <condition_variable>
#include <exception>
#include <functional>
#include <mutex>
#include <optional>
#include <type_traits>
#include <utility>

template<typename Callback>
class PackagedTask;

class StopToken
{
public:
	explicit StopToken(const std::atomic<bool>& request) noexcept :
		m_Request{request}
	{}

	bool IsStopRequested() const noexcept
	{
		return m_Request.load();
	}
private:
	const std::atomic<bool>& m_Request;
};

template<typename Callback>
using CallbackResult = typename std::conditional_t<std::is_invocable_v<Callback, StopToken>,
	std::invoke_result<Callback, StopToken>, std::invoke_result<Callback>>::type;

namespace FutureSharedStateDetail
{
template<typename T>
using ResultHolder = std::conditional_t<std::is_void_v<T>, std::nullopt_t, std::optional<T>>;

/**
 * Responsible for syncronization between the task and the receiving thread.
 */
template<typename ResultType>
class Receiver
{
public:
	Receiver() = default;
	~Receiver()
	{
		ENSURE(IsDone());
	}

	Receiver(const Receiver&) = delete;
	Receiver(Receiver&&) = delete;

	bool IsDone() const noexcept
	{
		return m_Done.load();
	}

	void Wait()
	{
		// Fast path: we're already done.
		if (IsDone())
			return;
		// Slow path: we aren't done when we run the above check. Lock and wait until we are.
		std::unique_lock<std::mutex> lock(m_Mutex);
		m_ConditionVariable.wait(lock, [this]{ return IsDone(); });
	}

	/**
	 * Request the executing thread to stop as fast as possible. This is only
	 * a request the execution therad might ignore it.
	 * @see GetResult must not be called after a call to @p RequestStop.
	 */
	void RequestStop() noexcept
	{
		m_StopRequest.store(true);
	}

	/**
	 * Move the result away from the shared state, mark the future invalid.
	 */
	ResultType GetResult()
	{
		// The caller must ensure that this is only called if there is a result.
		ENSURE(IsDone());
		if constexpr (!std::is_void_v<ResultType>)
			ENSURE(std::get<ResultHolder<ResultType>>(m_Outcome).has_value() ||
				std::get<std::exception_ptr>(m_Outcome));

		if (std::get<std::exception_ptr>(m_Outcome))
			std::rethrow_exception(std::exchange(std::get<std::exception_ptr>(m_Outcome), {}));

		if constexpr (std::is_void_v<ResultType>)
			return;
		else
		{
			ResultType ret = std::move(*std::get<ResultHolder<ResultType>>(m_Outcome));
			std::get<ResultHolder<ResultType>>(m_Outcome).reset();
			return ret;
		}
	}

	// This is only set by the executing thread and read by the receiving thread. It is never reset.
	std::atomic<bool> m_Done{false};
	// This is only set by the receiving thread and read by the executing thread. It is never reset.
	std::atomic<bool> m_StopRequest{false};
	std::mutex m_Mutex;
	std::condition_variable m_ConditionVariable;

	// There can't be a result and an exception.
	std::tuple<ResultHolder<ResultType>, std::exception_ptr> m_Outcome{std::nullopt,
		std::exception_ptr{}};
};

/**
 * The shared state between futures and packaged state.
 */
template<typename Callback>
struct SharedState
{
	SharedState(Callback&& callbackFunc) :
		callback{std::forward<Callback>(callbackFunc)}
	{}

	Callback callback;
	Receiver<CallbackResult<Callback>> receiver;
};

} // namespace FutureSharedStateDetail

/**
 * Corresponds to std::future.
 * Unlike std::future, Future can request the cancellation of the task that would produce the result.
 * This makes it more similar to Java's CancellableTask or C#'s Task.
 * The name Future was kept over Task so it would be more familiar to C++ users,
 * but this all should be revised once Concurrency TS wraps up.
 *
 * Future is _not_ thread-safe. Call it from a single thread or ensure synchronization externally.
 *
 * The callback never runs after the @p Future is destroyed.
 */
template<typename ResultType>
class Future
{
	template<typename T>
	friend class PackagedTask;

public:
	Future() = default;
	Future(const Future& o) = delete;
	Future(Future&&) = default;
	Future& operator=(Future&& other)
	{
		CancelOrWait();
		m_Receiver = std::move(other.m_Receiver);
		return *this;
	}
	~Future()
	{
		CancelOrWait();
	}

	/**
	 * Make the future wait for the result of @a callback.
	 */
	template<typename Callback>
	PackagedTask<Callback> Wrap(Callback&& callback);

	/**
	 * Move the result out of the future, and invalidate the future.
	 * If the future is not complete, calls Wait().
	 * If the future is invalid, asserts.
	 */
	ResultType Get()
	{
		ENSURE(!!m_Receiver);

		Wait();
		// This mark the state invalid - can't call Get again.
		return std::exchange(m_Receiver, nullptr)->GetResult();
	}

	/**
	 * @return true if the shared state is valid and has a result (i.e. Get can be called).
	 */
	bool IsDone() const
	{
		return !!m_Receiver && m_Receiver->IsDone();
	}

	/**
	 * @return true if the future has a shared state and it's not been invalidated, ie. pending, started or done.
	 */
	bool Valid() const
	{
		return !!m_Receiver;
	}

	void Wait()
	{
		if (Valid())
			m_Receiver->Wait();
	}

	void CancelOrWait()
	{
		if (!Valid())
			return;
		m_Receiver->RequestStop();
		m_Receiver->Wait();
		m_Receiver.reset();
	}

protected:
	std::shared_ptr<FutureSharedStateDetail::Receiver<ResultType>> m_Receiver;
};

/**
 * Corresponds somewhat to std::packaged_task.
 * Like packaged_task, this holds a function acting as a promise.
 * This type is mostly just the shared state and the call operator,
 * handling the promise & continuation logic.
 */
template<typename Callback>
class PackagedTask
{
public:
	PackagedTask() = delete;
	PackagedTask(std::shared_ptr<FutureSharedStateDetail::SharedState<Callback>> ss) :
		m_SharedState(std::move(ss))
	{}

	void operator()()
	{
		if (!m_SharedState->receiver.m_StopRequest.load())
		{
			try
			{
				using ResultType = CallbackResult<Callback>;
				if constexpr (std::is_void_v<ResultType>)
					Invoke();
				else
					std::get<FutureSharedStateDetail::ResultHolder<ResultType>>(
						m_SharedState->receiver.m_Outcome).emplace(Invoke());
			}
			catch(...)
			{
				std::get<std::exception_ptr>(m_SharedState->receiver.m_Outcome) =
					std::current_exception();
			}
		}

		// Because we might have threads waiting on us, we need to make sure that they either:
		// - don't wait on our condition variable
		// - receive the notification when we're done.
		// This requires locking the mutex (@see Wait).
		{
			std::lock_guard<std::mutex> lock(m_SharedState->receiver.m_Mutex);
			m_SharedState->receiver.m_Done.store(true);
		}

		m_SharedState->receiver.m_ConditionVariable.notify_all();

		// We no longer need the shared state, drop it immediately.
		m_SharedState.reset();
	}

private:
	CallbackResult<Callback> Invoke()
	{
		if constexpr (std::is_invocable_v<Callback, StopToken>)
			return m_SharedState->callback(StopToken{m_SharedState->receiver.m_StopRequest});
		else
			return m_SharedState->callback();
	}

	std::shared_ptr<FutureSharedStateDetail::SharedState<Callback>> m_SharedState;
};

template<typename ResultType>
template<typename Callback>
PackagedTask<Callback> Future<ResultType>::Wrap(Callback&& callback)
{
	static_assert(std::is_same_v<CallbackResult<Callback>, ResultType>,
		"The return type of the wrapped function is not the same as the type the Future expects.");
	static_assert(std::is_invocable_v<Callback, StopToken> || !std::is_invocable_v<Callback, StopToken&>,
		"Consider taking the `StopToken` by value");
	CancelOrWait();
	auto temp = std::make_shared<FutureSharedStateDetail::SharedState<Callback>>(std::move(callback));
	m_Receiver = {temp, &temp->receiver};
	return PackagedTask<Callback>(std::move(temp));
}

#endif // INCLUDED_FUTURE
