/**********************************************************************
 *
 * GEOS - Geometry Engine Open Source
 * http://geos.osgeo.org
 *
 * Copyright (C) 2022 ISciences LLC
 *
 * 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/BoundaryOp.java fd5aebb
 *
 **********************************************************************/

#include <geos/operation/BoundaryOp.h>
#include <geos/algorithm/BoundaryNodeRule.h>
#include <geos/geom/Geometry.h>
#include <geos/geom/GeometryFactory.h>
#include <geos/geom/CompoundCurve.h>
#include <geos/geom/Curve.h>
#include <geos/geom/LineString.h>
#include <geos/geom/MultiCurve.h>
#include <geos/geom/MultiLineString.h>
#include <geos/geom/SimpleCurve.h>
#include <geos/util.h>
#include <map>

using geos::geom::Coordinate;
using geos::geom::CoordinateSequence;
using geos::geom::Curve;
using geos::geom::Dimension;
using geos::geom::Geometry;
using geos::geom::LineString;
using geos::geom::MultiCurve;
using geos::geom::MultiLineString;
using geos::geom::Point;
using geos::algorithm::BoundaryNodeRule;

namespace geos {
namespace operation {

BoundaryOp::BoundaryOp(const Geometry& geom) :
    m_geom(geom),
    m_geomFact(*geom.getFactory()),
    m_bnRule(BoundaryNodeRule::getBoundaryRuleMod2())
{}

BoundaryOp::BoundaryOp(const geom::Geometry& geom, const algorithm::BoundaryNodeRule& bnRule) :
    m_geom(geom),
    m_geomFact(*geom.getFactory()),
    m_bnRule(bnRule)
{}

std::unique_ptr<geom::Geometry>
BoundaryOp::getBoundary()
{
    if (auto ls = dynamic_cast<const Curve*>(&m_geom)) {
        return boundaryCurve(*ls);
    }

    if (auto mls = dynamic_cast<const MultiLineString*>(&m_geom)) {
        return boundaryMultiCurve(*mls);
    }

    if (auto mc = dynamic_cast<const MultiCurve*>(&m_geom)) {
        return boundaryMultiCurve(*mc);
    }

    return m_geom.getBoundary();
}

std::unique_ptr<geom::Geometry>
BoundaryOp::getBoundary(const geom::Geometry& g)
{
    BoundaryOp bop(g);
    return bop.getBoundary();
}

std::unique_ptr<geom::Geometry>
BoundaryOp::getBoundary(const geom::Geometry& g, const algorithm::BoundaryNodeRule& bnRule)
{
    BoundaryOp bop(g, bnRule);
    return bop.getBoundary();
}

bool
BoundaryOp::hasBoundary(const geom::Geometry& geom, const algorithm::BoundaryNodeRule& boundaryNodeRule)
{
    // Note that this does not handle geometry collections with a non-empty linear element
    if (geom.isEmpty()) {
        return false;
    }

    switch (geom.getDimension()) {
        case Dimension::P: return false;
        /**
         * Linear geometries might have an empty boundary due to boundary node rule.
         */
        case Dimension::L:
            {

            auto boundary = getBoundary(geom, boundaryNodeRule);
            return !boundary->isEmpty();
            }
        default:
            return true;
        }
}

std::unique_ptr<Geometry>
BoundaryOp::boundaryCurve(const geom::Curve& line) const
{
    if (m_geom.isEmpty()) {
        return m_geomFact.createMultiPoint();
    }

    if (line.isClosed()) {
        // check whether endpoints of valence 2 are on the boundary or not
        const bool closedEndpointOnBoundary = m_bnRule.isInBoundary(2);
        if (closedEndpointOnBoundary) {
            return line.getStartPoint();
        }
        else {
            return m_geomFact.createMultiPoint();
        }
    }

    std::vector<std::unique_ptr<Point>> pts(2);
    pts[0] = line.getStartPoint();
    pts[1] = line.getEndPoint();

    return m_geomFact.createMultiPoint(std::move(pts));
}

std::unique_ptr<Geometry>
BoundaryOp::boundaryMultiCurve(const geom::GeometryCollection& mLine)
{
    if (m_geom.isEmpty()) {
        return m_geomFact.createMultiPoint();
    }

    auto bdyPts = computeBoundaryCoordinates(mLine);

    // return Point or MultiPoint
    if (bdyPts->size() == 1) {
        return bdyPts->applyAt(0, [this](const auto& c) {
            return m_geomFact.createPoint(c);
        });
    }

    // this handles 0 points case as well
    return std::unique_ptr<Geometry>(m_geomFact.createMultiPoint(*bdyPts));
}

static const CoordinateSequence*
getFirstSequence(const Curve& curve)
{
    if (curve.getGeometryTypeId() == geom::GEOS_COMPOUNDCURVE) {
        const auto& cc = static_cast<const geom::CompoundCurve&>(curve);
        for (std::size_t i = 0; i < cc.getNumCurves(); i++) {
            if (!cc.getCurveN(i)->isEmpty()) {
                return cc.getCurveN(i)->getCoordinatesRO();
            }
        }

        return nullptr;
    }

    return static_cast<const geom::SimpleCurve&>(curve).getCoordinatesRO();
}

static const CoordinateSequence*
getLastSequence(const Curve& curve)
{
    if (curve.getGeometryTypeId() == geom::GEOS_COMPOUNDCURVE) {
        const auto& cc = static_cast<const geom::CompoundCurve&>(curve);
        for (std::size_t i = cc.getNumCurves(); i != 0; i--) {
            if (!cc.getCurveN(i-1)->isEmpty()) {
                return cc.getCurveN(i-1)->getCoordinatesRO();
            }
        }

        return nullptr;
    }

    return static_cast<const geom::SimpleCurve&>(curve).getCoordinatesRO();
}

std::unique_ptr<CoordinateSequence>
BoundaryOp::computeBoundaryCoordinates(const geom::GeometryCollection& mLine) const
{
    assert(mLine.getGeometryTypeId() == geom::GEOS_MULTILINESTRING || mLine.getGeometryTypeId() == geom::GEOS_MULTICURVE);

    auto bdyPts = detail::make_unique<CoordinateSequence>(0, mLine.hasZ(), mLine.hasM());
    std::map<geom::CoordinateXYZM, int> endpointMap;

    for (std::size_t i = 0; i < mLine.getNumGeometries(); i++) {
      const Curve* line = detail::down_cast<const Curve*>(mLine.getGeometryN(i));

      if (line->getNumPoints() == 0) {
        continue;
      }

      geom::CoordinateXYZM start;
      geom::CoordinateXYZM end;

      const geom::CoordinateSequence* firstPts = getFirstSequence(*line);
      const geom::CoordinateSequence* lastPts = getLastSequence(*line);

      firstPts->getAt(0, start);
      lastPts->getAt(lastPts->size() - 1, end);

      endpointMap[start]++;
      endpointMap[end]++;
    }

    for (const auto& [coord, valence] : endpointMap) {
        if (m_bnRule.isInBoundary(valence)) {
            bdyPts->add(coord);
        }
    }

    return bdyPts;
}


}
}
