#include "xml_parser.h"

#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/utils.h>
#include "common.h"

#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>

namespace maps {
namespace wiki {

namespace {

std::string
generateUUID()
{
    std::ostringstream uuidStream;
    uuidStream << boost::uuids::random_generator()();
    return uuidStream.str();
}

} // namespace

void
registerNamespaces(xml3::Doc& doc)
{
    doc.addNamespace("gml", "http://www.opengis.net/gml");
    doc.addNamespace("ym", "http://maps.yandex.ru/ymaps/1.x");
    doc.addNamespace(PROJECT_NAMESPACE_ALIAS, PROJECT_NAMESPACE);
}

maps::xml3::Nodes
XmlParser::objectNodes(maps::xml3::Doc& doc)
{
    maps::xml3::Nodes nodes = doc.nodes("//wmp:editor/wmp:objects/wmp:object", true);
    if (nodes.size() == 0) {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_DATA, "SaveObject can't read from input XML");
    }
    return nodes;
}

Geom
XmlParser::readObjectGeometry(const maps::xml3::Node& objNode)
{
    maps::xml3::Node geomNode = objNode.firstElementChild(STR_GEOMETRY);
    if (geomNode.isNull()) {
        return Geom();
    }
    return Geom(createGeomFromJsonStr(geomNode.value<std::string>()));
}

UUID
XmlParser::readObjectUUID(const maps::xml3::Node& objNode)
{
    auto id = objNode.attr<std::string>(STR_ID);
    TOid objectId = boost::lexical_cast<TOid>(id);
    return objectId ? "" : generateUUID();
}

TRevisionId
XmlParser::readObjectRevision(const maps::xml3::Node& objNode)
{
    maps::xml3::Node revNode = objNode.firstElementChild(STR_REVISION);
    if (revNode.isNull() || revNode.attr<std::string>(STR_ID, "").length() < 2) {
        return TRevisionId();
    }
    return revNode.attr<TRevisionId>(STR_ID, TRevisionId());
}

std::string
XmlParser::readObjectCategory(const maps::xml3::Node& objNode)
{
    maps::xml3::Node catNode = objNode.firstElementChild("category-id");
    if (catNode.isNull()) {
        return s_emptyString;
    }
    return catNode.value<std::string>();
}

boost::optional<std::string>
XmlParser::readObjectRichContent(const maps::xml3::Node& objNode)
{
    //exchanging logic with empty and missing nodes here
    maps::xml3::Node node = objNode.firstElementChild("rich-content");
    if (node.isNull()) {
        return boost::optional<std::string>("");
    }

    const auto& value = node.value<std::string>();
    if (value.empty()) {
        return boost::optional<std::string>();
    } else {
        return boost::optional<std::string>(value);
    }
}

namespace {

StringMultiMap
readPlainAttributes(const maps::xml3::Node& attributesNode)
{
    StringMultiMap attributes;
    const auto* editorCfg = cfg()->editor();
    maps::xml3::Node attrNode = attributesNode.firstElementChild(STR_ATTRIBUTE);
    while (!attrNode.isNull()) {
        std::string attributeId = attrNode.attr<std::string>(STR_ID);
        if ( !editorCfg->isAttributeDefined(attributeId)
             || editorCfg->attribute(attributeId)->table())
        {
             attrNode = attrNode.nextElementSibling(STR_ATTRIBUTE);
             continue;
        }
        maps::xml3::Node valuesNode = attrNode.firstElementChild(STR_VALUES);
        const auto& attrDef = editorCfg->attribute(attributeId);
        if (!valuesNode.isNull()) {
            maps::xml3::Node valueNode = valuesNode.firstElementChild(STR_VALUE);
            if (valueNode.isNull() && attrDef->multiValue()) {
                attributes.insert(
                    {
                        attributeId,
                        attrDef->allowedValue(s_emptyString)
                    });
            }
            while (!valueNode.isNull()) {
                attributes.insert(
                    {
                        attributeId,
                        attrDef->allowedValue(valueNode.value<std::string>())
                    });
                DEBUG() << BOOST_CURRENT_FUNCTION << " attr: " << attributeId
                    << " value: " << valueNode.value<std::string>();
                valueNode = valueNode.nextElementSibling(STR_VALUE);
            }
        }
        attrNode = attrNode.nextElementSibling(STR_ATTRIBUTE);
    }
    return attributes;
}

} // namespace

StringMultiMap
XmlParser::readObjectAttributes(const maps::xml3::Node& objNode)
{
    StringMultiMap attributes;
    maps::xml3::Node attributesNode = objNode.firstElementChild(STR_ATTRIBUTES);
    if (!attributesNode.isNull()) {
        attributes = readPlainAttributes(attributesNode);
    }
    return attributes;
}


TableAttributesValues
XmlParser::readObjectTableAttributes(const maps::xml3::Node& objNode)
{
    TableAttributesValues tableAttributes;
    maps::xml3::Node attributesNode = objNode.firstElementChild(STR_ATTRIBUTES);

    if (attributesNode.isNull()) {
        return tableAttributes;
    }

    for (maps::xml3::Node attrNode = attributesNode.firstElementChild(STR_ATTRIBUTE);
        !attrNode.isNull();
        attrNode = attrNode.nextElementSibling(STR_ATTRIBUTE))
    {
        std::string attributeId = attrNode.attr<std::string>(STR_ID);
        if (!cfg()->editor()->isAttributeDefined(attributeId)) {
            continue;
        }
        const auto& attrDef = cfg()->editor()->attribute(attributeId);
        if (!attrDef->table()) {
            continue;
        }
        TableValues tableValues;
        DEBUG() << "TABLE: " << attributeId;
        maps::xml3::Node valuesNode = attrNode.firstElementChild(STR_VALUES);
        if (!valuesNode.isNull()) {
            maps::xml3::Node valueNode = valuesNode.firstElementChild(STR_VALUE);
            while (!valueNode.isNull()) {
                StringMultiMap valuesMap = readPlainAttributes(valueNode);
                if (!isEmptyValues(valuesMap)) {
                    tableValues.addRow(std::move(valuesMap));
                }
                valueNode = valueNode.nextElementSibling(STR_VALUE);
            }
        }
        DEBUG() << "TABLE DONE";
        validateTableValuesSize(attrDef, tableValues, attributeId);
        tableAttributes.add(attributeId, std::move(tableValues));
    }
    return tableAttributes;
}

ObjectEditContextPtr
XmlParser::readObjectEditContext(const maps::xml3::Node& objNode)
{
    maps::xml3::Node ecNode = objNode.firstElementChild("edit-context");
    if (ecNode.isNull()) {
        return nullptr;
    }

    ObjectEditContext::SplitPoints splitPoints;
    maps::xml3::Node splitPointsNode = ecNode.firstElementChild("split-points");
    if (!splitPointsNode.isNull()) {
        splitPoints = createGeoPointCollectionFromJsonStr(
            splitPointsNode.value<std::string>());
    }
    ObjectEditContext::SplitLines splitLines;
    maps::xml3::Node splitLinesNode = ecNode.firstElementChild("split-lines");
    if (!splitLinesNode.isNull()) {
        splitLines = createGeomCollectionFromJsonStr(
            splitLinesNode.value<std::string>());

        for (const auto& geom : splitLines) {
            WIKI_REQUIRE(geom->getGeometryTypeId() == geos::geom::GEOS_LINESTRING,
                         ERR_BAD_REQUEST,
                         "Split geometry type is not a line string");
        }
    }
    maps::xml3::Node centerNode = ecNode.firstElementChild(STR_CENTER);
    if (centerNode.isNull()) {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_DATA, "View center not passed within edit context.");
    }
    maps::xml3::Node zoomNode = ecNode.firstElementChild(STR_ZOOM);
    if (zoomNode.isNull()) {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_DATA, "View zoom not passed within edit context.");
    }

    return std::make_shared<ObjectEditContext>(
        View(createGeoPointFromJsonStr(centerNode.value<std::string>()),
             zoomNode.value<TZoom>()),
        ObjectEditContext::defaultAllowIntersections(),
        ObjectEditContext::defaultAllowInvalidContours(),
        std::move(splitPoints),
        std::move(splitLines));
}

} // namespace wiki
} // namespace maps
