#ifndef TILE_BASE_CASTING_H
#define TILE_BASE_CASTING_H

#pragma once

#include "tile/base/demangle.h"
#include "tile/base/internal/index_alloc.h"
#include "tile/base/internal/logging.h"
#include "tile/base/internal/meta.h"
#include "tile/base/likely.h"

namespace tile {
namespace detail {
// Has classof
template<typename T, typename Base>
struct HasClassofImpl {
    template<typename TT,
             typename TBase,
             typename = enable_if_t<std::is_same<decltype(TT::classof(std::declval<const TBase &>())), bool>::value>>
    static std::true_type test(int);
    template<typename TT, typename TBase>
    static std::false_type test(...);

    static constexpr bool value = decltype(test<T, Base>(0))::value;
};
}// namespace detail

template<typename T, typename = void>
struct CastingTraits {
    template<typename Base>
    static auto RuntimeTypeCheck(const Base &val) -> enable_if_t<detail::HasClassofImpl<T, Base>::value, bool>
    {
        return T::classof(val);
    }

    template<typename Base>
    static auto RuntimeTypeCheck(const Base &val) -> enable_if_t<!detail::HasClassofImpl<T, Base>::value, bool>
    {
        return dynamic_cast<const T *>(&val) != nullptr;
    }
};

class Castable {
    class Dummy {};

public:
    friend void SetRuntimeType(Castable *ptr, int type, Dummy = {}) { ptr->type_ = type; }

    friend int GetRuntimeType(const Castable &val, Dummy = {}) { return val.type_; }

protected:
    template<typename, typename>
    friend struct CastingTraits;
    ~Castable() = default;

    template<typename T>
    void SetRuntimeTypeTo()
    {
        SetRuntimeType(this, GetRuntimeTypeId<T>());
    }

    template<typename T>
    int GetRuntimeType() const
    {
        return GetRuntimeTypeId<T>();
    }

    template<typename T>
    static int GetRuntimeTypeId()
    {
        static const int value = internal::IndexAlloc::For<Castable>()->Next();
        return value;
    }

private:
    std::int_fast32_t type_ = GetRuntimeTypeId<Castable>();
};

// ExactMatchCastable
class ExactMatchCastable : public Castable {
protected:
    ~ExactMatchCastable() = default;
};

template<typename T>
struct CastingTraits<T, enable_if_t<std::is_base_of<ExactMatchCastable, T>::value>> {
    static bool RuntimeTypeCheck(const ExactMatchCastable &object)
    {
        return GetRuntimeType(object) == Castable::GetRuntimeTypeId<T>();
    }
};

namespace casting {
namespace detail {
template<typename T,
         typename Base,
         typename = enable_if_t<std::is_base_of<Base, T>::value && !std::is_same<Base, T>::value>>
inline bool
RuntimeTypeCheck(const Base &val)
{
    return CastingTraits<T>::RuntimeTypeCheck(val);
}

template<typename T>
inline bool
RuntimeTypeCheck(const T &val)
{
    return true;
}

template<typename T, typename Base>
using casted_type_t = internal::conditional_t<std::is_const<Base>::value, const T *, T *>;

template<typename T, typename Base>
auto
ReallyCastable(Base *ptr) -> enable_if_t<std::has_virtual_destructor<T>::value, bool>
{
    return dynamic_cast<const T *>(ptr) != nullptr;
}

template<typename T, typename Base>
auto
ReallyCastable(Base *ptr) -> enable_if_t<!std::has_virtual_destructor<T>::value, bool>
{
    return typeid(T) == typeid(*ptr);
}

template<typename T, typename Base>
void
InvalidCast(Base *ptr)
{
    if (ReallyCastable<T, Base>(ptr)) {
        TILE_LOG_FATAL("Casting to type [{}] failed. However, the C++ runtime reports that "
                       "you're indeed casting to the (right) runtime type of the object. This "
                       "can happen when either: 1) you haven't initialize object's runtime "
                       "type correctly (e.g. via `SetRuntimeTypeTo` if you're inheriting from "
                       "`ExactMatchCast`), or 2) the implementation of your `classof` is "
                       "incorrect.",
                       GetTypeName<T>());
    } else {
        TILE_LOG_FATAL("Invalid cast: Runtime type [{}] expected, got [{}]. If you believe "
                       "this is an error, check if your `classof` is implemented correctly",
                       GetTypeName<T>(), GetTypeName(*ptr));
    }
    TILE_UNREACHABLE("");
}

}// namespace detail
}// namespace casting

template<typename T, typename Base, typename = enable_if_t<!std::is_pointer<Base>::value>>
inline bool
isa(const Base &val)
{
    return casting::detail::RuntimeTypeCheck<T>(val);
}

template<typename T, typename Base>
inline bool
isa(const Base *val)
{
    return isa<T>(*val);
}

template<typename T, typename Base, typename R = casting::detail::casted_type_t<T, Base>>
R
dyn_cast(Base *ptr)
{
    return isa<T>(ptr) ? static_cast<R>(ptr) : nullptr;
}

template<typename T, typename Base, typename = enable_if_t<!std::is_pointer<Base>::value>>
inline auto
dyn_cast(Base &val) -> decltype(dyn_cast<T>(std::declval<Base *>()))
{
    return dyn_cast<T>(&val);
}

template<typename T, typename Base>
inline auto
dyn_cast_or_null(Base *ptr) -> decltype(dyn_cast<T>(ptr))
{
    return ptr ? dyn_cast<T>(ptr) : nullptr;
}

template<typename T, typename Base, typename R = casting::detail::casted_type_t<T, Base>>
inline R
cast(Base *ptr)
{
    if (TILE_LIKELY(isa<T>(ptr))) { return static_cast<R>(ptr); }
    casting::detail::InvalidCast<T>(ptr);
    return nullptr;
}

template<typename T, typename Base, typename = enable_if_t<!std::is_pointer<Base>::value>>
inline auto
cast(Base &val) -> decltype(cast<T>(std::declval<Base *>()))
{
    return cast<T>(&val);
}

template<typename T, typename Base>
inline auto
cast_or_null(Base *ptr) -> decltype(cast<T>(ptr))
{
    return ptr ? cast<T>(ptr) : nullptr;
}

}// namespace tile

#endif// TILE_BASE_CASTING_H
