#pragma once

#include "converter.h"
#include "hilighter.h"

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

#include <library/cpp/logger/global/global.h>

#include <util/stream/buffer.h>

namespace {
    static const TString modes[] = {
        TString("flat"),
        TString("deep"),
        TString("wide")
    };
}

class IJsonFormatterBase : public IReportFormatter {
public:
    IJsonFormatterBase(const TCgiParameters& cgi)
        : Result(NJson::JSON_MAP)
        , JsonpCallbackName(cgi.Get("callback", 0))
    {
    }

    virtual TBuffer GetResult() const override {
        TBuffer buf;
        TBufferOutput out(buf);
        if (IsJsonpMode()) {
            out << GetJsonpFunctionName() << '(';
        }
        NJson::WriteJson(&out, &Result);
        if (IsJsonpMode()) {
            out << ");";
        }

        return buf;
    }

    virtual void FillHeaders(IReportBuilderContext& context) const override {
        const TString& mimeType    = IsJsonpMode() ? "application/javascript" : "application/json";
        const TString& contentType = mimeType + "; charset=" + NameByCharset(CODES_UTF8);

        context.AddReplyInfo("Content-Type", contentType);
        context.AddReplyInfo("Cache-Control", "max-age=3600");
    }

    static void AddValueToJson(NJson::TJsonValue& parentValue, const TString& name, const TString& value) {
        NJson::TJsonValue& propertyValue = parentValue[name];
        if (propertyValue.IsDefined()) {
            if (!propertyValue.IsArray()) {
                const TString old = propertyValue.GetString();
                propertyValue.AppendValue(old);
            }
            propertyValue.AppendValue(value);
        } else {
            propertyValue.SetValue(value);
        }
    }

protected:
    bool IsJsonpMode() const {
        return !JsonpCallbackName.empty();
    }

    TString GetJsonpFunctionName() const {
        return JsonpCallbackName;
    }

protected:
    NJson::TJsonValue Result;

private:
    TString JsonpCallbackName;
};

class TJsonFormatter : public IJsonFormatterBase {
public:
    TJsonFormatter(const TCgiParameters& cgi)
        : IJsonFormatterBase(cgi)
    {}

    virtual bool OnDocument(const ISearchReplyData::TMainDocArchiveInfo& info) override {
        NJson::TJsonValue& docs = Result["response"]["results"].Back()["groups"].Back()["documents"];
        NJson::TJsonValue& doc = docs.AppendValue(NJson::TJsonValue(NJson::JSON_MAP));

        doc["docId"] = info.DocId.ToString(TDocHandle::PrintAll);
        doc["url"] = info.UrlProperty;
        doc["relevance"] = ToString(info.Relevance);
        if (!info.HeadLine.empty()) {
            doc["headline"] = Highlight(info.HeadLine);
        }
        if (!info.Title.empty()) {
            doc["title"] = Highlight(info.Title);
        }

        if (!info.Passages.empty()) {
            FillPassages(info.Passages, doc["passages"]);
        }

        return true;
    }

    virtual void OnReportGroupingStat(const TGroupingIndex& gi, int numGroups,
        int numDocs, const TString& currentCateg) override
    {
        NJson::TJsonValue& groupingStat = Result["groupings"].AppendValue(NJson::TJsonValue(NJson::JSON_MAP));
        groupingStat["attr"] = gi.Attr;
        groupingStat["mode"] = modes[gi.Mode];
        groupingStat["groups-on-page"] = ToString(numGroups);
        groupingStat["docs"] = ToString(numDocs);
        groupingStat["categ"] = ToString(currentCateg);
    };

    virtual bool OnGrouping(const TGroupingIndex& gi, const ISearchReplyData::TGroupingInfo& info,
        const ISearchReplyData::TPagingInfo&, const int numGroups) override
    {
        NJson::TJsonValue& results = Result["response"]["results"];
        NJson::TJsonValue& grouping = results.AppendValue(NJson::TJsonValue(NJson::JSON_MAP));

        FillStat(info.Stat, grouping["found"]);

        grouping["attr"] = gi.Attr;
        grouping["mode"] = gi.Mode;
        grouping["groups-on-page"] = ToString(numGroups);

        return true;
    }

    virtual bool OnGroup(const ISearchReplyData::TGroupInfo& info) override {
        NJson::TJsonValue& groups = Result["response"]["results"].Back()["groups"];
        NJson::TJsonValue& group = groups.AppendValue(NJson::TJsonValue(NJson::JSON_MAP));

        group["relevance"] = ToString(info.Relevance);
        group["doccount"] = ToString(info.DocsCount);
        group["group_attr"] = info.CategString;
        FillStat(info.Stat, group["found"]);

        return true;
    }

    virtual void OnCateg(const ISearchReplyData::TDocCategInfo& info, const TString& attr, ui64 index) override {
        Y_UNUSED(info);
        Y_UNUSED(attr);
        Y_UNUSED(index);
        FAIL_LOG("Not implemented OnCateg");
    }

    virtual void OnReportStat(const TRelevStatEx& stat) override {
        FillStat(stat, Result["response"]["found"]);
    }

    virtual void OnSearchProperties(TSearcherPropsRef props) override {
        if (!props || props->empty())
            return;

        NJson::TJsonValue& sp = Result["response"]["searcher_properties"];

        for (TSearcherProps::const_iterator i = props->begin(); i != props->end(); ++i) {
            const TString& name = i->first;
            sp[name] = i->second;
        }
    }

    virtual void OnError(const TString& errorString) override {
        Result["errors"].AppendValue(errorString);
    }

    virtual void OnDocAttributesStart() override {
        DocRef = &Result["response"]["results"].Back()["groups"].Back()["documents"].Back();
    }

    virtual void OnDocAttributesEnd() override {
        DocRef = nullptr;
    }

    virtual void OnDocAttribute(const TString& name, const TString& value) override {
        CHECK_WITH_LOG(DocRef);
        NJson::TJsonValue& propVals = (*DocRef)["properties"];
        AddValueToJson(propVals, name, value);
    }

    virtual void OnRequestText(const TString& text) override {
        Result["request_query"] = text;
    }

    virtual void OnRequestPage(const long page) override {
        Result["page"] = ToString(page);
    };

private:
    static void FillStat(const TRelevStatEx& stat, NJson::TJsonValue& json) {
        json["all"] = ToString(stat[0]);
        json["strict"] = ToString(stat[1]);
        json["phrase"] = ToString(stat[2]);
    }

    static void FillPassages(const TVector<TString>& texts, NJson::TJsonValue& json) {
        json.SetType(NJson::JSON_ARRAY);
        for (const TString& text: texts) {
            if (!text.empty()) {
                json.AppendValue(Highlight(text));
            }
        }
    }

    static NJson::TJsonValue Highlight(const TString& text) {
        return Highlight(text.c_str());
    }

    static NJson::TJsonValue Highlight(const char* text) {
        return THilighter::Highlight(text);
    }

private:
    static TFactory::TRegistrator<TJsonFormatter> Registrator;
    NJson::TJsonValue* DocRef = nullptr;
};
