#ifndef TILE_BASE_FUTURE_WHEN_ALL_H
#define TILE_BASE_FUTURE_WHEN_ALL_H

#pragma once

#include <memory>

#include "tile/base/future/basic.h"
#include "tile/base/future/future.h"
#include "tile/base/future/impls.h"
#include "tile/base/future/promise.h"
#include "tile/base/future/utils.h"

namespace tile {
namespace future {
namespace detail {

struct Flatten {
    template<typename T, typename R = internal::conditional_t<(std::tuple_size<T>::value >= 2), std::tuple<T &&>, T>>
    static auto Rebox(T &&t) -> R
    {
        return R(std::forward<T>(t));
    };

    template<typename T,
             int... Is,
             typename R = decltype(std::tuple_cat(Rebox(std::move(std::get<Is>(std::declval<T>()).GetRaw()))...))>
    auto operator()(T &&t, index_sequence<Is...>) -> R
    {
        return std::tuple_cat(Rebox(std::move(std::get<Is>(t).GetRaw()))...);
    }
};

template<typename... Ts>
struct WhenAllImpl : public std::enable_shared_from_this<WhenAllImpl<Ts...>> {
    struct Context {
        Promise<as_boxed_t<Ts>...> promise;
        std::tuple<as_boxed_t<Ts>...> receiver{future::detail::RetrieveBoxed<as_boxed_t<Ts>>()...};
        std::atomic<std::size_t> left{sizeof...(Ts)};
    };

    std::shared_ptr<Context> context{nullptr};

    // template <int I, typename...> void ForEach() {}

    template<int I, typename T>
    void ForEach(T &&future)
    {
        auto self = this->shared_from_this();
        std::move(future).Then([self, this](as_boxed_t<internal::remove_reference_t<decltype(future)>> boxed) {
            std::get<I>(context->receiver) = std::move(boxed);
            if (context->left.fetch_sub(1) == 1) { context->promise.SetValue(std::move(context->receiver)); }
        });
    }

    template<int I, typename T, typename... Us>
    void ForEach(T &&future, Us &&...futures)
    {
        ForEach<I>(std::move(future));
        ForEach<I + 1>(std::move(futures)...);
    }

    template<
        typename = enable_if_t<(sizeof...(Ts) > 0) && is_futures<Ts...>::value && are_rvalue_refs<Ts &&...>::value>>
    auto WhenAllPreservingErrors(Ts &&...futures) -> Future<as_boxed_t<Ts>...>
    {
        context = std::make_shared<Context>();
        ForEach<0>(std::move(futures)...);
        return context->promise.GetFuture();
    }

    template<
        typename   = enable_if_t<(sizeof...(Ts) > 0) && is_futures<Ts...>::value && are_rvalue_refs<Ts &&...>::value>,
        typename R = rebind_t<types_erase_t<Types<decltype(BlockingGet(std::declval<Ts>()))...>, void>, Future>>
    auto WhenAll(Ts &&...futures) -> R
    {
        // Future<...>, Future<...>, ...
        return WhenAllPreservingErrors(std::move(futures)...).Then([](Boxed<as_boxed_t<Ts>...> boxes) -> R {
            // std::tuple<Boxed<...>, Boxed<...>, ...>
            auto &&raw       = boxes.GetRaw();

            auto &&flattened = detail::Flatten()(raw, index_sequence_for<Ts...>());

            return R(futurize_tuple, std::move(flattened));
        });
    }
};

template<template<typename...> class C, typename... Ts>
struct WhenAllCImpl {
    // Collection of futures
    struct Context {
        Promise<C<Boxed<Ts...>>> promise;
        C<Boxed<Ts...>> values;
        std::atomic<std::size_t> left;
    };

    static auto WhenAllPreservingErrors(C<Future<Ts...>> &&futures) -> Future<C<Boxed<Ts...>>>
    {
        using NewFuture = Future<C<Boxed<Ts...>>>;
        if (futures.empty()) { return NewFuture(futurize_values, C<Boxed<Ts...>>()); }

        auto context = std::make_shared<Context>();
        context->values.reserve(futures.size());
        context->left.store(futures.size());

        for (std::size_t index = 0; index != futures.size(); ++index) {
            std::move(futures[index]).Then([=](Boxed<Ts...> boxed) mutable {
                context->values[index] = std::move(boxed);
                if (context->left.fetch_sub(1) == 1) { context->promise.SetValue(std::move(context->values)); }
            });
        }

        return context->promise.GetFuture();
    }

    template<typename R = internal::
                 conditional_t<std::is_void<unboxed_type_t<Ts...>>::value, Future<>, Future<C<unboxed_type_t<Ts...>>>>>
    static auto WhenAll(C<Future<Ts...>> &&futures) -> enable_if_t<!std::is_void<unboxed_type_t<Ts...>>::value, R>
    {

        return WhenAllPreservingErrors(std::move(futures)).Then([](C<Boxed<Ts...>> boxed_values) {
            C<unboxed_type_t<Ts...>> result;
            result.reserve(boxed_values.size());
            for (auto &&e : boxed_values) {
                // Mac C++11 can't support empalce_back
                result.push_back(std::move(e).Get());
            }

            return result;
        });
    }

    template<typename R = internal::
                 conditional_t<std::is_void<unboxed_type_t<Ts...>>::value, Future<>, Future<C<unboxed_type_t<Ts...>>>>>
    static auto WhenAll(C<Future<Ts...>> &&futures) -> enable_if_t<std::is_void<unboxed_type_t<Ts...>>::value, R>
    {
        return WhenAllPreservingErrors(std::move(futures)).Then([](C<Boxed<Ts...>> boxed_values) {
            (void) boxed_values;
            return;
        });
    }
};

}// namespace detail
}// namespace future
}// namespace tile

namespace tile {
namespace future {
template<typename... Ts, typename = enable_if_t<tile::future::is_futures<Ts...>::value>>
auto
WhenAll(Ts &&...futures) -> decltype(std::declval<future::detail::WhenAllImpl<Ts...>>().WhenAll(std::move(futures)...))
{
    auto impl = std::make_shared<future::detail::WhenAllImpl<Ts...>>();
    return impl->WhenAll(std::move(futures)...);
}

template<class... Ts, class = enable_if_t<tile::future::is_futures<Ts...>::value>>
auto
WhenAll(Ts *...futures) -> decltype(WhenAll(std::move(*futures)...))
{
    return WhenAll(std::move(*futures)...);
}

template<template<typename...> class C,
         class... Ts,
         typename R = internal::conditional_t<std::is_void<tile::future::unboxed_type_t<Ts...>>::value,
                                              tile::future::Future<>,
                                              tile::future::Future<C<tile::future::unboxed_type_t<Ts...>>>>>

auto
WhenAll(C<tile::future::Future<Ts...>> &&futures) -> R
{
    return future::detail::WhenAllCImpl<C, Ts...>::WhenAll(std::move(futures));
}

template<template<typename...> class C, class... Ts>
auto
WhenAll(C<tile::future::Future<Ts...>> *futures) -> decltype(WhenAll(std::move(*futures)))
{
    return WhenAll(std::move(*futures));
}
}// namespace future
}// namespace tile
#endif// TILE_BASE_FUTURE_WHEN_ALL_H
