#pragma once

#include <library/cpp/json/json_writer.h>
#include <library/cpp/json/json_reader.h>

#include <util/stream/str.h>
#include <util/stream/mem.h>
#include <library/cpp/cgiparam/cgiparam.h>
#include <util/string/vector.h>
#include <util/generic/set.h>

namespace NUtil {
    const TString DefaultDescription = "json value";
    // all functions throw exceptions with customizable error message

    inline TString JsonToString(const NJson::TJsonValue& value, bool validateUtf8 = false) {
        TStringStream ss;
        NJson::WriteJson(&ss, &value, false, false, validateUtf8);
        return ss.Str();
    }

    inline NJson::TJsonValue JsonFromString(const TString& string) {
        TStringInput si(string);
        NJson::TJsonValue value;
        if (!ReadJsonTree(&si, &value))
            throw yexception() << "cannot read json from string :" << string;
        return value;
    }

    inline bool JsonFromString(const TString& string, NJson::TJsonValue& json) {
        TStringInput si(string);
        return ReadJsonTree(&si, &json);
    }

    inline bool ValidateJsonString(const TString& string) {
        NJson::TJsonValue discarded;
        return JsonFromString(string, discarded);
    }

    inline const NJson::TJsonValue::TMapType& GetMap(const NJson::TJsonValue& source, const TString& sourceName = DefaultDescription) {
        if (!source.IsMap())
            throw yexception() << sourceName << " must be a map";
        return source.GetMap();
    }

    inline const NJson::TJsonValue::TArray& GetArray(const NJson::TJsonValue& source, const TString& sourceName = DefaultDescription) {
        if (!source.IsArray())
            throw yexception() << sourceName << " must be an array";
        return source.GetArray();
    }

    inline TString ConvertToString(const NJson::TJsonValue& source, const TString& sourceName = DefaultDescription) {
        if (source.IsMap() || source.IsArray())
            throw yexception() << sourceName << " is not convertible to string";
        return source.GetStringRobust();
    }

    inline TString ConvertToString(const NJson::TJsonValue* source, const TString& sourceName = DefaultDescription) {
        if (!source)
            throw yexception() << sourceName << " is missing";
        return ConvertToString(*source, sourceName);
    }

    class TJsonFilter {
    public:
        typedef TSet<TString> TFilters;

        TJsonFilter()
            : IgnoreUnknownPath(false)
        {}

        inline const TFilters& GetFilters() const {
            return Filters;
        }

        inline TFilters& GetFilters() {
            return Filters;
        }

        void AddFilters(const TCgiParameters& cgi, const TString& prefix = TString()) {
            for (size_t i = 0; i < cgi.NumOfValues("filter"); ++i) {
                TVector<TString> filterPortion = SplitString(cgi.Get("filter", i), ",");
                if (prefix) {
                    for (const TString& f : filterPortion) {
                        if (f.StartsWith(prefix)) {
                            Filters.insert(f.substr(prefix.length()));
                        }
                    }
                } else
                    Filters.insert(filterPortion.begin(), filterPortion.end());
            }
        }

        template <class TSource, class TDest = TSource>
        inline bool Apply(const TSource& source, TDest& dest) const {
            NJson::TJsonValue tempSource;
            const NJson::TJsonValue& convertedSource = ConvertSource(source, tempSource);
            bool result = true;
            if (Filters.empty())
                dest = ConvertDest<TDest>(convertedSource);
            else {
                NJson::TJsonValue jsonDest(NJson::JSON_MAP);
                for (TFilters::const_iterator f = Filters.begin(); f != Filters.end(); ++f) {
                    const NJson::TJsonValue* value = convertedSource.GetValueByPath(*f, '.');
                    if (value)
                        jsonDest.SetValueByPath(*f, *value);
                    else if (!IgnoreUnknownPath) {
                        jsonDest.SetValueByPath(*f, "Incorrect path");
                        result = false;
                    }
                }
                dest = ConvertDest<TDest>(jsonDest);
            }
            return result;
        }

        inline void SetIgnoreUnknownPath(bool value) {
            IgnoreUnknownPath = value;
        }
    private:
        inline static const NJson::TJsonValue& ConvertSource(TStringBuf source, NJson::TJsonValue& tempSource) {
            TMemoryInput input(source.data(), source.size());
            NJson::ReadJsonTree(&input, &tempSource, true);
            return tempSource;
        }
        inline static const NJson::TJsonValue& ConvertSource(const NJson::TJsonValue& source, NJson::TJsonValue& /*tempSource*/) {
            return source;
        }

        template <class TDest>
        inline static TDest ConvertDest(const NJson::TJsonValue& dest);

        TFilters Filters;
        bool IgnoreUnknownPath;
    };

    template <>
    inline TString TJsonFilter::ConvertDest<TString>(const NJson::TJsonValue& dest) {
        return dest.GetStringRobust();
    }

    template <>
    inline NJson::TJsonValue TJsonFilter::ConvertDest<NJson::TJsonValue>(const NJson::TJsonValue& dest) {
        return dest;
    }

#define GET_WITH_CUSTOM_EXCEPTION(Type, JsonType) \
    inline Type Get ## JsonType(const NJson::TJsonValue& source, const TString& sourceName = DefaultDescription) { \
    if (!source.Is ## JsonType()) \
        throw yexception() << sourceName << " must be of type " << #JsonType; \
    return source.Get ## JsonType(); } \
    \
    inline Type Get ## JsonType(const NJson::TJsonValue* source, const TString& sourceName = DefaultDescription) { \
    if (!source) \
        throw yexception() << sourceName << " is missing"; \
    return Get ## JsonType(*source, sourceName); }\

    GET_WITH_CUSTOM_EXCEPTION(ui64, UInteger)
    GET_WITH_CUSTOM_EXCEPTION(TString, String)
    GET_WITH_CUSTOM_EXCEPTION(i64, Integer)
    GET_WITH_CUSTOM_EXCEPTION(bool, Boolean)
    GET_WITH_CUSTOM_EXCEPTION(double, Double)
}
