#include "object_relation.h"

#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/metadata.h>

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/pg_locks.h>

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>

#include <pqxx/pqxx>


namespace maps::mrc::eye {

namespace {

template<class T>
db::TIds extractIds(const T& items) {
    db::TIds ids(items.size());
    for (size_t i = 0; i < items.size(); i++) {
        ids[i] = items[i].id();
    }
    return ids;
}

template<class T>
std::set<db::TId> extractIdSet(const T& items) {
    std::set<db::TId> ids;
    for (size_t i = 0; i < items.size(); i++) {
        ids.insert(items[i].id());
    }
    return ids;
}

template <class K, class V>
std::vector<K> extractKeys(const std::map<K, V>& m)
{
    std::vector<K> result;
    for (auto it = m.begin(); it != m.end(); it++)
    {
        result.emplace_back(it->first);
    }
    return result;
}

// получаем Id объектов, которые основаны на детекциях из ids
db::TIds getObjectIdsFromDetectionIds(
    pqxx::transaction_base& txn,
    db::TIds ids)
{
    const db::eye::PrimaryDetectionRelations pdrels = db::eye::PrimaryDetectionRelationGateway(txn).load(
        db::eye::table::PrimaryDetectionRelation::detectionId.in(ids) &&
        !db::eye::table::PrimaryDetectionRelation::deleted
    );

    for (size_t i = 0; i < pdrels.size(); i++) {
        ids.push_back(pdrels[i].primaryDetectionId());
    }
    const db::TIds objectIds = db::eye::ObjectGateway(txn).loadIds(
        db::eye::table::Object::primaryDetectionId.in(ids) &&
        db::eye::table::Object::type == db::eye::ObjectType::Sign
    );
    return objectIds;
}

// загружает объекты дорожных знаков удаленные (deleted = true) или неудаленные (deleted = false)
db::eye::Objects loadObjects(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds,
    bool deleted)
{
    maps::sql_chemistry::FiltersCollection filter{ maps::sql_chemistry::op::Logical::And };
    filter.add(
        db::eye::table::Object::id.in(objectIds)
    );
    filter.add(
        db::eye::table::Object::type == db::eye::ObjectType::Sign
    );
    filter.add(deleted ? db::eye::table::Object::deleted : !db::eye::table::Object::deleted);
    return db::eye::ObjectGateway(txn).load(filter);
}

db::eye::ObjectRelations loadObjectRelations(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds)
{
    return db::eye::ObjectRelationGateway(txn).load(
        (db::eye::table::ObjectRelation::masterObjectId.in(objectIds) ||
         db::eye::table::ObjectRelation::slaveObjectId.in(objectIds))
    );
}

// помечаем связи удаленными
void markObjectRelationsDeleted(db::eye::ObjectRelations& objRelations)
{
    for (size_t i = 0; i < objRelations.size(); i++) {
        objRelations[i].setDeleted(true);
    }
}

// загружаем удаленные объекты дорожных знаков и помечаем удаленными связи на одном
// из концов которых такой объект и делаем upsert в базу
size_t updateDeletedObjectRelations(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds)
{
    db::eye::ObjectRelations objRelations =
        loadObjectRelations(txn,
            extractIds(loadObjects(txn, objectIds, true))
        );
    markObjectRelationsDeleted(objRelations);
    db::eye::ObjectRelationGateway(txn).upsertx(objRelations);
    return objRelations.size();
}

// загружаем неудаленные объекты дорожных знаков и разбиваем их на две части
// ->first - таблички
// ->second - знаки
std::pair<db::eye::Objects, db::eye::Objects> loadNotDeletedObjects(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds)
{
    db::eye::Objects objects = loadObjects(txn, objectIds, false);
    std::pair<db::eye::Objects, db::eye::Objects> result;
    for (size_t i = 0; i < objects.size(); i++) {
        if (maps::mrc::traffic_signs::isAdditionalTable(objects[i].attrs<db::eye::SignAttrs>().type)) {
            result.first.emplace_back(std::move(objects[i]));
        } else {
            result.second.emplace_back(std::move(objects[i]));
        }
    }
    return result;
}

// загружаем detectionId всех переданных объектов
db::TIds loadObjectsDetectionIds(
    pqxx::transaction_base& txn,
    const db::eye::Objects& objects)
{
    db::TIds result;
    for (size_t i = 0; i < objects.size(); i++) {
        result.push_back(objects[i].primaryDetectionId());
    }
    db::eye::PrimaryDetectionRelations pdrels =
        db::eye::PrimaryDetectionRelationGateway(txn).load(
            db::eye::table::PrimaryDetectionRelation::primaryDetectionId.in(result) &&
            !db::eye::table::PrimaryDetectionRelation::deleted
        );
    for (size_t i = 0; i < pdrels.size(); i++) {
        result.push_back(pdrels[i].detectionId());
    }
    return result;
}

// загружаем детекции переданных объектов
//    map - ключ Id детекции
//          значение Id объекта
std::map<db::TId, db::TId> loadObjectIdByDetectionId(
    pqxx::transaction_base& txn,
    const db::eye::Objects& objects)
{
    std::map<db::TId, db::TId> result;
    db::TIds primaryIds;
    for (size_t i = 0; i < objects.size(); i++) {
        const db::eye::Object& object = objects[i];
        result[object.primaryDetectionId()] = object.id();
        primaryIds.push_back(object.primaryDetectionId());
    }

    db::eye::PrimaryDetectionRelations pdrels =
        db::eye::PrimaryDetectionRelationGateway(txn).load(
            db::eye::table::PrimaryDetectionRelation::primaryDetectionId.in(primaryIds) &&
            !db::eye::table::PrimaryDetectionRelation::deleted
        );

    for (size_t i = 0; i < pdrels.size(); i++) {
        const db::eye::PrimaryDetectionRelation& pdrel = pdrels[i];
        result[pdrel.detectionId()] = result[pdrel.primaryDetectionId()];
    }
    return result;
}

// 4. ищем все связи между объектами у которых slave в наборе slaveIds и делаем map по slave
//    (может быть несколько связей от одного slave, но только одна неудаленная)
std::map<db::TId, db::eye::ObjectRelations> loadObjectRelationBySlaveId(
    pqxx::transaction_base& txn,
    const db::TIds& slaveIds)
{
    db::eye::ObjectRelations objRels =
        db::eye::ObjectRelationGateway(txn).load(
             db::eye::table::ObjectRelation::slaveObjectId.in(slaveIds)
        );
    std::map<db::TId, db::eye::ObjectRelations> result;
    for (size_t i = 0; i < objRels.size(); i++) {
        result[objRels[i].slaveObjectId()].emplace_back(std::move(objRels[i]));
    }
    return result;
}

// загружает связи детекций у которых
//    slaveDetectionId в slaveDetectionIds
//    masterDetectionId в masterDetectionIds
db::eye::DetectionRelations loadDetectionRelations(
    pqxx::transaction_base& txn,
    const db::TIds& slaveDetectionIds,
    const db::TIds& masterDetectionIds)
{
    return db::eye::DetectionRelationGateway(txn).load(
        db::eye::table::DetectionRelation::masterDetectionId.in(masterDetectionIds) &&
        db::eye::table::DetectionRelation::slaveDetectionId.in(slaveDetectionIds) &&
        ! db::eye::table::DetectionRelation::deleted
    );
}


// расширяем набор знаков всеми знаками, которые имеют связь с табличками
// через не удалённые DetectionRelation и через ObjectRelation
void expandSignObjectsByTableObjects(
    pqxx::transaction_base& txn,
    const db::eye::Objects& tableObjects,
    db::eye::Objects& signObjects)
{
    const std::set<db::TId> signIds = extractIdSet(signObjects);

    const db::eye::ObjectRelations objRels =
        db::eye::ObjectRelationGateway(txn).load(
             db::eye::table::ObjectRelation::slaveObjectId.in(extractIds(tableObjects))
        );

    std::set<db::TId> signExpIds;
    for (size_t i = 0; i < objRels.size(); i++) {
        const db::TId id = objRels[i].masterObjectId();
        if (signIds.count(id) == 0) {
            signExpIds.insert(id);
        }
    }

    const db::eye::DetectionRelations drels = db::eye::DetectionRelationGateway(txn).load(
        db::eye::table::DetectionRelation::slaveDetectionId.in(
            loadObjectsDetectionIds(txn, tableObjects)
        ) &&
        ! db::eye::table::DetectionRelation::deleted
    );

    db::TIds signDetectionIds;
    for (size_t i = 0; i < drels.size(); i++) {
        signDetectionIds.push_back(drels[i].masterDetectionId());
    }
    const db::TIds objectIds = getObjectIdsFromDetectionIds(txn, std::move(signDetectionIds));

    for (size_t i = 0; i < objectIds.size(); i++) {
        const db::TId id = objectIds[i];
        if (signIds.count(id) == 0) {
            signExpIds.insert(id);
        }
    }
    db::eye::Objects signExpObjects =
        db::eye::ObjectGateway(txn).load(
            db::eye::table::Object::id.in(db::TIds(signExpIds.begin(), signExpIds.end())) &&
            ! db::eye::table::Object::deleted
        );

    signObjects.insert(signObjects.end(), signExpObjects.begin(), signExpObjects.end());
}

// возвращает кол-во неудаленных связей детекций между объектами
//      ключ - пара <tableObjectId, signObjectId>
//      значение - кол-во связей между детекциями этих объектов
std::map<std::pair<db::TId, db::TId>, size_t> calculateDetectionRelationCountByObjectsPair(
    pqxx::transaction_base& txn,
    const std::map<db::TId, db::TId>& tableObjectIdByDetectionId,
    const std::map<db::TId, db::TId>& signObjectIdByDetectionId)
{
    db::eye::DetectionRelations drels =
        loadDetectionRelations(
            txn,
            extractKeys(tableObjectIdByDetectionId),
            extractKeys(signObjectIdByDetectionId)
        );

    std::map<std::pair<db::TId, db::TId>, size_t> result;
    for (size_t i = 0; i < drels.size(); i++) {
        db::TId slaveObjectId = tableObjectIdByDetectionId.at(drels[i].slaveDetectionId());
        db::TId masterObjectId = signObjectIdByDetectionId.at(drels[i].masterDetectionId());
        const std::pair<db::TId, db::TId> key =
            std::make_pair<db::TId, db::TId>(std::move(slaveObjectId), std::move(masterObjectId));
        auto it = result.find(key);
        if (it != result.end()) {
            it->second++;
        } else {
            result[key] = 1;
        }
    }
    return result;
}

// считаем число детекции для каждого объекта
std::map<db::TId, size_t> calculateDetectionCountByObjectId(const std::map<db::TId, db::TId>& objectIdByDetectionId)
{
    std::map<db::TId, size_t> result;
    for (auto it = objectIdByDetectionId.begin(); it != objectIdByDetectionId.end(); it++) {
        auto resultIt = result.find(it->second);
        if (resultIt != result.end()) {
            resultIt->second++;
        } else {
            result[it->second] = 1;
        }
    }
    return result;
}

// возвращаем правильные на текущий момент связи между объектами
//    objectsPairDetectionCount - map ключ пара <tableObjectId, signObjectId>
//                                    значение кол-во связей между детекциями этих объектов
//    detectionCountByTableObjectId - map ключ - tableObjectId
//                                        значение - кол-во детекций данного объекта
//    detectionCountBySignObjectId  - map ключ - signObjectId
//                                        значение - кол-во детекций данного объекта
//    Return:
//        вектор пар <tableObjectId, signObjectId> для которых кол-во связей между детекциями
//        больше чем 60% детекций каждого из объектов
std::vector<std::pair<db::TId, db::TId>> searchObjectRelations(
    const std::map<std::pair<db::TId, db::TId>, size_t>& objectsPairDetectionRelationsCount,
    const std::map<db::TId, size_t>& detectionCountByTableObjectId,
    const std::map<db::TId, size_t>& detectionCountBySignObjectId)
{
    constexpr double DETECTION_RELATION_FRAC = 0.6;
    REQUIRE(DETECTION_RELATION_FRAC > 0.5,
        "Part of detections of table related to detection of master sign have to be great than 0.6");
    // нам надо больше половины детекций связанных иначе мы можем получить ситуацию
    // когда будет привязка к нескольким объектам

    std::vector<std::pair<db::TId, db::TId>> result;
    for (auto it = objectsPairDetectionRelationsCount.begin(); it != objectsPairDetectionRelationsCount.end(); it++) {
        const size_t tableDetectionCount = detectionCountByTableObjectId.at(it->first.first);
        const size_t signDetectionCount = detectionCountBySignObjectId.at(it->first.second);
        const size_t detectionsRelationCount = it->second;
        if (detectionsRelationCount > DETECTION_RELATION_FRAC * std::max(tableDetectionCount, signDetectionCount)) {
            result.emplace_back(it->first);
        }
    }
    return result;
}

db::eye::ObjectRelations updateObjectRelation(
    const std::vector<std::pair<db::TId, db::TId>>& validObjectRelations,
    const std::map<db::TId, db::eye::ObjectRelations>& objectRelationBySlaveId,
    std::set<db::TId> tableIds)
{
    db::eye::ObjectRelations result;
    for (size_t i = 0; i < validObjectRelations.size(); i++) {
        const db::TId slaveObjectId = validObjectRelations[i].first;
        const db::TId masterObjectId = validObjectRelations[i].second;
        auto it = objectRelationBySlaveId.find(slaveObjectId);
        if (it == objectRelationBySlaveId.end()) {
            result.emplace_back(masterObjectId, slaveObjectId);
        } else {
            bool found = false;
            const db::eye::ObjectRelations& objRelations = it->second;
            for (size_t j = 0; j < objRelations.size(); j++) {
                if (objRelations[j].masterObjectId() == masterObjectId) {
                    if (objRelations[j].deleted()) {
                        db::eye::ObjectRelation relation = objRelations[j];
                        relation.setDeleted(false);
                        result.emplace_back(relation);
                    }
                    found = true;
                } else {
                    if (!objRelations[j].deleted()) {
                        db::eye::ObjectRelation relation = objRelations[j];
                        relation.setDeleted(true);
                        result.emplace_back(relation);
                    }
                }
            }
            if (!found) {
                result.emplace_back(masterObjectId, slaveObjectId);
            }
        }
        tableIds.erase(slaveObjectId);
    }
    for (auto itId = tableIds.begin(); itId != tableIds.end(); itId++) {
        const db::TId slaveObjectId = (*itId);
        auto it = objectRelationBySlaveId.find(slaveObjectId);
        if (it != objectRelationBySlaveId.end()) {
            const db::eye::ObjectRelations& objRelations = it->second;
            for (size_t j = 0; j < objRelations.size(); j++) {
                if (!objRelations[j].deleted()) {
                    db::eye::ObjectRelation relation = objRelations[j];
                    relation.setDeleted(true);
                    result.emplace_back(relation);
                }
            }
        }
    }
    return result;
}

} // namespace

void updateObjectRelations(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds)
{
    // 1. удаляем связи между объектами, если один из объектов на конце связи удален
    updateDeletedObjectRelations(txn, objectIds);

    // 2. неудаленные объекты разбиваем на две части
    //    2.1. таблички
    //    2.2. знаки
    db::eye::Objects tableObjects;
    db::eye::Objects signObjects;
    tie(tableObjects, signObjects) = loadNotDeletedObjects(txn, objectIds);
    // Расширим набор знаков всеми знаками, которые имеют связь с табличками
    // через не удалённые DetectionRelation и через ObjectRelation
    expandSignObjectsByTableObjects(txn, tableObjects, signObjects);
    if ((0 == tableObjects.size()) || (0 == signObjects.size())) {
        return;
    }

    // 3. ищем все детекции
    //    3.1. для объектов из п.2.1
    //    3.2. для объектов из п.2.2
    std::map<db::TId, db::TId> tableObjectIdByDetectionId =
        loadObjectIdByDetectionId(txn, tableObjects);
    std::map<db::TId, db::TId> signObjectIdByDetectionId =
        loadObjectIdByDetectionId(txn, signObjects);

    // 4. ищем все связи между объектами у которых slave в наборе п.2.1. и делаем map по slave
    //    (может быть несколько связей от одного slave, но только одна неудаленная)
    std::map<db::TId, db::eye::ObjectRelations> objectRelationBySlaveId =
        loadObjectRelationBySlaveId(txn, extractIds(tableObjects));

    // 5. оставляем только те пары у которых RelationCount > 0.6 * max(детекций у tableObject, детекций у signObject)
    //    можно не 0.6, но больше чем 0.5
    std::vector<std::pair<db::TId, db::TId>> validObjectRelations =
        searchObjectRelations(
            // 5.1. ищем неудаленные связи детекций у которых slave в наборе п.3.1. а master в п.3.2.
            // и считаем кол-во связей между детекциями в map<<tableObjectId, signObjectId>, RelationCount>
            calculateDetectionRelationCountByObjectsPair(txn, tableObjectIdByDetectionId, signObjectIdByDetectionId),
            // 5.2. считаем кол-во детекций для каждой таблички
            calculateDetectionCountByObjectId(tableObjectIdByDetectionId),
            // 5.3. считаем кол-во детекций для каждого знака
            calculateDetectionCountByObjectId(signObjectIdByDetectionId)
        );

    // 6. теперь проверяем полученные новые связи из п.5 в map из п.4.
    //    есть такой table?
    //        6.1. да. есть связь с таким sign?
    //            6.1.1 да. метим эту связь неудаленной, остальные связи этой table метим как удаленные
    //            6.1.2. нет. метим все связи этой table как удаленные, и добавляем новую связь table-sign
    //        6.2. нет. добавляем новую связь table-sign
    // дополнительно проходим по тем slaveId, которых не было в validObjectRelations
    // и помечаем все связи удаленными
    db::eye::ObjectRelations objectRelations =
        updateObjectRelation(
            validObjectRelations,
            objectRelationBySlaveId,
            extractIdSet(tableObjects));

    db::eye::ObjectRelationGateway(txn).upsertx(objectRelations);
}


} // namespace maps::mrc::eye
