#include "scimpl.h"
#include "scimpl_private.h"

#include <library/cpp/json/json_prettifier.h>
#include <library/cpp/string_utils/relaxed_escaper/relaxed_escaper.h>

#include <util/charset/utf8.h>
#include <util/generic/algorithm.h>
#include <util/generic/ymath.h>
#include <util/system/tls.h>

namespace NSc {
    bool TJsonOpts::StringPolicySafe(TStringBuf& s) {
        return IsUtf(s);
    }

    bool TJsonOpts::NumberPolicySafe(double& d) {
        return IsFinite(d);
    }

    static inline void WriteString(IOutputStream& out, TStringBuf s) {
        NEscJ::EscapeJ<true, true>(s, out);
    }

    static inline const NSc::TValue& GetValue(size_t, TStringBuf key, const TDict& dict) {
        return dict.find(key)->second;
    }

    static inline const NSc::TValue& GetValue(TDict::const_iterator it, TStringBuf, const TDict&) {
        return it->second;
    }

    static inline TStringBuf GetKey(size_t it, const NImpl::TKeySortContext::TGuard& keys) {
        return keys.GetVector()[it];
    }

    static inline TStringBuf GetKey(TDict::const_iterator it, const TDict&) {
        return it->first;
    }

    template <typename TDictKeys>
    static inline void WriteDict(IOutputStream& out, const TDictKeys& keys, const TDict& dict,
                                 const TJsonOpts& jopts, NImpl::TKeySortContext& sortCtx, NImpl::TSelfLoopContext& loopCtx) {
        using const_iterator = typename TDictKeys::const_iterator;
        const_iterator begin = keys.begin();
        const_iterator end = keys.end();
        for (const_iterator it = begin; it != end; ++it) {
            TStringBuf key = GetKey(it, keys);

            if (jopts.StringPolicy && !jopts.StringPolicy(key)) {
                ++begin;
                continue;
            }

            if (it != begin) {
                out << ',';
            }

            NEscJ::EscapeJ<true, true>(key, out);
            out << ':';

            GetValue(it, key, dict).DoWriteJsonImpl(out, jopts, sortCtx, loopCtx);
        }
    }

    void TValue::DoWriteJsonImpl(IOutputStream& out, const TJsonOpts& jopts,
                                 NImpl::TKeySortContext& sortCtx, NImpl::TSelfLoopContext& loopCtx) const {
        const TScCore& core = Core();

        NImpl::TSelfLoopContext::TGuard loopCheck(loopCtx, core);

        if (!loopCheck.Ok) {
            out << TStringBuf("null"); // a loop encountered (and asserted), skip the back reference
            return;
        }

        switch (core.ValueType) {
            default: {
                Y_ASSERT(false);
                [[fallthrough]]; /* no break */
            }
            case EType::Null: {
                out << TStringBuf("null");
                break;
            }
            case EType::Bool: {
                out << (core.IntNumber ? TStringBuf("true") : TStringBuf("false"));
                break;
            }
            case EType::IntNumber: {
                out << core.IntNumber;
                break;
            }
            case EType::FloatNumber: {
                double d = core.FloatNumber;
                if (!jopts.NumberPolicy || jopts.NumberPolicy(d)) {
                    out << d;
                } else {
                    out << TStringBuf("null");
                }
                break;
            }
            case EType::String: {
                TStringBuf s = core.String;
                if (!jopts.StringPolicy || jopts.StringPolicy(s)) {
                    WriteString(out, s);
                } else {
                    out << TStringBuf("null");
                }
                break;
            }
            case EType::Array: {
                out << '[';
                const TArray& a = core.GetArray();
                for (TArray::const_iterator it = a.begin(); it != a.end(); ++it) {
                    if (it != a.begin()) {
                        out << ',';
                    }

                    it->DoWriteJsonImpl(out, jopts, sortCtx, loopCtx);
                }
                out << ']';
                break;
            }
            case EType::Dict: {
                out << '{';

                const TDict& dict = core.GetDict();

                if (jopts.SortKeys) {
                    NImpl::TKeySortContext::TGuard keys(sortCtx, dict);
                    WriteDict(out, keys, dict, jopts, sortCtx, loopCtx);
                } else {
                    WriteDict(out, dict, dict, jopts, sortCtx, loopCtx);
                }

                out << '}';
                break;
            }
        }
    }

    const TValue& TValue::ToJson(IOutputStream& out, const TJsonOpts& jopts) const {
        using namespace NImpl;

        if (jopts.FormatJson) {
            TStringStream str;
            DoWriteJsonImpl(str, jopts, GetTlsInstance<TKeySortContext>(), GetTlsInstance<TSelfLoopContext>());
            NJson::PrettifyJson(str.Str(), out);
        } else {
            DoWriteJsonImpl(out, jopts, GetTlsInstance<TKeySortContext>(), GetTlsInstance<TSelfLoopContext>());
        }

        return *this;
    }

    TString TValue::ToJson(const TJsonOpts& jopts) const {
        TString s;
        {
            TStringOutput out(s);
            ToJson(out, jopts);
        }
        return s;
    }

    TJsonOpts TValue::MakeOptsSafeForSerializer(TJsonOpts opts) {
        opts.SortKeys = true;
        opts.StringPolicy = TJsonOpts::StringPolicySafe;
        opts.NumberPolicy = TJsonOpts::NumberPolicySafe;
        return opts;
    }

    TJsonOpts TValue::MakeOptsPrettyForSerializer(TJsonOpts opts) {
        opts.FormatJson = true;
        return MakeOptsSafeForSerializer(opts);
    }

    TString TValue::ToJsonSafe(const TJsonOpts& jopts) const {
        return ToJson(MakeOptsSafeForSerializer(jopts));
    }

    const TValue& TValue::ToJsonSafe(IOutputStream& out, const TJsonOpts& jopts) const {
        return ToJson(out, MakeOptsSafeForSerializer(jopts));
    }

    TString TValue::ToJsonPretty(const TJsonOpts& jopts) const {
        return ToJson(MakeOptsPrettyForSerializer(jopts));
    }

    const TValue& TValue::ToJsonPretty(IOutputStream& out, const TJsonOpts& jopts) const {
        return ToJson(out, MakeOptsPrettyForSerializer(jopts));
    }
}