#pragma once

#include "Future.h"
#include "Impl/ContinuationFutureType.h"
#include "Impl/RemoveFuture.h"
#include "Impl/WithTracing.h"
#include "Impl/cesium-async++.h"
#include "Library.h"
#include "Promise.h"
#include "ThreadPool.h"

#include <CesiumUtility/Tracing.h>

#include <memory>

namespace CesiumAsync {
class ITaskProcessor;

class AsyncSystem;

/**
 * @brief A system for managing asynchronous requests and tasks.
 *
 * Instances of this class may be safely and efficiently stored and passed
 * around by value. However, it is essential that the _last_ AsyncSystem
 * instance be destroyed only after all continuations have run to completion.
 * Otherwise, continuations may be scheduled using invalid scheduler instances,
 * leading to a crash. Broadly, there are two ways to achieve this:
 *
 *   * Wait until all Futures complete before destroying the "owner" of the
 *     AsyncSystem.
 *   * Make the AsyncSystem a global or static local in order to extend its
 *     lifetime all the way until program termination.
 */
class CESIUMASYNC_API AsyncSystem final {
public:
  /**
   * @brief Constructs a new instance.
   *
   * @param pTaskProcessor The interface used to run tasks in background
   * threads.
   */
  AsyncSystem(const std::shared_ptr<ITaskProcessor>& pTaskProcessor) noexcept;

  /**
   * @brief Creates a new Future by immediately invoking a function and giving
   * it the opportunity to resolve or reject a {@link Promise}.
   *
   * The {@link Promise} passed to the callback `f` may be resolved or rejected
   * asynchronously, even after the function has returned.
   *
   * This method is very similar to {@link AsyncSystem::createPromise}, except
   * that that method returns the Promise directly. The advantage of using this
   * method instead is that it is more exception-safe.  If the callback `f`
   * throws an exception, the `Future` will be rejected automatically and the
   * exception will not escape the callback.
   *
   * @tparam T The type that the Future resolves to.
   * @tparam Func The type of the callback function.
   * @param f The callback function to invoke immediately to create the Future.
   * @return A Future that will resolve when the callback function resolves the
   * supplied Promise.
   */
  template <typename T, typename Func> Future<T> createFuture(Func&& f) const {
    std::shared_ptr<async::event_task<T>> pEvent =
        std::make_shared<async::event_task<T>>();

    Promise<T> promise(this->_pSchedulers, pEvent);

    try {
      f(promise);
    } catch (...) {
      promise.reject(std::current_exception());
    }

    return Future<T>(this->_pSchedulers, pEvent->get_task());
  }

  /**
   * @brief Create a Promise that can be used at a later time to resolve or
   * reject a Future.
   *
   * Use {@link Promise<T>::getFuture} to get the Future that is resolved
   * or rejected when this Promise is resolved or rejected.
   *
   * Consider using {@link AsyncSystem::createFuture} instead of this method.
   *
   * @tparam T The type that is provided when resolving the Promise and the type
   * that the associated Future resolves to. Future.
   * @return The Promise.
   */
  template <typename T> Promise<T> createPromise() const {
    return Promise<T>(
        this->_pSchedulers,
        std::make_shared<async::event_task<T>>());
  }

  /**
   * @brief Runs a function in a worker thread, returning a Future that
   * resolves when the function completes.
   *
   * If the function itself returns a `Future`, the function will not be
   * considered complete until that returned `Future` also resolves.
   *
   * If this method is called from a designated worker thread, the
   * callback will be invoked immediately and complete before this function
   * returns.
   *
   * @tparam Func The type of the function.
   * @param f The function.
   * @return A future that resolves after the supplied function completes.
   */
  template <typename Func>
  CesiumImpl::ContinuationFutureType_t<Func, void>
  runInWorkerThread(Func&& f) const {
    static const char* tracingName = "waiting for worker thread";

    CESIUM_TRACE_BEGIN_IN_TRACK(tracingName);

    return CesiumImpl::ContinuationFutureType_t<Func, void>(
        this->_pSchedulers,
        async::spawn(
            this->_pSchedulers->workerThread.immediate,
            CesiumImpl::WithTracing<void>::end(
                tracingName,
                std::forward<Func>(f))));
  }

  /**
   * @brief Runs a function in the main thread, returning a Future that
   * resolves when the function completes.
   *
   * If the function itself returns a `Future`, the function will not be
   * considered complete until that returned `Future` also resolves.
   *
   * If this method is called from the main thread, the callback will be invoked
   * immediately and complete before this function returns.
   *
   * @tparam Func The type of the function.
   * @param f The function.
   * @return A future that resolves after the supplied function completes.
   */
  template <typename Func>
  CesiumImpl::ContinuationFutureType_t<Func, void>
  runInMainThread(Func&& f) const {
    static const char* tracingName = "waiting for main thread";

    CESIUM_TRACE_BEGIN_IN_TRACK(tracingName);

    return CesiumImpl::ContinuationFutureType_t<Func, void>(
        this->_pSchedulers,
        async::spawn(
            this->_pSchedulers->mainThread.immediate,
            CesiumImpl::WithTracing<void>::end(
                tracingName,
                std::forward<Func>(f))));
  }

  /**
   * @brief Runs a function in a thread pool, returning a Future that resolves
   * when the function completes.
   *
   * @tparam Func The type of the function.
   * @param threadPool The thread pool in which to run the function.
   * @param f The function to run.
   * @return A future that resolves after the supplied function completes.
   */
  template <typename Func>
  CesiumImpl::ContinuationFutureType_t<Func, void>
  runInThreadPool(const ThreadPool& threadPool, Func&& f) const {
    static const char* tracingName = "waiting for thread pool";

    CESIUM_TRACE_BEGIN_IN_TRACK(tracingName);

    return CesiumImpl::ContinuationFutureType_t<Func, void>(
        this->_pSchedulers,
        async::spawn(
            threadPool._pScheduler->immediate,
            CesiumImpl::WithTracing<void>::end(
                tracingName,
                std::forward<Func>(f))));
  }

  /**
   * @brief Creates a Future that resolves when every Future in a vector
   * resolves, and rejects when any Future in the vector rejects.
   *
   * If any of the Futures rejects, the returned Future rejects as well. The
   * exception included in the rejection will be from the first Future in the
   * vector that rejects.
   *
   * To get detailed rejection information from each of the Futures,
   * attach a `catchInMainThread` continuation prior to passing the
   * list into `all`.
   *
   * @tparam T The type that each Future resolves to.
   * @param futures The list of futures.
   * @return A Future that resolves when all the given Futures resolve, and
   * rejects when any Future in the vector rejects.
   */
  template <typename T>
  Future<std::vector<T>> all(std::vector<Future<T>>&& futures) const {
    return this->all<T, Future<T>>(
        std::forward<std::vector<Future<T>>>(futures));
  }

  /**
   * @brief Creates a Future that resolves when every Future in a vector
   * resolves, and rejects when any Future in the vector rejects.
   *
   * If any of the Futures rejects, the returned Future rejects as well. The
   * exception included in the rejection will be from the first Future in the
   * vector that rejects.
   *
   * To get detailed rejection information from each of the Futures,
   * attach a `catchInMainThread` continuation prior to passing the
   * list into `all`.
   *
   * @tparam T The type that each Future resolves to.
   * @param futures The list of futures.
   * @return A Future that resolves when all the given Futures resolve, and
   * rejects when any Future in the vector rejects.
   */
  template <typename T>
  Future<std::vector<T>> all(std::vector<SharedFuture<T>>&& futures) const {
    return this->all<T, SharedFuture<T>>(
        std::forward<std::vector<SharedFuture<T>>>(futures));
  }

  /**
   * @brief Creates a future that is already resolved.
   *
   * @tparam T The type of the future.
   * @param value The value for the future.
   * @return The future.
   */
  template <typename T> Future<T> createResolvedFuture(T&& value) const {
    return Future<T>(
        this->_pSchedulers,
        async::make_task<T>(std::forward<T>(value)));
  }

  /**
   * @brief Creates a future that is already resolved and resolves to no value.
   *
   * @return The future.
   */
  Future<void> createResolvedFuture() const {
    return Future<void>(this->_pSchedulers, async::make_task());
  }

  /**
   * @brief Runs all tasks that are currently queued for the main thread.
   *
   * The tasks are run in the calling thread.
   */
  void dispatchMainThreadTasks();

  /**
   * @brief Runs a single waiting task that is currently queued for the main
   * thread. If there are no tasks waiting, it returns immediately without
   * running any tasks.
   *
   * The task is run in the calling thread.
   *
   * @return true A single task was executed.
   * @return false No task was executed because none are waiting.
   */
  bool dispatchOneMainThreadTask();

  /**
   * @brief Creates a new thread pool that can be used to run continuations.
   *
   * @param numberOfThreads The number of threads in the pool.
   * @return The thread pool.
   */
  ThreadPool createThreadPool(int32_t numberOfThreads) const;

private:
  // Common implementation of 'all' for both Future and SharedFuture.
  template <typename T, typename TFutureType>
  Future<std::vector<T>> all(std::vector<TFutureType>&& futures) const {
    using TTaskType = decltype(TFutureType::_task);
    std::vector<TTaskType> tasks;
    tasks.reserve(futures.size());

    for (auto it = futures.begin(); it != futures.end(); ++it) {
      tasks.emplace_back(std::move(it->_task));
    }

    futures.clear();

    async::task<std::vector<T>> task =
        async::when_all(tasks.begin(), tasks.end())
            .then(
                async::inline_scheduler(),
                [](std::vector<TTaskType>&& tasks) {
                  // Get all the results. If any tasks rejected, we'll bail with
                  // an exception.
                  std::vector<T> results;
                  results.reserve(tasks.size());

                  for (auto it = tasks.begin(); it != tasks.end(); ++it) {
                    results.emplace_back(it->get());
                  }
                  return results;
                });
    return Future<std::vector<T>>(this->_pSchedulers, std::move(task));
  }

  std::shared_ptr<CesiumImpl::AsyncSystemSchedulers> _pSchedulers;

  template <typename T> friend class Future;
};
} // namespace CesiumAsync
