/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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
 *
 *     http://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 <folly/experimental/coro/AutoCleanup-fwd.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/Task.h>

#if FOLLY_HAS_COROUTINES

namespace folly::coro {

namespace detail {
template <typename T>
struct ScopeExitArg {
  explicit ScopeExitArg(T&) {}

  folly::coro::Task<> cleanup() && { co_return; }

  void update(T&) {}
};
} // namespace detail

/// The user can use AutoCleanup to wrap arguments passed to a
/// CleanableAsyncGenerator. When the coroutine promise of
/// CleanableAsyncGenerator is created it will automatically attach
/// co_scope_exit task that performs async cleanup for all the arguments wrapped
/// in AutoCleanup. This allows to ensure cleanup of the arguments even when
/// next() of the CleanableAsyncGenerator is never co_awaited.
///
/// Example usage:
///  folly::coro::CleanableAsyncGenerator<std::pair<int, int>> zip(
///      folly::coro::AutoCleanup<folly::coro::CleanableAsyncGenerator<int>> a,
///      folly::coro::AutoCleanup<folly::coro::CleanableAsyncGenerator<int>> b
///  ) {
///    while (true) {
///      auto x = co_await a->next();
///      if (!x) {
///        break;
///      }
///      auto y = co_await b->next();
///      if (!y) {
///        break;
///      }
///      co_yield std::make_pair(*x, *y);
///    }
///  }
///
/// In the example above, a and b will be cleaned up automatically when
/// cleanup() of the zip genertor is co_awaited, even if next() of the zip
/// generator have been never co_awaited.

template <typename T, typename CleanupFn>
class AutoCleanup : MoveOnly {
 public:
  using type = T;
  using cleanup_fn = CleanupFn;

  explicit AutoCleanup(T&& object, CleanupFn cleanupFn = CleanupFn{}) noexcept
      : ptr_{std::addressof(object)}, cleanupFn_{std::move(cleanupFn)} {}

  AutoCleanup(const AutoCleanup&) = delete;

  AutoCleanup(AutoCleanup&& other) noexcept
      : ptr_{std::exchange(other.ptr_, nullptr)},
        cleanupFn_{std::move(other.cleanupFn_)} {}

  ~AutoCleanup() { DCHECK(!kIsDebug || ptr_ == nullptr || scheduled_); }

  AutoCleanup& operator=(const AutoCleanup&) = delete;

  AutoCleanup& operator=(AutoCleanup&&) = delete;

  std::add_lvalue_reference_t<T> operator*() const
      noexcept(noexcept(*std::declval<T*>())) {
    return *get();
  }

  T* operator->() const noexcept { return get(); }

  T* get() const noexcept {
    DCHECK(!kIsDebug || scheduled_);
    return ptr_;
  }

 private:
  using BoolIfDebug = conditional_t<kIsDebug, bool, std::false_type>;

  T* ptr_;
  CleanupFn cleanupFn_;
  [[FOLLY_ATTR_NO_UNIQUE_ADDRESS]] BoolIfDebug scheduled_{};

  friend struct detail::ScopeExitArg<AutoCleanup<T, CleanupFn>>;
};

namespace detail {

template <typename T, typename CleanupFn>
struct ScopeExitArg<AutoCleanup<T, CleanupFn>> {
  T object;
  CleanupFn cleanupFn;

  explicit ScopeExitArg(AutoCleanup<T, CleanupFn>& autoCleanup)
      : object{std::move(*autoCleanup.ptr_)},
        cleanupFn{autoCleanup.cleanupFn_} {}

  auto cleanup() && { return cleanupFn(std::move(object)); }

  void update(AutoCleanup<T, CleanupFn>& autoCleanup) {
    autoCleanup.ptr_ = std::addressof(object);
    if constexpr (kIsDebug) {
      DCHECK(!autoCleanup.scheduled_);
      autoCleanup.scheduled_ = true;
    }
  }
};

template <typename Promise, typename Action, typename... Args>
auto attachScopeExit(
    coroutine_handle<Promise> coro, Action&& action, Args&&... args) {
  auto scopeExitAwaiter = co_viaIfAsync(
      nullptr,
      co_scope_exit(
          static_cast<Action&&>(action), static_cast<Args&&>(args)...));
  auto ready = scopeExitAwaiter.await_ready();
  DCHECK(!ready);
  auto suspend = scopeExitAwaiter.await_suspend(coro);
  DCHECK(!suspend);
  return scopeExitAwaiter.await_resume();
}

template <typename Promise, typename... Args>
void scheduleAutoCleanup(coroutine_handle<Promise> coro, Args&... args) {
  auto result = attachScopeExit(
      coro,
      [](auto&&... scopeExitArgs) -> Task<> {
        co_await collectAll(std::move(scopeExitArgs).cleanup()...);
      },
      detail::ScopeExitArg<Args>(args)...);
  std::apply([&](auto&... objs) { ((objs.update(args)), ...); }, result);
}

} // namespace detail

} // namespace folly::coro

#endif
