//
// experimental/awaitable_operators.hpp
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2021 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#ifndef ASIO_EXPERIMENTAL_AWAITABLE_OPERATORS_HPP
#define ASIO_EXPERIMENTAL_AWAITABLE_OPERATORS_HPP

#if defined(_MSC_VER) && (_MSC_VER >= 1200)
# pragma once
#endif // defined(_MSC_VER) && (_MSC_VER >= 1200)

#include "asio/detail/config.hpp"
#include <optional>
#include <stdexcept>
#include <tuple>
#include <variant>
#include "asio/awaitable.hpp"
#include "asio/detail/type_traits.hpp"
#include "asio/experimental/deferred.hpp"
#include "asio/experimental/parallel_group.hpp"
#include "asio/multiple_exceptions.hpp"
#include "asio/this_coro.hpp"
#include "asio/use_awaitable.hpp"

#include "asio/detail/push_options.hpp"

namespace asio {
namespace experimental {
namespace awaitable_operators {
namespace detail {

template <typename T, typename Executor>
awaitable<T, Executor> awaitable_wrap(awaitable<T, Executor> a,
    typename constraint<is_constructible<T>::value>::type* = 0)
{
  return a;
}

template <typename T, typename Executor>
awaitable<std::optional<T>, Executor> awaitable_wrap(awaitable<T, Executor> a,
    typename constraint<!is_constructible<T>::value>::type* = 0)
{
  co_return std::optional<T>(co_await std::move(a));
}

template <typename T>
T& awaitable_unwrap(typename conditional<true, T, void>::type& r,
    typename constraint<is_constructible<T>::value>::type* = 0)
{
  return r;
}

template <typename T>
T& awaitable_unwrap(std::optional<typename conditional<true, T, void>::type>& r,
    typename constraint<!is_constructible<T>::value>::type* = 0)
{
  return *r;
}

} // namespace detail

/// Wait for both operations to succeed.
/**
 * If one operations fails, the other is cancelled as the AND-condition can no
 * longer be satisfied.
 */
template <typename Executor>
awaitable<void, Executor> operator&&(
    awaitable<void, Executor> t, awaitable<void, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, ex1] =
    co_await make_parallel_group(
      co_spawn(ex, std::move(t), deferred),
      co_spawn(ex, std::move(u), deferred)
    ).async_wait(
      wait_for_one_error(),
      use_awaitable_t<Executor>{}
    );

  if (ex0 && ex1)
    throw multiple_exceptions(ex0);
  if (ex0)
    std::rethrow_exception(ex0);
  if (ex1)
    std::rethrow_exception(ex1);
  co_return;
}

/// Wait for both operations to succeed.
/**
 * If one operations fails, the other is cancelled as the AND-condition can no
 * longer be satisfied.
 */
template <typename U, typename Executor>
awaitable<U, Executor> operator&&(
    awaitable<void, Executor> t, awaitable<U, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, ex1, r1] =
    co_await make_parallel_group(
      co_spawn(ex, std::move(t), deferred),
      co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)
    ).async_wait(
      wait_for_one_error(),
      use_awaitable_t<Executor>{}
    );

  if (ex0 && ex1)
    throw multiple_exceptions(ex0);
  if (ex0)
    std::rethrow_exception(ex0);
  if (ex1)
    std::rethrow_exception(ex1);
  co_return std::move(detail::awaitable_unwrap<U>(r1));
}

/// Wait for both operations to succeed.
/**
 * If one operations fails, the other is cancelled as the AND-condition can no
 * longer be satisfied.
 */
template <typename T, typename Executor>
awaitable<T, Executor> operator&&(
    awaitable<T, Executor> t, awaitable<void, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, std::move(u), deferred)
    ).async_wait(
      wait_for_one_error(),
      use_awaitable_t<Executor>{}
    );

  if (ex0 && ex1)
    throw multiple_exceptions(ex0);
  if (ex0)
    std::rethrow_exception(ex0);
  if (ex1)
    std::rethrow_exception(ex1);
  co_return std::move(detail::awaitable_unwrap<T>(r0));
}

/// Wait for both operations to succeed.
/**
 * If one operations fails, the other is cancelled as the AND-condition can no
 * longer be satisfied.
 */
template <typename T, typename U, typename Executor>
awaitable<std::tuple<T, U>, Executor> operator&&(
    awaitable<T, Executor> t, awaitable<U, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1, r1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)
    ).async_wait(
      wait_for_one_error(),
      use_awaitable_t<Executor>{}
    );

  if (ex0 && ex1)
    throw multiple_exceptions(ex0);
  if (ex0)
    std::rethrow_exception(ex0);
  if (ex1)
    std::rethrow_exception(ex1);
  co_return std::make_tuple(
      std::move(detail::awaitable_unwrap<T>(r0)),
      std::move(detail::awaitable_unwrap<U>(r1)));
}

/// Wait for both operations to succeed.
/**
 * If one operations fails, the other is cancelled as the AND-condition can no
 * longer be satisfied.
 */
template <typename... T, typename Executor>
awaitable<std::tuple<T..., std::monostate>, Executor> operator&&(
    awaitable<std::tuple<T...>, Executor> t, awaitable<void, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1, r1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, std::move(u), deferred)
    ).async_wait(
      wait_for_one_error(),
      use_awaitable_t<Executor>{}
    );

  if (ex0 && ex1)
    throw multiple_exceptions(ex0);
  if (ex0)
    std::rethrow_exception(ex0);
  if (ex1)
    std::rethrow_exception(ex1);
  co_return std::move(detail::awaitable_unwrap<std::tuple<T...>>(r0));
}

/// Wait for both operations to succeed.
/**
 * If one operations fails, the other is cancelled as the AND-condition can no
 * longer be satisfied.
 */
template <typename... T, typename U, typename Executor>
awaitable<std::tuple<T..., U>, Executor> operator&&(
    awaitable<std::tuple<T...>, Executor> t, awaitable<U, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1, r1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)
    ).async_wait(
      wait_for_one_error(),
      use_awaitable_t<Executor>{}
    );

  if (ex0 && ex1)
    throw multiple_exceptions(ex0);
  if (ex0)
    std::rethrow_exception(ex0);
  if (ex1)
    std::rethrow_exception(ex1);
  co_return std::tuple_cat(
      std::move(detail::awaitable_unwrap<std::tuple<T...>>(r0)),
      std::make_tuple(std::move(detail::awaitable_unwrap<U>(r1))));
}

/// Wait for one operation to succeed.
/**
 * If one operations succeeds, the other is cancelled as the OR-condition is
 * already satisfied.
 */
template <typename Executor>
awaitable<std::variant<std::monostate, std::monostate>, Executor> operator||(
    awaitable<void, Executor> t, awaitable<void, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, ex1] =
    co_await make_parallel_group(
      co_spawn(ex, std::move(t), deferred),
      co_spawn(ex, std::move(u), deferred)
    ).async_wait(
      wait_for_one_success(),
      use_awaitable_t<Executor>{}
    );

  if (order[0] == 0)
  {
    if (!ex0)
      co_return std::variant<std::monostate, std::monostate>{
          std::in_place_index<0>};
    if (!ex1)
      co_return std::variant<std::monostate, std::monostate>{
          std::in_place_index<1>};
    throw multiple_exceptions(ex0);
  }
  else
  {
    if (!ex1)
      co_return std::variant<std::monostate, std::monostate>{
          std::in_place_index<1>};
    if (!ex0)
      co_return std::variant<std::monostate, std::monostate>{
          std::in_place_index<0>};
    throw multiple_exceptions(ex1);
  }
}

/// Wait for one operation to succeed.
/**
 * If one operations succeeds, the other is cancelled as the OR-condition is
 * already satisfied.
 */
template <typename U, typename Executor>
awaitable<std::variant<std::monostate, U>, Executor> operator||(
    awaitable<void, Executor> t, awaitable<U, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, ex1, r1] =
    co_await make_parallel_group(
      co_spawn(ex, std::move(t), deferred),
      co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)
    ).async_wait(
      wait_for_one_success(),
      use_awaitable_t<Executor>{}
    );

  if (order[0] == 0)
  {
    if (!ex0)
      co_return std::variant<std::monostate, U>{
          std::in_place_index<0>};
    if (!ex1)
      co_return std::variant<std::monostate, U>{
          std::in_place_index<1>,
          std::move(detail::awaitable_unwrap<U>(r1))};
    throw multiple_exceptions(ex0);
  }
  else
  {
    if (!ex1)
      co_return std::variant<std::monostate, U>{
          std::in_place_index<1>,
          std::move(detail::awaitable_unwrap<U>(r1))};
    if (!ex0)
      co_return std::variant<std::monostate, U>{
          std::in_place_index<0>};
    throw multiple_exceptions(ex1);
  }
}

/// Wait for one operation to succeed.
/**
 * If one operations succeeds, the other is cancelled as the OR-condition is
 * already satisfied.
 */
template <typename T, typename Executor>
awaitable<std::variant<T, std::monostate>, Executor> operator||(
    awaitable<T, Executor> t, awaitable<void, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, std::move(u), deferred)
    ).async_wait(
      wait_for_one_success(),
      use_awaitable_t<Executor>{}
    );

  if (order[0] == 0)
  {
    if (!ex0)
      co_return std::variant<T, std::monostate>{
          std::in_place_index<0>,
          std::move(detail::awaitable_unwrap<T>(r0))};
    if (!ex1)
      co_return std::variant<T, std::monostate>{
          std::in_place_index<1>};
    throw multiple_exceptions(ex0);
  }
  else
  {
    if (!ex1)
      co_return std::variant<T, std::monostate>{
          std::in_place_index<1>};
    if (!ex0)
      co_return std::variant<T, std::monostate>{
          std::in_place_index<0>,
          std::move(detail::awaitable_unwrap<T>(r0))};
    throw multiple_exceptions(ex1);
  }
}

/// Wait for one operation to succeed.
/**
 * If one operations succeeds, the other is cancelled as the OR-condition is
 * already satisfied.
 */
template <typename T, typename U, typename Executor>
awaitable<std::variant<T, U>, Executor> operator||(
    awaitable<T, Executor> t, awaitable<U, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1, r1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)
    ).async_wait(
      wait_for_one_success(),
      use_awaitable_t<Executor>{}
    );

  if (order[0] == 0)
  {
    if (!ex0)
      co_return std::variant<T, U>{
          std::in_place_index<0>,
          std::move(detail::awaitable_unwrap<T>(r0))};
    if (!ex1)
      co_return std::variant<T, U>{
          std::in_place_index<1>,
          std::move(detail::awaitable_unwrap<U>(r1))};
    throw multiple_exceptions(ex0);
  }
  else
  {
    if (!ex1)
      co_return std::variant<T, U>{
          std::in_place_index<1>,
          std::move(detail::awaitable_unwrap<U>(r1))};
    if (!ex0)
      co_return std::variant<T, U>{
          std::in_place_index<0>,
          std::move(detail::awaitable_unwrap<T>(r0))};
    throw multiple_exceptions(ex1);
  }
}

namespace detail {

template <typename... T>
struct widen_variant
{
  template <std::size_t I, typename SourceVariant>
  static std::variant<T...> call(SourceVariant& source)
  {
    if (source.index() == I)
      return std::variant<T...>{
          std::in_place_index<I>, std::move(std::get<I>(source))};
    else if constexpr (I + 1 < std::variant_size_v<SourceVariant>)
      return call<I + 1>(source);
    else
      throw std::logic_error("empty variant");
  }
};

} // namespace detail

/// Wait for one operation to succeed.
/**
 * If one operations succeeds, the other is cancelled as the OR-condition is
 * already satisfied.
 */
template <typename... T, typename Executor>
awaitable<std::variant<T..., std::monostate>, Executor> operator||(
    awaitable<std::variant<T...>, Executor> t, awaitable<void, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, std::move(u), deferred)
    ).async_wait(
      wait_for_one_success(),
      use_awaitable_t<Executor>{}
    );

  using widen = detail::widen_variant<T..., std::monostate>;
  if (order[0] == 0)
  {
    if (!ex0)
      co_return widen::template call<0>(
          detail::awaitable_unwrap<std::variant<T...>>(r0));
    if (!ex1)
      co_return std::variant<T..., std::monostate>{
          std::in_place_index<sizeof...(T)>};
    throw multiple_exceptions(ex0);
  }
  else
  {
    if (!ex1)
      co_return std::variant<T..., std::monostate>{
          std::in_place_index<sizeof...(T)>};
    if (!ex0)
      co_return widen::template call<0>(
          detail::awaitable_unwrap<std::variant<T...>>(r0));
    throw multiple_exceptions(ex1);
  }
}

/// Wait for one operation to succeed.
/**
 * If one operations succeeds, the other is cancelled as the OR-condition is
 * already satisfied.
 */
template <typename... T, typename U, typename Executor>
awaitable<std::variant<T..., U>, Executor> operator||(
    awaitable<std::variant<T...>, Executor> t, awaitable<U, Executor> u)
{
  auto ex = co_await this_coro::executor;

  auto [order, ex0, r0, ex1, r1] =
    co_await make_parallel_group(
      co_spawn(ex, detail::awaitable_wrap(std::move(t)), deferred),
      co_spawn(ex, detail::awaitable_wrap(std::move(u)), deferred)
    ).async_wait(
      wait_for_one_success(),
      use_awaitable_t<Executor>{}
    );

  using widen = detail::widen_variant<T..., U>;
  if (order[0] == 0)
  {
    if (!ex0)
      co_return widen::template call<0>(
          detail::awaitable_unwrap<std::variant<T...>>(r0));
    if (!ex1)
      co_return std::variant<T..., U>{
          std::in_place_index<sizeof...(T)>,
          std::move(detail::awaitable_unwrap<U>(r1))};
    throw multiple_exceptions(ex0);
  }
  else
  {
    if (!ex1)
      co_return std::variant<T..., U>{
          std::in_place_index<sizeof...(T)>,
          std::move(detail::awaitable_unwrap<U>(r1))};
    if (!ex0)
      co_return widen::template call<0>(
          detail::awaitable_unwrap<std::variant<T...>>(r0));
    throw multiple_exceptions(ex1);
  }
}

} // namespace awaitable_operators
} // namespace experimental
} // namespace asio

#include "asio/detail/pop_options.hpp"

#endif // ASIO_EXPERIMENTAL_AWAITABLE_OPERATORS_HPP
