#ifndef TILE_BASE_FUTURE_BOXED_H
#define TILE_BASE_FUTURE_BOXED_H

#pragma once

#include "tile/base/future/basic.h"
#include "tile/base/internal/logging.h"
#include "tile/base/variant.h"

#include <tuple>

namespace tile {
namespace future {
namespace detail {
// T = Boxed<...>
template<typename T>
T RetrieveBoxed();
}// namespace detail

struct box_values_t {
    explicit box_values_t() = default;
};

constexpr auto box_values = box_values_t();

template<typename... Ts>
class Boxed {

public:
    static_assert(!types_contains<Types<Ts...>, void>::value, "Don't allow void in Boxed<Ts...>");
    using value_type = std::tuple<Ts...>;

    using lvalue_ref = internal::add_lvalue_reference_t<unboxed_type_t<Ts...>>;

    using rvalue_ref = internal::add_rvalue_reference_t<unboxed_type_t<Ts...>>;

    template<typename... Us, typename = enable_if_t<std::is_constructible<value_type, Us &&...>::value>>
    explicit Boxed(box_values_t, Us &&...imms);
    template<typename... Us, typename = enable_if_t<std::is_constructible<value_type, std::tuple<Us &&...>>::value>>
    Boxed(Boxed<Us...> boxed);

    lvalue_ref Get() &;
    rvalue_ref Get() &&;
    // internal::add_lvalue_reference_t<unboxed_type_t<Ts...>> Get() &;
    // internal::add_rvalue_reference_t<unboxed_type_t<Ts...>> Get() &&;

    value_type &GetRaw() &;
    value_type &&GetRaw() &&;

private:
    template<typename T>
    friend T detail::RetrieveBoxed();
    template<typename... Us>
    friend class Boxed;

    static constexpr std::size_t kEmpty = 0;
    static constexpr std::size_t kValue = 1;

    Boxed()                             = default;

    template<typename... Us>
    enable_if_t<sizeof...(Us) == 0, lvalue_ref> GetImpl() &
    {
        return (void) GetRaw();
    }

    template<typename... Us>
    enable_if_t<sizeof...(Us) == 1, lvalue_ref> GetImpl() &
    {
        return std::get<0>(GetRaw());
    }

    template<typename... Us>
    enable_if_t<(sizeof...(Us) > 1), lvalue_ref> GetImpl() &
    {
        return GetRaw();
    }

    template<typename... Us>
    enable_if_t<std::is_void<decltype(std::declval<Boxed<Us...>>().Get())>::value, rvalue_ref> GetMoveImpl() &
    {
        return Get();
    }

    template<typename... Us>
    enable_if_t<!std::is_void<decltype(std::declval<Boxed<Us...>>().Get())>::value, rvalue_ref> GetMoveImpl() &
    {
        return std::move(Get());
    }

private:
    mpark::variant<mpark::monostate, value_type> holding_;
};

static_assert(std::is_void<decltype(std::declval<Boxed<>>().Get())>::value, "");
static_assert(!std::is_void<decltype(std::declval<Boxed<int>>().Get())>::value, "");
static_assert(!std::is_void<decltype(std::declval<Boxed<int, char>>().Get())>::value, "");

static_assert(std::is_constructible<Boxed<int, double>, box_values_t, int, char>::value, "");
static_assert(std::is_constructible<Boxed<int, double>, Boxed<double, float>>::value, "");

namespace detail {
template<typename T>
T
RetrieveBoxed()
{
    return T();
}
}// namespace detail

template<typename... Ts>
template<typename... Us, typename>
Boxed<Ts...>::Boxed(box_values_t, Us &&...imms) : holding_(mpark::in_place_index_t<kValue>(), std::forward<Us>(imms)...)
{}

template<typename... Ts>
template<typename... Us, typename>
Boxed<Ts...>::Boxed(Boxed<Us...> boxed)
{
    holding_.template emplace<kValue>(mpark::get<kValue>(std::move(boxed).holding_));
}

template<class... Ts>
typename Boxed<Ts...>::lvalue_ref
Boxed<Ts...>::Get() &
{
    TILE_CHECK_NE(holding_.index(), kEmpty);
    return GetImpl<Ts...>();
}

template<class... Ts>
typename Boxed<Ts...>::rvalue_ref
Boxed<Ts...>::Get() &&
{
    return GetMoveImpl<Ts...>();
}

template<class... Ts>
typename Boxed<Ts...>::value_type &
Boxed<Ts...>::GetRaw() &
{
    TILE_CHECK_NE(holding_.index(), kEmpty);
    return mpark::get<kValue>(holding_);
}

template<class... Ts>
typename Boxed<Ts...>::value_type &&
Boxed<Ts...>::GetRaw() &&
{
    return std::move(GetRaw());
}

}// namespace future
}// namespace tile

namespace tile {
namespace future {

// callable by unboxed
template<typename F, typename... Ts>
auto
CallByBoxed(F &&f, Boxed<Ts...> &&boxed) -> enable_if_t<is_invocable<F, Ts...>::value, invoke_result_t<F, Ts...>>
{
    return apply(std::forward<F>(f), std::move(boxed.GetRaw()));
}

// callable by boxed
template<typename F, typename... Ts>
auto
CallByBoxed(F &&f,
            Boxed<Ts...> &&boxed) -> enable_if_t<is_invocable<F, Boxed<Ts...>>::value, invoke_result_t<F, Boxed<Ts...>>>
{
    return invoke(f, std::move(boxed));
}

template<typename F, typename... Ts>
struct call_by_boxed {
    using type = decltype(CallByBoxed(std::declval<F>(), std::declval<Boxed<Ts...> &&>()));
};

template<typename F, typename... Ts>
using call_by_boxed_t = typename call_by_boxed<F, Ts...>::type;

}// namespace future
}// namespace tile

#endif// TILE_BASE_FUTURE_BOXED_H
