//
// Test Suite for C-API GEOSUnaryUnion

#include <tut/tut.hpp>
// geos
#include <geos_c.h>
// std
#include <algorithm>
#include <cstring>

#include "capi_test_utils.h"

namespace tut {
//
// Test Group
//

// Common data used in test cases.
struct test_capiunaryunion_data : public capitest::utility {

    test_capiunaryunion_data() {
    }

};

typedef test_group<test_capiunaryunion_data> group;
typedef group::object object;

group test_capiunaryunion_group("capi::GEOSUnaryUnion");

//
// Test Cases
//


// Self-union an empty point
template<>
template<>
void object::test<1>
()
{
    geom1_ = GEOSGeomFromWKT("POINT EMPTY");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    ensure_equals(toWKT(geom2_), std::string("POINT EMPTY"));
}

// Self-union a 2d point
template<>
template<>
void object::test<2>
()
{
    geom1_ = GEOSGeomFromWKT("POINT (6 3)");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    ensure_equals(toWKT(geom2_), std::string("POINT (6 3)"));
}

// Self-union a 3d point
template<>
template<>
void object::test<3>
()
{
    geom1_ = GEOSGeomFromWKT("POINT (4 5 6)");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    ensure_equals(toWKT(geom2_), std::string("POINT Z (4 5 6)"));
}

// Self-union a multipoint with duplicated points
template<>
template<>
void object::test<4>
()
{
    geom1_ = GEOSGeomFromWKT("MULTIPOINT ((4 5), (6 7), (4 5), (6 5), (6 7))");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    ensure_equals(toWKT(geom2_), std::string("MULTIPOINT ((4 5), (6 5), (6 7))"));
}

// Self-union a collection of puntal and lineal geometries
template<>
template<>
void object::test<5>
()
{
    geom1_ = GEOSGeomFromWKT("GEOMETRYCOLLECTION (POINT(4 5), MULTIPOINT((6 7), (6 5), (6 7)), LINESTRING(0 5, 10 5), LINESTRING(4 -10, 4 10))");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    GEOSGeometry* expected = GEOSGeomFromWKT("GEOMETRYCOLLECTION (POINT (6 7), LINESTRING (4 -10, 4 5), LINESTRING (4 5, 4 10), LINESTRING (0 5, 4 5), LINESTRING (4 5, 10 5))");

    ensure(GEOSEquals(geom2_, expected) != 0);

    GEOSGeom_destroy(expected);
}

// Self-union a collection of puntal and polygonal geometries
template<>
template<>
void object::test<6>
()
{
    geom1_ = GEOSGeomFromWKT("GEOMETRYCOLLECTION (POINT(4 5), MULTIPOINT((6 7), (6 5), (6 7)), POLYGON((0 0, 10 0, 10 10, 0 10, 0 0),(5 6, 7 6, 7 8, 5 8, 5 6)))");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    expected_ = GEOSGeomFromWKT("GEOMETRYCOLLECTION (POINT (6 7), POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (5 6, 7 6, 7 8, 5 8, 5 6)))");
    ensure(nullptr != expected_);

    ensure(GEOSEquals(geom2_, expected_));
}

// Self-union a collection of lineal and polygonal geometries
template<>
template<>
void object::test<7>
()
{
    geom1_ = GEOSGeomFromWKT("GEOMETRYCOLLECTION (MULTILINESTRING((5 7, 12 7), (4 5, 6 5), (5.5 7.5, 6.5 7.5)), POLYGON((0 0, 10 0, 10 10, 0 10, 0 0),(5 6, 7 6, 7 8, 5 8, 5 6)))");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    GEOSGeometry* expected = GEOSGeomFromWKT("GEOMETRYCOLLECTION (LINESTRING (5 7, 7 7), LINESTRING (10 7, 12 7), LINESTRING (5.5 7.5, 6.5 7.5), POLYGON ((10 7, 10 0, 0 0, 0 10, 10 10, 10 7), (5 6, 7 6, 7 7, 7 8, 5 8, 5 7, 5 6)))");
    GEOSNormalize(geom2_);
    GEOSNormalize(expected);

    ensure(GEOSEquals(geom2_, expected) != 0);
    GEOSGeom_destroy(expected);
}

// Self-union a collection of puntal, lineal and polygonal geometries
template<>
template<>
void object::test<8>
()
{
    geom1_ = GEOSGeomFromWKT("GEOMETRYCOLLECTION (MULTILINESTRING((5 7, 12 7), (4 5, 6 5), (5.5 7.5, 6.5 7.5)), POLYGON((0 0, 10 0, 10 10, 0 10, 0 0),(5 6, 7 6, 7 8, 5 8, 5 6)), MULTIPOINT((6 6.5), (6 1), (12 2), (6 1)))");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    GEOSGeometry* expected = GEOSGeomFromWKT("GEOMETRYCOLLECTION (POINT (6 6.5), POINT (12 2), LINESTRING (5 7, 7 7), LINESTRING (10 7, 12 7), LINESTRING (5.5 7.5, 6.5 7.5), POLYGON ((10 7, 10 0, 0 0, 0 10, 10 10, 10 7), (5 6, 7 6, 7 7, 7 8, 5 8, 5 7, 5 6)))");
    GEOSNormalize(geom2_);
    GEOSNormalize(expected);

    ensure(GEOSEquals(geom2_, expected));
    GEOSGeom_destroy(expected);
}

// Self-union a geometry with NaN coordinates
// See http://trac.osgeo.org/geos/ticket/530
template<>
template<>
void object::test<9>
()
{
    const char* hexwkb = "010200000002000000000000000000F8FF000000000000F8FF0000000000000000000000000000F03F";
    geom1_ = GEOSGeomFromHEX_buf((const unsigned char*)hexwkb, std::strlen(hexwkb));
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr == geom2_);

}

// Self-union an empty linestring
template<>
template<>
void object::test<10>
()
{
    geom1_ = GEOSGeomFromWKT("LINESTRING EMPTY");
    ensure(nullptr != geom1_);

    geom2_ = GEOSUnaryUnion(geom1_);
    ensure(nullptr != geom2_);

    ensure_equals(toWKT(geom2_), std::string("LINESTRING EMPTY"));
}

// https://github.com/libgeos/geos/issues/830
// Self-union on collection with an empty point
template<>
template<>
void object::test<11>
()
{
    input_ = GEOSGeomFromWKT("GEOMETRYCOLLECTION (POINT EMPTY, LINESTRING (0 0, 1 1))");

    result_ = GEOSUnaryUnion(input_);
    expected_ = GEOSGeomFromWKT("LINESTRING (0 0, 1 1)");

    ensure_geometry_equals(result_, expected_);
}

template<>
template<>
void object::test<12>()
{
    set_test_name("curved inputs");
    useContext();

    input_ = fromWKT("MULTICURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (-5 3, 5 3))");
    ensure(input_);
    GEOSSetSRID_r(ctxt_, input_, 4326);
    double input_length = -1;
    ensure(GEOSLength_r(ctxt_, input_, &input_length));

    result_ = GEOSUnaryUnion_r(ctxt_, input_);
    ensure(result_ == nullptr);

    GEOSCurveToLineParams_setMaxStepDegrees_r(ctxt_, curveToLineParams_, 1);
    GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);

    // Input converted to line, output not converted to curve
    result_ = GEOSUnaryUnion_r(ctxt_, input_);
    ensure(result_);
    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTILINESTRING);
    ensure_equals(GEOSGetSRID_r(ctxt_, result_), 4326);
    GEOSGeom_destroy_r(ctxt_, result_);

    // Input converted to line, output converted to curve
    GEOSContext_setLineToCurveParams_r(ctxt_, lineToCurveParams_);
    result_ = GEOSUnaryUnion_r(ctxt_, input_);
    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTICURVE);
    double result_length = -1;
    ensure(GEOSLength_r(ctxt_, result_, &result_length));

    ensure_equals(GEOSGetSRID_r(ctxt_, result_), 4326);
    ensure_equals("length does not match", result_length, input_length, 1e-5);
}

template<>
template<>
void object::test<13>()
{
    set_test_name("progress callback invoked (cascaded union)");
    useContext();

    std::vector<GEOSGeometry*> geoms;
    for (int i = 0; i < 10; i++) {
        GEOSGeometry* pt = GEOSGeom_createPointFromXY_r(ctxt_, static_cast<double>(i), static_cast<double>(i));
        GEOSGeometry* circle = GEOSBuffer_r(ctxt_, pt, 3, 8);
        geoms.push_back(circle);
        GEOSGeom_destroy_r(ctxt_, pt);
    }
    input_ = GEOSGeom_createCollection_r(ctxt_, GEOS_GEOMETRYCOLLECTION, geoms.data(), static_cast<unsigned>(geoms.size()));

    std::vector<double> fracs;

    auto progress = [](double f, const char*, void* userData) {
        static_cast<std::vector<double>*>(userData)->push_back(f);
    };

    GEOSContext_setProgressCallback_r(ctxt_, progress, &fracs);
    result_ = GEOSUnaryUnion_r(ctxt_, input_);

    ensure(fracs.size() > 3); // arbitrary

    // progress reported in-order
    ensure(std::is_sorted(fracs.begin(), fracs.end()));

    // no duplicate progress values
    ensure(std::adjacent_find(fracs.begin(), fracs.end()) == fracs.end());
}

template<>
template<>
void object::test<14>() {
    set_test_name("progress callback invoked (input already unioned)");
    useContext();

    input_ = fromWKT("POINT (1 1)");

    std::vector<double> fracs;

    auto progress = [](double f, const char*, void* userData) {
        static_cast<std::vector<double>*>(userData)->push_back(f);
    };

    GEOSContext_setProgressCallback_r(ctxt_, progress, &fracs);
    result_ = GEOSUnaryUnion_r(ctxt_, input_);

    ensure_equals(fracs.size(), 1u);
    ensure_equals(fracs[0], 1.0);
}

} // namespace tut


