#pragma once

#include <util/generic/bt_exception.h>
#include <util/generic/cast.h>
#include <util/generic/hash.h>
#include <util/generic/vector.h>
#include <util/generic/yexception.h>
#include <util/generic/ylimits.h>
#include <util/string/cast.h>

#include <cmath>
#include <variant>

class IInputStream;
class IOutputStream;

namespace NYT {

////////////////////////////////////////////////////////////////////////////////

class TNode
{
public:
    class TLookupError
        : public TWithBackTrace<yexception>
    { };

    class TTypeError
        : public TWithBackTrace<yexception>
    { };

    enum EType {
        Undefined = 0   /*"undefined"*/,

        // NOTE: string representation of all node types
        // are compatible with server node type (except `Undefined' which is missing on server).
        String = 1  /*"string_node"*/,
        Int64 = 2   /*"int64_node"*/,
        Uint64 = 3  /*"uint64_node"*/,
        Double = 4  /*"double_node"*/,
        Bool = 5    /*"boolean_node"*/,
        List = 6    /*"list_node"*/,
        Map = 7     /*"map_node"*/,
        Null = 8    /*"null"*/,
    };

    using TListType = TVector<TNode>;
    using TMapType = THashMap<TString, TNode>;

private:
    struct TNull {
        bool operator==(const TNull&) const;
    };

    struct TUndefined {
        bool operator==(const TUndefined&) const;
    };

    using TValue = std::variant<
        bool,
        i64,
        ui64,
        double,
        TString,
        TListType,
        TMapType,
        TNull,
        TUndefined
        >;

public:

    TNode();
    TNode(const char* s);
    TNode(TStringBuf s);
    explicit TNode(std::string_view s);
    explicit TNode(const std::string& s);
    TNode(TString s);
    TNode(int i);

    //this case made speccially for prevent mess cast of EType into TNode through TNode(int) constructor
    //usual case of error SomeNode == TNode::Undefined <-- SomeNode indeed will be compared with TNode(0) without this method
    //correct way is SomeNode.GetType() == TNode::Undefined
    template<class T = EType>
    Y_FORCE_INLINE TNode(EType)
    {
        static_assert(!std::is_same<T, EType>::value, "looks like a mistake, may be you forget .GetType()");
    }

    //this case made speccially for prevent mess cast of T* into TNode through implicit bool ctr
    template<class T = int>
    Y_FORCE_INLINE TNode(const T*) : TNode() {
        static_assert(!std::is_same<T,T>::value, "looks like a mistake, and pointer have converted to bool");
    }

    TNode(unsigned int ui);
    TNode(long i);
    TNode(unsigned long ui);
    TNode(long long i);
    TNode(unsigned long long ui);
    TNode(double d);
    TNode(bool b);
    TNode(TMapType map);

    TNode(const TNode& rhs);
    TNode& operator=(const TNode& rhs);

    TNode(TNode&& rhs) noexcept;
    TNode& operator=(TNode&& rhs) noexcept;

    ~TNode();

    void Clear();

    bool IsString() const;
    bool IsInt64() const;
    bool IsUint64() const;
    bool IsDouble() const;
    bool IsBool() const;
    bool IsList() const;
    bool IsMap() const;

    // `IsEntity' is deprecated use `IsNull' instead.
    bool IsEntity() const;
    bool IsNull() const;
    bool IsUndefined() const;
    // Returns true if TNode is neither Null, nor Undefined
    bool HasValue() const;

    template<typename T>
    bool IsOfType() const noexcept;

    // Int64, Uint64, Double, or Bool
    bool IsArithmetic() const;

    bool Empty() const;
    size_t Size() const;

    EType GetType() const;

    const TString& AsString() const;
    i64 AsInt64() const;
    ui64 AsUint64() const;
    double AsDouble() const;
    bool AsBool() const;
    const TListType& AsList() const;
    const TMapType& AsMap() const;
    TListType& AsList();
    TMapType& AsMap();

    const TString& UncheckedAsString() const noexcept;
    i64 UncheckedAsInt64() const noexcept;
    ui64 UncheckedAsUint64() const noexcept;
    double UncheckedAsDouble() const noexcept;
    bool UncheckedAsBool() const noexcept;
    const TListType& UncheckedAsList() const noexcept;
    const TMapType& UncheckedAsMap() const noexcept;
    TListType& UncheckedAsList() noexcept;
    TMapType& UncheckedAsMap() noexcept;

    // integer types cast
    // makes overflow checks
    template<typename T>
    T IntCast() const;

    // integers <-> double <-> string
    // makes overflow checks
    template<typename T>
    T ConvertTo() const;

    template<typename T>
    T& As();

    template<typename T>
    const T& As() const;

    static TNode CreateList();
    static TNode CreateList(TListType list);
    static TNode CreateMap();
    static TNode CreateMap(TMapType map);
    static TNode CreateEntity();

    const TNode& operator[](size_t index) const;
    TNode& operator[](size_t index);
    const TNode& At(size_t index) const;
    TNode& At(size_t index);

    TNode& Add() &;
    TNode Add() &&;
    TNode& Add(const TNode& node) &;
    TNode Add(const TNode& node) &&;
    TNode& Add(TNode&& node) &;
    TNode Add(TNode&& node) &&;

    bool HasKey(const TStringBuf key) const;

    TNode& operator()(const TString& key, const TNode& value) &;
    TNode operator()(const TString& key, const TNode& value) &&;
    TNode& operator()(const TString& key, TNode&& value) &;
    TNode operator()(const TString& key, TNode&& value) &&;

    const TNode& operator[](const TStringBuf key) const;
    TNode& operator[](const TStringBuf key);
    const TNode& At(const TStringBuf key) const;
    TNode& At(const TStringBuf key);

    // map getters
    // works the same way like simple getters
    const TString& ChildAsString(const TStringBuf key) const;
    i64 ChildAsInt64(const TStringBuf key) const;
    ui64 ChildAsUint64(const TStringBuf key) const;
    double ChildAsDouble(const TStringBuf key) const;
    bool ChildAsBool(const TStringBuf key) const;
    const TListType& ChildAsList(const TStringBuf key) const;
    const TMapType& ChildAsMap(const TStringBuf key) const;
    TListType& ChildAsList(const TStringBuf key);
    TMapType& ChildAsMap(const TStringBuf key);

    template<typename T>
    T ChildIntCast(const TStringBuf key) const;

    template<typename T>
    T ChildConvertTo(const TStringBuf key) const;

    template<typename T>
    const T& ChildAs(const TStringBuf key) const;

    template<typename T>
    T& ChildAs(const TStringBuf key);

    // list getters
    // works the same way like simple getters
    const TString& ChildAsString(size_t index) const;
    i64 ChildAsInt64(size_t index) const;
    ui64 ChildAsUint64(size_t index) const;
    double ChildAsDouble(size_t index) const;
    bool ChildAsBool(size_t index) const;
    const TListType& ChildAsList(size_t index) const;
    const TMapType& ChildAsMap(size_t index) const;
    TListType& ChildAsList(size_t index);
    TMapType& ChildAsMap(size_t index);

    template<typename T>
    T ChildIntCast(size_t index) const;

    template<typename T>
    T ChildConvertTo(size_t index) const;

    template<typename T>
    const T& ChildAs(size_t index) const;

    template<typename T>
    T& ChildAs(size_t index);


    // attributes
    bool HasAttributes() const;
    void ClearAttributes();
    const TNode& GetAttributes() const;
    TNode& Attributes();

    void MoveWithoutAttributes(TNode&& rhs);

    // Serialize TNode using binary yson format.
    // Methods for ysaveload.
    void Save(IOutputStream* output) const;
    void Load(IInputStream* input);

private:
    void Move(TNode&& rhs);

    void CheckType(EType type) const;

    void AssureMap();
    void AssureList();

    void CreateAttributes();

private:
    TValue Value_;
    THolder<TNode> Attributes_;

    friend bool operator==(const TNode& lhs, const TNode& rhs);
    friend bool operator!=(const TNode& lhs, const TNode& rhs);
};

bool operator==(const TNode& lhs, const TNode& rhs);
bool operator!=(const TNode& lhs, const TNode& rhs);

bool GetBool(const TNode& node);

/// Debug printer for gtest
void PrintTo(const TNode& node, std::ostream* out);

inline bool TNode::IsArithmetic() const {
    return IsInt64() || IsUint64() || IsDouble() || IsBool();
}

template<typename T>
inline T TNode::IntCast() const {
    if constexpr (std::is_integral<T>::value) {
        try {
            switch (GetType()) {
                case TNode::Uint64:
                    return SafeIntegerCast<T>(AsUint64());
                case TNode::Int64:
                    return SafeIntegerCast<T>(AsInt64());
                default:
                    ythrow TTypeError() << "IntCast() called for type " << GetType();
            }
        } catch(TBadCastException& exc) {
            ythrow TTypeError() << "TBadCastException during IntCast(): " << exc.what();
        }
    } else {
        static_assert(sizeof(T) != sizeof(T), "implemented only for std::is_integral types");
    }
}

template<typename T>
inline T TNode::ConvertTo() const {
    if constexpr (std::is_integral<T>::value) {
        switch (GetType()) {
            case NYT::TNode::String:
                return ::FromString(AsString());
            case NYT::TNode::Int64:
            case NYT::TNode::Uint64:
                return IntCast<T>();
            case NYT::TNode::Double:
                if (AsDouble() < Min<T>() || AsDouble() > MaxFloor<T>() || !std::isfinite(AsDouble())) {
                    ythrow TTypeError() << AsDouble() << " can't be converted to " << TypeName<T>();
                }
                return AsDouble();
            case NYT::TNode::Bool:
                return AsBool();
            case NYT::TNode::List:
            case NYT::TNode::Map:
            case NYT::TNode::Null:
            case NYT::TNode::Undefined:
                ythrow TTypeError() << "ConvertTo<" << TypeName<T>() << ">() called for type " << GetType();
        };
    } else {
        static_assert(sizeof(T) != sizeof(T), "should have template specialization");
    }
}

template<>
inline TString TNode::ConvertTo<TString>() const {
    switch (GetType()) {
        case NYT::TNode::String:
            return AsString();
        case NYT::TNode::Int64:
            return ::ToString(AsInt64());
        case NYT::TNode::Uint64:
            return ::ToString(AsUint64());
        case NYT::TNode::Double:
            return ::ToString(AsDouble());
        case NYT::TNode::Bool:
            return ::ToString(AsBool());
        case NYT::TNode::List:
        case NYT::TNode::Map:
        case NYT::TNode::Null:
        case NYT::TNode::Undefined:
            ythrow TTypeError() << "ConvertTo<TString>() called for type " << GetType();
    }
    Y_UNREACHABLE();
}

template<>
inline double TNode::ConvertTo<double>() const {
    switch (GetType()) {
        case NYT::TNode::String:
            return ::FromString(AsString());
        case NYT::TNode::Int64:
            return AsInt64();
        case NYT::TNode::Uint64:
            return AsUint64();
        case NYT::TNode::Double:
            return AsDouble();
        case NYT::TNode::Bool:
            return AsBool();
        case NYT::TNode::List:
        case NYT::TNode::Map:
        case NYT::TNode::Null:
        case NYT::TNode::Undefined:
            ythrow TTypeError() << "ConvertTo<double>() called for type " << GetType();
    }
}

template<>
inline bool TNode::ConvertTo<bool>() const {
    switch (GetType()) {
        case NYT::TNode::String:
            return ::FromString(AsString());
        case NYT::TNode::Int64:
            return AsInt64();
        case NYT::TNode::Uint64:
            return AsUint64();
        case NYT::TNode::Double:
            return AsDouble();
        case NYT::TNode::Bool:
            return AsBool();
        case NYT::TNode::List:
        case NYT::TNode::Map:
        case NYT::TNode::Null:
        case NYT::TNode::Undefined:
            ythrow TTypeError() << "ConvertTo<bool>() called for type " << GetType();
    }
}

template<typename T>
inline T TNode::ChildIntCast(const TStringBuf key) const {
    const auto& node = At(key);
    try {
        return node.IntCast<T>();
    } catch (TTypeError& e) {
        e << ", during getting key=" << key;
        throw e;
    } catch (...) {
        ythrow TTypeError() << CurrentExceptionMessage() << ", during getting key=" << key;
    }
}

template<typename T>
inline T TNode::ChildIntCast(size_t index) const {
    const auto& node = At(index);
    try {
        return node.IntCast<T>();
    } catch (TTypeError& e) {
        e << ", during getting index=" << index;
        throw e;
    } catch (...) {
        ythrow TTypeError() << CurrentExceptionMessage() << ", during getting index=" << index;
    }
}

template<typename T>
inline T TNode::ChildConvertTo(const TStringBuf key) const {
    const auto& node = At(key);
    try {
        return node.ConvertTo<T>();
    } catch (TTypeError& e) {
        e << ", during getting key=" << key;
        throw e;
    } catch (...) {
        ythrow TTypeError() << CurrentExceptionMessage() << ", during getting key=" << key;
    }
}

template<typename T>
inline T TNode::ChildConvertTo(size_t index) const {
    const auto& node = At(index);
    try {
        return node.ConvertTo<T>();
    } catch (TTypeError& e) {
        e << ", during getting index=" << index;
        throw e;
    } catch (...) {
        ythrow TTypeError() << CurrentExceptionMessage() << ", during getting index=" << index;
    }
}

template<typename T>
inline const T& TNode::ChildAs(const TStringBuf key) const {
    const auto& node = At(key);
    try {
        return node.As<T>();
    } catch (TTypeError& e) {
        e << ", during getting key=" << key;
        throw e;
    } catch (...) {
        ythrow TTypeError() << CurrentExceptionMessage() << ", during getting key=" << key;
    }
}

template<typename T>
inline const T& TNode::ChildAs(size_t index) const {
    const auto& node = At(index);
    try {
        return node.As<T>();
    } catch (TTypeError& e) {
        e << ", during getting index=" << index;
        throw e;
    } catch (...) {
        ythrow TTypeError() << CurrentExceptionMessage() << ", during getting index=" << index;
    }
}

template<typename T>
inline T& TNode::ChildAs(const TStringBuf key) {
    return const_cast<T&>(static_cast<const TNode*>(this)->ChildAs<T>(key));
}

template<typename T>
inline T& TNode::ChildAs(size_t index) {
    return const_cast<T&>(static_cast<const TNode*>(this)->ChildAs<T>(index));
}

template<typename T>
inline bool TNode::IsOfType() const noexcept {
    return std::holds_alternative<T>(Value_);
}

template<typename T>
inline T& TNode::As() {
    return std::get<T>(Value_);
}

template<typename T>
inline const T& TNode::As() const {
    return std::get<T>(Value_);
}

////////////////////////////////////////////////////////////////////////////////

namespace NNodeCmp {
    bool operator<(const TNode& lhs, const TNode& rhs);
    bool operator<=(const TNode& lhs, const TNode& rhs);
    bool operator>(const TNode& lhs, const TNode& rhs);
    bool operator>=(const TNode& lhs, const TNode& rhs);
    bool IsComparableType(const TNode::EType type);
}

////////////////////////////////////////////////////////////////////////////////

} // namespace NYT