#include "record_polygon.h"
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/work/transform/id_manager.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/data_error.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/log.h>
#include <yandex/maps/wiki/common/geom.h>
#include <maps/libs/geolib/include/serialization.h>

namespace maps::wiki::json2ymapsdf::transformers {

namespace {

const std::string STR_FACE = "face";
const std::string STR_EDGE = "edge";
const std::string STR_NODE = "node";
const std::string STR_FACE_EDGE = "face_edge";
const std::string STR_FACE_ID = "face_id";
const std::string STR_EDGE_ID = "edge_id";
const std::string STR_SHAPE = "shape";
const std::string STR_F_NODE_ID = "f_node_id";
const std::string STR_T_NODE_ID = "t_node_id";
const std::string STR_F_ZLEV = "f_zlev";
const std::string STR_T_ZLEV = "t_zlev";
const std::string STR_NODE_ID = "node_id";
const std::string STR_IS_INTERIOR = "is_interior";

} // namespace

namespace {
const ymapsdf::schema::Column& objIdColumn(
    const tds::schema::Category& category,
    const ymapsdf::schema::Table& table)
{
    REQUIRE(category.tablePtr() != nullptr, "Category " << category.name()
        << " has no associated table, unable to configure POLYGON transformer");
    for (size_t i = 0; i < table.size(); ++i) {
        const ymapsdf::schema::Column& column = table[i];
        if (category.tablePtr() == column.foreignPtr()) {
            return column;
        }
    }
    throw LogicError() << "Table " << table.name() << " has no column references to '"
        << category.name() << "' category";
}
} // namespace

PolygonTransformer::PolygonTransformer(
    const ymapsdf::schema::Schema& ymapsdfSchema,
    const ymapsdf::schema::Table* table,
    const tds::schema::Category* category,
    const xml3::Node*)
        : RecordTransformer{ymapsdfSchema}
        , objFaceTable_(*table)
        , objIdColumn_(objIdColumn(*category, objFaceTable_))
{ }

ymapsdf::Records
PolygonTransformer::transform(const tds::Item& object) const
{
    const auto objId = std::stoul(object[tds::ATTRIBUTE_OBJECT_ID]);
    const auto& shape = object[tds::ATTRIBUTE_SHAPE];
    if (shape.empty()) {
        WARN() << "Polygonal object " << objId << " without geometry";
        return {};
    }
    try {
        ymapsdf::Records records;
        processPolygon(objId, shape, records);
        return records;
    } catch (const std::exception& ex) {
        throw TdsDataError() << "Unable to process polygon geometry: " << ex.what();
    }
}

ymapsdf::schema::TableSet
PolygonTransformer::tables() const
{
    return {
        &objFaceTable_,
        &ymapsdfSchema_.table(STR_FACE),
        &ymapsdfSchema_.table(STR_FACE_EDGE),
        &ymapsdfSchema_.table(STR_EDGE),
        &ymapsdfSchema_.table(STR_NODE)
    };
}

void
PolygonTransformer::processPolygon(
    const DBID objId,
    const std::string& shape,
    ymapsdf::Records& records) const
{
    const auto polygon = geolib3::WKB::read<geolib3::Polygon2>(shape);
    processFace(objId, polygon.exteriorRing(), false, records);
    for (size_t i = 0; i < polygon.interiorRingsNumber(); ++i) {
        processFace(objId, polygon.interiorRingAt(i), true, records);
    }
}

void
PolygonTransformer::processFace(
    const DBID objId,
    const geolib3::LinearRing2& linearRing,
    bool isInterior,
    ymapsdf::Records& records) const
{
    const auto faceId = idManager().uniqueDBID();

    auto objFaceRecord = ymapsdf::Record::defaultRecord(objFaceTable_);
    objFaceRecord[ymapsdf::PRIMARY_KEY_ID] = std::to_string(objId);
    objFaceRecord[objIdColumn_.id()] = objId;
    objFaceRecord[STR_FACE_ID] = faceId;
    objFaceRecord[STR_IS_INTERIOR] = isInterior;
    records.push_back(std::move(objFaceRecord));

    auto faceRecord = ymapsdf::Record::defaultRecord(ymapsdfSchema_, STR_FACE);
    faceRecord[STR_FACE_ID] = faceId;
    records.push_back(std::move(faceRecord));

    geolib3::PointsVector points;
    for (size_t i = 0; i < linearRing.pointsNumber(); ++i) {
        points.push_back(linearRing.pointAt(i));
    }
    points.push_back(linearRing.pointAt(0));

    processEdge(faceId, geolib3::Polyline2(points), records);
}

void
PolygonTransformer::processEdge(
    const DBID faceId,
    const geolib3::Polyline2& polyline,
    ymapsdf::Records& records) const
{
    const auto edgeId = idManager().uniqueDBID();
    auto faceEdgeRecord = ymapsdf::Record::defaultRecord(ymapsdfSchema_, STR_FACE_EDGE);
    faceEdgeRecord[STR_FACE_ID] = faceId;
    faceEdgeRecord[STR_EDGE_ID] = edgeId;
    records.push_back(std::move(faceEdgeRecord));

    auto edgeWkb = geolib3::WKB::toString(polyline);
    const auto nodeId = idManager().uniqueDBID();

    auto edgeRecord = ymapsdf::Record::defaultRecord(ymapsdfSchema_, STR_EDGE);
    edgeRecord[STR_EDGE_ID] = edgeId;
    edgeRecord[STR_SHAPE] = edgeWkb;
    edgeRecord[STR_F_NODE_ID] = nodeId;
    edgeRecord[STR_T_NODE_ID] = nodeId;
    records.push_back(std::move(edgeRecord));

    auto nodeWkb = geolib3::WKB::toString(polyline.pointAt(0));

    auto nodeRecord = ymapsdf::Record::defaultRecord(ymapsdfSchema_, STR_NODE);
    nodeRecord[STR_NODE_ID] = nodeId;
    nodeRecord[STR_SHAPE] = nodeWkb;
    records.push_back(std::move(nodeRecord));
}

} // namespace maps::wiki::json2ymapsdf::transformers
