/**********************************************************************
 *
 * GEOS - Geometry Engine Open Source
 * http://geos.osgeo.org
 *
 * Copyright (C) 2011 Sandro Santilli <strk@kbt.io>
 * Copyright (C) 2006 Refractions Research Inc.
 * Copyright (C) 2001-2002 Vivid Solutions Inc.
 *
 * This is free software; you can redistribute and/or modify it under
 * the terms of the GNU Lesser General Public Licence as published
 * by the Free Software Foundation.
 * See the COPYING file for more information.
 *
 **********************************************************************
 *
 * Last port: operation/distance/DistanceOp.java r335 (JTS-1.12-)
 *
 **********************************************************************/

#include <geos/constants.h>
#include <geos/operation/distance/DistanceOp.h>
#include <geos/operation/distance/GeometryLocation.h>
#include <geos/operation/distance/ConnectedElementLocationFilter.h>
#include <geos/algorithm/PointLocator.h>
#include <geos/algorithm/Distance.h>
#include <geos/geom/Coordinate.h>
#include <geos/geom/CoordinateSequence.h>
#include <geos/geom/CoordinateArraySequence.h>
#include <geos/geom/LineString.h>
#include <geos/geom/Point.h>
#include <geos/geom/Polygon.h>
#include <geos/geom/Envelope.h>
#include <geos/geom/LineSegment.h>
#include <geos/geom/util/PolygonExtracter.h>
#include <geos/geom/util/LinearComponentExtracter.h>
#include <geos/geom/util/PointExtracter.h>
#include <geos/util/IllegalArgumentException.h>

#include <vector>
#include <iostream>

#ifndef GEOS_DEBUG
#define GEOS_DEBUG 0
#endif

using namespace std;
using namespace geos::geom;
//using namespace geos::algorithm;

namespace geos {
namespace operation { // geos.operation
namespace distance { // geos.operation.distance

using namespace geom;
//using namespace geom::util;

/*public static (deprecated)*/
double
DistanceOp::distance(const Geometry* g0, const Geometry* g1)
{
    DistanceOp distOp(g0, g1);
    return distOp.distance();
}

/*public static*/
double
DistanceOp::distance(const Geometry& g0, const Geometry& g1)
{
    DistanceOp distOp(g0, g1);
    return distOp.distance();
}

/*public static*/
std::unique_ptr<CoordinateSequence>
DistanceOp::nearestPoints(const Geometry* g0, const Geometry* g1)
{
    DistanceOp distOp(g0, g1);
    return distOp.nearestPoints();
}

DistanceOp::DistanceOp(const Geometry* g0, const Geometry* g1):
    geom{g0, g1},
    terminateDistance(0.0),
    minDistance(DoubleMax)
{}

DistanceOp::DistanceOp(const Geometry& g0, const Geometry& g1):
    geom{&g0, &g1},
    terminateDistance(0.0),
    minDistance(DoubleMax)
{}

DistanceOp::DistanceOp(const Geometry& g0, const Geometry& g1, double tdist)
    :
    geom{&g0, &g1},
    terminateDistance(tdist),
    minDistance(DoubleMax)
{}

/**
* Report the distance between the closest points on the input geometries.
*
* @return the distance between the geometries
*/
double
DistanceOp::distance()
{
    using geos::util::IllegalArgumentException;

    if(geom[0] == nullptr || geom[1] == nullptr) {
        throw IllegalArgumentException("null geometries are not supported");
    }
    if(geom[0]->isEmpty() || geom[1]->isEmpty()) {
        return 0.0;
    }
    computeMinDistance();
    return minDistance;
}

/* public */
std::unique_ptr<CoordinateSequence>
DistanceOp::nearestPoints()
{
    // lazily creates minDistanceLocation
    computeMinDistance();

    auto& locs = minDistanceLocation;

    // Empty input geometries result in this behaviour
    if(locs[0] == nullptr || locs[1] == nullptr) {
        // either both or none are set..
        assert(locs[0] == nullptr && locs[1] == nullptr);

        return nullptr;
    }

    std::unique_ptr<std::vector<Coordinate>> nearestPts(new std::vector<Coordinate>(2));
    (*nearestPts)[0] = locs[0]->getCoordinate();
    (*nearestPts)[1] = locs[1]->getCoordinate();

    return std::unique_ptr<CoordinateSequence>(new CoordinateArraySequence(nearestPts.release()));
}

void
DistanceOp::updateMinDistance(array<unique_ptr<GeometryLocation>, 2> & locGeom, bool flip)
{
    // if not set then don't update
    if(locGeom[0] == nullptr) {
#if GEOS_DEBUG
        std::cerr << "updateMinDistance called with loc[0] == null and loc[1] == " << locGeom[1] << std::endl;
#endif
        assert(locGeom[1] == nullptr);
        return;
    }

    if(flip) {
        minDistanceLocation[0] = std::move(locGeom[1]);
        minDistanceLocation[1] = std::move(locGeom[0]);
    }
    else {
        minDistanceLocation[0] = std::move(locGeom[0]);
        minDistanceLocation[1] = std::move(locGeom[1]);
    }
}

/*private*/
void
DistanceOp::computeMinDistance()
{
    // only compute once!
    if(computed) {
        return;
    }

#if GEOS_DEBUG
    std::cerr << "---Start: " << geom[0]->toString() << " - " << geom[1]->toString() << std::endl;
#endif

    computeContainmentDistance();

    if(minDistance <= terminateDistance) {
        computed = true;
        return;
    }

    computeFacetDistance();
    computed = true;

#if GEOS_DEBUG
    std::cerr << "---End " << std::endl;
#endif
}

/*private*/
void
DistanceOp::computeContainmentDistance()
{
    using geom::util::PolygonExtracter;

    Polygon::ConstVect polys1;
    PolygonExtracter::getPolygons(*(geom[1]), polys1);


#if GEOS_DEBUG
    std::cerr << "PolygonExtracter found " << polys1.size() << " polygons in geometry 2" << std::endl;
#endif

    // NOTE:
    // Expected to fill minDistanceLocation items
    // if minDistance <= terminateDistance

    array<std::unique_ptr<GeometryLocation>, 2> locPtPoly;
    // test if either geometry has a vertex inside the other
    if(! polys1.empty()) {
        auto insideLocs0 = ConnectedElementLocationFilter::getLocations(geom[0]);
        computeInside(insideLocs0, polys1, locPtPoly);

        if(minDistance <= terminateDistance) {
            assert(locPtPoly[0]);
            assert(locPtPoly[1]);

            minDistanceLocation[0] = std::move(locPtPoly[0]);
            minDistanceLocation[1] = std::move(locPtPoly[1]);

            return;
        }
    }

    Polygon::ConstVect polys0;
    PolygonExtracter::getPolygons(*(geom[0]), polys0);

#if GEOS_DEBUG
    std::cerr << "PolygonExtracter found " << polys0.size() << " polygons in geometry 1" << std::endl;
#endif


    if(! polys0.empty()) {
        auto insideLocs1 = ConnectedElementLocationFilter::getLocations(geom[1]);
        computeInside(insideLocs1, polys0, locPtPoly);
        if(minDistance <= terminateDistance) {
            // flip locations, since we are testing geom 1 VS geom 0
            assert(locPtPoly[0]);
            assert(locPtPoly[1]);

            minDistanceLocation[0] = std::move(locPtPoly[1]);
            minDistanceLocation[1] = std::move(locPtPoly[0]);

            return;
        }
    }
}


/*private*/
void
DistanceOp::computeInside(vector<unique_ptr<GeometryLocation>> & locs,
                          const Polygon::ConstVect& polys,
                          array<unique_ptr<GeometryLocation>, 2> & locPtPoly)
{
    for(auto& loc : locs) {
        for(const auto& poly : polys) {
            computeInside(loc, poly, locPtPoly);
            if(minDistance <= terminateDistance) {
                return;
            }
        }
    }
}

/*private*/
void
DistanceOp::computeInside(std::unique_ptr<GeometryLocation> & ptLoc,
                          const Polygon* poly,
                          array<std::unique_ptr<GeometryLocation>, 2> & locPtPoly)
{
    const Coordinate& pt = ptLoc->getCoordinate();

    // if pt is not in exterior, distance to geom is 0
    if(Location::EXTERIOR != ptLocator.locate(pt, static_cast<const Geometry*>(poly))) {
        minDistance = 0.0;
        locPtPoly[0] = std::move(ptLoc);
        locPtPoly[1].reset(new GeometryLocation(poly, pt));
        return;
    }
}

/*private*/
void
DistanceOp::computeFacetDistance()
{
    using geom::util::LinearComponentExtracter;
    using geom::util::PointExtracter;

    array<unique_ptr<GeometryLocation>, 2> locGeom;

    /**
     * Geometries are not wholly inside, so compute distance from lines
     * and points of one to lines and points of the other
     */
    LineString::ConstVect lines0;
    LineString::ConstVect lines1;
    LinearComponentExtracter::getLines(*(geom[0]), lines0);
    LinearComponentExtracter::getLines(*(geom[1]), lines1);

#if GEOS_DEBUG
    std::cerr << "LinearComponentExtracter found "
              << lines0.size() << " lines in geometry 1 and "
              << lines1.size() << " lines in geometry 2 "
              << std::endl;
#endif

    // exit whenever minDistance goes LE than terminateDistance
    computeMinDistanceLines(lines0, lines1, locGeom);
    updateMinDistance(locGeom, false);
    if(minDistance <= terminateDistance) {
#if GEOS_DEBUG
        std::cerr << "Early termination after line-line distance" << std::endl;
#endif
        return;
    }

    Point::ConstVect pts1;
    PointExtracter::getPoints(*(geom[1]), pts1);

#if GEOS_DEBUG
    std::cerr << "PointExtracter found " << pts1.size() << " points in geometry 2" << std::endl;
#endif

    locGeom[0] = nullptr;
    locGeom[1] = nullptr;
    computeMinDistanceLinesPoints(lines0, pts1, locGeom);
    updateMinDistance(locGeom, false);
    if(minDistance <= terminateDistance) {
#if GEOS_DEBUG
        std::cerr << "Early termination after lines0-points1 distance" << std::endl;
#endif
        return;
    }

    Point::ConstVect pts0;
    PointExtracter::getPoints(*(geom[0]), pts0);

#if GEOS_DEBUG
    std::cerr << "PointExtracter found " << pts0.size() << " points in geometry 1" << std::endl;
#endif

    locGeom[0] = nullptr;
    locGeom[1] = nullptr;
    computeMinDistanceLinesPoints(lines1, pts0, locGeom);
    updateMinDistance(locGeom, true);
    if(minDistance <= terminateDistance) {
#if GEOS_DEBUG
        std::cerr << "Early termination after lines1-points0 distance" << std::endl;
#endif
        return;
    }

    locGeom[0] = nullptr;
    locGeom[1] = nullptr;
    computeMinDistancePoints(pts0, pts1, locGeom);
    updateMinDistance(locGeom, false);

#if GEOS_DEBUG
    std::cerr << "termination after pts-pts distance" << std::endl;
#endif
}

/*private*/
void
DistanceOp::computeMinDistanceLines(
    const LineString::ConstVect& lines0,
    const LineString::ConstVect& lines1,
    std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
{
    for(const LineString* line0 : lines0) {
        for(const LineString* line1 : lines1) {
            computeMinDistance(line0, line1, locGeom);
            if(minDistance <= terminateDistance) {
                return;
            }
        }
    }
}

/*private*/
void
DistanceOp::computeMinDistancePoints(
    const Point::ConstVect& points0,
    const Point::ConstVect& points1,
    array<unique_ptr<GeometryLocation>, 2> & locGeom)
{
    for(const Point* pt0 : points0) {
        for(const Point* pt1 : points1) {
            double dist = pt0->getCoordinate()->distance(*(pt1->getCoordinate()));

#if GEOS_DEBUG
            std::cerr << "Distance "
                      << pt0->toString() << " - "
                      << pt1->toString() << ": "
                      << dist << ", minDistance: " << minDistance
                      << std::endl;
#endif

            if(dist < minDistance) {
                minDistance = dist;
                // this is wrong - need to determine closest points on both segments!!!
                locGeom[0].reset(new GeometryLocation(pt0, 0, *(pt0->getCoordinate())));
                locGeom[1].reset(new GeometryLocation(pt1, 0, *(pt1->getCoordinate())));
            }

            if(minDistance <= terminateDistance) {
                return;
            }
        }
    }
}

/*private*/
void
DistanceOp::computeMinDistanceLinesPoints(
    const LineString::ConstVect& lines,
    const Point::ConstVect& points,
    std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
{
    for(const LineString* line : lines) {
        for(const Point* pt : points) {
            computeMinDistance(line, pt, locGeom);
            if(minDistance <= terminateDistance) {
                return;
            }
        }
    }
}

/*private*/
void
DistanceOp::computeMinDistance(
    const LineString* line0,
    const LineString* line1,
    std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
{
    using geos::algorithm::Distance;

    const Envelope* env0 = line0->getEnvelopeInternal();
    const Envelope* env1 = line1->getEnvelopeInternal();
    if(env0->distance(env1) > minDistance) {
        return;
    }

    const CoordinateSequence* coord0 = line0->getCoordinatesRO();
    const CoordinateSequence* coord1 = line1->getCoordinatesRO();
    size_t npts0 = coord0->getSize();
    size_t npts1 = coord1->getSize();

    // brute force approach!
    for(size_t i = 0; i < npts0 - 1; ++i) {
        for(size_t j = 0; j < npts1 - 1; ++j) {
            double dist = Distance::segmentToSegment(coord0->getAt(i), coord0->getAt(i + 1),
                          coord1->getAt(j), coord1->getAt(j + 1));
            if(dist < minDistance) {
                minDistance = dist;

                // TODO avoid copy from constructing segs, maybe
                // by making a static closestPoints that takes four
                // coordinate references
                LineSegment seg0(coord0->getAt(i), coord0->getAt(i + 1));
                LineSegment seg1(coord1->getAt(j), coord1->getAt(j + 1));
                auto closestPt = seg0.closestPoints(seg1);

                locGeom[0].reset(new GeometryLocation(line0, i, closestPt[0]));
                locGeom[1].reset(new GeometryLocation(line1, j, closestPt[1]));
            }
            if(minDistance <= terminateDistance) {
                return;
            }
        }
    }
}

/*private*/
void
DistanceOp::computeMinDistance(const LineString* line,
                               const Point* pt,
                               std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
{
    using geos::algorithm::Distance;

    const Envelope* env0 = line->getEnvelopeInternal();
    const Envelope* env1 = pt->getEnvelopeInternal();
    if(env0->distance(env1) > minDistance) {
        return;
    }
    const CoordinateSequence* coord0 = line->getCoordinatesRO();
    const Coordinate* coord = pt->getCoordinate();

    // brute force approach!
    size_t npts0 = coord0->getSize();
    for(size_t i = 0; i < npts0 - 1; ++i) {
        double dist = Distance::pointToSegment(*coord, coord0->getAt(i), coord0->getAt(i + 1));
        if(dist < minDistance) {
            minDistance = dist;

            // TODO avoid copy from constructing segs, maybe
            // by making a static closestPoints that takes three
            // coordinate references
            LineSegment seg(coord0->getAt(i), coord0->getAt(i + 1));
            Coordinate segClosestPoint;
            seg.closestPoint(*coord, segClosestPoint);

            locGeom[0].reset(new GeometryLocation(line, i, segClosestPoint));
            locGeom[1].reset(new GeometryLocation(pt, 0, *coord));
        }
        if(minDistance <= terminateDistance) {
            return;
        }
    }
}

/* public static */
bool
DistanceOp::isWithinDistance(const geom::Geometry& g0,
                             const geom::Geometry& g1,
                             double distance)
{
    DistanceOp distOp(g0, g1, distance);
    return distOp.distance() <= distance;
}

} // namespace geos.operation.distance
} // namespace geos.operation
} // namespace geos
