#include "relation_data.h"
#include "utils/geom.h"
#include <maps/libs/geolib/include/variant.h>

#include <yandex/maps/wiki/groupedit/object.h>
#include <maps/libs/common/include/exception.h>

#include <algorithm>

namespace maps {
namespace wiki {
namespace groupedit {

namespace {

const std::string CATEGORY_ATTR_PREFIX = "cat:";
const std::string BOUND_JC_CATEGORY_ATTR = "cat:bound_jc";

} // namespace

Object::Object(
        revision::RevisionID revisionId,
        revision::ObjectData objectData,
        std::list<Relation> relations)
    : revisionId_(revisionId)
    , data_(std::move(objectData))
    , relations_(std::move(relations))
    , isDeleted_(false)
{
    REQUIRE(data_.attributes, "Object has no attributes");
    REQUIRE(!data_.deleted, "Deleted objects are not supported yet");

    for (const auto& [key, _] : *data_.attributes) {
        if (key.starts_with(CATEGORY_ATTR_PREFIX)
                && key != BOUND_JC_CATEGORY_ATTR) { // MAPSPRO-1529
            REQUIRE(category_.empty(), "Object has multiple categories");
            category_ = key.substr(CATEGORY_ATTR_PREFIX.size());
        }
    }
    REQUIRE(!category_.empty(), "Object has no category");
}

void Object::changeCategory(const TCategoryId& newCategory)
{
    REQUIRE(!newCategory.empty(), "Empty category");
    REQUIRE(newCategory.find(':') == std::string::npos, "Category should not include colon");
    if (newCategory == category()) {
        return;
    }

    auto categoryAttrName = CATEGORY_ATTR_PREFIX + category();
    auto categoryWithColon = category() + ":";

    for (auto& pair : attributes()) {
        const auto& name = pair.first;
        auto& value = pair.second;

        auto newName = name;
        if (name == categoryAttrName) {
            newName = CATEGORY_ATTR_PREFIX + newCategory;
        } else if (name.starts_with(categoryWithColon)) {
            newName.replace(0, category().size(), newCategory);
        }

        if (newName != name) {
            removeAttributeImpl(name);
            setAttributeImpl(newName, std::move(value));
        }
    }

    category_ = newCategory;

    std::vector<std::shared_ptr<RelationData>> newRelDataPtrs;
    for (auto& relation : relations()) {
        relation.setDeleted();

        if (relation.type() == Relation::Type::Slave) {
            newRelDataPtrs.push_back(std::make_shared<RelationData>(
                relation.role(), id(), newCategory, relation.otherId(), relation.otherCategory()));
        } else {
            newRelDataPtrs.push_back(std::make_shared<RelationData>(
                relation.role(), relation.otherId(), relation.otherCategory(), id(), newCategory));
        }
    }

    for (const auto& relDataPtr : newRelDataPtrs) {
        relations_.emplace_back(id(), relDataPtr);
    }
}

std::optional<std::string> Object::geometryWkb() const
{
    return newGeometryWkb_
        ? newGeometryWkb_
        : data_.geometry;
}

void Object::setGeometryWkb(std::string wkb)
{
    REQUIRE(data_.geometry, "Object has no geometry");

    if (*data_.geometry == wkb) {
        newGeometryWkb_ = std::nullopt;
    } else {
        auto oldGeometryVariant = geolib3::WKB::read<geolib3::SimpleGeometryVariant>(*data_.geometry);
        auto newGeometryVariant = geolib3::WKB::read<geolib3::SimpleGeometryVariant>(wkb);

        REQUIRE(oldGeometryVariant.geometryType() == newGeometryVariant.geometryType(),
            "Attempt to change geometry type");

        newGeometryWkb_ = std::move(wkb);
    }
}

std::optional<std::string> Object::attribute(const std::string& name) const
{
    if (removedAttributeNames_.count(name)) {
        return std::nullopt;
    }

    auto diffIt = attributesDiff_.find(name);
    if (diffIt != std::end(attributesDiff_)) {
        return diffIt->second;
    }

    auto origIt = data_.attributes->find(name);
    if (origIt != std::end(*data_.attributes)) {
        return origIt->second;
    }
    return std::nullopt;
}

void Object::setAttribute(const std::string& name, std::string value)
{
    REQUIRE(!name.starts_with(CATEGORY_ATTR_PREFIX),
        "Attempt to modify category");

    setAttributeImpl(name, std::move(value));
}

void Object::removeAttribute(const std::string& name)
{
    REQUIRE(!name.starts_with(CATEGORY_ATTR_PREFIX),
        "Attempt to modify category");

    removeAttributeImpl(name);
}

Attributes Object::attributes() const
{
    REQUIRE(data_.attributes, "Object has no attributes");

    Attributes attributes = *data_.attributes;

    for (const auto& name : removedAttributeNames_) {
        attributes.erase(name);
    }

    for (const auto& pair : attributesDiff_) {
        attributes[pair.first] = pair.second;
    }

    return attributes;
}

Object::RelationsRange Object::relations()
{ return RelationsRange(std::begin(relations_), std::end(relations_)); }

Object::ConstRelationsRange Object::relations() const
{
    return ConstRelationsRange(
            std::begin(relations_), std::end(relations_));
}

void Object::addRelationToMaster(
            TObjectId otherId, TCategoryId otherCategory, std::string role)
{
    TObjectId myId = revisionId_.objectId();
    auto relDataPtr = std::make_shared<RelationData>(
            std::move(role), otherId, std::move(otherCategory), myId, category_);
    relations_.emplace_back(myId, relDataPtr);
}

void Object::addRelationToSlave(
            TObjectId otherId, TCategoryId otherCategory, std::string role)
{
    TObjectId myId = revisionId_.objectId();
    auto relDataPtr = std::make_shared<RelationData>(
            std::move(role), myId, category_, otherId, std::move(otherCategory));
    relations_.emplace_back(myId, relDataPtr);
}


void Object::setDeleted()
{
    isDeleted_ = true;
    for (Relation& rel : relations_) {
        rel.setDeleted();
    }
}

bool Object::hasChanges() const
{
    return newGeometryWkb_
        || !attributesDiff_.empty()
        || !removedAttributeNames_.empty()
        || isDeleted_;
}

std::optional<revision::Attributes> Object::newAttributesImpl() const
{
    if (attributesDiff_.empty() && removedAttributeNames_.empty()) {
        return std::nullopt;
    }

    std::optional<revision::Attributes> attributes(attributesDiff_);

    for (const auto& origAttr : *data_.attributes) {
        if (!removedAttributeNames_.count(origAttr.first)) {
            attributes->insert(origAttr);
        }
    }

    return attributes;
}

void Object::setAttributeImpl(const std::string& name, std::string&& value)
{
    removedAttributeNames_.erase(name);

    auto it = data_.attributes->find(name);
    if (it != std::end(*data_.attributes)
            && it->second == value) {
        attributesDiff_.erase(name);
    } else {
        attributesDiff_[name] = std::move(value);
    }
}

void Object::removeAttributeImpl(const std::string& name)
{
    attributesDiff_.erase(name);
    if (data_.attributes->count(name)) {
        removedAttributeNames_.insert(name);
    }
}

} // namespace groupedit
} // namespace wiki
} // namespace maps
