#include "edit_notes.h"
#include "magic_strings.h"
#include "objectvisitor.h"
#include "configs/config.h"
#include "objects/attr_object.h"
#include "objects/object.h"
#include "objects/areal_object.h"
#include "objects/junction.h"
#include "objects/linear_element.h"
#include "objects/line_object.h"
#include "objects/point_object.h"
#include "objects/complex_object.h"
#include "objects/relation_object.h"
#include "objects/category_traits.h"
#include "objects_cache.h"
#include "check_permissions.h"
#include "configs/categories_strings.h"
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/slave_role.h>
#include <yandex/maps/wiki/configs/editor/category_template.h>

namespace maps {
namespace wiki {
namespace edit_notes {

namespace {
const char LEVELS_DELIMITER {'-'};

const std::string CREATED_SUFFIX = "-created";

enum class RelationNotesGeomPartPolicy
{
    Skip,
    Keep
};

enum class RelationNotesSlavesPolicy
{
    PrimaryOnly,
    Always
};

class EditNotesVisitor : public ObjectVisitor
{
public:
    EditNotesVisitor(const TOIds& modifiedGeomElementsObjects)
        : modifiedGeomElementsObjects_(modifiedGeomElementsObjects)
    {}

    StringSet result() const
    {
        return result_;
    }

private:
    void visitGeoObject(const GeoObject* obj) override;
    void visitArealObject(const ArealObject* obj) override;
    void visitJunction(const Junction* obj) override;
    void visitLinearElement(const LinearElement* obj) override;
    void visitLineObject(const LineObject* obj) override;
    void visitPointObject(const PointObject* obj) override;
    void visitComplexObject(const ComplexObject* obj) override;
    void visitAttrObject(const AttrObject* obj) override;
    void visitRelationObject(const RelationObject* obj) override;

    void blockNotes(const GeoObject* obj);
    void attributesNotes(const GeoObject* obj);
    void relationsNotes(const GeoObject* obj,
        RelationNotesGeomPartPolicy geomPartPolicy,
        RelationNotesSlavesPolicy slavesPolicy);

    void routeNotes(const ComplexObject* obj);
    void threadNotes(const ComplexObject* obj);
    void contourNotes(const ComplexObject* obj);
    void linearNotes(const ComplexObject* obj);
    void centerNotes(const ComplexObject* obj);

    void entrancesNotes(const PointObject* obj);

    StringSet result_;
    const TOIds& modifiedGeomElementsObjects_;
};
}//namespace

StringSet
editNotes(const GeoObject* object, const TOIds& modifiedGeomElementsObjects)
{
    EditNotesVisitor env(modifiedGeomElementsObjects);
    object->applyVisitor(env);
    return env.result();
};

namespace {
struct NamesModified
{
    NamesModified() : official(0), alternative(0) { }
    size_t official;
    size_t alternative;
};

NamesModified
checkSlavesDiffForNames(const MixedRolesInfosContainer& diff1, const MixedRolesInfosContainer& diff2)
{
    NamesModified namesModified;
    for (const auto& slaveRel : diff1) {
        auto it = std::find_if(diff2.begin(), diff2.end(),
            [&slaveRel](const RelationInfo& slaveRel2) {
                return
                    slaveRel.roleId() == slaveRel2.roleId() &&
                    slaveRel.relative()->attributes() == slaveRel2.relative()->attributes();
            });
        if (it != diff2.end()) {
            continue;
        }
        if (slaveRel.roleId() == NAME_TYPE_OFFICIAL) {
            ++namesModified.official;
        } else if (!namesModified.alternative) {
            if (NAME_TYPES_PRIORITY.end() !=
                std::find(
                    NAME_TYPES_PRIORITY.begin(),
                    NAME_TYPES_PRIORITY.end(),
                    slaveRel.roleId()))
            {
                ++namesModified.alternative;
            }
        }
    }
    return namesModified;
}

std::string
linearComplexObjectElementCategory(const std::string& categoryId)
{
    const Category& cat = cfg()->editor()->categories()[categoryId];
    const auto& tmpl = cat.categoryTemplate();
    if (!cat.complex() || tmpl.hasGeometryType()) {
        return s_emptyString;
    }
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    const auto contourPartType = contourDefs.partType(categoryId);
    if (contourPartType != ContourObjectsDefs::PartType::None) {
        return s_emptyString;
    }
    auto slaveCatIds = cfg()->editor()->categories()[categoryId].slaveCategoryIds(roles::filters::IsGeom);
    for (const auto& slaveCatId : slaveCatIds) {
        const Category& slaveCat = cfg()->editor()->categories()[slaveCatId];
        const auto& slaveTmpl = slaveCat.categoryTemplate();
        if (slaveTmpl.isGeometryTypeValid(Geom::geomTypeNameLine)) {
            return slaveCatId;
        }
    }
    return s_emptyString;
}
}//namespace


void
EditNotesVisitor::blockNotes(const GeoObject* obj)
{
    if (!obj->original()) {
        return;
    }
    auto blocked = obj->isBlocked();
    if (blocked == obj->original()->isBlocked()) {
        return;
    }
    result_.insert(blocked ? MODIFIED_BLOCKED : MODIFIED_UNBLOCKED);
}

void
EditNotesVisitor::attributesNotes(const GeoObject* obj)
{
    auto slavesDiff = obj->slaveRelations().diff();
    auto addedNamesCheck = checkSlavesDiffForNames(slavesDiff.added, slavesDiff.deleted);
    auto deletedNamesCheck = checkSlavesDiffForNames(slavesDiff.deleted, slavesDiff.added);
    bool namesModified =
        addedNamesCheck.official ||
        addedNamesCheck.alternative ||
        deletedNamesCheck.official ||
        deletedNamesCheck.alternative;
    if (addedNamesCheck.official > 0 &&
        addedNamesCheck.official == deletedNamesCheck.official) {
        result_.insert(MODIFIED_ATTRIBUTES_NAMES_OFFICIAL);
    } else {
        if (addedNamesCheck.official) {
            result_.insert(MODIFIED_ATTRIBUTES_NAMES_OFFICIAL_ADDED);
        }
        if (deletedNamesCheck.official) {
            result_.insert(MODIFIED_ATTRIBUTES_NAMES_OFFICIAL_REMOVED);
        }
    }
    if (addedNamesCheck.alternative > 0 &&
        addedNamesCheck.alternative == deletedNamesCheck.alternative) {
        result_.insert(MODIFIED_ATTRIBUTES_NAMES_ALTERNATIVE);
    } else {
        if (addedNamesCheck.alternative) {
            result_.insert(MODIFIED_ATTRIBUTES_NAMES_ALTERNATIVE_ADDED);
        }
        if (deletedNamesCheck.alternative) {
            result_.insert(MODIFIED_ATTRIBUTES_NAMES_ALTERNATIVE_REMOVED);
        }
    }

    if (obj->isModifiedAttr() ||
        (obj->isModifiedTableAttrs() && !namesModified)) {
        result_.insert(edit_notes::MODIFIED_ATTRIBUTES_OTHER);
    }
}

void
EditNotesVisitor::relationsNotes(
    const GeoObject* obj,
    RelationNotesGeomPartPolicy geomPartPolicy,
    RelationNotesSlavesPolicy slavesPolicy)
{
    static const StringSet NO_NOTES_ON_RELATION_CHANGE_ROLES =
    {
        ROLE_CENTER,
        ROLE_START,
        ROLE_END
    };
    const auto genericNotesRole =
    [&] (const SlaveRole& role) -> bool {
        return
            (geomPartPolicy == RelationNotesGeomPartPolicy::Keep || !role.geomPart()) &&
            !role.tableRow() &&
            !NO_NOTES_ON_RELATION_CHANGE_ROLES.count(role.roleId());
    };
    const auto addRelationNotesWithRole = [&](
        RelationType relType,
        const MixedRolesInfosContainer& relChanged,
        const std::string& prefix,
        const std::string& categoryId = std::string()) {
        for (const auto& rel : relChanged) {
            const auto& relative = rel.relative();
            if (relType == RelationType::Master &&
                relative->category().slaveRole(rel.roleId()).limitSlaveEditNotes())
            {
                continue;
            }
            if (relType == RelationType::Slave &&
                relative->id() == obj->id())
            {
                continue;
            }
            result_.insert(prefix +
                (relative->isCreated()
                    ? CREATED_SUFFIX
                    : LEVELS_DELIMITER + rel.roleId())
                + LEVELS_DELIMITER +
                (categoryId.empty()
                    ? rel.categoryId()
                    : categoryId));
        }
    };
    auto mastersDiff = obj->masterRelations().diff();
    addRelationNotesWithRole(
        RelationType::Master,
        mastersDiff.added,
        edit_notes::MODIFIED_RELATIONS_MASTERS_ADDED);
    addRelationNotesWithRole(
        RelationType::Master,
        mastersDiff.deleted,
        edit_notes::MODIFIED_RELATIONS_MASTERS_REMOVED);
    if (slavesPolicy == RelationNotesSlavesPolicy::PrimaryOnly &&
        !obj->primaryEdit()) {
        return;
    }
    auto roleIds = obj->category().slaveRoleIds(genericNotesRole);
    auto slavesDiff = obj->slaveRelations().diff(roleIds);
    addRelationNotesWithRole(
        RelationType::Slave,
        slavesDiff.added,
        edit_notes::MODIFIED_RELATIONS_SLAVES_ADDED,
        obj->categoryId());
    addRelationNotesWithRole(
        RelationType::Slave,
        slavesDiff.deleted,
        edit_notes::MODIFIED_RELATIONS_SLAVES_REMOVED,
        obj->categoryId());

}

void
EditNotesVisitor::visitGeoObject(const GeoObject* obj)
{
    relationsNotes(obj, RelationNotesGeomPartPolicy::Skip, RelationNotesSlavesPolicy::PrimaryOnly);
    if (obj->isCreated()) {
        result_.insert(edit_notes::CREATED);
        return;
    }
    if (obj->original() && obj->categoryId() != obj->original()->categoryId()) {
        result_.insert(edit_notes::MODIFIED_CATEGORY);
    }
    if (obj->isModifiedState()) {
        result_.insert(edit_notes::DELETED);
    }
    if (obj->isDeleted()) {
        return;
    }
    if (obj->isModifiedRichContent()) {
        result_.insert(edit_notes::MODIFIED_DESCRIPTION);
    }
    if (obj->isModifiedGeom()) {
        result_.insert(edit_notes::MODIFIED_GEOMETRY);
    }
    blockNotes(obj);
    attributesNotes(obj);
}

void
EditNotesVisitor::visitArealObject(const ArealObject* obj)
{
    visitGeoObject(obj);
}

void
EditNotesVisitor::visitJunction(const Junction* obj)
{
    visitGeoObject(obj);
}

void
EditNotesVisitor::visitLinearElement(const LinearElement* obj)
{
    visitGeoObject(obj);
    if (obj->affectedBySplitId()) {
        result_.insert(
            obj->isCreated()
            ? edit_notes::CREATED_SPLIT
            : edit_notes::MODIFIED_SPLIT);
    }
}

void
EditNotesVisitor::visitLineObject(const LineObject* obj)
{
    visitGeoObject(obj);
}

void
EditNotesVisitor::visitPointObject(const PointObject* obj)
{
    visitGeoObject(obj);
    if (obj->isCreated()) {
        return;
    }
    entrancesNotes(obj);
}

void
EditNotesVisitor::routeNotes(const ComplexObject* obj)
{
    if (!isTransportRoute(obj->categoryId())) {
        return;
    }
    relationsNotes(obj, RelationNotesGeomPartPolicy::Keep,
        RelationNotesSlavesPolicy::Always);
}


void
EditNotesVisitor::threadNotes(const ComplexObject* obj)
{
    if (!isTransportThread(obj->categoryId())) {
        return;
    }
    const auto& stopRefs = obj->slaveRelations().range(ROLE_PART);
    for (const auto& stopRef : stopRefs) {
        if (stopRef.relative()->isModifiedAttr()) {
            result_.insert(edit_notes::MODIFIED_ATTRIBUTES);
            return;
        }
    }
}

void
EditNotesVisitor::contourNotes(const ComplexObject* obj)
{
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    const auto contourPartType = contourDefs.partType(obj->categoryId());
    if (contourPartType == ContourObjectsDefs::PartType::None) {
        return;
    }
    const auto& contourDef = contourDefs.contourDef(obj->categoryId());
    if (contourPartType == ContourObjectsDefs::PartType::Contour) {
        const auto& elementsDiff =
            obj->slaveRelations().diff(contourDef.contour.linearElement.roleId);
        if (!elementsDiff.added.empty()) {
            result_.insert(edit_notes::MODIFIED_GEOMETRY_ELEMENTS_ADDED);
        }
        if (!elementsDiff.deleted.empty()) {
            result_.insert(edit_notes::MODIFIED_GEOMETRY_ELEMENTS_REMOVED);
        }
        if (modifiedGeomElementsObjects_.count(obj->id())) {
            result_.insert(edit_notes::MODIFIED_GEOMETRY_ELEMENTS);
        }
    }
    if (contourPartType == ContourObjectsDefs::PartType::Object) {
        const auto& contoursDiff =
            obj->slaveRelations().diff(contourDef.contour.roleId);
        if (!contoursDiff.added.empty()) {
            result_.insert(edit_notes::MODIFIED_GEOMETRY_CONTOURS_ADDED);
        }
        if (!contoursDiff.deleted.empty()) {
            result_.insert(edit_notes::MODIFIED_GEOMETRY_CONTOURS_REMOVED);
        }
        if (modifiedGeomElementsObjects_.count(obj->id())) {
            result_.insert(edit_notes::MODIFIED_GEOMETRY_CONTOURS);
        }
    }
}

void
EditNotesVisitor::linearNotes(const ComplexObject* obj)
{
    const auto& lnPartCat = linearComplexObjectElementCategory(obj->categoryId());
    if (!lnPartCat.empty()) {
        const auto& elementsDiff = obj->slaveRelations().diff();
        for (const auto& added : elementsDiff.added) {
            if (added.categoryId() == lnPartCat) {
                result_.insert(edit_notes::MODIFIED_GEOMETRY_ELEMENTS_ADDED);
                break;
            }
        }
        for (const auto& removed : elementsDiff.deleted) {
            if (removed.categoryId() == lnPartCat) {
                result_.insert(edit_notes::MODIFIED_GEOMETRY_ELEMENTS_REMOVED);
                break;
            }
        }
        if (modifiedGeomElementsObjects_.count(obj->id())) {
            result_.insert(edit_notes::MODIFIED_GEOMETRY_ELEMENTS);
        }
    }
}

void
EditNotesVisitor::centerNotes(const ComplexObject* obj)
{
    const auto& centersDiff = obj->slaveRelations().diff(ROLE_CENTER);
    bool relationWithCenterModified = false;
    if (!centersDiff.added.empty()) {
        result_.insert(edit_notes::MODIFIED_GEOMETRY_CENTER_ADDED);
        relationWithCenterModified = true;
    }
    if (!centersDiff.deleted.empty()) {
        result_.insert(edit_notes::MODIFIED_GEOMETRY_CENTER_REMOVED);
        relationWithCenterModified = true;
    }
    if (!relationWithCenterModified) {
        for (const auto& slaveRel : obj->slaveRelations().range(ROLE_CENTER)) {
            if (slaveRel.relative()->isModifiedGeom()) {
                result_.insert(edit_notes::MODIFIED_GEOMETRY_CENTER);
                break;
            }
        }
    }
}

void
EditNotesVisitor::entrancesNotes(const PointObject* obj)
{
    for (const auto& slaveRel : obj->slaveRelations().range(ROLE_ENTRANCE_ASSIGNED)) {
        if (slaveRel.relative()->isModifiedGeom()) {
            result_.insert(
                edit_notes::MODIFIED_RELATIONS_SLAVES_GEOMETRY +
                LEVELS_DELIMITER +
                ROLE_ENTRANCE_ASSIGNED);
            break;
        }
    }
}

void
EditNotesVisitor::visitComplexObject(const ComplexObject* obj)
{
    visitGeoObject(obj);
    if (obj->isCreated()) {
        return;
    }
    routeNotes(obj);
    threadNotes(obj);
    contourNotes(obj);
    linearNotes(obj);
    centerNotes(obj);
}

void
EditNotesVisitor::visitAttrObject(const AttrObject* /*obj*/)
{
    //attr objects have no edit notes
}

void
EditNotesVisitor::visitRelationObject(const RelationObject* /* obj */)
{
    //relation objects have no edit notes
}

namespace {
void
insert(EditNotesTree& tree, const std::string& editNote)
{
    auto path = split<StringVec>(editNote, LEVELS_DELIMITER);
    auto* nodes = &tree.rootNotes;
    for (const auto& nodeNote : path) {
        auto where = std::find_if(nodes->begin(), nodes->end(),
            [&](const EditNotesTree::Node& node) { return node.note == nodeNote; });
        if (where != nodes->end()) {
            nodes = &(where->subNotes);
        } else {
            nodes->push_back({nodeNote, {}});
            nodes = &(nodes->back().subNotes);
        }
    }
}
}//namespace

EditNotesTree
treeRepresentation(const StringSet& editNotesSet)
{
    EditNotesTree tree;
    for (const auto& editNote : editNotesSet) {
        insert(tree, editNote);
    }
    return tree;
}

} // edit_notes
} // namespace wiki
} // namespace maps
