#include "buffer_difference.h"

#include "difference_result.h"
#include "editor_interaction.h"
#include "../edge.h"
#include "grid.h"

#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>

#include <geos/geom/GeometryFactory.h>
#include <geos/geom/Point.h>
#include <geos/geom/Polygon.h>
#include <geos/util/TopologyException.h>

#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/geom_utils.h>

#include <maps/libs/log8/include/log8.h>

#include <fstream>
#include <iostream>
#include <iomanip>

namespace maps {
namespace wiki {
namespace signals_graph {
namespace {

std::vector<std::unique_ptr<geos::geom::Geometry>> collectLinestrings(
    const std::vector<common::Geom>& source
) {
    std::vector<std::unique_ptr<geos::geom::Geometry>> buffers;

    for (const auto& road : source) {
        common::ConstGeosGeometryPtr geosPtr = road.geosGeometryPtr();
        auto typeId = geosPtr->getGeometryTypeId();

        if (typeId == geos::geom::GeometryTypeId::GEOS_LINESTRING) {
            buffers.push_back(geosPtr->clone());
        } else if (typeId == geos::geom::GeometryTypeId::GEOS_MULTILINESTRING) {
            for (const auto& elem : *dynamic_cast<const geos::geom::GeometryCollection*>(geosPtr)) {
                buffers.push_back(elem->clone());
            }
        } else if (typeId == geos::geom::GEOS_GEOMETRYCOLLECTION) {
            for (const auto& elem : *dynamic_cast<const geos::geom::GeometryCollection*>(geosPtr)) {
                DEBUG() << "Source geometry collection elem: " << elem->getGeometryType();
                auto elemTypeId = elem->getGeometryTypeId();
                if (elemTypeId == geos::geom::GEOS_LINESTRING) {
                    buffers.push_back(elem->clone());
                }
            }
        } else {
            DEBUG() << "Source not (multi)linestring or collection:";
            DEBUG() << "Type: " << geosPtr->getGeometryType();
        }
    }

    return buffers;
}

/**
 * Construct buffer around all geometries in source. For each geometry in
 * candidates geometrically substract that buffer. Return one output geometry
 * for each candidate -- same number of geometries and in the same order --
 * output is aligned with candidates.
 */
std::vector<common::Geom> geomBufferDifference(
    const std::vector<common::Geom>& source,
    const std::vector<common::Geom>& candidates,
    double width
) {
    if (source.empty() || candidates.empty()) {
        INFO() << "Empty source or candidates in region";
        return candidates;
    }

    DEBUG() << "Collecting linestrings";
    auto linestrings = collectLinestrings(source);

    if (linestrings.empty()) {
        INFO() << "source evaluated to empty linestrings collection";
        return candidates;
    }

    auto collection = geos::geom::GeometryFactory::getDefaultInstance()->
        createMultiLineString(std::move(linestrings));

    const double mercWidth = geolib3::toMercatorUnits(
        width,
        geolib3::internal::geos2geolibGeometry(collection->getCentroid().get())
    );

    DEBUG() << "Build buffered union";
    common::Geom bufferUnion;
    try {
        bufferUnion = common::Geom(collection->buffer(mercWidth));
    } catch (const geos::util::TopologyException& exception) {
        ERROR() << "GEOS TopologyException caught: " << exception.what();
        return {};
    }
    DEBUG() << "Cutting roads";

    std::vector<common::Geom> result;

    for (const auto& road : candidates) {
        auto diff = road.difference(bufferUnion);

        geos::geom::GeometryTypeId typeId =
            diff.geosGeometryPtr()->getGeometryTypeId();

        if (typeId == geos::geom::GeometryTypeId::GEOS_LINESTRING ||
            typeId == geos::geom::GeometryTypeId::GEOS_MULTILINESTRING) {
            result.emplace_back(std::move(diff));
        } else if (typeId == geos::geom::GeometryTypeId::GEOS_GEOMETRYCOLLECTION) {
            const geos::geom::GeometryCollection* geom =
                dynamic_cast<const geos::geom::GeometryCollection*>(diff.geosGeometryPtr());

            if (!geom->isEmpty()) {
                WARN() << "Got non-empty GeometryCollection";
            }

            result.emplace_back();
        } else {
            WARN() << "Diff type " << typeId <<
                   " is neither LineString nor MultiLineString nor GeometryCollection";
            result.emplace_back();
        }
    }

    return result;
}

} // namespace

/**
 * For each cell in grid find all edge hypotheses intersecting it.
 *
 * @return indices of hypotheses grouped per grid cell.
 */
std::vector<std::vector<std::vector<size_t>>> calculateIndices(
    const Grid& grid,
    const std::vector<geolib3::Polyline2>& gpsEdges
) {
    geolib3::StaticGeometrySearcher<geolib3::Polyline2, size_t> target_searcher;

    size_t nonzero = 0;
    for (size_t index = 0; index < gpsEdges.size(); ++index) {
        if (gpsEdges[index].pointsNumber() > 0) {
            target_searcher.insert(&gpsEdges[index], index);
            nonzero++;
        }
    }
    target_searcher.build();

    INFO() << "Nonzero candidates: " << nonzero;

    std::vector<std::vector<std::vector<size_t>>> indices(
        grid.heightCells, std::vector<std::vector<size_t>>(grid.widthCells)
    );

    for (size_t i = 0; i < grid.heightCells; ++i) {
        for (size_t j = 0; j < grid.widthCells; ++j) {
            auto searchResult = target_searcher.find(grid.boxes[i][j]);

            for (auto iter = searchResult.first; iter != searchResult.second; ++iter) {
                indices[i][j].push_back(iter->value());
            }
        }
    }

    return indices;
}

std::vector<common::Geom> cutCellEdgesGeoms(
    const std::vector<geolib3::Polyline2>& edges,
    const std::vector<size_t>& indices,
    const geolib3::BoundingBox& bbox
) {
    auto bboxPolygon = bbox.polygon();
    auto bboxGeosPolygon = geolib3::internal::geolib2geosGeometry(bboxPolygon);
    std::vector<common::Geom> cellEdges;
    for (size_t index : indices) {
        auto geosCandidate = geolib3::internal::geolib2geosGeometry(edges[index]);
        auto innerCandidate = geosCandidate->intersection(bboxGeosPolygon.get());
        cellEdges.push_back(common::Geom(std::move(innerCandidate)));
    }
    return cellEdges;
}

DifferenceResult geomBufferDifference(
    const std::vector<geolib3::Polyline2>& from,
    const std::vector<geolib3::Polyline2>& toSubtract,
    const Grid& grid,
    double width
) {
    DifferenceResult result{ std::vector<common::Geom>(from.size()) };

    auto indicesFrom = calculateIndices(grid, from);
    auto indicesTo = calculateIndices(grid, toSubtract);

    INFO() << "Indices calculated. Subtracting cells";

    for (size_t i = 0; i < grid.heightCells; ++i) {
        for (size_t j = 0; j < grid.widthCells; ++j) {
            INFO() << "Tile [" << i << " / " << grid.heightCells << ", "
                    << j << " / " << grid.widthCells << "]";
            auto cellFrom = cutCellEdgesGeoms(from, indicesFrom[i][j], grid.boxes[i][j]);
            auto cellTo = cutCellEdgesGeoms(toSubtract, indicesTo[i][j], grid.boxes[i][j]);

            DEBUG() << "Edges / Graph: " << cellFrom.size() << " / " << cellTo.size();

            auto cellDiff = geomBufferDifference(cellTo, cellFrom, width);
            result.appendGeometries(cellDiff, indicesFrom[i][j]);
        }
    }

    return result;
}

} // namespace signals_graph
} // namespace wiki
} // namespace maps
