#include "utils.h"

#include "exception.h"
#include "geom.h"
#include "configs/config.h"

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/base64.h>
#include <maps/libs/geolib/include/conversion.h>

#include <geos/geom/CoordinateArraySequenceFactory.h>
#include <geos/geom/CoordinateFilter.h>
#include <geos/geom/CoordinateSequenceFactory.h>
#include <geos/geom/GeometryFactory.h>
#include <geos/geom/LineString.h>
#include <geos/geom/Polygon.h>
#include <geos/geom/Point.h>
#include <geos/geom/PrecisionModel.h>

#include <geos/io/WKBReader.h>
#include <geos/operation/valid/IsValidOp.h>
#include <geos/operation/IsSimpleOp.h>

namespace maps{
namespace wiki{

ReadingPipe::ReadingPipe(const std::string& command)
    : f_(nullptr)
{
    ASSERT(!command.empty());
    f_ = ::popen(command.c_str(), "r");
    ASSERT(f_ != nullptr);
}

ReadingPipe::~ReadingPipe()
{
    if (f_ != nullptr) {
        ::pclose(f_);
    }
}

std::string
ReadingPipe::read() const
{
    ASSERT(f_ != nullptr);
    const size_t SIZE = 1024;

    std::stringstream ss;
    char buf[SIZE + 1];
    while(!feof(f_)) {
        buf[::fread(buf, 1, SIZE, f_)] = '\0';
        ss << buf;
    }
    return ss.str();
}

int
ReadingPipe::wait()
{
    ASSERT(f_ != nullptr);
    int status = ::pclose(f_);
    f_ = nullptr;
    return status;
}

//Inplace convert array of coordinates from
//geodetic to mercator
static void
convertToMercator(std::vector<geos::geom::Coordinate> *points)
{
    for(size_t p = 0; p < points->size(); p++){
         TMercatorPoint mercator =
             common::geodeticTomercator((*points)[p].x, (*points)[p].y);
        (*points)[p].x = mercator.x();
        (*points)[p].y = mercator.y();
    }
}

GeometryPtr
createPolygon(
    std::vector<geos::geom::Coordinate> *points,
    std::vector<GeosGeometryPtr> *holes,
    SpatialRefSystem spatialRefSystem,
    AntiMeridianAdjustPolicy adjustPolicy)
{
    if (spatialRefSystem == SpatialRefSystem::Geodetic) {
        convertToMercator(points);
    }
    auto cs = geos::geom::DefaultCoordinateSequenceFactory().create(points);
    if (adjustPolicy == AntiMeridianAdjustPolicy::Adjust) {
        adjustToAntiMeridian(cs.get(), cfg()->editor()->system().antiMeridianOverlapThreshold());
    }
    auto lr = geos::geom::GeometryFactory::getDefaultInstance()->createLinearRing(std::move(cs));

    auto linearRingHoles = std::make_unique<std::vector<geos::geom::LinearRing*>>();
    if (holes != nullptr) {
        linearRingHoles->reserve(holes->size());
        for (auto* geom: *holes) {
            linearRingHoles->push_back(dynamic_cast<geos::geom::LinearRing*>(geom));
        }
    }

    geos::geom::Polygon *poly =
        geos::geom::GeometryFactory::getDefaultInstance()->createPolygon(lr.release(), linearRingHoles.release());

    return GeometryPtr(poly);
}

GeometryPtr
createPolyline(
    std::vector<geos::geom::Coordinate> *points,
    SpatialRefSystem spatialRefSystem,
    AntiMeridianAdjustPolicy adjustPolicy)
{
    if (spatialRefSystem == SpatialRefSystem::Geodetic) {
        convertToMercator(points);
    }
    auto cs = geos::geom::DefaultCoordinateSequenceFactory().create(points);
    if (adjustPolicy == AntiMeridianAdjustPolicy::Adjust) {
        adjustToAntiMeridian(cs.get(), cfg()->editor()->system().antiMeridianOverlapThreshold());
    }
    auto line = geos::geom::GeometryFactory::getDefaultInstance()->createLineString(std::move(cs));
    return GeometryPtr(std::move(line));
}

GeometryPtr
createPoint(std::vector<geos::geom::Coordinate> *points,
            SpatialRefSystem spatialRefSystem)
{
    if (spatialRefSystem == SpatialRefSystem::Geodetic) {
        convertToMercator(points);
    }
    auto cs = geos::geom::DefaultCoordinateSequenceFactory().create(points);
    geos::geom::Point *point =
        geos::geom::GeometryFactory::getDefaultInstance()->createPoint(*cs);
    return GeometryPtr(point);
}

GeometryPtr
createPoint(double x, double y, SpatialRefSystem spatialRefSystem)
{
    std::unique_ptr<std::vector<geos::geom::Coordinate>>
        points(make_unique<std::vector<geos::geom::Coordinate>>());
    points->emplace_back(x, y);
    if (spatialRefSystem == SpatialRefSystem::Geodetic) {
        convertToMercator(points.get());
    }
    auto cs = geos::geom::DefaultCoordinateSequenceFactory().create(points.release());
    geos::geom::Point *point =
        geos::geom::GeometryFactory::getDefaultInstance()->createPoint(*cs);
    return GeometryPtr(point);
}

GeometryPtr
createGeom(double xMin, double yMin, double xMax, double yMax,
           SpatialRefSystem spatialRefSystem)
{
    std::vector<geos::geom::Coordinate> *points = new std::vector<geos::geom::Coordinate>;
    points->resize(5);
    points->at(0).x = points->at(3).x = points->at(4).x = xMin;
    points->at(1).x = points->at(2).x = xMax;
    points->at(0).y = points->at(1).y = points->at(4).y= yMax;
    points->at(2).y = points->at(3).y = yMin;
    // TODO envelope can intersect 180 meridian
    return createPolygon(points, nullptr, spatialRefSystem, AntiMeridianAdjustPolicy::Ignore);
}

GeometryPtr
createGeom(const geos::geom::Envelope& envelope, SpatialRefSystem spatialRefSystem)
{
    REQUIRE(!envelope.isNull(), "Null envelope passed to createGeom");
    return createGeom(
        envelope.getMinX(), envelope.getMinY(), envelope.getMaxX(), envelope.getMaxY(),
        spatialRefSystem);
}

GeometryPtr
createGeom(const geolib3::BoundingBox& bbox, SpatialRefSystem spatialRefSystem)
{
    //REQUIRE(!bbox.isDegenerate(), "Degenerate bbox passed to createGeom");
    return createGeom(
        bbox.minX(), bbox.minY(), bbox.maxX(), bbox.maxY(),
        spatialRefSystem);
}

GeometryPtr
createGeom(const pqxx::field& dbfield)
{
    pqxx::binarystring binGeom( dbfield );
    geos::io::WKBReader wkbr(*geos::geom::GeometryFactory::getDefaultInstance());
    std::stringstream sGeom(binGeom.str(), std::ios::in | std::ios::binary );
    return GeometryPtr(wkbr.read(sGeom));
}

std::list<Geom>
createPolygonBuffers(const Geom& geom, double width, PolygonBufferPolicy policy)
{
    REQUIRE(geom->getGeometryTypeId() == geos::geom::GEOS_POLYGON,
        "Wrong geometry type. POLYGON is expected.");
    if (policy == PolygonBufferPolicy::Self) {
        return {geom};
    }
    const auto* geosPolygon = dynamic_cast<const geos::geom::Polygon*>(geom.geosGeometryPtr());
    ASSERT(geosPolygon);
    std::list<Geom> buffers;
    const auto* geosFactory = geos::geom::GeometryFactory::getDefaultInstance();
    Geom exteriorRing(geosFactory->createLineString(
        geosPolygon->getExteriorRing()->getCoordinates()));
    ASSERT(exteriorRing->getGeometryTypeId() == geos::geom::GEOS_LINESTRING);
    buffers.push_back(exteriorRing.createBuffer(width));
    for (size_t i = 0; i < geosPolygon->getNumInteriorRing(); ++i) {
        Geom interiorRing(geosFactory->createLineString(
            geosPolygon->getInteriorRingN(i)->getCoordinates()));
        ASSERT(interiorRing->getGeometryTypeId() == geos::geom::GEOS_LINESTRING);
        buffers.push_back(interiorRing.createBuffer(width));
    }
    return buffers;
}

std::string
json(const geos::geom::Envelope& envelope)
{
    std::stringstream out;
    out.precision(DOUBLE_FORMAT_PRECISION);
    const TGeoPoint lt = common::mercatorToGeodetic(envelope.getMinX(), envelope.getMinY());
    const TGeoPoint rb = common::mercatorToGeodetic(envelope.getMaxX(), envelope.getMaxY());
    out << "[" << lt.x() << ", " << lt.y() << ", " << rb.x() << ", " << rb.y() << "]";
    return out.str();
}

std::string
json(const geolib3::BoundingBox& bbox)
{
    std::stringstream out;
    out.precision(DOUBLE_FORMAT_PRECISION);
    const TGeoPoint lt = common::mercatorToGeodetic(bbox.minX(), bbox.minY());
    const TGeoPoint rb = common::mercatorToGeodetic(bbox.maxX(), bbox.maxY());
    out << "[" << lt.x() << ", " << lt.y() << ", " << rb.x() << ", " << rb.y() << "]";
    return out.str();
}

geos::geom::Envelope
createEnvelope(const std::string& boundingBoxCoords,
               SpatialRefSystem spatialRefSystem)
{
    auto coords = splitCast<std::vector<double>>(boundingBoxCoords, ',');
    if (coords.size() != 4) {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_REQUEST, " Bounding box ["
            << boundingBoxCoords << "] is wrong formatted.");
    }
    if (SpatialRefSystem::Geodetic == spatialRefSystem) {
        TMercatorPoint ltm = common::geodeticTomercator(coords[0], coords[1]);
        TMercatorPoint rbm = common::geodeticTomercator(coords[2], coords[3]);
        return geos::geom::Envelope(ltm.x(), rbm.x(), ltm.y(), rbm.y());
    } else {
        return geos::geom::Envelope(coords[0], coords[2], coords[1], coords[3]);
    }
}

geolib3::BoundingBox
createBbox(const std::string& boundingBoxCoords, SpatialRefSystem spatialRefSystem)
{
    auto coords = splitCast<std::vector<double>>(boundingBoxCoords, ',');
    if (coords.size() != 4) {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_REQUEST, " Bounding box ["
            << boundingBoxCoords << "] is wrong formatted.");
    }

    geolib3::Point2 ltm(coords[0], coords[1]);
    geolib3::Point2 rbm(coords[2], coords[3]);

    if (SpatialRefSystem::Geodetic == spatialRefSystem) {
        ltm = geolib3::geoPoint2Mercator(ltm);
        rbm = geolib3::geoPoint2Mercator(rbm);
    }

    return geolib3::BoundingBox(ltm, rbm);
}

namespace
{

geos::geom::Coordinate
jsonToCoordinate(const json::Value& coord)
{
    WIKI_REQUIRE(coord.isArray(), ERR_BAD_DATA,
        "Expected array of doubles to represent point");
    WIKI_REQUIRE(coord.size() == 2, ERR_BAD_DATA,
        "Expected 2 numbers to represent point");
    return geos::geom::Coordinate(coord[0].as<double>(), coord[1].as<double>());
}

std::unique_ptr<std::vector<geos::geom::Coordinate>>
jsonToCoordinates(const json::Value& coordsArray)
{
    auto ret = make_unique<std::vector<geos::geom::Coordinate>>();
    WIKI_REQUIRE(coordsArray.isArray(), ERR_BAD_DATA,
        "Expected array of arrays to represent linestring");
    for(const auto& coord : coordsArray){
        ret->push_back(jsonToCoordinate(coord));
    }
    return ret;
}
}//namespace

TGeoPoint
createGeoPointFromJson(const json::Value& json)
{
    Geom geom(createGeomFromJson(json));
    REQUIRE(!geom.isNull() && geom->getGeometryTypeId()
        == geos::geom::GEOS_POINT,
        "Point expected.");
    const auto* pointPtr = dynamic_cast<const geos::geom::Point*>(geom.geosGeometryPtr());
    return TGeoPoint(pointPtr->getX(), pointPtr->getY());
}

TGeoPoint
createGeoPointFromJsonStr(const std::string& jsonText)
{
    auto json = json::Value::fromString(jsonText);
    return createGeoPointFromJson(json);
}

std::vector<TGeoPoint>
createGeoPointCollectionFromJson(const json::Value& json)
{
    WIKI_REQUIRE(
        json.isObject() && json.hasField("type") && json.hasField("geometries"),
        ERR_BAD_DATA,
        "Malformed geometry collection");
    std::vector<TGeoPoint> collection;
    for (const auto& geometryJson : json["geometries"]) {
        TGeoPoint point = createGeoPointFromJson(geometryJson);
        collection.push_back(point);
    }
    return collection;
}

std::vector<TGeoPoint>
createGeoPointCollectionFromJsonStr(const std::string& jsonText)
{
    return createGeoPointCollectionFromJson(json::Value::fromString(jsonText));
}

std::vector<Geom> createGeomCollectionFromJson(const json::Value& json)
{
    WIKI_REQUIRE(
        json.isObject() && json.hasField("type") && json.hasField("geometries"),
        ERR_BAD_DATA,
        "Malformed geometry collection");
    std::vector<Geom> collection;
    for (const auto& geometryJson : json["geometries"]) {
        auto geom = createGeomFromJson(geometryJson);
        collection.emplace_back(std::move(geom));
    }
    return collection;
}

namespace { // Helpers for findGeomError

const double GEO_COORDS_PRECISION = 1e-9;

class MercatorToRPGeoPointFilter : public geos::geom::CoordinateFilter
{
public:
    MercatorToRPGeoPointFilter()
        : precisionModel_(1.0 / GEO_COORDS_PRECISION)
    {}

    void filter_rw(geos::geom::Coordinate* c) const override
    {
        auto geoPoint = geolib3::mercator2GeoPoint({c->x, c->y});
        c->x = geoPoint.x();
        c->y = geoPoint.y();
        precisionModel_.makePrecise(c);
    }

    void filter_ro(const geos::geom::Coordinate*) override
    {}

private:
    geos::geom::PrecisionModel precisionModel_;
};

static const MercatorToRPGeoPointFilter coordToGeodeticFilter;

TGeoPoint
toGeoPoint(const geos::geom::Coordinate& coord)
{
    return {coord.x, coord.y};
}

TGeoPoint
geodeticToMercator(const geos::geom::Coordinate& coord)
{
    return geolib3::geoPoint2Mercator({coord.x, coord.y});
}

} // namespace

std::optional<TGeoPoint>
findGeomError(const Geom& geom)
{
    geos::operation::IsSimpleOp isSimpleMeractor(*geom.geosGeometryPtr());
    if (!isSimpleMeractor.isSimple()) {
        return toGeoPoint(*isSimpleMeractor.getNonSimpleLocation());
    }

    geos::operation::valid::IsValidOp isValidOpMercator(geom.geosGeometryPtr());
    if (!isValidOpMercator.isValid()) {
        return toGeoPoint(isValidOpMercator.getValidationError()->getCoordinate());
    }

    Geom geomGeodetic(geom->clone());
    geomGeodetic->apply_rw(&coordToGeodeticFilter);
    geos::operation::valid::IsValidOp isValidOpGeodetic(geomGeodetic.geosGeometryPtr());
    if (!isValidOpGeodetic.isValid()) {
        return geodeticToMercator(isValidOpGeodetic.getValidationError()->getCoordinate());
    }
    return std::nullopt;
}

std::vector<Geom> createGeomCollectionFromJsonStr(const std::string& jsonText)
{
    return createGeomCollectionFromJson(json::Value::fromString(jsonText));
}

GeometryPtr
createGeomFromJson(const json::Value& json)
{
    REQUIRE(json.isObject(), "Expected json object for geometry");
    REQUIRE(json.hasField("type"), "Geojson must contain 'type' field");
    REQUIRE(json.hasField("coordinates"), "Geojson must contain 'type' field");

    std::string geomType = json["type"].as<std::string>();

    if (geomType == s_geojsonGeomTypeNamePoint) {
        std::unique_ptr<std::vector<geos::geom::Coordinate>> points(make_unique<std::vector<geos::geom::Coordinate>>());
        points->push_back(jsonToCoordinate(json["coordinates"]));
        return createPoint(points.release(), SpatialRefSystem::Geodetic);
    } else if(geomType == s_geojsonGeomTypeNameLine) {
        return createPolyline(jsonToCoordinates(json["coordinates"]).release(),
            SpatialRefSystem::Geodetic, AntiMeridianAdjustPolicy::Adjust);
    } else if(geomType == s_geojsonGeomTypeNamePolygon) {
        WIKI_REQUIRE(json["coordinates"].isArray(), ERR_BAD_DATA,
            "Array of linestrings arrays expected for polygon");
        auto exterior = jsonToCoordinates(json["coordinates"][0]);
        if (exterior->empty()) {
            return nullptr;
        }
        if(exterior->front() != exterior->back()){
            exterior->push_back(exterior->front());
        }
        convertToMercator(exterior.get());
        tryFixRing(*exterior);

        struct Deleter
        {
            void operator()(std::vector<GeosGeometryPtr>* geoms) const
            {
                if (geoms) {
                    for (auto* geom : *geoms) {
                        delete geom;
                    }
                }
                delete geoms;
            }
        };

        std::unique_ptr<std::vector<GeosGeometryPtr>, Deleter> holes;
        if (json["coordinates"].size() > 1) {
            holes.reset(new std::vector<GeosGeometryPtr>());
            for (size_t i = 1; i < json["coordinates"].size(); ++i) {
                std::unique_ptr<std::vector<geos::geom::Coordinate>> holeCoords(
                    jsonToCoordinates(json["coordinates"][i]));
                if (holeCoords->empty())
                    continue;
                if (holeCoords->front() != holeCoords->back()) {
                    holeCoords->push_back(holeCoords->front());
                }
                convertToMercator(holeCoords.get());
                tryFixRing(*holeCoords);
                std::unique_ptr<geos::geom::Geometry> hole(createPolygon(
                    holeCoords.release(), nullptr,
                    SpatialRefSystem::Mercator,
                    AntiMeridianAdjustPolicy::Adjust));
                holes->emplace_back(dynamic_cast<geos::geom::Polygon*>(
                    hole.get())->getExteriorRing()->clone().release());
            }
        }
        return createPolygon(
            exterior.release(), holes.release(),
            SpatialRefSystem::Mercator,
            AntiMeridianAdjustPolicy::Adjust);
    }
    THROW_WIKI_LOGIC_ERROR(ERR_BAD_DATA, "Unsupported geometry type:" << geomType);
    return nullptr;
}

GeometryPtr
createGeomFromJsonStr(const std::string& jsonText)
{
    return createGeomFromJson(json::Value::fromString(jsonText));
}

std::string
decodeBase64(const std::string& base64data)
{
    std::string encodedData = base64data;
    encodedData.erase(
        std::remove_if(encodedData.begin(), encodedData.end(), isspace),
        encodedData.end()
    );
    return base64Decode(encodedData);
}

}//namespace wiki
}//namespace maps
