#include "xml_writer.h"
#include "common.h"
#include <maps/wikimap/mapspro/services/editor/src/objects/object.h>
#include <maps/wikimap/mapspro/services/editor/src/collection.h>
#include <maps/wikimap/mapspro/services/editor/src/common.h>
#include <maps/wikimap/mapspro/services/editor/src/objectvisitor.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/areal_object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/junction.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/linear_element.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/point_object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/complex_object.h>
#include <maps/wikimap/mapspro/services/editor/src/attributes.h>
#include <maps/wikimap/mapspro/services/editor/src/relation_infos.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/relation_object.h>
#include <maps/wikimap/mapspro/services/editor/src/edit_options.h>
#include "xml_parser.h"
#include "formatter_context.h"
#include <maps/wikimap/mapspro/services/editor/src/commit.h>
#include <maps/wikimap/mapspro/services/editor/src/edit_notes.h>

#include <maps/wikimap/mapspro/services/editor/src/actions/tile/gettile.h>
#include <maps/wikimap/mapspro/services/editor/src/actions/tile/hotspot.h>
#include <maps/wikimap/mapspro/services/editor/src/views/view_object.h>
#include <maps/wikimap/mapspro/services/editor/src/context.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/srv_attrs/registry.h>

#include <maps/wikimap/mapspro/libs/gdpr/include/user.h>
#include <yandex/maps/wiki/configs/editor/slave_role.h>
#include <yandex/maps/wiki/common/date_time.h>

namespace maps {
namespace wiki {

namespace {

const std::string XML_TRUE = "true";
const std::string XML_FALSE = "false";

const StringMap XML_ACTION_NOTES = {
    {common::COMMIT_PROPKEY_GROUP_MOVED_DLON, "dlon"},
    {common::COMMIT_PROPKEY_GROUP_MOVED_DLAT, "dlat"},
    {common::COMMIT_PROPKEY_GROUP_MOVED_OBJECTS_COUNT, "objects-count"},
    {common::COMMIT_PROPKEY_REVERT_REASON, "revert-reason"}
};

}

XmlWriter::XmlWriter(FormatterContext& formatterContext)
    : formatterContext_(formatterContext)
{}

std::ostream&
XmlWriter::put(std::ostream& output, const GeoObjectCollection* collection, ObjectsCache& cache)
{
    output << "<objects>";
    for (const auto& object : *collection) {
        put(output, object.get(), cache);
    }
    output << "</objects>";
    return output;
}

namespace {

std::ostream&
putParents(std::ostream& os, const GeoObject* object, size_t level = 0)
{
    if (level > MAX_PARENT_PATH_LEVELS) {
        WARN() << BOOST_CURRENT_FUNCTION << "MAX_PARENT_PATH_LEVELS "
            << MAX_PARENT_PATH_LEVELS << " reached.";
        return os;
    }
    auto parent = object->parent();
    if (!parent) {
        return os;
    }
    os << "<parent id=\"" << parent->id() <<"\">";
    XmlWriter::putRevision(os, parent);
    XmlWriter::putCategory(os, parent);
    XmlWriter::putScreenLabel(os, parent);
    putParents(os, parent, level + 1);
    os << "</parent>";
    return os;
}

void
putActionNotes(std::ostream& os, const StringMap& actionNotes)
{
    for (auto i : actionNotes) {
        auto j = XML_ACTION_NOTES.find(i.first);
        if (j != XML_ACTION_NOTES.end()) {
            os << "<" << j->second << ">";
            os << i.second;
            os << "</" << j->second << ">";
        }
    }
}

void
putEditNotes(std::ostream& os, const std::vector<edit_notes::EditNotesTree::Node>& nodes)
{
    for (const auto& node : nodes) {
        os << "<note id=\"" << node.note << "\">";
        putEditNotes(os, node.subNotes);
        os << "</note>";
    }
}

} // namespace

const std::string&
XmlWriter::boolToStr(bool val)
{
    return val
        ? XML_TRUE
        : XML_FALSE;
}

std::ostream&
XmlWriter::putCommitModel(
    std::ostream& os,
    const CommitModel& commitModel,
    bool putRootTag)
{
    const auto& commit = commitModel.commit();

    if (putRootTag) {
        os << "<commit id=\"" << commit.id() << "\">";
    }

    os << "<created-by>" << gdpr::User(commit.createdBy()).uid() << "</created-by>";
    os << "<created>" << common::canonicalDateTimeString(commit.createdAt(),
        common::WithTimeZone::Yes) << "</created>";
    os << "<action>" << commit.action() << "</action>";

    if (commitModel.state()) {
        os << "<state>" << *commitModel.state() << "</state>";
    }
    os << "<source-branch-id>" << sourceBranchId(commit) << "</source-branch-id>";
    if (commit.stableBranchId()) {
        os << "<stable-branch-id>" << commit.stableBranchId() << "</stable-branch-id>";
    }

    auto revertedCommitIds = commit.revertedCommitIds();
    if (!revertedCommitIds.empty()) {
        putXmlIds(os, "reverted-commit-ids", revertedCommitIds);
    }
    auto revertedDirectlyCommitIds = commit.revertedDirectlyCommitIds();
    if (!revertedDirectlyCommitIds.empty()) {
        putXmlIds(os, "reverted-directly-commit-ids", revertedDirectlyCommitIds);
    }
    auto revertingCommitIds = commit.revertingCommitIds();
    if (!revertingCommitIds.empty()) {
        putXmlIds(os, "reverting-commit-ids", revertingCommitIds);
    }
    auto revertingDirectlyCommitId = commit.revertingDirectlyCommitId();
    if (revertingDirectlyCommitId) {
        os << "<reverting-directly-commit-id>" << *revertingDirectlyCommitId << "</reverting-directly-commit-id>";
    }

    auto actNotes = actionNotes(commit);
    if (!actNotes.empty()) {
        os << "<action-notes>";
        os << "<" << commit.action() << ">";
        putActionNotes(os, actNotes);
        os << "</" << commit.action() << ">";
        os << "</action-notes>";
    }

    if (commitModel.contextPrimaryObjectId()) {
        auto primaryOid = primaryObjectId(commit);
        Y_UNUSED(primaryOid);
        os << "<primary>"
           << boolToStr(commitModel.primary())
           << "</primary>";
        os << "<primary-object-id>"
           << *commitModel.contextPrimaryObjectId()
           << "</primary-object-id>";
    }
    if (commitModel.isRevertible()) {
        os
            << "<" << STR_REVERTIBLE << ">"
            << boolToStr(*commitModel.isRevertible())
            << "</" << STR_REVERTIBLE << ">";
    }

    if (commitModel.editNotesTree()) {
        os << "<edit-notes>";
        putEditNotes(os, commitModel.editNotesTree()->rootNotes);
        os << "</edit-notes>";
    }

    if (commitModel.approveStatus()) {
        os << "<approve-status>" << boost::lexical_cast<std::string>(
            *commitModel.approveStatus()) << "</approve-status>";
    }
    if (putRootTag) {
        os << "</commit>";
    }
    return os;

}

std::ostream&
XmlWriter::put(std::ostream& os, const GeoObject* object, ObjectsCache& cache)
{
    auto recentCommit = recentAffectingCommit(
        cache, formatterContext_.actualObjectRevisionId(object->revision()));

    auto firstCommit = wiki::firstCommit(cache, object->id());

    BatchCommitPreparedFields preparedFields;
    preparedFields.prepareStates(cache.branchContext(), {recentCommit, firstCommit});

    os << "<object id=\"" << object->id() << "\"";
    if (!object->serviceAttrValue(srv_attrs::SRV_INVALID).empty()) {
        os << " valid=\"false\"";
    }
    os << ">";
    putRevision(os, object);
    os << "<recent-commit>";
    CommitModel recentCommitModel(std::move(recentCommit));
    preparedFields.fillCommitModel(recentCommitModel);
    recentCommitModel.setCustomContextObjectId(object->id());
    putCommitModel(os, recentCommitModel);
    os << "</recent-commit>";
    os << "<first-commit>";
    CommitModel firstCommitModel(std::move(firstCommit));
    preparedFields.fillCommitModel(recentCommitModel);
    firstCommitModel.setCustomContextObjectId(object->id());
    putCommitModel(os, firstCommitModel);
    os << "</first-commit>";
    putCategory(os, object);
    putRichContent(os, object);
    os << "<state>"
       << object->state()
       << "</state>";
    os << "<editing-options>";
    os << "<deletable>" << boolToStr(canDelete(object, formatterContext_.userId())) << "</deletable>";
    os << "<editable>" << boolToStr(canEdit(object, formatterContext_.userId())) << "</editable>";
    os << "</editing-options>";
    putScreenLabel(os, object);
    Geom c = object->center();
    if (!c.isNull()) {
        os << "<center>";
        putGeometry(os, c, SpatialRefSystem::Geodetic);
        os  << "</center>";
    }
    geos::geom::Envelope envelope = object->envelope();
    if (!envelope.isNull()) {
        os << "<bbox>" << json(envelope) << "</bbox>";
    }
    os << "<zmin>" << object->zmin() << "</zmin>";
    os << "<zmax>" << object->zmax() << "</zmax>";
    if (!object->attributes().areAllSystem()) {
        put(os, &object->attributes(), &object->tableAttributes());
    }
    putGeometry(os, object->geom(), SpatialRefSystem::Geodetic);
    const Category& category = object->category();
    putSlaves(os, category.slavesRoles(), formatterContext_, object);
    RelationInfos masterInfos = object->relations(RelationType::Master);
    put(os, &masterInfos);
    putParents(os, object);
    os << "</object>";
    return os;
}

void
XmlWriter::putRichContent (std::ostream& os, const GeoObject* object)
{
    if (object->hasRichContent()) {
        os << "<rich-content/>";
    }
}

std::ostream&
XmlWriter::putTableAttributeContents(std::ostream& output,
    const TableValues& contents)
{
    output << "<values>";
    for (size_t row = 0; row < contents.numRows(); ++row) {
        output << "<value>";
        const StringMultiMap& rowValues = contents.values(row);
        StringSet columns = readKeys(rowValues);
        for (const auto& column : columns) {
            output << "<attribute id=\"" << column << "\">";
            output << "<values>";
            auto values = rowValues.equal_range(column);
            for (auto itVal = values.first; itVal != values.second; ++itVal) {
                output << "<value>";
                output << common::escapeCData(itVal->second);
                output << "</value>";
            }
            output << "</values>";
            output << "</attribute>";
        }
        output << "</value>";
    }
    output << "</values>";
    return output;
}

std::ostream&
XmlWriter::put(std::ostream& output,
    const Attributes* attributes,
    const TableAttributesValues* tableAttributes)
{
    if (attributes->empty()) {
        return output;
    }
    output << "<attributes>";
    for (const auto& attrDef : attributes->definitions()) {
        if (attrDef->system()) {
            continue;
        }
        if (attrDef->table() && tableAttributes) {
            const auto& contents = tableAttributes->find(attrDef->id());
            if(!contents.empty()){
                output << "<attribute id=\"" << attrDef->id() << "\">";
                putTableAttributeContents(output, contents);
                output << "</attribute>";
            }
        } else {
            auto aIt = attributes->find(attrDef->id());
            if ( aIt != attributes->end()) {
                put(output, &(*aIt));
            }
        }
    }
    output << "</attributes>";
    return output;
}

std::ostream&
XmlWriter::put(std::ostream& output, const Attribute* attribute)
{
    if (!attribute->def().anyValue()
        && attribute->def().values().size() < 2) {
            return output;
    }
    output << "<attribute id=\"" << attribute->def().id() << "\"";
    output << ">";
    output << "<values>";
    for (const auto& attrValue : attribute->values()) {
        if (attrValue.empty()) {
            continue;
        }
        output << "<value>";
        if (attribute->def().anyValue()) {
            output << common::escapeCData(attrValue);
        } else {
            output << attrValue;
        }
        output << "</value>";
    }
    output << "</values>";
    output << "</attribute>";
    return output;
}

std::ostream&
XmlWriter::put(std::ostream& os, const Hotspot* hotspot)
{
    int oldPrecision = os.precision();
    os.precision(0);
    os << "<hotspot>";
    for (const auto& hostpotGeom : hotspot->geoms()) {
        putGeometry(os, hostpotGeom, SpatialRefSystem::Mercator);
    }
    os << "</hotspot>";
    os.precision(oldPrecision);
    return os;
}

std::ostream&
XmlWriter::putSequentialRelatives(std::ostream& os, const std::string& roleId,
    const RelationInfos::Range& range)
{
    std::multimap<size_t, const RelationInfo*> indexBySeqNum;
    for (const auto& rInfo : range) {
        indexBySeqNum.insert(std::make_pair(rInfo.seqNum(), &rInfo));
    }
    os << "<role id=\"" << roleId << "\">";
    for (const auto& slave : indexBySeqNum) {
        putRelative(os, slave.second->relative());
    }
    os << "</role>";
    return os;
}

namespace {
bool
emptySlavesOutput(const std::string& categoryId, const RelationInfos& slaves)
{
    const auto& slavesRange = slaves.range(FormatterContext::slavesPerRoleLimitMax(categoryId));
    return !slavesRange ||
        (slavesRange->empty() &&
        slavesRange->rolesWithExceededLimit().empty());
}
} //  namespace

std::ostream&
XmlWriter::putSlaves(std::ostream& os, const SlaveRoles& roles,
    FormatterContext& formatterContext, const GeoObject* object)
{
    const auto& categoryId = object->categoryId();
    RelationInfos slaves = object->relations(RelationType::Slave);
    if (emptySlavesOutput(categoryId, slaves)) {
        return os;
    }
    const auto& putAllRoles = formatterContext.putAllSlavesRoles(object);
    os << "<slaves>";
    for (const auto& role : roles) {
        const std::string& roleId = role.roleId();
        bool putAll = putAllRoles && putAllRoles->count(roleId);
        const Category& slaveCategory = cfg()->editor()->categories()[role.categoryId()];
        if (slaveCategory.system()) {
            continue;
        }
        auto slavesByRole = putAll
                ? slaves.range(roleId)
                : *slaves.range(roleId, FormatterContext::slavesPerRoleLimit(roleId, categoryId));
        if (!putAll && !role.tableRow() && !slavesByRole.rolesWithExceededLimit().empty()) {
            DEBUG() << "XmlWriter::put slave role exceeded limit : " << roleId;
            os << "<role id=\"" << roleId << "\"";
            os << " limit-exceeded=\"" <<
                FormatterContext::slavesPerRoleLimit(roleId, categoryId).value << "\"/>";
            continue;
        }
        if (slavesByRole.empty()) {
            continue;
        }
        if (role.sequential()) {
            putSequentialRelatives(os, roleId, slavesByRole);
        } else {
            os << "<role id=\"" << roleId << "\">";
            for (const auto& slaveInfo : slavesByRole) {
                putRelative(os, slaveInfo.relative());
            }
            os << "</role>";
        }
    }
    os << "</slaves>";
    return os;
}

std::ostream&
XmlWriter::put(std::ostream& os, const RelationInfos* masterInfos)
{
    const RelationInfosRange& mastersRange = masterInfos->range();
    if (!mastersRange.empty()) {
        os << "<masters>";
        std::multimap<std::string, const RelationInfo*> orderedMasterInfos;
        StringSet orderedRoles;
        for(const auto& link : mastersRange) {
            if (link.categoryId() == CATEGORY_AD_SUBST_FC ||
                cfg()->editor()->categories()[link.categoryId()].system()) {
                continue;
            }
            const std::string& roleId = link.roleId();
            orderedMasterInfos.insert(std::make_pair(roleId, &link));
            orderedRoles.insert(roleId);
        }
        for (const auto& roleId : orderedRoles) {
            os << "<role id=\"" << roleId << "\">";
            auto masterItRange = orderedMasterInfos.equal_range(roleId);
            for(auto masterIt = masterItRange.first;
                masterIt != masterItRange.second;
                ++masterIt) {
                const GeoObject* master = masterIt->second->relative();
                putRelative(os, master);
            }
            os << "</role>";
        }
        os << "</masters>";
    }
    return os;
}

std::ostream&
XmlWriter::put(std::ostream& os, const TileObject* tileObject)
{
    os << "<object id=\"" << tileObject->id() << "\">";
    XmlWriter::putRevision(os, tileObject);
    XmlWriter::putCategory(os, tileObject);
    const auto& label = tileObject->label();
    if (!label.empty()) {
        os << "<hotspot-label>" << common::escapeCData(label) << "</hotspot-label>";
    } else {
        const Category& cat =
            cfg()->editor()->categories()[tileObject->categoryId()];
        os << "<hotspot-label>" << common::escapeCData(cat.label()) << "</hotspot-label>";
    }
    size_t oldPrecision = os.precision(TILE_OBJECT_GEOM_PRECISION);
    putGeometry(os, tileObject->geom(), SpatialRefSystem::Geodetic);
    put(os, &tileObject->hotspot());
    os<<"</object>";
    os.precision(oldPrecision);
    return os;
}

std::ostream&
XmlWriter::put(std::ostream& os, const views::ViewObject* viewObject)
{
    os << "<object id=\"" << viewObject->id() << "\">";
    putRevision(os, viewObject);
    putCategory(os, viewObject);
    putScreenLabel(os, viewObject);
    putGeometry(os, viewObject->geom(), SpatialRefSystem::Geodetic);
    os << "</object>";
    return os;
}

std::ostream&
XmlWriter::putGeometry(
        std::ostream& os, const Geom& geom,
        SpatialRefSystem outputRefSys)
{
    os << "<geometry>";
    geom.geoJson(os, outputRefSys);
    os << "</geometry>";
    return os;
}

std::ostream&
XmlWriter::putHeader(std::ostream& os)
{
    os << "<editor xmlns=\"" << PROJECT_NAMESPACE << "\">";
    return os;
}

std::ostream&
XmlWriter::putToken(std::ostream& os, const std::string& methodName, const Token& token)
{
    os << "<method>" << methodName << "</method>";
    os << "<token>"<< token << "</token>";
    return os;
}

std::ostream&
XmlWriter::putFooter(std::ostream& os)
{
    os << "</editor>";
    return os;
}

} // namespace wiki
} // namespace maps
