#pragma once

#include "fwd.h"
#include "value.h"

#include <library/cpp/json/json_value.h>

#include <util/generic/hash.h>
#include <util/generic/ptr.h>
#include <util/generic/deque.h>
#include <util/system/type_name.h>
#include <util/generic/vector.h>
#include <util/generic/yexception.h>
#include <util/generic/bt_exception.h>
#include <util/ysaveload.h>

class IInputStream;
class IOutputStream;

namespace NConfig {
    typedef THashMap<TString, NJson::TJsonValue> TGlobals;

    class TConfigError: public TWithBackTrace<yexception> {
    };

    class TConfigParseError: public TConfigError {
    };

    class TTypeMismatch: public TConfigError {
    };

    struct TArray;
    struct TDict;

    class TConfig {
    public:
        inline TConfig()
            : V_(Null())
        {
        }

        inline TConfig(IValue* v)
            : V_(v)
        {
        }

        TConfig(const TConfig& config) = default;
        TConfig& operator=(const TConfig& config) = default;

        template <class T>
        inline bool IsA() const {
            return V_->IsA(typeid(T));
        }

        inline bool IsNumeric() const {
            return IsA<double>() || IsA<i64>() || IsA<ui64>();
        }

        template <class T>
        inline const T& Get() const {
            return GetNonConstant<T>();
        }

        template <class T>
        inline T& GetNonConstant() const {
            if (this->IsA<T>()) {
                return *(T*)V_->Ptr();
            }

            if constexpr (std::is_same_v<T, ::NConfig::TArray>) {
                NCfgPrivate::ReportTypeMismatch(V_->TypeName(), "array");
            } else if constexpr (std::is_same_v<T, ::NConfig::TDict>) {
                NCfgPrivate::ReportTypeMismatch(V_->TypeName(), "dict");
            } else if constexpr (std::is_same_v<T, TString>) {
                NCfgPrivate::ReportTypeMismatch(V_->TypeName(), "string");
            } else {
                NCfgPrivate::ReportTypeMismatch(V_->TypeName(), ::TypeName<T>());
            }
        }

        template <class T>
        inline T As() const {
            return ValueAs<T>(V_.Get());
        }

        template <class T>
        inline T As(T def) const {
            return IsNull() ? def : As<T>();
        }

        inline bool IsNull() const noexcept {
            return V_.Get() == Null();
        }

        const TConfig& Or(const TConfig& r) const {
            return IsNull() ? r : *this;
        }

        //assume value is dict
        bool Has(const TStringBuf& key) const;
        const TConfig& operator[](const TStringBuf& key) const;
        const TConfig& At(const TStringBuf& key) const;

        //assume value is array
        const TConfig& operator[](size_t index) const;
        size_t GetArraySize() const;

        static TConfig FromIni(IInputStream& in, const TGlobals& g = TGlobals());
        static TConfig FromJson(IInputStream& in, const TGlobals& g = TGlobals());
        static TConfig FromLua(IInputStream& in, const TGlobals& g = TGlobals());
        //load yconf format. unsafe, but natural mapping
        static TConfig FromMarkup(IInputStream& in, const TGlobals& g = TGlobals());

        static TConfig FromStream(IInputStream& in, const TGlobals& g = TGlobals());

        inline void ToJson(IOutputStream& out) const {
            V_->ToJson(out);
        }

        void DumpJson(IOutputStream& out) const;
        void DumpLua(IOutputStream& out) const;

        static TConfig ReadJson(TStringBuf in, const TGlobals& g = TGlobals());
        static TConfig ReadLua(TStringBuf in, const TGlobals& g = TGlobals());
        static TConfig ReadMarkup(TStringBuf in, const TGlobals& g = TGlobals());
        static TConfig ReadIni(TStringBuf in, const TGlobals& g = TGlobals());

        void Load(IInputStream* stream);
        void Save(IOutputStream* stream) const;

    private:
        TIntrusivePtr<IValue> V_;
    };

    struct TArray: public TDeque<TConfig> {
        const TConfig& Index(size_t index) const;
        const TConfig& At(size_t index) const;
    };

    struct TDict: public THashMap<TString, TConfig> {
        const TConfig& Find(const TStringBuf& key) const;
        const TConfig& At(const TStringBuf& key) const;
    };

    THolder<IInputStream> CreatePreprocessor(const TGlobals& g, IInputStream& in);
}