// Copyright 2021 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#pragma once

#include <chrono>
#include <optional>
#include <type_traits>

#include "pw_assert/assert.h"
#include "pw_sync/lock_annotations.h"
#include "pw_sync/lock_traits.h"
#include "pw_sync/virtual_basic_lockable.h"

namespace pw::sync {

/// The `BorrowedPointer` is an RAII handle which wraps a pointer to a borrowed
/// object along with a held lock which is guarding the object. When destroyed,
/// the lock is released.
template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class BorrowedPointer {
 public:
  /// Release the lock on destruction.
  ~BorrowedPointer() {
    if (lock_ != nullptr) {
      lock_->unlock();
    }
  }

  /// Move-constructs a ``BorrowedPointer<T>`` from a ``BorrowedPointer<U>``.
  ///
  /// This allows not only pure move construction where
  /// ``GuardedType == OtherType`` and ``Lock == OtherLock``, but also
  /// converting construction where ``GuardedType`` is a base class of
  /// ``OtherType`` and ``Lock`` is a base class of ``OtherLock``, like
  /// ``BorrowedPointer<Base> base_ptr(derived_borrowable.acquire());`
  ///
  /// @b Postcondition: The other BorrowedPointer is no longer valid and will
  ///     assert if the GuardedType is accessed.
  template <typename OtherType, typename OtherLock>
  BorrowedPointer(BorrowedPointer<OtherType, OtherLock>&& other)
      : lock_(other.lock_), object_(other.object_) {
    static_assert(
        std::is_assignable_v<GuardedType*&, OtherType*>,
        "Attempted to construct a BorrowedPointer from another whose "
        "GuardedType* is not assignable to this object's GuardedType*.");
    static_assert(std::is_assignable_v<Lock*&, OtherLock*>,
                  "Attempted to construct a BorrowedPointer from another whose "
                  "Lock* is not assignable to this object's Lock*.");
    other.lock_ = nullptr;
    other.object_ = nullptr;
  }

  /// Move-assigns a ``BorrowedPointer<T>`` from a ``BorrowedPointer<U>``.
  ///
  /// This allows not only pure move construction where
  /// ``GuardedType == OtherType`` and ``Lock == OtherLock``, but also
  /// converting construction where ``GuardedType`` is a base class of
  /// ``OtherType`` and ``Lock`` is a base class of ``OtherLock``, like
  /// ``BorrowedPointer<Base> base_ptr = derived_borrowable.acquire();`
  ///
  /// @b Postcondition: The other BorrowedPointer is no longer valid and will
  ///     assert if the GuardedType is accessed.
  template <typename OtherType, typename OtherLock>
  BorrowedPointer& operator=(BorrowedPointer<OtherType, OtherLock>&& other) {
    static_assert(
        std::is_assignable_v<GuardedType*&, OtherType*>,
        "Attempted to construct a BorrowedPointer from another whose "
        "GuardedType* is not assignable to this object's GuardedType*.");
    static_assert(std::is_assignable_v<Lock*&, OtherLock*>,
                  "Attempted to construct a BorrowedPointer from another whose "
                  "Lock* is not assignable to this object's Lock*.");
    lock_ = other.lock_;
    object_ = other.object_;
    other.lock_ = nullptr;
    other.object_ = nullptr;
    return *this;
  }
  BorrowedPointer(const BorrowedPointer&) = delete;
  BorrowedPointer& operator=(const BorrowedPointer&) = delete;

  /// Provides access to the borrowed object's members.
  GuardedType* operator->() {
    PW_ASSERT(object_ != nullptr);  // Ensure this isn't a stale moved instance.
    return object_;
  }

  /// Const overload
  const GuardedType* operator->() const {
    PW_ASSERT(object_ != nullptr);  // Ensure this isn't a stale moved instance.
    return object_;
  }

  /// Provides access to the borrowed object directly.
  ///
  /// @rst
  /// .. note::
  ///    The member of pointer member access operator, ``operator->()``, is
  ///    recommended over this API as this is prone to leaking references.
  ///    However, this is sometimes necessary.
  ///
  /// .. warning:
  ///    Be careful not to leak references to the borrowed object!
  /// @endrst
  GuardedType& operator*() {
    PW_ASSERT(object_ != nullptr);  // Ensure this isn't a stale moved instance.
    return *object_;
  }

  /// Const overload
  const GuardedType& operator*() const {
    PW_ASSERT(object_ != nullptr);  // Ensure this isn't a stale moved instance.
    return *object_;
  }

 private:
  /// Allow BorrowedPointer creation inside of Borrowable's acquire methods.
  template <typename G, typename L>
  friend class Borrowable;

  constexpr BorrowedPointer(Lock& lock, GuardedType& object)
      : lock_(&lock), object_(&object) {}

  Lock* lock_;
  GuardedType* object_;

  /// Allow converting move constructor and assignment to access fields of
  /// this class.
  ///
  /// Without this, ``BorrowedPointer<OtherType, OtherLock>`` would not be able
  /// to access fields of ``BorrowedPointer<GuardedType, Lock>``.
  template <typename OtherType, typename OtherLock>
  friend class BorrowedPointer;
};

/// The `Borrowable` is a helper construct that enables callers to borrow an
/// object which is guarded by a lock.
///
/// Users who need access to the guarded object can ask to acquire a
/// `BorrowedPointer` which permits access while the lock is held.
///
/// Thread-safety analysis is not supported for this class, as the
/// `BorrowedPointer`s it creates conditionally releases the lock. See also
/// https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#no-conditionally-held-locks
///
/// This class is compatible with locks which comply with `BasicLockable`,
/// `Lockable`, and `TimedLockable` C++ named requirements.
///
/// `Borrowable<T>` is covariant with respect to `T`, so that `Borrowable<U>`
/// can be converted to `Borrowable<T>`, if `U` is a subclass of `T`.
///
/// `Borrowable` has pointer-like semantics and should be passed by value.
template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class Borrowable {
 public:
  static_assert(is_basic_lockable_v<Lock>,
                "lock type must satisfy BasicLockable");

  constexpr Borrowable(GuardedType& object, Lock& lock) noexcept
      : lock_(&lock), object_(&object) {}

  template <typename U>
  constexpr Borrowable(const Borrowable<U, Lock>& other)
      : lock_(other.lock_), object_(other.object_) {}

  Borrowable(const Borrowable&) = default;
  Borrowable& operator=(const Borrowable&) = default;
  Borrowable(Borrowable&& other) = default;
  Borrowable& operator=(Borrowable&& other) = default;

  /// Blocks indefinitely until the object can be borrowed. Failures are fatal.
  BorrowedPointer<GuardedType, Lock> acquire() const
      PW_NO_LOCK_SAFETY_ANALYSIS {
    lock_->lock();
    return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
  }

  /// Tries to borrow the object in a non-blocking manner. Returns a
  /// BorrowedPointer on success, otherwise `std::nullopt` (nothing).
  template <int&... ExplicitArgumentBarrier,
            typename T = Lock,
            typename = std::enable_if_t<is_lockable_v<T>>>
  std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire() const
      PW_NO_LOCK_SAFETY_ANALYSIS {
    if (!lock_->try_lock()) {
      return std::nullopt;
    }
    return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
  }

  /// Tries to borrow the object. Blocks until the specified timeout has elapsed
  /// or the object has been borrowed, whichever comes first. Returns a
  /// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
  template <class Rep,
            class Period,
            int&... ExplicitArgumentBarrier,
            typename T = Lock,
            typename = std::enable_if_t<
                is_lockable_for_v<T, std::chrono::duration<Rep, Period>>>>
  std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(
      std::chrono::duration<Rep, Period> timeout) const
      PW_NO_LOCK_SAFETY_ANALYSIS {
    if (!lock_->try_lock_for(timeout)) {
      return std::nullopt;
    }
    return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
  }

  /// Tries to borrow the object. Blocks until the specified deadline has passed
  /// or the object has been borrowed, whichever comes first. Returns a
  /// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
  template <
      class Clock,
      class Duration,
      int&... ExplicitArgumentBarrier,
      typename T = Lock,
      typename = std::enable_if_t<
          is_lockable_until_v<T, std::chrono::time_point<Clock, Duration>>>>
  std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(
      std::chrono::time_point<Clock, Duration> deadline) const
      PW_NO_LOCK_SAFETY_ANALYSIS {
    if (!lock_->try_lock_until(deadline)) {
      return std::nullopt;
    }
    return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
  }

 private:
  Lock* lock_;
  GuardedType* object_;

  // Befriend all template instantiations of this class.
  template <typename, typename>
  friend class Borrowable;
};

}  // namespace pw::sync
