//
// Copyright (c) 2022 Klemens Morgenstern (klemens.morgenstern@gmx.net)
//
// 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 BOOST_COBALT_DETAIL_RACE_HPP
#define BOOST_COBALT_DETAIL_RACE_HPP

#include <boost/cobalt/detail/await_result_helper.hpp>
#include <boost/cobalt/detail/fork.hpp>
#include <boost/cobalt/detail/handler.hpp>
#include <boost/cobalt/detail/forward_cancellation.hpp>
#include <boost/cobalt/result.hpp>
#include <boost/cobalt/this_thread.hpp>
#include <boost/cobalt/detail/util.hpp>

#include <boost/asio/bind_allocator.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/associated_cancellation_slot.hpp>
#include <boost/core/no_exceptions_support.hpp>


#include <boost/intrusive_ptr.hpp>
#include <boost/core/demangle.hpp>
#include <boost/core/span.hpp>
#include <boost/variant2/variant.hpp>

#include <coroutine>
#include <optional>
#include <algorithm>


namespace boost::cobalt::detail
{

struct left_race_tag {};

// helpers it determining the type of things;
template<typename Base, // range of aw
         typename Awaitable = Base>
struct race_traits
{
  // for a ranges race this is based on the range, not the AW in it.
  constexpr static bool is_lvalue = std::is_lvalue_reference_v<Base>;

  // what the value is supposed to be cast to before the co_await_operator
  using awaitable = std::conditional_t<is_lvalue, std::decay_t<Awaitable> &, Awaitable &&>;

  // do we need operator co_await
  constexpr static bool is_actual = awaitable_type<awaitable>;

  // the type with .await_ functions & interrupt_await
  using actual_awaitable
        = std::conditional_t<
            is_actual,
              awaitable,
              decltype(get_awaitable_type(std::declval<awaitable>()))>;

  // the type to be used with interruptible
  using interruptible_type
        = std::conditional_t<
              std::is_lvalue_reference_v<Base>,
              std::decay_t<actual_awaitable> &,
              std::decay_t<actual_awaitable> &&>;

  constexpr static bool interruptible =
      cobalt::interruptible<interruptible_type>;

  static void do_interrupt(std::decay_t<actual_awaitable> & aw)
  {
    if constexpr (interruptible)
        static_cast<interruptible_type>(aw).interrupt_await();
  }

};

struct interruptible_base
{
  virtual void interrupt_await() = 0;
};

template<asio::cancellation_type Ct, typename URBG, typename ... Args>
struct race_variadic_impl
{

  template<typename URBG_>
  BOOST_COBALT_MSVC_NOINLINE
  race_variadic_impl(URBG_ && g, Args && ... args)
      : args{std::forward<Args>(args)...}, g(std::forward<URBG_>(g))
  {
  }

  std::tuple<Args...> args;
  URBG g;

  constexpr static std::size_t tuple_size = sizeof...(Args);

  struct awaitable : fork::static_shared_state<256 * tuple_size>
  {

    boost::source_location loc;

    template<std::size_t ... Idx>
    awaitable(std::tuple<Args...> & args, URBG & g, std::index_sequence<Idx...>) :
        aws{args}
    {
      if constexpr (!std::is_same_v<URBG, left_race_tag>)
        std::shuffle(impls.begin(), impls.end(), g);
      std::fill(working.begin(), working.end(), nullptr);
    }

    std::tuple<Args...> & aws;
    std::array<asio::cancellation_signal, tuple_size> cancel_;

    template<typename > constexpr static auto make_null() {return nullptr;};
    std::array<asio::cancellation_signal*, tuple_size> cancel = {make_null<Args>()...};

    std::array<interruptible_base*, tuple_size> working;

    std::size_t index{std::numeric_limits<std::size_t>::max()};

    constexpr static bool all_void = (std::is_void_v<co_await_result_t<Args>> && ... );
    std::optional<variant2::variant<void_as_monostate<co_await_result_t<Args>>...>> result;
    std::exception_ptr error;

    bool has_result() const
    {
      return index != std::numeric_limits<std::size_t>::max();
    }

    void cancel_all()
    {
      interrupt_await();
      for (auto i = 0u; i < tuple_size; i++)
        if (auto &r = cancel[i]; r)
          std::exchange(r, nullptr)->emit(Ct);
    }

    void interrupt_await()
    {
      for (auto i : working)
        if (i)
          i->interrupt_await();
    }

    template<typename T, typename Error>
    void assign_error(system::result<T, Error> & res)
    BOOST_TRY
    {
      std::move(res).value(loc);
    }
    BOOST_CATCH(...)
    {
      error = std::current_exception();
    }
    BOOST_CATCH_END

    template<typename T>
    void assign_error(system::result<T, std::exception_ptr> & res)
    {
      error = std::move(res).error();
    }

    template<std::size_t Idx>
    static detail::fork await_impl(awaitable & this_)
    BOOST_TRY
    {
      using traits = race_traits<mp11::mp_at_c<mp11::mp_list<Args...>, Idx>>;

      typename traits::actual_awaitable aw_{
          get_awaitable_type(
              static_cast<typename traits::awaitable>(std::get<Idx>(this_.aws))
              )
      };

      as_result_t aw{aw_};


      struct interruptor final : interruptible_base
      {
        std::decay_t<typename traits::actual_awaitable> & aw;
        interruptor(std::decay_t<typename traits::actual_awaitable> & aw) : aw(aw) {}
        void interrupt_await() override
        {
          traits::do_interrupt(aw);
        }
      };
      interruptor in{aw_};
      //if constexpr (traits::interruptible)
        this_.working[Idx] = &in;

      auto transaction = [&this_, idx = Idx] {
        if (this_.has_result())
          boost::throw_exception(std::runtime_error("Another transaction already started"));
        this_.cancel[idx] = nullptr;
        // reserve the index early bc
        this_.index = idx;
        this_.cancel_all();
      };

      co_await fork::set_transaction_function(transaction);
      // check manually if we're ready
      auto rd = aw.await_ready();
      if (!rd)
      {
        this_.cancel[Idx] = &this_.cancel_[Idx];
        co_await this_.cancel[Idx]->slot();
        // make sure the executor is set
        co_await detail::fork::wired_up;

        // do the await - this doesn't call await-ready again
        if constexpr (std::is_void_v<decltype(aw_.await_resume())>)
        {
          auto res = co_await aw;
          if (!this_.has_result())
          {
            this_.index = Idx;
            if (res.has_error())
              this_.assign_error(res);
          }
          if constexpr(!all_void)
            if (this_.index == Idx && !res.has_error())
              this_.result.emplace(variant2::in_place_index<Idx>);
        }
        else
        {
          auto val = co_await aw;
          if (!this_.has_result())
            this_.index = Idx;
          if (this_.index == Idx)
          {
            if (val.has_error())
              this_.assign_error(val);
            else
              this_.result.emplace(variant2::in_place_index<Idx>, *std::move(val));
          }
        }
        this_.cancel[Idx] = nullptr;
      }
      else
      {
        if (!this_.has_result())
          this_.index = Idx;
        if constexpr (std::is_void_v<decltype(aw_.await_resume())>)
        {
          auto res = aw.await_resume();
          if (this_.index == Idx)
          {
            if (res.has_error())
              this_.assign_error(res);
            else
              this_.result.emplace(variant2::in_place_index<Idx>);
          }
        }
        else
        {
          if (this_.index == Idx)
          {
            auto res = aw.await_resume();
            if (res.has_error())
              this_.assign_error(res);
            else
              this_.result.emplace(variant2::in_place_index<Idx>, *std::move(res));
          }
          else
            aw.await_resume();
        }
        this_.cancel[Idx] = nullptr;
      }
      this_.cancel_all();
      this_.working[Idx] = nullptr;
    }
    BOOST_CATCH(...)
    {
      if (!this_.has_result())
        this_.index = Idx;
      if (this_.index == Idx)
        this_.error = std::current_exception();
      this_.working[Idx] = nullptr;
    }
    BOOST_CATCH_END

    std::array<detail::fork(*)(awaitable&), tuple_size> impls {
        []<std::size_t ... Idx>(std::index_sequence<Idx...>)
        {
          return std::array<detail::fork(*)(awaitable&), tuple_size>{&await_impl<Idx>...};
        }(std::make_index_sequence<tuple_size>{})
    };

    detail::fork last_forked;

    bool await_ready()
    {
      last_forked = impls[0](*this);
      return last_forked.done();
    }

    template<typename H>
    auto await_suspend(
        std::coroutine_handle<H> h,
        const boost::source_location & loc = BOOST_CURRENT_LOCATION)
    {
      this->loc = loc;

      this->exec = cobalt::detail::get_executor(h);
      last_forked.release().resume();

      if (!this->outstanding_work()) // already done, resume rightaway.
        return false;

      for (std::size_t idx = 1u;
           idx < tuple_size; idx++) // we'
      {
        auto l = impls[idx](*this);
        const auto d = l.done();
        l.release();
        if (d)
          break;
      }

      if (!this->outstanding_work()) // already done, resume rightaway.
        return false;

      // arm the cancel
      assign_cancellation(
          h,
          [&](asio::cancellation_type ct)
          {
            for (auto & cs : cancel)
              if (cs)
                cs->emit(ct);
          });

      this->coro.reset(h.address());
      return true;
    }

    BOOST_COBALT_MSVC_NOINLINE
    auto await_resume()
    {
      if (error)
        std::rethrow_exception(error);
      if constexpr (all_void)
        return index;
      else
        return std::move(*result);
    }

    auto await_resume(const as_tuple_tag &)
    {
      if constexpr (all_void)
        return std::make_tuple(error, index);
      else
        return std::make_tuple(error, std::move(*result));
    }

    auto await_resume(const as_result_tag & )
        -> system::result<std::conditional_t<all_void, std::size_t, variant2::variant<void_as_monostate<co_await_result_t<Args>>...>>, std::exception_ptr>
    {
      if (error)
        return {system::in_place_error, error};
      if constexpr (all_void)
        return {system::in_place_value, index};
      else
        return {system::in_place_value, std::move(*result)};
    }
  };
  awaitable operator co_await() &&
  {
    return awaitable{args, g, std::make_index_sequence<tuple_size>{}};
  }
};


template<asio::cancellation_type Ct, typename URBG, typename Range>
struct race_ranged_impl
{

  using result_type = co_await_result_t<std::decay_t<decltype(*std::begin(std::declval<Range>()))>>;
  template<typename URBG_>
  BOOST_COBALT_MSVC_NOINLINE
  race_ranged_impl(URBG_ && g, Range && rng)
      : range{std::forward<Range>(rng)}, g(std::forward<URBG_>(g))
  {
  }

  Range range;
  URBG g;

  struct awaitable : fork::shared_state
  {

#if !defined(BOOST_ASIO_ENABLE_HANDLER_TRACKING)
    boost::source_location loc;
#endif

    using type = std::decay_t<decltype(*std::begin(std::declval<Range>()))>;
    using traits = race_traits<Range, type>;

    std::size_t index{std::numeric_limits<std::size_t>::max()};

    std::conditional_t<
        std::is_void_v<result_type>,
        variant2::monostate,
        std::optional<result_type>> result;

    std::exception_ptr error;

#if !defined(BOOST_COBALT_NO_PMR)
    pmr::polymorphic_allocator<void> alloc{&resource};

    Range &aws;

    struct dummy
    {
      template<typename ... Args>
      dummy(Args && ...) {}
    };

    std::conditional_t<traits::interruptible,
      pmr::vector<std::decay_t<typename traits::actual_awaitable>*>,
      dummy> working{std::size(aws), alloc};

    /* all below `reorder` is reordered
     *
     * cancel[idx] is for aws[reorder[idx]]
    */
    pmr::vector<std::size_t> reorder{std::size(aws), alloc};
    pmr::vector<asio::cancellation_signal> cancel_{std::size(aws), alloc};
    pmr::vector<asio::cancellation_signal*> cancel{std::size(aws), alloc};

#else
    Range &aws;

    struct dummy
    {
      template<typename ... Args>
      dummy(Args && ...) {}
    };

    std::conditional_t<traits::interruptible,
        std::vector<std::decay_t<typename traits::actual_awaitable>*>,
    dummy> working{std::size(aws), std::allocator<void>()};

    /* all below `reorder` is reordered
     *
     * cancel[idx] is for aws[reorder[idx]]
    */
    std::vector<std::size_t> reorder{std::size(aws), std::allocator<void>()};
    std::vector<asio::cancellation_signal> cancel_{std::size(aws), std::allocator<void>()};
    std::vector<asio::cancellation_signal*> cancel{std::size(aws), std::allocator<void>()};

#endif

    bool has_result() const {return index != std::numeric_limits<std::size_t>::max(); }


    awaitable(Range & aws, URBG & g)
        : fork::shared_state((256 + sizeof(co_awaitable_type<type>) + sizeof(std::size_t)) * std::size(aws))
        , aws(aws)
    {
      std::generate(reorder.begin(), reorder.end(), [i = std::size_t(0u)]() mutable {return i++;});
      if constexpr (traits::interruptible)
        std::fill(working.begin(), working.end(), nullptr);
      if constexpr (!std::is_same_v<URBG, left_race_tag>)
        std::shuffle(reorder.begin(), reorder.end(), g);
    }

    void cancel_all()
    {
      interrupt_await();
      for (auto & r : cancel)
        if (r)
          std::exchange(r, nullptr)->emit(Ct);
    }
    void interrupt_await()
    {
      if constexpr (traits::interruptible)
        for (auto aw : working)
          if (aw)
            traits::do_interrupt(*aw);
    }


    template<typename T, typename Error>
    void assign_error(system::result<T, Error> & res)
    BOOST_TRY
    {
      std::move(res).value(loc);
    }
    BOOST_CATCH(...)
    {
      error = std::current_exception();
    }
    BOOST_CATCH_END

    template<typename T>
    void assign_error(system::result<T, std::exception_ptr> & res)
    {
      error = std::move(res).error();
    }

    static detail::fork await_impl(awaitable & this_, std::size_t idx)
    BOOST_TRY
    {
      typename traits::actual_awaitable aw_{
          get_awaitable_type(
              static_cast<typename traits::awaitable>(*std::next(std::begin(this_.aws), idx))
              )};

      as_result_t aw{aw_};

      if constexpr (traits::interruptible)
        this_.working[idx] = &aw_;

      auto transaction = [&this_, idx = idx] {
        if (this_.has_result())
          boost::throw_exception(std::runtime_error("Another transaction already started"));
        this_.cancel[idx] = nullptr;
        // reserve the index early bc
        this_.index = idx;
        this_.cancel_all();
      };

      co_await fork::set_transaction_function(transaction);
      // check manually if we're ready
      auto rd = aw.await_ready();
      if (!rd)
      {
        this_.cancel[idx] = &this_.cancel_[idx];
        co_await this_.cancel[idx]->slot();
        // make sure the executor is set
        co_await detail::fork::wired_up;

        // do the await - this doesn't call await-ready again
        if constexpr (std::is_void_v<result_type>)
        {
          auto res = co_await aw;
          if (!this_.has_result())
          {
            if (res.has_error())
              this_.assign_error(res);
            this_.index = idx;
          }
        }
        else
        {
          auto val = co_await aw;
          if (!this_.has_result())
            this_.index = idx;
          if (this_.index == idx)
          {
            if (val.has_error())
              this_.assign_error(val);
            else
              this_.result.emplace(*std::move(val));
          }
        }
        this_.cancel[idx] = nullptr;
      }
      else
      {

        if (!this_.has_result())
          this_.index = idx;
        if constexpr (std::is_void_v<decltype(aw_.await_resume())>)
        {
          auto val = aw.await_resume();
          if (val.has_error())
            this_.assign_error(val);
        }
        else
        {
          if (this_.index == idx)
          {
            auto val = aw.await_resume();
            if (val.has_error())
              this_.assign_error(val);
            else
              this_.result.emplace(*std::move(val));
          }
          else
            aw.await_resume();
        }
        this_.cancel[idx] = nullptr;
      }
      this_.cancel_all();
      if constexpr (traits::interruptible)
        this_.working[idx] = nullptr;
    }
    BOOST_CATCH(...)
    {
      if (!this_.has_result())
        this_.index = idx;
      if (this_.index == idx)
        this_.error = std::current_exception();
      if constexpr (traits::interruptible)
        this_.working[idx] = nullptr;
    }
    BOOST_CATCH_END

    detail::fork last_forked;

    bool await_ready()
    {

      last_forked = await_impl(*this, reorder.front());
      return last_forked.done();
    }

    template<typename H>
    auto await_suspend(std::coroutine_handle<H> h,
                       const boost::source_location & loc = BOOST_CURRENT_LOCATION)
    {
      this->loc = loc;
      this->exec = detail::get_executor(h);
      last_forked.release().resume();

      if (!this->outstanding_work()) // already done, resume rightaway.
        return false;

      for (auto itr = std::next(reorder.begin());
           itr < reorder.end(); std::advance(itr, 1)) // we'
      {
        auto l = await_impl(*this, *itr);
        auto d = l.done();
        l.release();
        if (d)
          break;
      }

      if (!this->outstanding_work()) // already done, resume rightaway.
        return false;

      // arm the cancel
      assign_cancellation(
          h,
          [&](asio::cancellation_type ct)
          {
            for (auto & cs : cancel)
              if (cs)
                cs->emit(ct);
          });

      this->coro.reset(h.address());
      return true;
    }

    BOOST_COBALT_MSVC_NOINLINE
    auto await_resume()
    {
      if (error)
        std::rethrow_exception(error);
      if constexpr (std::is_void_v<result_type>)
        return index;
      else
        return std::make_pair(index, *result);
    }

    auto await_resume(const as_tuple_tag &)
    {
      if constexpr (std::is_void_v<result_type>)
        return std::make_tuple(error, index);
      else
        return std::make_tuple(error, std::make_pair(index, std::move(*result)));
    }

    auto await_resume(const as_result_tag & )
    -> system::result<std::conditional_t<std::is_void_v<result_type>, std::size_t, std::pair<std::size_t, result_type>>, std::exception_ptr>
    {
      if (error)
        return {system::in_place_error, error};
      if constexpr (std::is_void_v<result_type>)
        return {system::in_place_value, index};
      else
        return {system::in_place_value, std::make_pair(index, std::move(*result))};
    }

  };
  awaitable operator co_await() &&
  {
    return awaitable{range, g};
  }
};

}

#endif //BOOST_COBALT_DETAIL_RACE_HPP
