#include "view_syncronizer.h"

#include <maps/wikimap/mapspro/services/editor/src/objects/object.h>
#include <maps/wikimap/mapspro/services/editor/src/collection.h>
#include <maps/wikimap/mapspro/services/editor/src/srv_attrs/calc.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/sync/sync_view.h>
#include <maps/wikimap/mapspro/services/editor/src/sync/sync_attrs.h>

#include <maps/wikimap/mapspro/libs/dbutils/include/parser.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>

namespace maps {
namespace wiki {

namespace {

const bool CHECK_VIEW_EXISTS = true;
const bool CHECK_SUGGEST_EXISTS = true;

}

/// ViewSyncronizer

Observer::ContextDataPtr
ViewSyncronizer::beforeCommit(
    ObjectsCache& cache,
    const GeoObjectCollection&,
    UserContext& /*userContext*/,
    const CommitContext& /*commitContext*/) const
{
    auto updateViewsFilter = [] (const GeoObject* object) {
        return object->revision().valid()
            && (object->isDeleted()
                || object->isModifiedGeom()
                || object->isModifiedAttr()
                || object->isModifiedCategory());
    };
    sync::updateViews(cache, updateViewsFilter, CHECK_VIEW_EXISTS);

    auto updateAttrsFilter = [] (const GeoObject* object) {
        return object->revision().valid() && object->isUpdatePropertiesRequested();
    };
    sync::updateAttrs(cache, updateAttrsFilter);
    updateObjectsSuggests(cache, updateAttrsFilter, CHECK_SUGGEST_EXISTS);

    return Observer::ContextDataPtr();
}

namespace {

typedef std::unordered_set<TOid> TOidSet;

template <typename Cont>
std::string
queryInFilter(const Cont& cont)
{
    ASSERT(!cont.empty());

    auto it = cont.begin();
    std::ostringstream query;
    query << " IN (" << *it;
    while (++it != cont.end()) {
        query << ',' << *it;
    }
    query << ")";
    return query.str();
}

template <typename Cont>
std::string
queryKeysInFilter(const Cont& cont)
{
    ASSERT(!cont.empty());

    auto it = cont.begin();
    std::ostringstream query;
    query << " IN (" << it->first;
    while (++it != cont.end()) {
        query << ',' << it->first;
    }
    query << ")";
    return query.str();
}

void
deleteSuggestData(Transaction& work, const TOidSet& oids)
{
    if (!oids.empty()) {
        auto r = work.exec(
            "DELETE FROM suggest_data WHERE object_id" + queryInFilter(oids));
    }
}

Geom objectSuggestGeom(const ObjectPtr& obj)
{
    if (!obj->geom().isNull()) {
        return obj->geom();
    }
    const auto& envelope = obj->envelope();
    if (envelope.isNull()) {
        return Geom();
    }
    return Geom(createGeom(envelope, SpatialRefSystem::Mercator));
}

class SuggestDataBuilder
{
public:
    SuggestDataBuilder(
            const srv_attrs::CalcSrvAttrs& srvAttrsCalculator,
            const GeoObjectCollection& collection,
            bool checkExists)
        : srvAttrsCalculator_(srvAttrsCalculator)
        , checkExists_(checkExists)
    {
        for (const auto& objPtr : collection) {
            if (!objPtr->category().suggest()) {
                continue;
            }
            auto oid = objPtr->id();
            auto suggestTexts = srvAttrsCalculator_.suggestTexts(oid);
            if (suggestTexts.empty()) {
                deleteOids_.insert(oid);
            } else {
                namedObjects_[oid] = NamedObject{suggestTexts, objPtr};
            }
        }
    }

    void process()
    {
        deleteSuggestData(workView(), deleteOids_);

        if (!namedObjects_.empty()) {
            processNamedObjects();
        }
    }

private:
    Transaction& workView() const { return srvAttrsCalculator_.cache().workView(); }

    void processNamedObjects()
    {
        std::string updateQuery = checkExists_ ? buildUpdateQuery() : "";
        std::string insertQuery = buildInsertQuery();

        if (!updateQuery.empty() || !insertQuery.empty()) {
            workView().exec(updateQuery + insertQuery);
        }
    }

    std::string buildUpdateQuery()
    {
        Transaction& work = SuggestDataBuilder::workView();
        auto existsRecords = work.exec(
            "SELECT object_id, parent_id, text_data, commit_id, the_geom, lang, "
            "hstore_to_array(categories) as cats"
            " FROM suggest_data"
            " WHERE object_id " + queryKeysInFilter(namedObjects_));

        std::ostringstream query;
        std::map<TOid, std::set<std::string>> toDeleteOidAndLang;
        for (const auto& row : existsRecords) {
            const auto oid = row["object_id"].as<TOid>();
            const std::string lang = row["lang"].c_str();

            existsOidsAndLang_.insert({oid, lang});

            const auto it = namedObjects_.find(oid);
            ASSERT(it != namedObjects_.end());
            const auto itName = it->second.langsAndTexts.find(lang);
            if (itName == it->second.langsAndTexts.end()) {
                toDeleteOidAndLang[oid].insert(lang);
                continue;
            }
            const NamedObject& namedObject = it->second;
            const auto commitId = namedObject.objPtr->cache().headCommitId();
            if (row["commit_id"].as<TCommitId>() > commitId) {
                continue;
            }
            const auto& suggestText = itName->second;
            auto parent = namedObject.objPtr->parent();
            TOid parentId = parent ? parent->id() : 0;
            const auto& geom = objectSuggestGeom(namedObject.objPtr);
            const auto objCanonicalCategory = plainCategoryIdToCanonical(namedObject.objPtr->categoryId());

            if (row["parent_id"].as<TOid>()    == parentId &&
                row["text_data"].c_str()       == suggestText &&
                dbutils::parsePGHstore(row["cats"].c_str()).count(objCanonicalCategory) &&
                ((row["the_geom"].is_null() && geom.isNull())
                    || (Geom(row["the_geom"]).equal(geom, CALCULATION_TOLERANCE))))
            {
                continue;
            }

            query
                << "UPDATE suggest_data SET parent_id=" << parentId
                << " , text_data=" << work.quote(suggestText)
                << " , lower_text_data=lower(" << work.quote(suggestText) << " COLLATE \"C.UTF-8\")"
                << " , commit_id=" << commitId
                << " , categories=hstore("
                << work.quote(objCanonicalCategory) << ", '1') ";
            if (!geom.isNull()) {
                query << " , the_geom = ST_GeomFromWkb('"
                << work.esc_raw(geom.wkb())
                << "',3395)";
            }
            query << " WHERE object_id=" << oid << " AND lang='" << lang <<"';";
        }
        if (!toDeleteOidAndLang.empty()) {
            query << "DELETE FROM suggest_data WHERE FALSE ";
            for (const auto& oidAndLang : toDeleteOidAndLang) {
                query << " OR (object_id = " << oidAndLang.first;
                query << " AND lang IN ('";
                const auto& langs = oidAndLang.second;
                query << common::join(langs, "','");
                query << "'))";
            }
            query << ";";
        }
        return query.str();
    }

    std::string buildInsertQuery() const
    {
        Transaction& work = SuggestDataBuilder::workView();

        std::ostringstream query;
        bool empty = true;
        for (const auto& pair : namedObjects_) {
            TOid oid = pair.first;
            for (const auto& langAndText : pair.second.langsAndTexts) {
                const auto & lang = langAndText.first;
                if (existsOidsAndLang_.count({oid, lang})) {
                    continue;
                }
                const auto& suggestText = langAndText.second;
                const auto& namedObject = pair.second;
                auto parent = namedObject.objPtr->parent();
                TOid parentId = parent ? parent->id() : 0;
                auto commitId = namedObject.objPtr->revision().commitId();
                const auto& geom = objectSuggestGeom(namedObject.objPtr);

                if (empty) {
                    empty = false;
                } else {
                    query << ",";
                }
                query << "(" << oid << "," << parentId << "," <<
                work.quote(suggestText) << ",lower(" << work.quote(suggestText) << " COLLATE \"C.UTF-8\"),"
                << commitId << ","
                    "hstore(" << work.quote(plainCategoryIdToCanonical(
                        namedObject.objPtr->categoryId())) << ", '1')"
                <<  ",";
                if (!geom.isNull()) {
                    query << " ST_GeomFromWkb('"
                    << work.esc_raw(geom.wkb())
                    << "',3395)";
                } else {
                    query << " NULL ";
                }
                query << ", '" << lang << "'";
                query << ")";
            }
        }
        if (empty) {
            return "";
        }
        return "INSERT INTO suggest_data"
                "(object_id, parent_id, text_data, lower_text_data, commit_id, categories, the_geom, lang)"
                " VALUES " + query.str();
    }

    struct NamedObject
    {
        srv_attrs::SuggestTexts langsAndTexts;
        ObjectPtr   objPtr;
    };

    const srv_attrs::CalcSrvAttrs& srvAttrsCalculator_;
    std::unordered_map<TOid, NamedObject> namedObjects_;
    TOidSet deleteOids_;
    std::set<std::pair<TOid, std::string>> existsOidsAndLang_;
    bool checkExists_;
};

} // namespace

void
ViewSyncronizer::updateObjectsSuggests(
    ObjectsCache& cache, ObjectPredicate filter, bool checkSuggestExists) const
{
    auto completeFilter = [&] (const GeoObject* object) -> bool
    {
        return filter(object) &&
               object->revision().valid() &&
               !object->isDeleted() &&
               object->category().suggest();
    };
    GeoObjectCollection collection = cache.find(completeFilter);
    srv_attrs::CalcSrvAttrs srvAttrsCalculator(cache);
    SuggestDataBuilder suggestDataBuilder(
        srvAttrsCalculator, collection, checkSuggestExists);
    suggestDataBuilder.process();
}

} // namespace wiki
} // namespace maps
