/**********************************************************************
 *
 * GEOS - Geometry Engine Open Source
 * http://geos.osgeo.org
 *
 * Copyright (C) 2006 Refractions Research Inc.
 *
 * This is free software; you can redistribute and/or modify it under
 * the terms of the GNU Lesser General Licence as published
 * by the Free Software Foundation.
 * See the COPYING file for more information.
 *
 **********************************************************************
 *
 * Last port: simplify/TopologyPreservingSimplifier.java r536 (JTS-1.12+)
 *
 **********************************************************************/

#include <geos/simplify/TopologyPreservingSimplifier.h>
#include <geos/simplify/TaggedLinesSimplifier.h>
#include <geos/simplify/LineSegmentIndex.h> // for unique_ptr dtor
#include <geos/simplify/TaggedLineString.h>
#include <geos/simplify/TaggedLineStringSimplifier.h> // for unique_ptr dtor
#include <geos/algorithm/LineIntersector.h> // for unique_ptr dtor
// for LineStringTransformer inheritance
#include <geos/geom/util/GeometryTransformer.h>
// for LineStringMapBuilderFilter inheritance
#include <geos/geom/GeometryComponentFilter.h>
#include <geos/geom/Geometry.h> // for unique_ptr dtor
#include <geos/geom/LineString.h>
#include <geos/geom/LinearRing.h>
#include <geos/util/IllegalArgumentException.h>

#include <memory> // for unique_ptr
#include <unordered_map>
#include <cassert>
#include <iostream>

using namespace geos::geom;

namespace geos {
namespace simplify { // geos::simplify

using LinesMap = std::unordered_map<const geom::Geometry*, TaggedLineString*>;


namespace { // module-statics

class LineStringTransformer: public geom::util::GeometryTransformer {

public:

    /**
     * User's constructor.
     * @param simp - reference to LinesMap instance.
     */
    LineStringTransformer(LinesMap& simp);

protected:

    CoordinateSequence::Ptr transformCoordinates(
        const CoordinateSequence* coords,
        const Geometry* parent) override;

private:

    LinesMap& linestringMap;

};

/*
 * helper class to transform a map iterator so to return value_type
 * on dereference.
 * TODO: generalize this to be a "ValueIterator" with specializations
 *       for std::map and std::vector
 */
class LinesMapValueIterator {

    LinesMap::iterator _iter;

public:

    LinesMapValueIterator(LinesMap::iterator iter)
        :
        _iter(iter)
    {
    }

    // copy ctor
    LinesMapValueIterator(const LinesMapValueIterator& o)
        :
        _iter(o._iter)
    {
    }

    // assignment
    LinesMapValueIterator&
    operator=(const LinesMapValueIterator& o)
    {
        _iter = o._iter;
        return *this;
    }

    // ++suffix
    LinesMapValueIterator&
    operator++()
    {
        ++_iter;
        return *this;
    }

    // inequality operator
    bool
    operator!=(const LinesMapValueIterator& other) const
    {
        return _iter != other._iter;
    }

    TaggedLineString*
    operator*()
    {
        return _iter->second;
    }
};


/*public*/
LineStringTransformer::LineStringTransformer(LinesMap& nMap)
    :
    linestringMap(nMap)
{
}

/*protected*/
CoordinateSequence::Ptr
LineStringTransformer::transformCoordinates(
    const CoordinateSequence* coords,
    const Geometry* parent)
{
#ifdef GEOS_DEBUG
    std::cerr << __FUNCTION__ << ": parent: " << parent
              << std::endl;
#endif
    if(dynamic_cast<const LineString*>(parent)) {
        LinesMap::iterator it = linestringMap.find(parent);
        assert(it != linestringMap.end());

        TaggedLineString* taggedLine = it->second;
#ifdef GEOS_DEBUG
        std::cerr << "LineStringTransformer[" << this << "] "
                  << " getting result Coordinates from "
                  << " TaggedLineString[" << taggedLine << "]"
                  << std::endl;
#endif

        assert(taggedLine);
        assert(taggedLine->getParent() == parent);

        return taggedLine->getResultCoordinates();
    }

    // for anything else (e.g. points) just copy the coordinates
    return GeometryTransformer::transformCoordinates(coords, parent);
}

//----------------------------------------------------------------------

/*
 * A filter to add linear geometries to the linestring map
 * with the appropriate minimum size constraint.
 * Closed {@link LineString}s (including {@link LinearRing}s
 * have a minimum output size constraint of 4,
 * to ensure the output is valid.
 * For all other linestrings, the minimum size is 2 points.
 *
 * This class populates the given LineString=>TaggedLineString map
 * with newly created TaggedLineString objects.
 * Users must take care of deleting the map's values (elem.second).
 * TODO: Consider container of unique_ptr
 *
 */
class LineStringMapBuilderFilter: public geom::GeometryComponentFilter {

public:

    // no more needed
    //friend class TopologyPreservingSimplifier;

    /**
     * Filters linear geometries.
     *
     * geom a geometry of any type
     */
    void filter_ro(const Geometry* geom) override;


    /**
     * User's constructor.
     * @param nMap - reference to LinesMap instance.
     */
    LineStringMapBuilderFilter(LinesMap& nMap);

private:

    LinesMap& linestringMap;

    // Declare type as noncopyable
    LineStringMapBuilderFilter(const LineStringMapBuilderFilter& other) = delete;
    LineStringMapBuilderFilter& operator=(const LineStringMapBuilderFilter& rhs) = delete;
};

/*public*/
LineStringMapBuilderFilter::LineStringMapBuilderFilter(LinesMap& nMap)
    :
    linestringMap(nMap)
{
}

/*public*/
void
LineStringMapBuilderFilter::filter_ro(const Geometry* geom)
{
    TaggedLineString* taggedLine;

    if(const LineString* ls =
                dynamic_cast<const LineString*>(geom)) {
        std::size_t minSize = ls->isClosed() ? 4 : 2;
        taggedLine = new TaggedLineString(ls, minSize);
    }
    else {
        return;
    }

    // Duplicated Geometry pointers shouldn't happen
    if(! linestringMap.insert(std::make_pair(geom, taggedLine)).second) {
        delete taggedLine;
        throw util::GEOSException("Duplicated Geometry components detected");
    }
}


} // end of module-statics

/*public static*/
std::unique_ptr<geom::Geometry>
TopologyPreservingSimplifier::simplify(
    const geom::Geometry* geom,
    double tolerance)
{
    TopologyPreservingSimplifier tss(geom);
    tss.setDistanceTolerance(tolerance);
    return tss.getResultGeometry();
}

/*public*/
TopologyPreservingSimplifier::TopologyPreservingSimplifier(const Geometry* geom)
    :
    inputGeom(geom),
    lineSimplifier(new TaggedLinesSimplifier())
{
}

/*public*/
void
TopologyPreservingSimplifier::setDistanceTolerance(double d)
{
    using geos::util::IllegalArgumentException;

    if(d < 0.0) {
        throw IllegalArgumentException("Tolerance must be non-negative");
    }

    lineSimplifier->setDistanceTolerance(d);
}


/*public*/
std::unique_ptr<geom::Geometry>
TopologyPreservingSimplifier::getResultGeometry()
{

    // empty input produces an empty result
    if(inputGeom->isEmpty()) {
        return std::unique_ptr<Geometry>(inputGeom->clone());
    }

    LinesMap linestringMap;

    std::unique_ptr<geom::Geometry> result;

    try {
        LineStringMapBuilderFilter lsmbf(linestringMap);
        inputGeom->apply_ro(&lsmbf);

#ifdef GEOS_DEBUG
        std::cerr << "LineStringMapBuilderFilter applied, "
                  << " lineStringMap contains "
                  << linestringMap.size() << " elements\n";
#endif

        LinesMapValueIterator begin(linestringMap.begin());
        LinesMapValueIterator end(linestringMap.end());
        lineSimplifier->simplify(begin, end);


#ifdef GEOS_DEBUG
        std::cerr << "all TaggedLineString simplified\n";
#endif

        LineStringTransformer trans(linestringMap);
        result = trans.transform(inputGeom);

#ifdef GEOS_DEBUG
        std::cerr << "inputGeom transformed\n";
#endif

    }
    catch(...) {
        for(LinesMap::iterator
                it = linestringMap.begin(),
                itEnd = linestringMap.end();
                it != itEnd;
                ++it) {
            delete it->second;
        }

        throw;
    }

    for(LinesMap::iterator
            it = linestringMap.begin(),
            itEnd = linestringMap.end();
            it != itEnd;
            ++it) {
        delete it->second;
    }

#ifdef GEOS_DEBUG
    std::cerr << "returning result\n";
#endif

    return result;
}

} // namespace geos::simplify
} // namespace geos

