/**********************************************************************
*
* GEOS - Geometry Engine Open Source
* http://geos.osgeo.org
*
* Copyright (C) 2021 Paul Ramsey <pramsey@cleverelephant.ca>
* Copyright (C) 2021 Martin Davis
*
* 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.
*
**********************************************************************/

#include <geos/algorithm/locate/IndexedPointInAreaLocator.h>
#include <geos/geom/Coordinate.h>
#include <geos/geom/Geometry.h>
#include <geos/geom/GeometryCollection.h>
#include <geos/geom/LineString.h>
#include <geos/geom/LinearRing.h>
#include <geos/geom/Location.h>
#include <geos/geom/MultiPoint.h>
#include <geos/geom/MultiPolygon.h>
#include <geos/geom/Point.h>
#include <geos/geom/Polygon.h>
#include <geos/operation/valid/IsValidOp.h>
#include <geos/operation/valid/IndexedNestedHoleTester.h>
#include <geos/operation/valid/IndexedNestedPolygonTester.h>
#include <geos/util/UnsupportedOperationException.h>
#include <geos/util/IllegalArgumentException.h>

#include <cmath>

using namespace geos::geom;
using geos::algorithm::locate::IndexedPointInAreaLocator;

namespace geos {      // geos
namespace operation { // geos.operation
namespace valid {     // geos.operation.valid

/* public */
bool
IsValidOp::isValid()
{
    return isValidGeometry(inputGeometry);
}


/* public static */
bool
IsValidOp::isValid(const Coordinate* coord)
{
    if (std::isfinite(coord->x) && std::isfinite(coord->y)) {
        return true;
    }
    else {
        return false;
    }
}


/* public */
const TopologyValidationError *
IsValidOp::getValidationError()
{
    isValidGeometry(inputGeometry);
    return validErr.get();
}


/* private */
void
IsValidOp::logInvalid(int code, const Coordinate* pt)
{
    validErr.reset(new TopologyValidationError(code, *pt));
}


/* private */
bool
IsValidOp::isValidGeometry(const Geometry* g)
{
    validErr.reset(nullptr);

    if (!g)
        throw util::IllegalArgumentException("Null geometry argument to IsValidOp");

    // empty geometries are always valid
    if (g->isEmpty()) return true;
    switch (g->getGeometryTypeId()) {
        case GEOS_POINT:
            return isValid(static_cast<const Point*>(g));
        case GEOS_MULTIPOINT:
            return isValid(static_cast<const MultiPoint*>(g));
        case GEOS_LINEARRING:
            return isValid(static_cast<const LinearRing*>(g));
        case GEOS_LINESTRING:
            return isValid(static_cast<const LineString*>(g));
        case GEOS_POLYGON:
            return isValid(static_cast<const Polygon*>(g));
        case GEOS_MULTIPOLYGON:
            return isValid(static_cast<const MultiPolygon*>(g));
        case GEOS_MULTILINESTRING:
            return isValid(static_cast<const GeometryCollection*>(g));
        case GEOS_GEOMETRYCOLLECTION:
            return isValid(static_cast<const GeometryCollection*>(g));
    }

    // geometry type not known
    throw util::UnsupportedOperationException(g->getGeometryType());
}


/* private */
bool
IsValidOp::isValid(const Point* g)
{
    checkCoordinateInvalid(g->getCoordinatesRO());
    if (hasInvalidError()) return false;
    return true;
}


/* private */
bool
IsValidOp::isValid(const MultiPoint* g)
{
    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
        const Point* p = g->getGeometryN(i);
        if (p->isEmpty()) continue;
        if (!isValid(p->getCoordinate())) {
            logInvalid(TopologyValidationError::eInvalidCoordinate,
                       p->getCoordinate());
            return false;;
        }
    }
    return true;
}


/* private */
bool
IsValidOp::isValid(const LineString* g)
{
    checkCoordinateInvalid(g->getCoordinatesRO());
    if (hasInvalidError()) return false;

    checkTooFewPoints(g, MIN_SIZE_LINESTRING);
    if (hasInvalidError()) return false;

    return true;
}


/* private */
bool
IsValidOp::isValid(const LinearRing* g)
{
    checkCoordinateInvalid(g->getCoordinatesRO());
    if (hasInvalidError()) return false;

    checkRingNotClosed(g);
    if (hasInvalidError()) return false;

    checkRingTooFewPoints(g);
    if (hasInvalidError()) return false;

    checkSelfIntersectingRing(g);
    if (hasInvalidError()) return false;

    return true;
}


/* private */
bool
IsValidOp::isValid(const Polygon* g)
{
    checkCoordinateInvalid(g);
    if (hasInvalidError()) return false;

    checkRingsNotClosed(g);
    if (hasInvalidError()) return false;

    checkRingsTooFewPoints(g);
    if (hasInvalidError()) return false;

    PolygonTopologyAnalyzer areaAnalyzer(g, isInvertedRingValid);

    checkAreaIntersections(areaAnalyzer);
    if (hasInvalidError()) return false;

    checkHolesOutsideShell(g);
    if (hasInvalidError()) return false;

    checkHolesNested(g);
    if (hasInvalidError()) return false;

    checkInteriorDisconnected(areaAnalyzer);
    if (hasInvalidError()) return false;

    return true;
}


/* private */
bool
IsValidOp::isValid(const MultiPolygon* g)
{
    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
        const Polygon* p = g->getGeometryN(i);
        checkCoordinateInvalid(p);
        if (hasInvalidError()) return false;

        checkRingsNotClosed(p);
        if (hasInvalidError()) return false;

        checkRingsTooFewPoints(p);
        if (hasInvalidError()) return false;
    }

    PolygonTopologyAnalyzer areaAnalyzer(g, isInvertedRingValid);

    checkAreaIntersections(areaAnalyzer);
    if (hasInvalidError()) return false;

    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
        const Polygon* p = g->getGeometryN(i);
        checkHolesOutsideShell(p);
        if (hasInvalidError()) return false;
    }

    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
        const Polygon* p = g->getGeometryN(i);
        checkHolesNested(p);
        if (hasInvalidError()) return false;
    }

    checkShellsNested(g);
    if (hasInvalidError()) return false;

    checkInteriorDisconnected(areaAnalyzer);
    if (hasInvalidError()) return false;

    return true;
}


/* private */
bool
IsValidOp::isValid(const GeometryCollection* gc)
{
    for (std::size_t i = 0; i < gc->getNumGeometries(); i++) {
        if (! isValidGeometry(gc->getGeometryN(i)))
            return false;
    }
    return true;
}


/* private */
void
IsValidOp::checkCoordinateInvalid(const CoordinateSequence* coords)
{
    for (std::size_t i = 0; i < coords->size(); i++) {
        if (! isValid(coords->getAt(i))) {
            logInvalid(TopologyValidationError::eInvalidCoordinate,
                       &coords->getAt(i));
            return;
        }
    }
}


/* private */
void
IsValidOp::checkCoordinateInvalid(const Polygon* poly)
{
    checkCoordinateInvalid(poly->getExteriorRing()->getCoordinatesRO());
    if (hasInvalidError()) return;
    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
        checkCoordinateInvalid(poly->getInteriorRingN(i)->getCoordinatesRO());
        if (hasInvalidError()) return;
    }
}


/* private */
void
IsValidOp::checkRingNotClosed(const LinearRing* ring)
{
    if (ring->isEmpty()) return;
    if (! ring->isClosed()) {
        Coordinate pt = ring->getNumPoints() >= 1
                        ? ring->getCoordinateN(0)
                        : Coordinate();
        logInvalid(TopologyValidationError::eRingNotClosed, &pt);
        return;
    }
}


/* private */
void
IsValidOp::checkRingsNotClosed(const Polygon* poly)
{
    checkRingNotClosed(poly->getExteriorRing());
    if (hasInvalidError()) return;
    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
        checkRingNotClosed(poly->getInteriorRingN(i));
        if (hasInvalidError()) return;
    }
}


/* private */
void
IsValidOp::checkRingsTooFewPoints(const Polygon* poly)
{
    checkRingTooFewPoints(poly->getExteriorRing());
    if (hasInvalidError()) return;
    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
        checkRingTooFewPoints(poly->getInteriorRingN(i));
        if (hasInvalidError()) return;
    }
}


/* private */
void
IsValidOp::checkRingTooFewPoints(const LinearRing* ring)
{
    if (ring->isEmpty()) return;
    checkTooFewPoints(ring, MIN_SIZE_RING);
}


/* private */
void
IsValidOp::checkTooFewPoints(const LineString* line, std::size_t minSize)
{
    if (! isNonRepeatedSizeAtLeast(line, minSize) ) {
        Coordinate pt = line->getNumPoints() >= 1
                        ? line->getCoordinateN(0)
                        : Coordinate();
        logInvalid(TopologyValidationError::eTooFewPoints, &pt);
    }
}


/* private */
bool
IsValidOp::isNonRepeatedSizeAtLeast(const LineString* line, std::size_t minSize)
{
    std::size_t numPts = 0;
    const Coordinate* prevPt = nullptr;
    for (std::size_t i = 0; i < line->getNumPoints(); i++) {
        if (numPts >= minSize) return true;
        const Coordinate& pt = line->getCoordinateN(i);
        if (prevPt == nullptr || ! pt.equals2D(*prevPt))
            numPts++;
        prevPt = &pt;
    }
    return numPts >= minSize;
}


/* private */
void
IsValidOp::checkAreaIntersections(PolygonTopologyAnalyzer& areaAnalyzer)
{
    if (areaAnalyzer.hasInvalidIntersection()) {
        logInvalid(areaAnalyzer.getInvalidCode(),
                   &areaAnalyzer.getInvalidLocation());
    }
}


/* private */
void
IsValidOp::checkSelfIntersectingRing(const LinearRing* ring)
{
    Coordinate intPt = PolygonTopologyAnalyzer::findSelfIntersection(ring);
    if (! intPt.isNull()) {
        logInvalid(TopologyValidationError::eRingSelfIntersection,
            &intPt);
    }
}


/* private */
void
IsValidOp::checkHolesOutsideShell(const Polygon* poly)
{
    // skip test if no holes are present
    if (poly->getNumInteriorRing() <= 0) return;

    const LinearRing* shell = poly->getExteriorRing();
    bool isShellEmpty = shell->isEmpty();

    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
        const LinearRing* hole = poly->getInteriorRingN(i);
        if (hole->isEmpty()) continue;

        const Coordinate* invalidPt = nullptr;
        if (isShellEmpty) {
            invalidPt = hole->getCoordinate();
        }
        else {
            invalidPt = findHoleOutsideShellPoint(hole, shell);
        }
        if (invalidPt != nullptr) {
            logInvalid(
                TopologyValidationError::eHoleOutsideShell,
                invalidPt);
            return;
        }
    }
}


/* private */
const Coordinate *
IsValidOp::findHoleOutsideShellPoint(const LinearRing* hole, const LinearRing* shell)
{

    const Coordinate& holePt0 = hole->getCoordinateN(0);
    const Coordinate& holePt1 = hole->getCoordinateN(1);
    /**
     * If hole envelope is not covered by shell, it must be outside
     */
    if (! shell->getEnvelopeInternal()->covers(hole->getEnvelopeInternal()))
        return &holePt0;

    if (PolygonTopologyAnalyzer::isSegmentInRing(&holePt0, &holePt1, shell))
        return nullptr;
    return &holePt0;
}


/* private */
void
IsValidOp::checkHolesNested(const Polygon* poly)
{
    // skip test if no holes are present
    if (poly->getNumInteriorRing() <= 0) return;

    IndexedNestedHoleTester nestedTester(poly);
    if (nestedTester.isNested()) {
        logInvalid(TopologyValidationError::eNestedHoles,
                   &nestedTester.getNestedPoint());
    }
}


/* private */
void
IsValidOp::checkShellsNested(const MultiPolygon* mp)
{
    // skip test if only one shell present
    if (mp->getNumGeometries() <= 1)
        return;

    IndexedNestedPolygonTester nestedTester(mp);
    if (nestedTester.isNested()) {
        logInvalid(TopologyValidationError::eNestedShells,
                   &nestedTester.getNestedPoint());
    }
}


/* private */
void
IsValidOp::checkInteriorDisconnected(PolygonTopologyAnalyzer& analyzer)
{
    if (analyzer.isInteriorDisconnected())
        logInvalid(TopologyValidationError::eDisconnectedInterior,
                   &analyzer.getDisconnectionLocation());
}


} // namespace geos.operation.valid
} // namespace geos.operation
} // namespace geos
