/***********************************************************************
*
* Copyright (c) 2015-2020 Ansel Sermersheim
*
* This file is part of CsLibGuarded.
*
* CsLibGuarded is free software, released under the BSD 2-Clause license.
* For license details refer to LICENSE provided with this project.
*
* CopperSpice 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.
*
* https://opensource.org/licenses/BSD-2-Clause
*
***********************************************************************/

#ifndef CSLIBGUARDED_PLAIN_GUARDED_H
#define CSLIBGUARDED_PLAIN_GUARDED_H

#include <memory>
#include <mutex>

namespace libguarded
{

/**
   \headerfile cs_plain_guarded.h <CsLibGuarded/cs_plain_guarded.h>

   This templated class wraps an object and allows only one thread at a
   time to access the protected object.

   This class will use std::mutex for the internal locking mechanism by
   default. Other classes which are useful for the mutex type are
   std::recursive_mutex, std::timed_mutex, and
   std::recursive_timed_mutex.

   The handle returned by the various lock methods is moveable but not
   copyable.
*/
template <typename T, typename M = std::mutex>
class plain_guarded
{
   private:
      class deleter;

   public:
      using handle = std::unique_ptr<T, deleter>;

      /**
        Construct a guarded object. This constructor will accept any
        number of parameters, all of which are forwarded to the
        constructor of T.
       */
      template <typename... Us>
      plain_guarded(Us &&... data);

      /**
        Acquire a handle to the protected object. As a side effect, the
        protected object will be locked from access by any other
        thread. The lock will be automatically released when the handle
        is destroyed.
       */
      [[nodiscard]] handle lock();

      /**
        Attempt to acquire a handle to the protected object. Returns a
        null handle if the object is already locked. As a side effect,
        the protected object will be locked from access by any other
        thread. The lock will be automatically released when the handle
        is destroyed.
       */
      [[nodiscard]] handle try_lock();

      /**
        Attempt to acquire a handle to the protected object. As a side
        effect, the protected object will be locked from access by any
        other thread. The lock will be automatically released when the
        handle is destroyed.

        Returns a null handle if the object is already locked, and does
        not become available for locking before the time duration has
        elapsed.

        Calling this method requires that the underlying mutex type M
        supports the try_lock_for method.  This is not true if M is the
        default std::mutex.
       */
      template <class Duration>
      [[nodiscard]] handle try_lock_for(const Duration &duration);

      /**
        Attempt to acquire a handle to the protected object.  As a side
        effect, the protected object will be locked from access by any other
        thread. The lock will be automatically released when the handle is
        destroyed.

        Returns a null handle if the object is already locked, and does not
        become available for locking before reaching the specified timepoint.

        Calling this method requires that the underlying mutex type M
        supports the try_lock_until method.  This is not true if M is the
        default std::mutex.
       */
      template <class TimePoint>
      [[nodiscard]] handle try_lock_until(const TimePoint &timepoint);

   private:
      T m_obj;
      M m_mutex;
};

template <typename T, typename M>
class plain_guarded<T, M>::deleter
{
   public:
      using pointer = T *;

      deleter(std::unique_lock<M> lock);

      void operator()(T *ptr);

   private:
      std::unique_lock<M> m_lock;
};

template <typename T, typename M>
plain_guarded<T, M>::deleter::deleter(std::unique_lock<M> lock)
   : m_lock(std::move(lock))
{
}

template <typename T, typename M>
void plain_guarded<T, M>::deleter::operator()(T *)
{
   if (m_lock.owns_lock()) {
      m_lock.unlock();
   }
}

template <typename T, typename M>
template <typename... Us>
plain_guarded<T, M>::plain_guarded(Us &&... data)
   : m_obj(std::forward<Us>(data)...)
{
}

template <typename T, typename M>
auto plain_guarded<T, M>::lock() -> handle
{
   std::unique_lock<M> lock(m_mutex);
   return handle(&m_obj, deleter(std::move(lock)));
}

template <typename T, typename M>
auto plain_guarded<T, M>::try_lock() -> handle
{
   std::unique_lock<M> lock(m_mutex, std::try_to_lock);

   if (lock.owns_lock()) {
      return handle(&m_obj, deleter(std::move(lock)));
   } else {
      return handle(nullptr, deleter(std::move(lock)));
   }
}

template <typename T, typename M>
template <typename Duration>
auto plain_guarded<T, M>::try_lock_for(const Duration &d) -> handle
{
   std::unique_lock<M> lock(m_mutex, d);

   if (lock.owns_lock()) {
      return handle(&m_obj, deleter(std::move(lock)));
   } else {
      return handle(nullptr, deleter(std::move(lock)));
   }
}

template <typename T, typename M>
template <typename TimePoint>
auto plain_guarded<T, M>::try_lock_until(const TimePoint &tp) -> handle
{
   std::unique_lock<M> lock(m_mutex, tp);

   if (lock.owns_lock()) {
      return handle(&m_obj, deleter(std::move(lock)));
   } else {
      return handle(nullptr, deleter(std::move(lock)));
   }
}

template <typename T, typename M = std::mutex>
using guarded [[deprecated("renamed to plain_guarded")]] = plain_guarded<T, M>;

}  // namespace libguarded

#endif
