#pragma once

#include "object_constructors.h"
#include "relations_cardinality_checks.h"
#include <maps/wikimap/mapspro/libs/validator/common/exception.h>
#include <maps/wikimap/mapspro/libs/validator/common/magic_strings.h>
#include <maps/wikimap/mapspro/libs/validator/common/utils.h>

#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/intersection.h>

#include <set>
#include <map>
#include <string>
#include <utility>
#include <vector>


namespace maps {
namespace wiki {
namespace validator {


template<class TObject>
void BaseObjectConstructor<TObject>::report(
        Severity severity,
        const TCheckId& checkId,
        const std::string& message,
        RegionType regionType,
        const std::string& geom,
        const RevisionIds& revisionIds)
{
    messages_->emplace_back(
        severity, checkId, message, regionType, geom, revisionIds
    );
}

template<class TObject>
template<class ...Args>
void BaseObjectConstructor<TObject>::createObject(Args&& ...args)
{
    objects_->emplace_back(std::forward<Args>(args)...);
}

template<class TObject>
void BaseObjectConstructor<TObject>::removeRelationsWithMissingObject(
    const RevisionID& revisionId,
    Relations& relations)
{
    auto current = relations.begin();
    for (auto next = current; next != relations.end(); ++next) {
        if (!this->exists(next->other)) {
            this->report(
                Severity::Fatal,
                BASE_CHECK_ID,
                this->deleted(next->other)
                    ? MESSAGE_DELETED_RELATED_OBJECT
                    : MESSAGE_MISSING_RELATED_OBJECT,
                RegionType::Unimportant,
                NO_GEOMETRY,
                {revisionId, this->revisionId(next->other)}
            );
            continue;
        }

        if (current != next) {
            *current = std::move(*next);
        }
        ++current;
    }

    relations.erase(current, relations.end());
}

template<class TObject>
void BaseObjectConstructor<TObject>::removeRelationDuplicates(
    const RevisionID& revisionId,
    Relations& relations)
{
    auto less = [](const Relation& lhs, const Relation& rhs) {
        return (lhs.other < rhs.other) ||
            (lhs.other == rhs.other && lhs.role < rhs.role) ||
            (lhs.other == rhs.other && lhs.role == rhs.role && lhs.seqNum < rhs.seqNum);
    };

    auto equal = [](const Relation& lhs, const Relation& rhs) {
        return lhs.other == rhs.other &&
            lhs.role == rhs.role &&
            lhs.seqNum == rhs.seqNum;
    };

    std::sort(relations.begin(), relations.end(), less);

    std::set<RevisionID> revisionIds;
    auto current = relations.begin();
    for (auto next = std::next(current); next < relations.end(); ++next) {
        if (equal(*current, *next)) {
            revisionIds.insert(this->revisionId(current->other));
            continue;
        }
        ++current;
        if (next != current) {
            *current = std::move(*next);
        }
    }
    if (current != relations.end()) {
        relations.erase(current + 1, relations.end());
    }

    for (const auto& id: revisionIds) {
        report(
            Severity::Critical,
            BASE_CHECK_ID,
            MESSAGE_DUPLICATE_RELATION,
            RegionType::Unimportant,
            NO_GEOMETRY,
            {revisionId, id}
        );
    }
}

template<class TObject>
void BaseObjectConstructor<TObject>::removeBadRelations(ObjectDatum& datum)
{
    const RevisionID& revisionId = datum.revision.id();
    removeRelationsWithMissingObject(revisionId, datum.masters);
    removeRelationsWithMissingObject(revisionId, datum.slaves);
    removeRelationDuplicates(revisionId, datum.masters);
    removeRelationDuplicates(revisionId, datum.slaves);
}

template<class TObject>
void BaseObjectConstructor<TObject>::checkRelationsCardinality(const ObjectDatum& datum)
{
    if (!configCategory_) {
        return;
    }
    checkSlaveRelationsCardinality(*configCategory_, datum);
    checkMasterRelationsCardinality(*configCategory_, datum);
}

template<class TObject>
void SimpleObjectConstructor<TObject>::construct(ObjectDatum datum)
{
    requireOnObjectLoad(
        datum.revision.data().attributes.has_value(),
        MESSAGE_NO_ATTRIBUTES,
        datum.revision.id().objectId()
    );

    base_.removeBadRelations(datum);
    base_.checkRelationsCardinality(datum);

    base_.createObject(
        datum.revision.id().objectId(),
        std::move(*datum.revision.data().attributes),
        std::move(datum.masters),
        std::move(datum.slaves)
    );
}

template<class TObject>
void GeomObjectConstructor<TObject>::construct(ObjectDatum datum)
{
    const TId objectId = datum.revision.id().objectId();

    requireOnObjectLoad(
        datum.revision.data().attributes.has_value(), MESSAGE_NO_ATTRIBUTES, objectId
    );
    requireOnObjectLoad(
        datum.revision.data().geometry.has_value(), MESSAGE_NO_GEOMETRY, objectId
    );

    using TGeom = typename TObject::TGeom;

    TGeom geom;
    try {
        geom = geolib3::WKB::read<TGeom>(*datum.revision.data().geometry);
    } catch (maps::Exception& ex) {
        //geolib validation failed
        WARN()  << "maps::Exception while loading object "
                << objectId << ", " << ex.what();
        throw ObjectLoadingException(MESSAGE_BAD_GEOMETRY, objectId);
    }

    base_.removeBadRelations(datum);
    base_.checkRelationsCardinality(datum);

    const auto& aoi = base_.aoi();
    if (aoi.empty() || aoi.intersectsBuffered(geom)) {
        base_.createObject(
            objectId,
            std::move(geom),
            std::move(*datum.revision.data().attributes),
            std::move(datum.masters),
            std::move(datum.slaves)
        );
    }
}

template<class TObject>
void SystemObjectConstructor<TObject>::construct(ObjectDatum datum)
{
    if (datum.masters.empty()) {
        return;
    }

    requireOnObjectLoad(
        datum.revision.data().attributes.has_value(),
        MESSAGE_NO_ATTRIBUTES,
        datum.revision.id().objectId()
    );

    base_.removeBadRelations(datum);
    base_.checkRelationsCardinality(datum);

    base_.createObject(
        datum.revision.id().objectId(),
        std::move(*datum.revision.data().attributes),
        std::move(datum.masters),
        std::move(datum.slaves)
    );
}

} // namespace validator
} // namespace wiki
} // namespace maps
