#include <tut/tut.hpp>

#include <geos/algorithm/CurveToLineParams.h>
#include <geos/algorithm/LineToCurveParams.h>
#include <geos/io/WKTReader.h>

#include "utility.h"

using geos::algorithm::CurveToLineParams;
using geos::algorithm::LineToCurveParams;

namespace tut {

struct test_algorithm_linetocurveconverter_data {
    geos::io::WKTReader reader_;

    void checkRoundTrip(const std::string& wkt_in, const std::string& wkt_expected, double stepSizeDegrees) const {
        auto inCurve = reader_.read(wkt_in);

        auto ctlParams = CurveToLineParams::stepSizeDegrees(stepSizeDegrees);
        auto linearized = inCurve->getLinearized(ctlParams);

        auto ltcParams = geos::algorithm::LineToCurveParams();
        ltcParams.setRadiusTolerance(1e-6);
        ltcParams.setMaxStepDegrees(stepSizeDegrees*1.001);

        auto outCurve = linearized->getCurved(ltcParams);
        auto expected = reader_.read(wkt_expected);

        double distanceTolerance = 1e-4;

        ensure_equals_exact_geometry_xyzm(outCurve.get(), expected.get(), distanceTolerance);
    }

    void checkRoundTripUnchanged(const std::string& wkt_in, double stepSizeDegrees) const {
        checkRoundTrip(wkt_in, wkt_in, stepSizeDegrees);
    }

    void checkLineToCurve(const std::string& wkt_in, const std::string& wkt_expected, double distanceTolerance, double stepSizeDegrees) const {
        auto ls = reader_.read<LineString>(wkt_in);

        auto ltcParams = geos::algorithm::LineToCurveParams();
        ltcParams.setRadiusTolerance(distanceTolerance);
        ltcParams.setMaxStepDegrees(stepSizeDegrees);

        auto curve = ls->getCurved(ltcParams);
        auto expected = reader_.read(wkt_expected);

        ensure_equals_exact_geometry_xyzm(curve.get(), expected.get(), distanceTolerance);
    }

    void checkLineToCurveUnchanged(const std::string& wkt_in, double distanceTolerance) const {
        auto ltcParams = geos::algorithm::LineToCurveParams();
        ltcParams.setRadiusTolerance(distanceTolerance);

        auto ls = reader_.read<LineString>(wkt_in);
        auto expected = ls->clone();
        auto curve = ls->getCurved(ltcParams);

        ensure_equals_exact_geometry_xyzm(curve.get(), expected.get(), 0.0);
    }
};

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

group test_algorithm_linetocurveconverter_group("geos::algorithm::LineToCurveConverter");

template<>
template<>
void object::test<1>()
{
    set_test_name("two-point LineString");

    checkLineToCurveUnchanged("LINESTRING (3 4, 2 7)", 1);
}

template<>
template<>
void object::test<2>()
{
    set_test_name("LineString of three collinear points");

    checkLineToCurveUnchanged("LINESTRING (0 0, 1 2, 2 4)", 1);
}

template<>
template<>
void object::test<3>()
{
    set_test_name("coarsely linearized semicircle");

    checkLineToCurve("LINESTRING(0 0,29.2893 70.7107,100 100,170.7107 70.7107,200 0)",
                     "CIRCULARSTRING (0 0, 100 100, 200 0)",
                     2e-3, 45.1);
}

template<>
template<>
void object::test<4>()
{
    set_test_name("coarsely linearized closed semicircle");

    checkLineToCurve("LINESTRING(0 0,29.2893 70.7107,100 100,170.7107 70.7107,200 0, 0 0)",
                     "COMPOUNDCURVE (CIRCULARSTRING (0 0, 100 100, 200 0), LINESTRING (200 0, 0 0))",
                     2.5e-3, 45.1);
}

template<>
template<>
void object::test<5>()
{
    set_test_name("round-trip with 3-quadrant CircularString");

    // cu_lwstroke.c: 392
    checkRoundTrip("CIRCULARSTRING (-1 0, 0 1, 0 -1)",
                   "CIRCULARSTRING (-1 0, 0.70710678 0.707010678, 0 -1)",
                   90.0 / 8);
}

template<>
template<>
void object::test<6>()
{
    set_test_name("Two-part CompoundCurve round-trip");

    // cu_lwstroke.c: 404
    checkRoundTrip("COMPOUNDCURVE (CIRCULARSTRING (-1 0, 0 1, 0 -1), (0 -1, -1 -1))",
                   "COMPOUNDCURVE( CIRCULARSTRING (-1 0, 0.70710678 0.707010678, 0 -1), (0 -1, -1 -1))",
                   90.0 / 8);
}

template<>
template<>
void object::test<7>()
{
    set_test_name("Three-part CompoundCurve round-trip");

    // cu_lwstroke.c: 416
    checkRoundTrip("COMPOUNDCURVE((-3 -3,-1 0),CIRCULARSTRING(-1 0,0 1,0 -1),(0 -1,0 -1.5,0 -2),CIRCULARSTRING(0 -2,-1 -3,1 -3),(1 -3,5 5))",
                   "COMPOUNDCURVE((-3 -3,-1 0),CIRCULARSTRING(-1 0,0.70710678 0.70710678,0 -1),(0 -1,0 -1.5,0 -2),CIRCULARSTRING(0 -2,-0.70710678 -3.70710678,1 -3),(1 -3,5 5))",
                   90.0 / 8);
}

template<>
template<>
void object::test<8>()
{
    set_test_name("CompoundCurve with two CircularStrings, round-trip");

    // cu_lwstroke.c: 432
    // NOTE: The expected result here is modified from the original. It is not clear why liblwgeom returns a
    // CompoundCurve with two CircularStrings instead of a single CircularString (like GDAL). The expected result
    // used here is from GDAL.
    checkRoundTrip("COMPOUNDCURVE(CIRCULARSTRING(-1 0,0 1,0 -1),CIRCULARSTRING(0 -1,-1 -2,1 -2))",
                   "CIRCULARSTRING(-1 0,0.70710678 0.70710678,0 -1,-0.70710678 -2.70710678,1 -2)",
                   90.0 / 8);
}

template<>
template<>
void object::test<9>()
{
    set_test_name("CompoundCurve with a CircularString between two LineStrings, round-trip");

    // cu_lwstroke.c: 447
    checkRoundTripUnchanged("COMPOUNDCURVE((0 0, 1 1), CIRCULARSTRING(1 1, 2 2, 3 1), (3 1, 4 4))",
                       90.0 / 8);
}

template<>
template<>
void object::test<10>()
{
    set_test_name("LineString forming a square, round-trip");

    // cu_lwstroke.c: 461
    checkRoundTripUnchanged("LINESTRING(0 0,10 0,10 10,0 10,0 0)",
                       90.0 / 8);

    // cu_lwstroke.c: 469
    checkRoundTripUnchanged("LINESTRING(10 10,0 10,0 0,10 0)",
                            90.0 / 8);

    // cu_lwstroke.c: 478
    checkRoundTripUnchanged("LINESTRING(0 0,10 0,10 10,0 10)",
                       90.0 / 8);
}

template<>
template<>
void object::test<11>()
{
    set_test_name("collection of two-point LineStrings, round-trip");

    // cu_lwstroke.c: 497
    checkRoundTripUnchanged("GEOMETRYCOLLECTION(LINESTRING(10 10,10 11),LINESTRING(10 11,11 11),LINESTRING(11 11,10 10))",
                       90.0 / 8);
}

template<>
template<>
void object::test<13>()
{
    set_test_name("collection of two-point LineStrings and a CircularString, round-trip");

    // cu_lwstroke.c: 508
    checkRoundTripUnchanged("GEOMETRYCOLLECTION(LINESTRING(4 4,4 8),CIRCULARSTRING(4 8,6 10,8 8),LINESTRING(8 8,8 4))",
                       90.0 / 8);
}

template<>
template<>
void object::test<14>()
{
    set_test_name("round-trip with 5-point CircularString");

    checkRoundTripUnchanged("CIRCULARSTRING (-5 0, 0 5, 5 0, 4 1, 3 0)",
                   90.0 / 4);
}

template<>
template<>
void object::test<15>()
{
    set_test_name("constructed curve does not depend on direction of input");

    auto cs = reader_.read("CIRCULARSTRING (-5 0, 0 5, 5 0)");

    auto lin = cs->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4));
    auto linRev = lin->reverse();

    geos::algorithm::LineToCurveParams ltc;
    ltc.setRadiusTolerance(1e-4);

    auto curveRev = linRev->getCurved(ltc);
    auto curve1 = curveRev->reverse();

    auto curve2 = lin->getCurved(ltc);

    ensure_equals_exact_geometry_xyzm(curve1.get(), curve2.get(), 0);
}

template<>
template<>
void object::test<16>()
{
    set_test_name("Z/M preserved in round-trip");

    // Midpoint Z/M halfway between start/end
    // Linearized arc has an odd number of vertices
    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 3 11)", 22.5);

    // Midpoint Z/M not halfway between start/end
    // Linearized arc has an odd number of vertices
    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 4 13)", 22.5);

    // Midpoint Z/M halfway between start/end
    // Linearized arc has an even number of vertices
    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 3 11)", 20);

    // Midpoint Z/M not halfway between start/end
    // Linearized arc has an odd number of vertices
    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 4 13)", 20);
}

template<>
template<>
void object::test<17>() {
    set_test_name("big coordinates");

    checkRoundTripUnchanged("CIRCULARSTRING (426857.987717275 5427937.52346616,500000.000000001 5538630.70286887,573142.012282726 5427937.52346616)", 4);

    checkRoundTripUnchanged("CIRCULARSTRING (426858 5427938,500000 5538632,573142 5427938)", 4);
}

template<>
template<>
void object::test<18>()
{
    set_test_name("full circle, 2D");

    auto geom = reader_.read("POINT (0 0)")->buffer(5);
    LineToCurveParams params;
    auto curved = geom->getCurved(params);

    auto expected = reader_.read("CURVEPOLYGON (CIRCULARSTRING (5 0, 0 -5, -5 0, 0 5, 5 0))");

    ensure_equals_exact_geometry_xyzm(curved.get(), expected.get(), 1e-8);
}

template<>
template<>
void object::test<19>()
{
    set_test_name("full circle, XYZM");

    // with step size of 11.25 degrees, control points of the arc will appear in linearized
    // arc, so their Z/M values can be recovered for the result arcs
    checkRoundTripUnchanged("CIRCULARSTRING ZM (-5 0 4 3, 0 5 8 6, 5 0 12 9, 0 -5 15 11, -5 0 18 13)", 11.25);

    // with step size of 4 degrees, Z/M values for the result arc control points must be
    // interpolated from points in the linearized arc.
    checkRoundTripUnchanged("CIRCULARSTRING ZM (-5 0 4 3, 0 5 8 6, 5 0 12 9, 0 -5 15 11, -5 0 18 13)", 4);
}


}
