#pragma once

//
// Header-only module - add library/cpp/protobuf/util to PEERDIR
//

#include <library/cpp/protobuf/util/simple_reflection.h>
#include <library/cpp/string_utils/scan/scan.h>
#include <util/string/cast.h>
#include <util/string/printf.h>
#include <util/string/strip.h>
#include <util/stream/str.h>
#include <util/generic/hash.h>

namespace NFormatPrivate {
    namespace {
        using namespace NProtoBuf;

        template<typename T>
        inline TString PbToString(T vl) {
            return ToString<T>(vl);
        }
        template<>
        inline TString PbToString(const EnumValueDescriptor* vl) {
            return vl->name();
        }
        template<>
        inline TString PbToString(const google::protobuf::Message* msg) {
            ythrow TBadCastException() << "Unsupported value type " << msg->GetDescriptor()->name();
        }

        template<typename T>
        inline T PbFromString(const TMutableField&, const TString& vl) {
            return FromString<T>(vl);
        }
        template<>
        inline const EnumValueDescriptor* PbFromString<const EnumValueDescriptor*>(const TMutableField& fld, const TString& vl) {
            const EnumValueDescriptor* d = fld.Field()->enum_type()->FindValueByName(vl);
            if (!d)
                ythrow TBadCastException() << "Unknown enum value " << vl << " for " << fld.Field()->name();
            return d;
        }
        template<>
        inline const google::protobuf::Message* PbFromString<const google::protobuf::Message*>(const TMutableField& fld, const TString&) {
            ythrow TBadCastException() << "Unsupported value type " << "for " << fld.Field()->name();
        }


        TString PbGet(NProtoBuf::TConstField& field) {
#define TMP_MACRO_FOR_CPPTYPE(CPPTYPE) case CPPTYPE: return PbToString(field.Get<TCppTypeTraits<CPPTYPE>::T>()); break;
            switch(field.Field()->cpp_type()) {
                APPLY_TMP_MACRO_FOR_ALL_CPPTYPES();
                default:
                    ythrow TBadCastException();
            }
#undef TMP_MACRO_FOR_CPPTYPE
        }
        void PbSet(NProtoBuf::TMutableField& field, const TString& value) {
#define TMP_MACRO_FOR_CPPTYPE(CPPTYPE) case CPPTYPE: field.Set(PbFromString<TCppTypeTraits<CPPTYPE>::T>(field, value)); break;
            switch(field.Field()->cpp_type()) {
                APPLY_TMP_MACRO_FOR_ALL_CPPTYPES();
                default:
                    ythrow TBadCastException();
            }
#undef TMP_MACRO_FOR_CPPTYPE
        }

    } // namespace anonymous
}

namespace NUtil  {
    void ParseKvOptions(google::protobuf::Message& msg, const TStringBuf& strKvOpts) {
        TStringStream keyError;
        auto onOpt = [&](TStringBuf keyRef, TStringBuf valRef) {
            TString error;
            const google::protobuf::FieldDescriptor* fld = msg.GetDescriptor()->FindFieldByName(TString(StripString(keyRef)));
            if (nullptr == fld)
                return; //ignore unknown keys
            NProtoBuf::TMutableField field(msg, fld);
            try {
                NFormatPrivate::PbSet(field, TString(StripString(valRef)));
                return;
            } catch(yexception& e) {
                error = e.what();
            } catch(...) {
            }

            if (!keyError.Empty())
                keyError << ',';
            keyError << Sprintf("\"%.*s=%.*s\" [msg: %s]", (i32)keyRef.size(), keyRef.data(), (i32)valRef.size(), valRef.data(), error.data());
        };

        ScanKeyValue<false, ',', '='>(strKvOpts, onOpt);
        if (!keyError.Empty())
            ythrow yexception() << "Cannot parse options: " << keyError.Str();
    }


    template<typename TMessage>
    TMessage ParseKvOptions(const TStringBuf& strKvOpts) {
        TMessage result;
        ParseKvOptions(result, strKvOpts);
        return result;
    }

    TString FormatKvOptions(const google::protobuf::Message& msg) {
        TStringStream result;
        const google::protobuf::Descriptor* d = msg.GetDescriptor();
        const int nFields = d->field_count();
        for(int i = 0; i < nFields; ++i) {
            const google::protobuf::FieldDescriptor* fld = d->FindFieldByNumber(i);
            if (!fld)
                continue;
            NProtoBuf::TConstField field(msg, fld);
            if (!field.HasValue())
                continue;
            TString value = NFormatPrivate::PbGet(field);
            if (!result.Empty())
                result << ',' << ' ';
            result << fld->name() << '=' << value;
        }
        return result.Str();
    }
} // namespace NUtil
