#include <maps/wikimap/mapspro/services/editor/src/objects/object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/attr_object.h>
#include <maps/wikimap/mapspro/services/editor/src/exception.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/complex_object.h>

#include <maps/wikimap/mapspro/services/editor/src/configs/categories.h>
#include <maps/wikimap/mapspro/services/editor/src/common.h>
#include <maps/wikimap/mapspro/services/editor/src/utils.h>
#include <maps/wikimap/mapspro/services/editor/src/objectvisitor.h>
#include <maps/wikimap/mapspro/services/editor/src/relations_manager.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/revisions_facade.h>
#include <maps/wikimap/mapspro/services/editor/src/revision_meta/bbox_provider.h>
#include "relation_object.h"

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/restrictions.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/tile/include/tile.h>
#include <yandex/maps/wiki/revision/objectrevision.h>
#include <maps/libs/common/include/exception.h>

#include <boost/optional.hpp>

namespace maps {
namespace wiki {

namespace {

const boost::optional<std::string> EMPTY_RICH_CONTENT("");

const std::string STR_DRAFT = "draft";
const std::string STR_DELETED = "deleted";

const std::map<std::string, GeoObject::State> OBJECT_STATE_VALUES = {
    {STR_DRAFT, GeoObject::State::Draft},
    {STR_DELETED, GeoObject::State::Deleted}
};
} // namespace

std::istream& operator >> (std::istream& is, GeoObject::State& state)
{
    std::string str;
    is >> str;
    auto it = OBJECT_STATE_VALUES.find(str);
    REQUIRE(it != OBJECT_STATE_VALUES.end(),
        "Invalid object state name: " << str);
    state = it->second;
    return is;
}

std::ostream& operator << (std::ostream& os, GeoObject::State state)
{
    switch (state)
    {
        case GeoObject::State::Draft:
            os << STR_DRAFT;
            break;
        case GeoObject::State::Deleted:
            os << STR_DELETED;
            break;
        default:
            throw InternalErrorException() <<
                "Invalid object state value: " << static_cast<int>(state);
            break;
    }
    return os;
}

GeoObject::GeoObject(TRevisionId revId, ObjectsCache& cache)
    : revision_(revId)
    , hasRichContent_(false)
    , loadedRichContent_(false)
    , zmin_(0)
    , zmax_(0)
    , primaryEdit_(false)
    , modifiedGeom_(false)
    , modifiedAttr_(false)
    , modifiedSysAttr_(false)
    , updatePropertiesRequested_(false)
    , cache_(cache)
    , masterRelationsHolder_(RelationType::Master, revId.objectId(), cache_.relationsManager())
    , slaveRelationsHolder_(RelationType::Slave, revId.objectId(), cache_.relationsManager())
    , state_(State::Draft)
{}

GeoObject::GeoObject(const GeoObject& oth)
    : categoryId_(oth.categoryId_)
    , revision_(oth.revision_)
    , richContent_(oth.richContent_)
    , hasRichContent_(oth.hasRichContent_)
    , loadedRichContent_(oth.loadedRichContent_)
    , geom_(oth.geom_)
    , zmin_(oth.zmin_)
    , zmax_(oth.zmax_)
    , attributes_(oth.attributes_)
    , serviceAttributes_(oth.serviceAttributes_)
    , primaryEdit_(oth.primaryEdit_)
    , modifiedGeom_(oth.modifiedGeom_)
    , modifiedAttr_(oth.modifiedAttr_)
    , modifiedSysAttr_(oth.modifiedSysAttr_)
    , updatePropertiesRequested_(oth.updatePropertiesRequested_)
    , original_(oth.original_)
    , cache_(oth.cache_)
    , masterRelationsHolder_(RelationType::Master, revision_.objectId(), cache_.relationsManager())
    , slaveRelationsHolder_(RelationType::Slave, revision_.objectId(), cache_.relationsManager())
    , state_(oth.state_)
    , tableAttributes_(oth.tableAttributes_)
{}

GeoObject::~GeoObject()
{}

bool
GeoObject::syncView() const
{
    return category().syncView();
}

bool
GeoObject::isPrivate() const
{
    return category().isPrivate();
}

Geom
GeoObject::center() const
{
    return geom_.isNull() ? geom_ : geom_.center();
}

geos::geom::Envelope
GeoObject::envelope() const
{
    geos::geom::Envelope result;
    result.init();
    Geom geom = cache_.bboxProvider().geometryForObject(*this);
    if (!geom.isNull()) {
        result.expandToInclude(geom.geosGeometryPtr()->getEnvelopeInternal());
    }
    return result;
}

const Generalization&
GeoObject::generalization() const
{
    REQUIRE(!category().generalizationId().empty(),
        " No generalization set for category: " << category().id());
    return *cfg()->editor()->generalization(category().generalizationId());
}

void GeoObject::generalize(Transaction& work)
{
    if (geom_.isNull()) {
        return;
    }

    ZoomInterval visibility = generalization().objectVisibility(this, work);
    zmin_ = visibility.zmin();
    zmax_ = visibility.zmax();

    DEBUG() << BOOST_CURRENT_FUNCTION << " OID: " << id() << " ZMIN: " << zmin_ << " ZMAX: " << zmax_;
}

void
GeoObject::initalizeAttributes()
{
    attributes_.load(StringMultiMap());
}

void
GeoObject::setGeometry(const Geom& geom)
{
    if (geom.isNull()) {
        return;
    }
    geom_ = geom;
    calcModifiedGeom();
}

void
GeoObject::setCategory(const std::string& categoryId)
{
    const Category& cat = cfg()->editor()->categories()[categoryId];
    categoryId_ = categoryId;
    attributes_.redefine(cat.attrDefs().begin(), cat.attrDefs().end());
    masterRelationsHolder_.setCategory(categoryId);
    slaveRelationsHolder_.setCategory(categoryId);
}

bool
GeoObject::isModified() const
{
    return modifiedGeom_
        || modifiedAttr_
        || modifiedSysAttr_
        || isModifiedRichContent()
        || isModifiedState()
        || isModifiedClass()
        || isModifiedLinksToSlaves()
        || isModifiedLinksToMasters();
}

bool
GeoObject::isModifiedGeom() const
{
    return modifiedGeom_;
}

bool
GeoObject::isModifiedCategory() const
{
    return hasExistingOriginal() && categoryId() != original()->categoryId();
}

bool
GeoObject::isModifiedAttr() const
{
    return modifiedAttr_;
}

bool
GeoObject::isModifiedAttr(const std::string& attrId) const
{
    const auto& attrDef = cfg()->editor()->attribute(attrId);
    if (!attrDef->table()) {
        return hasExistingOriginal()
            ? attributes().value(attrId) != original()->attributes().value(attrId)
            : attributes().value(attrId) != s_emptyString;
    }
    const std::string& slaveCatId = attrDef->rowObjCategory();
    for (const auto& slaveRole : category().slavesRoles()) {
        if (slaveRole.categoryId() == slaveCatId) {
            if (hasExistingOriginal()
                ? !slaveRelations().diff(slaveRole.roleId()).empty()
                : !slaveRelations().range(slaveRole.roleId()).empty()) {
                return true;
            }
        }
    }
    return false;
}

bool
GeoObject::isModifiedTableAttrs() const
{
    return !slaveRelations().diff(category().slaveRoleIds(roles::filters::IsTable)).empty();
}

bool
GeoObject::isModifiedSysAttr() const
{
    return modifiedSysAttr_;
}

bool
GeoObject::isModifiedRichContent() const
{
    //boost::none means "rich content wasn't modified"
    if (!richContent_) {
        return false;
    }

    //non-empty rich content means "rich content was modified"
    //(no comparisons with original are performed)
    if (!richContent_->empty()) {
        return true;
    }

    //empty rich content means "remove rich content if it was present"
    return (original_ && original_->hasRichContent());
}

bool
GeoObject::isModifiedState() const
{
    return !original() || original()->state() != state();
}

bool
GeoObject::isModifiedClass() const
{
    if (hasExistingOriginal()) {
        return categoryId_ != original()->categoryId_;
    }
    return true;
}

bool
GeoObject::isModifiedLinksToSlaves() const
{
    if (!slaveRelationsHolder_.diffEmpty()) {
        return true;
    };
    for (const auto& relation : slaveRelationsHolder_.cachedRelations()) {
        if (relation->isModified()) {
            return true;
        }
    }
    return false;
}

bool
GeoObject::isModifiedLinksToMasters() const
{
    if (!masterRelationsHolder_.diffEmpty()) {
        return true;
    };
    for (const auto& relation : masterRelationsHolder_.cachedRelations()) {
        if (relation->isModified()) {
            return true;
        }
    }
    return false;
}

bool
GeoObject::isUpdatePropertiesRequested() const
{
    return updatePropertiesRequested_;
}

void
GeoObject::requestPropertiesUpdate()
{
    updatePropertiesRequested_ = true;
}

bool
GeoObject::hasExistingOriginal() const
{
    return original() && !original()->isDeleted();
}

bool
GeoObject::isCreated() const
{
    return !isDeleted() && !hasExistingOriginal();
}

bool
GeoObject::isBlocked() const
{
    return !attributes().value(ATTR_SYS_BLOCKED).empty();
}

void
GeoObject::setState(State s)
{
    state_ = s;
}

const Category&
GeoObject::category() const
{
    return cfg()->editor()->categories()[categoryId_];
}

const boost::optional<std::string>&
GeoObject::richContent() const
{
    if (!hasRichContent_) {
        return EMPTY_RICH_CONTENT;
    }

    if (richContent_) {
        return richContent_;
    }

    if (original_) {
        return original_->richContent();
    }

    //We are in original object
    //It is safe to lazy-load richContent from database
    if (loadedRichContent_) {
        return richContent_;
    }

    auto& snapshot = cache_.revisionsFacade().snapshot();
    snapshot.reader().setDescriptionLoadingMode(revision::DescriptionLoadingMode::Load);

    const auto& revision = snapshot.objectRevision(revision_.objectId());

    //TODO: std::optional richContent
    const auto& description = revision->data().description;
    if (description) {
        richContent_ = *description;
    } else {
        richContent_ = boost::none;
    }
    loadedRichContent_ = true;

    snapshot.reader().setDescriptionLoadingMode(revision::DescriptionLoadingMode::Skip);

    return richContent_;
}

void
GeoObject::initAttributes(const std::string& categoryId,
    const StringMultiMap& attributes,
    const TableAttributesValues& tableAttributes)
{
    setCategory(categoryId);
    attributes_.load(attributes);
    tableAttributes_ = tableAttributes;
    calcModifiedAttributes();
    calcModifiedSystemAttributes();
}

void GeoObject::setRichContent(const boost::optional<std::string>& data)
{
    richContent_ = data;
    if (data) {
        WIKI_REQUIRE(
            data->size() <= restrictions().maxRichContentLength(),
            ERR_BAD_REQUEST,
            "RichContent data size shouldn't exceed " << restrictions().maxRichContentLength());

        hasRichContent_ = !(richContent_->empty());
    } else {
        hasRichContent_ = (original_ && original_->hasRichContent_);
    }
}

void
GeoObject::init(const revision::ObjectRevision& objectRev)
{
    const auto& objectRevData = objectRev.data();

    prevRevisionId_ = objectRev.prevId();
    geom_ = objectRevData.geometry ? Geom(*objectRevData.geometry) : Geom();

    //thegeorg@FIXME:
    //here is a hidden knowledge about
    //description <-> richContent correspondence

    //TODO: std::optional richContent
    if (objectRevData.description) {
        richContent_ = *objectRevData.description;
    } else {
        richContent_ = boost::none;
    }
    hasRichContent_ = objectRev.hasDescription();

    state_ = objectRevData.deleted ? State::Deleted : State::Draft;

    attributes(objectRevData.attributes ? *objectRevData.attributes : StringMap());
}

void
GeoObject::primaryEdit(bool b)
{
    primaryEdit_ = b;
}

void
GeoObject::cloneAttributes(const GeoObject* source)
{
    attributes_ = source->attributes_;
}

void
GeoObject::setAllModified()
{
    modifiedGeom_ = true;
    modifiedAttr_ = true;
    modifiedSysAttr_ = true;
}

void
GeoObject::calcModifiedGeom()
{
    if (modifiedGeom_) {
        return;
    }
    if ((!hasExistingOriginal() && !isDeleted()) ||
        (!geom_.isNull() && !original()->geom_.isNull() &&
            geom_->getNumPoints() != original()->geom_->getNumPoints()) ||
        !geom_.equal(
            original()->geom_,
            geometryCompareTolerance()))
    {
        setModifiedGeom();
    }
}

void
GeoObject::calcModifiedAttributes()
{
    if (modifiedAttr_) {
        return;
    }
    if ((!hasExistingOriginal() && !isDeleted()) ||
        !(attributes_ == original()->attributes_))
    {
        setModifiedAttr();
    }
}

void
GeoObject::calcModifiedSystemAttributes()
{
    if (modifiedSysAttr_) {
        return;
    }
    if ((!hasExistingOriginal() && !isDeleted()) ||
        !attributes_.areSystemEqual(original()->attributes_))
    {
        setModifiedSysAttr();
    }
}

double
GeoObject::geometryCompareTolerance() const
{
    return CALCULATION_TOLERANCE;
}

void
GeoObject::calcModified()
{
    calcModifiedGeom();
    calcModifiedAttributes();
    calcModifiedSystemAttributes();
    DEBUG() << BOOST_CURRENT_FUNCTION << " id: " << id()
        << " modifiedGeom: "<< modifiedGeom_
        << " modifiedAttr: " << modifiedAttr_
        << " modifiedSysAttr: " << modifiedSysAttr_
        << " modifiedRichContent: " << isModifiedRichContent()
        << " modifiedClass: " << isModifiedClass()
        << " modifiedSlaveInfos: " << isModifiedLinksToSlaves();
}

void
GeoObject::keepOriginal()
{
    DEBUG() << BOOST_CURRENT_FUNCTION << " oid:" << id() << " revision " << revision_;
    original_ = clone();
    DEBUG() << BOOST_CURRENT_FUNCTION << " cloned";
}

void
GeoObject::setRevisionCommit(TCommitId commitId)
{
    prevRevisionId_ = revision_;
    revision_ = TRevisionId(id(), commitId);
}

void
GeoObject::attributes(const StringMap& attributes)
{
    setCategory(categoryFromAttributes(attributes));
    attributes_.load(attributes);
}

const Restrictions&
GeoObject::restrictions() const
{
    return category().restrictions();
}

const GeoObject*
GeoObject::parent() const
{
    auto parentRelationDefs = cfg()->editor()->parentRelationDefs();
    if (!parentRelationDefs) {
        return nullptr;
    }
    auto parentDefinition = parentRelationDefs->parentDefinition(categoryId());
    if (!parentDefinition) {
        return nullptr;
    }
    for (const auto& relation :
        relations(parentDefinition->relationType).range(parentDefinition->roleId)) {
        if (relation.categoryId() == parentDefinition->categoryId) {
            return relation.relative();
        }
    };
    return nullptr;
}

std::string
GeoObject::serviceAttrValue(const std::string& attrName) const
{
    StringMap::const_iterator it = serviceAttributes_.find(attrName);
    return it == serviceAttributes_.end() ? s_emptyString : it->second;
}

void
GeoObject::setServiceAttrValue(const std::string& attrName, const std::string& attrValue)
{
    if (serviceAttrValue(attrName) == attrValue) {
        return;
    }
    serviceAttributes_[attrName] = attrValue;
    requestPropertiesUpdate();
}

void
GeoObject::removeServiceAttributes(const std::string& namePrefix)
{
    for (auto mapIt = serviceAttributes_.begin();
        mapIt != serviceAttributes_.end();) {
            if (mapIt->first.starts_with(namePrefix)) {
                mapIt = serviceAttributes_.erase(mapIt);
            } else {
                ++mapIt;
            }
    };
}

std::string
GeoObject::screenLabel() const
{
    return serviceAttrValue("srv:screen_label");
};

void
GeoObject::setScreenLabel(const std::string& l)
{
    setServiceAttrValue("srv:screen_label", l);
}

std::string
GeoObject::renderLabel() const
{
    return serviceAttrValue("srv:render_label");
};

void
GeoObject::setRenderLabel(const std::string& l)
{
    setServiceAttrValue("srv:render_label", l);
}

void
GeoObject::applyVisitor(ObjectVisitor& visitor) const
{
    visitor.visitGeoObject(this);
}

void
GeoObject::applyProcessor(ObjectProcessor& processor)
{
    processor.processGeoObject(this);
}

} // namespace wiki
} // namespace maps
