/**********************************************************************
 *
 * GEOS - Geometry Engine Open Source
 * http://trac.osgeo.org/geos
 *
 * Copyright (C) 2011 Sandro Santilli <strk@kbt.io>
 *
 * 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: ORIGINAL WORK, generalization of CascadedPolygonUnion
 *
 **********************************************************************/

#include <geos/operation/union/CascadedUnion.h>
#include <geos/geom/Geometry.h>
#include <geos/geom/GeometryFactory.h>
#include <geos/geom/Geometry.h>
#include <geos/geom/util/GeometryCombiner.h>
#include <geos/index/strtree/STRtree.h>
// std
#include <cassert>
#include <cstddef>
#include <memory>
#include <vector>

namespace geos {
namespace operation { // geos.operation
namespace geounion {  // geos.operation.geounion

geom::Geometry* CascadedUnion::Union(std::vector<geom::Geometry*>* polys)
{
    CascadedUnion op (polys);
    return op.Union();
}

geom::Geometry* CascadedUnion::Union()
{
    if (inputGeoms->empty())
        return nullptr;

    geomFactory = inputGeoms->front()->getFactory();

    /**
     * A spatial index to organize the collection
     * into groups of close geometries.
     * This makes unioning more efficient, since vertices are more likely
     * to be eliminated on each round.
     */
    index::strtree::STRtree index(STRTREE_NODE_CAPACITY);

    typedef std::vector<geom::Geometry*>::const_iterator iterator_type;
    iterator_type end = inputGeoms->end();
    for (iterator_type i = inputGeoms->begin(); i != end; ++i) {
        geom::Geometry* g = *i;
        index.insert(g->getEnvelopeInternal(), g);
    }

    std::unique_ptr<index::strtree::ItemsList> itemTree (index.itemsTree());

    return unionTree(itemTree.get());
}

geom::Geometry* CascadedUnion::unionTree(
    index::strtree::ItemsList* geomTree)
{
    /**
     * Recursively unions all subtrees in the list into single geometries.
     * The result is a list of Geometry's only
     */
    std::unique_ptr<GeometryListHolder> geoms(reduceToGeometries(geomTree));
    return binaryUnion(geoms.get());
}

geom::Geometry* CascadedUnion::binaryUnion(GeometryListHolder* geoms)
{
    return binaryUnion(geoms, 0, geoms->size());
}

geom::Geometry* CascadedUnion::binaryUnion(GeometryListHolder* geoms,
    std::size_t start, std::size_t end)
{
    if (end - start <= 1) {
        return unionSafe(geoms->getGeometry(start), nullptr);
    }
    else if (end - start == 2) {
        return unionSafe(geoms->getGeometry(start), geoms->getGeometry(start + 1));
    }
    else {
        // recurse on both halves of the list
        std::size_t mid = (end + start) / 2;
        std::unique_ptr<geom::Geometry> g0 (binaryUnion(geoms, start, mid));
        std::unique_ptr<geom::Geometry> g1 (binaryUnion(geoms, mid, end));
        return unionSafe(g0.get(), g1.get());
    }
}

GeometryListHolder*
CascadedUnion::reduceToGeometries(index::strtree::ItemsList* geomTree)
{
    std::unique_ptr<GeometryListHolder> geoms (new GeometryListHolder());

    typedef index::strtree::ItemsList::iterator iterator_type;
    iterator_type end = geomTree->end();
    for (iterator_type i = geomTree->begin(); i != end; ++i) {
        if ((*i).get_type() == index::strtree::ItemsListItem::item_is_list) {
            std::unique_ptr<geom::Geometry> geom (unionTree((*i).get_itemslist()));
            geoms->push_back_owned(geom.get());
            geom.release();
        }
        else if ((*i).get_type() == index::strtree::ItemsListItem::item_is_geometry) {
            geoms->push_back(reinterpret_cast<geom::Geometry*>((*i).get_geometry()));
        }
        else {
            assert(!static_cast<bool>("should never be reached"));
        }
    }

    return geoms.release();
}

geom::Geometry*
CascadedUnion::unionSafe(geom::Geometry* g0, geom::Geometry* g1)
{
    if (g0 == nullptr && g1 == nullptr)
        return nullptr;

    if (g0 == nullptr)
        return g1->clone();
    if (g1 == nullptr)
        return g0->clone();

    return unionOptimized(g0, g1);
}

geom::Geometry*
CascadedUnion::unionOptimized(geom::Geometry* g0, geom::Geometry* g1)
{
    geom::Envelope const* g0Env = g0->getEnvelopeInternal();
    geom::Envelope const* g1Env = g1->getEnvelopeInternal();

    if (!g0Env->intersects(g1Env))
        return geom::util::GeometryCombiner::combine(g0, g1);

    if (g0->getNumGeometries() <= 1 && g1->getNumGeometries() <= 1)
        return unionActual(g0, g1);

    geom::Envelope commonEnv;
    g0Env->intersection(*g1Env, commonEnv);
    return unionUsingEnvelopeIntersection(g0, g1, commonEnv);
}

geom::Geometry*
CascadedUnion::unionUsingEnvelopeIntersection(geom::Geometry* g0,
    geom::Geometry* g1, geom::Envelope const& common)
{
    std::vector<geom::Geometry*> disjointPolys;

    std::unique_ptr<geom::Geometry> g0Int(extractByEnvelope(common, g0, disjointPolys));
    std::unique_ptr<geom::Geometry> g1Int(extractByEnvelope(common, g1, disjointPolys));

    std::unique_ptr<geom::Geometry> u(unionActual(g0Int.get(), g1Int.get()));
    disjointPolys.push_back(u.get());

    return geom::util::GeometryCombiner::combine(disjointPolys);
}

geom::Geometry*
CascadedUnion::extractByEnvelope(geom::Envelope const& env,
    geom::Geometry* geom, std::vector<geom::Geometry*>& disjointGeoms)
{
    std::vector<geom::Geometry*> intersectingGeoms;

    for (std::size_t i = 0; i < geom->getNumGeometries(); i++) {
        geom::Geometry* elem = const_cast<geom::Geometry*>(geom->getGeometryN(i));
        if (elem->getEnvelopeInternal()->intersects(env))
            intersectingGeoms.push_back(elem);
        else
            disjointGeoms.push_back(elem);
    }

    return geomFactory->buildGeometry(intersectingGeoms);
}

geom::Geometry*
CascadedUnion::unionActual(geom::Geometry* g0, geom::Geometry* g1)
{
    return g0->Union(g1);
}

} // namespace geos.operation.union
} // namespace geos.operation
} // namespace geos
