#include "enum_runtime.h"

#include <util/generic/algorithm.h>
#include <util/generic/map.h>
#include <util/generic/yexception.h>
#include <util/stream/output.h>

namespace NEnumSerializationRuntime {
    template <typename TEnumRepresentationType>
    [[noreturn]] static void ThrowUndefinedValueException(const TEnumRepresentationType key, const TStringBuf className) {
        throw yexception() << "Undefined value " << key << " in " << className << ". ";
    }

    template <typename TEnumRepresentationType>
    const TString& TEnumDescriptionBase<TEnumRepresentationType>::ToString(TRepresentationType key) const {
        const auto it = Names.find(key);
        if (Y_LIKELY(it != Names.end())) {
            return it->second;
        }
        ThrowUndefinedValueException(key, ClassName);
    }

    template <typename TEnumRepresentationType>
    std::pair<bool, TEnumRepresentationType> TEnumDescriptionBase<TEnumRepresentationType>::TryFromString(const TStringBuf name) const {
        const auto it = Values.find(name);
        if (it != Values.end()) {
            return {true, it->second};
        }
        return {false, TRepresentationType()};
    }

    template <class TContainer, class TNeedle, class TGetKey>
    static typename TContainer::value_type const* FindPtrInSortedContainer(const TContainer& vec, const TNeedle& needle, TGetKey&& getKey) {
        const auto it = LowerBoundBy(vec.begin(), vec.end(), needle, getKey);
        if (it == vec.end()) {
            return nullptr;
        }
        if (getKey(*it) != needle) {
            return nullptr;
        }
        return std::addressof(*it);
    }

    template <typename TEnumRepresentationType>
    std::pair<bool, TEnumRepresentationType> TEnumDescriptionBase<TEnumRepresentationType>::TryFromStringSorted(const TStringBuf name, const TInitializationData& enumInitData) {
        const auto& vec = enumInitData.ValuesInitializer;
        const auto* ptr = FindPtrInSortedContainer(vec, name, std::mem_fn(&TEnumStringPair::Name));
        if (ptr) {
            return {true, ptr->Key};
        }
        return {false, TRepresentationType()};
    }

    template <typename TEnumRepresentationType>
    std::pair<bool, TEnumRepresentationType> TEnumDescriptionBase<TEnumRepresentationType>::TryFromStringFullScan(const TStringBuf name, const TInitializationData& enumInitData) {
        const auto& vec = enumInitData.ValuesInitializer;
        const auto* ptr = FindIfPtr(vec, [&](const auto& item) { return item.Name == name; });
        if (ptr) {
            return {true, ptr->Key};
        }
        return {false, TRepresentationType()};
    }

    [[noreturn]] static void ThrowUndefinedNameException(const TStringBuf name, const TStringBuf className, const TStringBuf allEnumNames) {
        ythrow yexception() << "Key '" << name << "' not found in enum " << className << ". Valid options are: " << allEnumNames << ". ";
    }

    template <typename TEnumRepresentationType>
    [[noreturn]] static void ThrowUndefinedNameException(const TStringBuf name, const typename TEnumDescriptionBase<TEnumRepresentationType>::TInitializationData& enumInitData) {
        auto exc = __LOCATION__ + yexception() << "Key '" << name << "' not found in enum " << enumInitData.ClassName << ". Valid options are: ";
        const auto& vec = enumInitData.NamesInitializer;
        for (size_t i = 0; i < vec.size(); ++i) {
            if (i != 0) {
                exc << ", ";
            }
            exc << '\'' << vec[i].Name << '\'';
        }
        exc << ". ";
        throw exc;
    }

    template <typename TEnumRepresentationType>
    auto TEnumDescriptionBase<TEnumRepresentationType>::FromString(const TStringBuf name) const -> TRepresentationType {
        const auto findResult = TryFromString(name);
        if (Y_LIKELY(findResult.first)) {
            return findResult.second;
        }
        ThrowUndefinedNameException(name, ClassName, AllEnumNames());
    }

    template <typename TEnumRepresentationType>
    TEnumRepresentationType TEnumDescriptionBase<TEnumRepresentationType>::FromStringFullScan(const TStringBuf name, const TInitializationData& enumInitData) {
        const auto findResult = TryFromStringFullScan(name, enumInitData);
        if (Y_LIKELY(findResult.first)) {
            return findResult.second;
        }
        ThrowUndefinedNameException<TEnumRepresentationType>(name, enumInitData);
    }

    template <typename TEnumRepresentationType>
    TEnumRepresentationType TEnumDescriptionBase<TEnumRepresentationType>::FromStringSorted(const TStringBuf name, const TInitializationData& enumInitData) {
        const auto findResult = TryFromStringSorted(name, enumInitData);
        if (Y_LIKELY(findResult.first)) {
            return findResult.second;
        }
        ThrowUndefinedNameException<TEnumRepresentationType>(name, enumInitData);
    }

    template <typename TEnumRepresentationType>
    TStringBuf TEnumDescriptionBase<TEnumRepresentationType>::ToStringBuf(TRepresentationType key) const {
        return this->ToString(key);
    }

    template <typename TEnumRepresentationType>
    TStringBuf TEnumDescriptionBase<TEnumRepresentationType>::ToStringBufFullScan(const TRepresentationType key, const TInitializationData& enumInitData) {
        const auto& vec = enumInitData.NamesInitializer;
        const auto* ptr = FindIfPtr(vec, [&](const auto& item) { return item.Key == key; });
        if (Y_UNLIKELY(!ptr)) {
            ThrowUndefinedValueException(key, enumInitData.ClassName);
        }
        return ptr->Name;
    }

    template <typename TEnumRepresentationType>
    TStringBuf TEnumDescriptionBase<TEnumRepresentationType>::ToStringBufSorted(const TRepresentationType key, const TInitializationData& enumInitData) {
        const auto& vec = enumInitData.NamesInitializer;
        const auto* ptr = FindPtrInSortedContainer(vec, key, std::mem_fn(&TEnumStringPair::Key));
        if (Y_UNLIKELY(!ptr)) {
            ThrowUndefinedValueException(key, enumInitData.ClassName);
        }
        return ptr->Name;
    }

    template <typename TEnumRepresentationType>
    TStringBuf TEnumDescriptionBase<TEnumRepresentationType>::ToStringBufDirect(const TRepresentationType key, const TInitializationData& enumInitData) {
        const auto& vec = enumInitData.NamesInitializer;
        if (Y_UNLIKELY(vec.empty() || key < vec.front().Key)) {
            ThrowUndefinedValueException(key, enumInitData.ClassName);
        }
        const size_t index = static_cast<size_t>(key - vec.front().Key);
        if (Y_UNLIKELY(index >= vec.size())) {
            ThrowUndefinedValueException(key, enumInitData.ClassName);
        }
        return vec[index].Name;
    }

    template <typename TEnumRepresentationType>
    void TEnumDescriptionBase<TEnumRepresentationType>::Out(IOutputStream* os, const TRepresentationType key) const {
        (*os) << this->ToStringBuf(key);
    }

    template <typename TEnumRepresentationType>
    void TEnumDescriptionBase<TEnumRepresentationType>::OutFullScan(IOutputStream* os, const TRepresentationType key, const TInitializationData& enumInitData) {
        (*os) << ToStringBufFullScan(key, enumInitData);
    }

    template <typename TEnumRepresentationType>
    void TEnumDescriptionBase<TEnumRepresentationType>::OutSorted(IOutputStream* os, const TRepresentationType key, const TInitializationData& enumInitData) {
        (*os) << ToStringBufSorted(key, enumInitData);
    }

    template <typename TEnumRepresentationType>
    void TEnumDescriptionBase<TEnumRepresentationType>::OutDirect(IOutputStream* os, const TRepresentationType key, const TInitializationData& enumInitData) {
        (*os) << ToStringBufDirect(key, enumInitData);
    }

    template <typename TEnumRepresentationType>
    TEnumDescriptionBase<TEnumRepresentationType>::TEnumDescriptionBase(const TInitializationData& enumInitData)
        : ClassName(enumInitData.ClassName)
    {
        const TArrayRef<const TEnumStringPair>& namesInitializer = enumInitData.NamesInitializer;
        const TArrayRef<const TEnumStringPair>& valuesInitializer = enumInitData.ValuesInitializer;
        const TArrayRef<const TStringBuf>& cppNamesInitializer = enumInitData.CppNamesInitializer;

        TMap<TRepresentationType, TString> mapValueToName;
        TMap<TStringBuf, TRepresentationType> mapNameToValue;
        for (const TEnumStringPair& it : namesInitializer) {
            mapValueToName.emplace(it.Key, TString(it.Name));
        }
        for (const TEnumStringPair& it : valuesInitializer) {
            mapNameToValue.emplace(it.Name, it.Key);
        }
        Names = std::move(mapValueToName);
        Values = std::move(mapNameToValue);

        AllValues.reserve(Names.size());
        for (const auto& it : Names) {
            if (!AllNames.empty()) {
                AllNames += ", ";
            }
            AllNames += TString::Join('\'', it.second, '\'');
            AllValues.push_back(it.first);
        }

        AllCppNames.reserve(cppNamesInitializer.size());
        for (const auto& cn : cppNamesInitializer) {
            AllCppNames.push_back(TString::Join(enumInitData.CppNamesPrefix, cn));
        }
    }

    template <typename TEnumRepresentationType>
    TEnumDescriptionBase<TEnumRepresentationType>::~TEnumDescriptionBase() = default;

    // explicit instantiation
    template class TEnumDescriptionBase<int>;
    template class TEnumDescriptionBase<unsigned>;
    template class TEnumDescriptionBase<long long>;
    template class TEnumDescriptionBase<unsigned long long>;
}