/**
 * @file safe.h
 * @author L.-C. C.
 * @brief
 * @version 0.1
 * @date 2018-09-21
 *
 * @copyright Copyright (c) 2018
 *
 */

#pragma once

#include "accessmode.h"
#include "defaulttypes.h"
#include "mutableref.h"

#include <type_traits>
#include <utility>

#if __cplusplus >= 201703L
#define EXPLICIT_IF_CPP17 explicit
#define EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17 ReturnType
#else
#define EXPLICIT_IF_CPP17
#define EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17
#endif

namespace safe
{
	/**
	 * @brief Use this tag to default construct the mutex when constructing a
	 * Safe object.
	 */
	struct DefaultConstructMutex {};
	static constexpr DefaultConstructMutex default_construct_mutex;

	/**
	 * @brief Wraps a value together with a mutex.
	 *
	 * @tparam ValueType The type of the value to protect.
	 * @tparam MutexType The type of the mutex.
	 */
	template<typename ValueType, typename MutexType = DefaultMutex>
	class Safe
	{
	private:
		/// Type ValueType with reference removed, if present
		using RemoveRefValueType = typename std::remove_reference<ValueType>::type;
		/// Type MutexType with reference removed, if present
		using RemoveRefMutexType = typename std::remove_reference<MutexType>::type;

		/**
		 * @brief Manages a mutex and gives pointer-like access to a value
		 * object.
		 *
		 * @tparam LockType The type of the lock object that manages the
		 * mutex, example: std::lock_guard.
		 * @tparam Mode Determines the access mode of the Access
		 * object. Can be either AccessMode::ReadOnly or
		 * AccessMode::ReadWrite.
		 */
		template<template<typename> class LockType, AccessMode Mode>
		class Access
		{
			// Make sure AccessMode is ReadOnly if a read-only lock is used
			static_assert(!(AccessTraits<LockType<RemoveRefMutexType>>::IsReadOnly && Mode==AccessMode::ReadWrite), "Cannot have ReadWrite access mode with ReadOnly lock. Check the value of AccessTraits<LockType>::IsReadOnly if it exists.");

			/// ValueType with const qualifier if AccessMode is ReadOnly.
			using ConstIfReadOnlyValueType = typename std::conditional<Mode==AccessMode::ReadOnly, const RemoveRefValueType, RemoveRefValueType>::type;

		public:
			/// Pointer-to-const ValueType
			using ConstPointerType = const ConstIfReadOnlyValueType*;
			/// Pointer-to-const ValueType if Mode is ReadOnly, pointer to ValueType otherwise.
			using PointerType = ConstIfReadOnlyValueType*;
			/// Reference-to-const ValueType
			using ConstReferenceType = const ConstIfReadOnlyValueType&;
			/// Reference-to-const ValueType if Mode is ReadOnly, reference to ValueType otherwise.
			using ReferenceType = ConstIfReadOnlyValueType&;

			/**
			 * @brief Construct an Access object from a possibly const
			 * reference to the value object and any additionnal argument
			 * needed to construct the Lock object.
			 *
			 * @tparam LockArgs Deduced from lockArgs.
			 * @param value Reference to the value.
			 * @param lockArgs Arguments needed to construct the lock object.
			 */
			template<typename... OtherLockArgs>
			EXPLICIT_IF_CPP17
			Access(ReferenceType value, MutexType& mutex, OtherLockArgs&&... otherLockArgs):
				lock(mutex, std::forward<OtherLockArgs>(otherLockArgs)...),
				m_value(value)
			{}

			/**
			 * @brief Construct a read-only Access object from a const
			 * safe::Safe object and any additionnal argument needed to
			 * construct the Lock object.
			 *
			 * If needed, you can provide additionnal arguments to construct
			 * the lock object (such as std::adopt_lock). The mutex from the
			 * safe::Locakble object is already passed to the lock object's
			 * constructor though, you must not provide it.
			 *
			 * @tparam OtherLockArgs Deduced from otherLockArgs.
			 * @param safe The const Safe object to give protected access to.
			 * @param otherLockArgs Other arguments needed to construct the lock
			 * object.
			 */
			template<typename... OtherLockArgs>
			EXPLICIT_IF_CPP17
			Access(const Safe& safe, OtherLockArgs&&... otherLockArgs):
				Access(safe.m_value, safe.m_mutex.get, std::forward<OtherLockArgs>(otherLockArgs)...)
			{}

			/**
			 * @brief Construct a read-write Access object from a
			 * safe::Safe object and any additionnal argument needed to
			 * construct the Lock object.
			 *
			 * If needed, you can provide additionnal arguments to construct
			 * the lock object (such as std::adopt_lock). The mutex from the
			 * safe object is already passed to the lock object's constructor
			 * though, you must not provide it.
			 *
			 * @tparam OtherLockArgs Deduced from otherLockArgs.
			 * @param safe The Safe object to give protected access to.
			 * @param otherLockArgs Other arguments needed to construct the lock
			 * object.
			 */
			template<typename... OtherLockArgs>
			EXPLICIT_IF_CPP17
			Access(Safe& safe, OtherLockArgs&&... otherLockArgs):
				Access(safe.m_value, safe.m_mutex.get, std::forward<OtherLockArgs>(otherLockArgs)...)
			{}

			/**
			 * @brief Construct an Access object from another one.
			 * OtherLockType must implement release() like std::unique_lock
			 * does.
			 *
			 * @tparam OtherLockType Deduced from otherAccess.
			 * @tparam OtherMode Deduced from otherAccess.
			 * @tparam OtherLockArgs Deduced from otherLockArgs.
			 * @param otherAccess The Access object to construct from.
			 * @param otherLockArgs Other arguments needed to construct the lock
			 * object.
			 */
			template<template<typename> class OtherLockType, AccessMode OtherMode, typename... OtherLockArgs>
			EXPLICIT_IF_CPP17
			Access(Access<OtherLockType, OtherMode>& otherAccess, OtherLockArgs&&... otherLockArgs):
				Access(*otherAccess, *otherAccess.lock.release(), std::adopt_lock, std::forward<OtherLockArgs>(otherLockArgs)...)
			{
				static_assert(OtherMode == AccessMode::ReadWrite || OtherMode == Mode, "Cannot construct a ReadWrite Access object from a ReadOnly one!");
			}

			/**
			 * @brief Const accessor to the value.
			 * @return ConstPointerType Const pointer to the protected value.
			 */
			ConstPointerType operator->() const noexcept
			{
				return &m_value;
			}

			/**
			 * @brief Accessor to the value.
			 * @return ValuePointerType Pointer to the protected value.
			 */
			PointerType operator->() noexcept
			{
				return &m_value;
			}

			/**
			 * @brief Const accessor to the value.
			 * @return ConstValueReferenceType Const reference to the protected
			 * value.
			 */
			ConstReferenceType operator*() const noexcept
			{
				return m_value;
			}

			/**
			 * @brief Accessor to the value.
			 * @return ValueReferenceType Reference to the protected.
			 */
			ReferenceType operator*() noexcept
			{
				return m_value;
			}

			/// The lock that manages the mutex.
			mutable LockType<RemoveRefMutexType> lock;

		private:
			/// The protected value.
			ReferenceType m_value;
		};

		/// Reference-to-const ValueType.
		using ConstValueReferenceType = const RemoveRefValueType&;
		/// Reference to ValueType.
		using ValueReferenceType = RemoveRefValueType&;
		/// Reference to MutexType.
		using MutexReferenceType = RemoveRefMutexType&;

	public:
		/// Aliases to ReadAccess and WriteAccess classes for this Safe class.
		template<template<typename> class LockType=DefaultReadOnlyLock>
		using ReadAccess = Access<LockType, AccessMode::ReadOnly>;
		template<template<typename> class LockType=DefaultReadWriteLock>
		using WriteAccess = Access<LockType, AccessMode::ReadWrite>;

		/**
		 * @brief Construct a Safe object
		 */
		Safe() = default;

		/**
		 * @brief Construct a Safe object with default construction of
		 * the mutex and perfect forwarding of the other arguments to
		 * construct the value object.
		 *
		 * @tparam ValueArgs Deduced from valueArgs.
		 * @param valueArgs Perfect forwarding arguments to construct the value object.
		 * @param tag Indicates that the mutex should be default constructed.
		 */
		template<typename... ValueArgs>
		explicit Safe(DefaultConstructMutex, ValueArgs&&... valueArgs):
			m_mutex(),
			m_value(std::forward<ValueArgs>(valueArgs)...)
		{}
		/**
		 * @brief Construct a Safe object, forwarding the first
		 * argument to construct the mutex and the other arguments to
		 * construct the value object.
		 *
		 * @tparam MutexArg Deduced from mutexArg.
		 * @tparam ValueArgs Deduced from valueArgs.
		 * @param valueArgs Perfect forwarding arguments to construct the
		 * value object.
		 * @param mutexArg Perfect forwarding argument to construct the
		 * mutex object.
		 */
		template<typename MutexArg, typename... ValueArgs>
		explicit Safe(MutexArg&& mutexArg, ValueArgs&&... valueArgs):
			m_mutex{std::forward<MutexArg>(mutexArg)},
			m_value(std::forward<ValueArgs>(valueArgs)...)
		{}

		/// Delete all copy/move construction/assignment, as these operations
		/// require locking the mutex under the covers.
		/// Use copy(), assign() and other defined constructors to get the behavior
		/// you need with an explicit syntax.
		Safe(const Safe&) = delete;
		Safe(Safe&&) = delete;
		Safe& operator =(const Safe&) = delete;
		Safe& operator =(Safe&&) = delete;

		template<template<typename> class LockType=DefaultReadOnlyLock, typename... LockArgs>
		ReadAccess<LockType> readAccess(LockArgs&&... lockArgs) const
		{
			// using ReturnType = ReadAccess<LockType>;
			return EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17{*this, std::forward<LockArgs>(lockArgs)...};
		}

		template<template<typename> class LockType=DefaultReadWriteLock, typename... LockArgs>
		WriteAccess<LockType> writeAccess(LockArgs&&... lockArgs)
		{
			// using ReturnType = WriteAccess<LockType>;
			return EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17{*this, std::forward<LockArgs>(lockArgs)...};
		}

		template<template<typename> class LockType=DefaultReadOnlyLock, typename... LockArgs>
		RemoveRefValueType copy(LockArgs&&... lockArgs) const
		{
			return *readAccess<LockType>(std::forward<LockArgs>(lockArgs)...);
		}

		template<template<typename> class LockType=DefaultReadWriteLock, typename... LockArgs>
		void assign(ConstValueReferenceType value, LockArgs&&... lockArgs)
		{
			*writeAccess<LockType>(std::forward<LockArgs>(lockArgs)...) = value;
		}
		template<template<typename> class LockType=DefaultReadWriteLock, typename... LockArgs>
		void assign(RemoveRefValueType&& value, LockArgs&&... lockArgs)
		{
			*writeAccess<LockType>(std::forward<LockArgs>(lockArgs)...) = std::move(value);
		}

		/**
		 * @brief Unsafe const accessor to the value. If you use this
		 * function, you exit the realm of safe!
		 *
		 * @return ConstValueReferenceType Const reference to the value
		 * object.
		 */
		ConstValueReferenceType unsafe() const noexcept
		{
			return m_value;
		}
		/**
		 * @brief Unsafe accessor to the value. If you use this function,
		 * you exit the realm of safe!
		 *
		 * @return ValueReferenceType Reference to the value object.
		 */
		ValueReferenceType unsafe() noexcept
		{
			return m_value;
		}

		/**
		 * @brief Accessor to the mutex.
		 *
		 * @return MutexReferenceType Reference to the mutex.
		 */
		MutexReferenceType mutex() const noexcept
		{
			return m_mutex.get;
		}

	private:
		/// The helper object that holds the mutable mutex, or a reference to a mutex.
		impl::MutableIfNotReference<MutexType> m_mutex;
		/// The value to protect.
		ValueType m_value;
	};

	/**
	 * @brief Type alias for read-only Access.
	 *
	 * @tparam SafeType The type of Safe object to give read-only access to.
	 * @tparam LockType=DefaultReadOnlyLock The type of lock.
	 */
	template<
		typename SafeType,
		template<typename> class LockType=DefaultReadOnlyLock>
	using ReadAccess = typename SafeType::template ReadAccess<LockType>;

	/**
	 * @brief Type alias for read-write Access.
	 *
	 * @tparam SafeType The type of Safe object to give read-write access to.
	 * @tparam LockType=DefaultReadWriteLock The type of lock.
	 */
	template<
		typename SafeType,
		template<typename> class LockType=DefaultReadWriteLock>
	using WriteAccess = typename SafeType::template WriteAccess<LockType>;
}  // namespace safe

#undef EXPLICIT_IF_CPP17
#undef EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17
