/***************************************************************************

  c_clipper.cpp

  (c) 2000-2017 Benoît Minisini <benoit.minisini@gambas-basic.org>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2, or (at your option)
  any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA.

***************************************************************************/

#define __C_CLIPPER_CPP

#include "main.h"
#include "gb.geom.h"
#include "c_clipper.h"

#define SCALE 1048576.0
#define DEFAULT_CLEAN_DIST 1.414213562373095

static IntPoint to_point_xy(double x, double y)
{
	return IntPoint(x * SCALE + 0.5, y * SCALE + 0.5);
}

static IntPoint to_point(GEOM_POINTF *point)
{
	return to_point_xy(point->x, point->y);
}

static GEOM_POINTF *from_point(IntPoint p)
{
	return GEOM.CreatePointF((double)p.X / SCALE, (double)p.Y / SCALE);
}

static inline int64_t int64_abs(int64_t v)
{
	return v >= 0 ? v : -v;
}

static bool is_polygon_closed(Path &p)
{
	int n = p.size() - 1;

	if (n <= 1)
		return false;

	return p[0].X == p[n].X && p[0].Y == p[n].Y;
}

static bool is_polygon_almost_closed(Path &p)
{
	int n = p.size() - 1;

	if (n <= 1)
		return false;

	return int64_abs(p[0].X - p[n].X) <= 1 && int64_abs(p[0].Y - p[n].Y) <= 1;
}

static void set_polygon_closed(Path &p, bool closed)
{
	if (is_polygon_closed(p) == closed)
		return;

	if (closed)
		p.push_back(p[0]);
	else
		p.erase(p.begin() + p.size() - 1);
}

static bool to_polygons(Paths &polygons, GB_ARRAY array)
{
	int count;
	CPOLYGON *p;
	int i;

	if (GB.CheckObject(array))
		return true;

	count = GB.Array.Count(array);
	if (count == 0)
		return false;

	polygons.clear();

	for(i = 0; i < count; i++)
	{
		p = *(CPOLYGON **)GB.Array.Get(array, i);
		if (!p)
			continue;

		polygons.push_back(*(p->poly));
	}

	return false;
}

static GB_ARRAY from_polygons(Paths &polygons, bool closed)
{
	GB_ARRAY a;
	CPOLYGON *p;
	uint i;

	GB.Array.New(&a, GB.FindClass("Polygon"), polygons.size());

	for (i = 0; i < polygons.size(); i++)
	{
		if (polygons[i].size() == 0)
			continue;
		
		set_polygon_closed(polygons[i], closed);

		p = (CPOLYGON *)GB.New(GB.FindClass("Polygon"), NULL, NULL);
		*(p->poly) = polygons[i];

		*(GB_ARRAY *)GB.Array.Get(a, i) = p;
		GB.Ref(p);
	}

	return a;
}

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

#define THIS ((CPOLYGON *)_object)
#define POLY THIS->poly

static bool _convert_polygon(CPOLYGON *_object, GB_TYPE type, GB_VALUE *conv)
{
	if (type != GB.FindClass("PointF[]"))
		return true;

	if (THIS)
	{
		// Polygon --> PointF[]
		GB_ARRAY a;
		int i;
		GEOM_POINTF **data;

		GB.Array.New(&a, GB.FindClass("PointF"), POLY->size()); // + THIS->closed);
		data = (GEOM_POINTF **)GB.Array.Get(a, 0);
		for(i = 0; i < (int)POLY->size(); i++)
		{
			data[i] = from_point((*POLY)[i]);
			GB.Ref(data[i]);
		}

		/*if (closed)
		{
			data[i] = from_point((*POLY)[0]);
			GB.Ref(data[i]);
		}*/

		conv->_object.value = a;
		return false;
	}
	else
	{
		// PointF[] --> Polygon
		CPOLYGON *p;
		GB_ARRAY a = (GB_ARRAY)conv->_object.value;
		int size = GB.Array.Count(a);
		int i;
		GEOM_POINTF **points;

		p = (CPOLYGON *)GB.New(GB.FindClass("Polygon"), NULL, NULL);

		points = (GEOM_POINTF **)GB.Array.Get(a, 0);
		for (i = 0; i < size; i++)
		{
			if (!points[i])
				continue;

			p->poly->push_back(to_point(points[i]));
		}

		conv->_object.value = p;
		return false;
	}
}

BEGIN_METHOD(Polygon_new, GB_INTEGER size)

	POLY = new Path;

	if (!MISSING(size))
		POLY->resize(VARG(size));

END_METHOD

BEGIN_METHOD_VOID(Polygon_free)

	delete POLY;

END_METHOD

BEGIN_METHOD(Polygon_get, GB_INTEGER index)

	int index = VARG(index);

	if (index < 0 || index >= (int)POLY->size())
	{
		GB.Error(GB_ERR_BOUND);
		return;
	}

	GB.ReturnObject(from_point((*POLY)[index]));

END_METHOD

BEGIN_METHOD(Polygon_put, GB_OBJECT point; GB_INTEGER index)

	int index = VARG(index);
	GEOM_POINTF *point = (GEOM_POINTF *)VARG(point);

	if (GB.CheckObject(point))
		return;

	if (index < 0 || index >= (int)POLY->size())
	{
		GB.Error(GB_ERR_BOUND);
		return;
	}

	(*POLY)[index] = to_point(point);

END_METHOD

BEGIN_PROPERTY(Polygon_Count)

	GB.ReturnInteger(POLY->size());

END_PROPERTY

BEGIN_PROPERTY(Polygon_Max)

	GB.ReturnInteger(POLY->size() - 1);

END_PROPERTY

BEGIN_PROPERTY(Polygon_Area)

	GB.ReturnFloat(Area(*POLY) / SCALE / SCALE);

END_PROPERTY

BEGIN_METHOD_VOID(Polygon_Reverse)

	ReversePath(*POLY);

END_METHOD

BEGIN_METHOD(Polygon_Simplify, GB_INTEGER fill)

	Paths result;

	SimplifyPolygon(*POLY, result, (PolyFillType)VARGOPT(fill, pftNonZero));

	GB.ReturnObject(from_polygons(result, is_polygon_closed(*POLY)));

END_METHOD

BEGIN_METHOD(Polygon_Clean, GB_FLOAT distance)

	bool closed;
	CPOLYGON *result = (CPOLYGON *)GB.New(GB.FindClass("Polygon"), NULL, 0);

	result->poly->resize(POLY->size());

	closed = is_polygon_closed(*POLY);

	CleanPolygon(*POLY, *(result->poly), VARGOPT(distance, DEFAULT_CLEAN_DIST));

	set_polygon_closed(*(result->poly), closed);

	GB.ReturnObject(result);

END_METHOD

BEGIN_METHOD_VOID(Polygon_Trim)

	bool closed;
	CPOLYGON *result = (CPOLYGON *)GB.New(GB.FindClass("Polygon"), NULL, 0);

	result->poly->resize(POLY->size());

	closed = is_polygon_closed(*POLY);

	CleanPolygon(*POLY, *(result->poly), DEFAULT_CLEAN_DIST);

	set_polygon_closed(*(result->poly), closed);

	GB.ReturnObject(result);

END_METHOD

BEGIN_METHOD(Polygon_Add, GB_FLOAT x; GB_FLOAT y)

	POLY->push_back(to_point_xy(VARG(x), VARG(y)));

END_METHOD

BEGIN_METHOD(Polygon_AddPoint, GB_OBJECT point)

	GEOM_POINTF *point = (GEOM_POINTF *)VARG(point);

	if (GB.CheckObject(point))
		return;

	POLY->push_back(to_point(point));

END_METHOD

BEGIN_METHOD(Polygon_Remove, GB_INTEGER index; GB_INTEGER count)

	int index = VARG(index);
	int count = VARGOPT(count, 1);
	int index2;

	if (index < 0 || index >= (int)POLY->size())
	{
		GB.Error(GB_ERR_BOUND);
		return;
	}

	if (count < 0)
		count = (int)POLY->size() - index;

	index2 = index + count;

	if (index2 > (int)POLY->size())
		index2 = POLY->size();

	if (count == 1)
		POLY->erase(POLY->begin() + index);
	else
		POLY->erase(POLY->begin() + index, POLY->begin() + index2);

END_METHOD

BEGIN_PROPERTY(Polygon_Orientation)

	GB.ReturnBoolean(Orientation(*POLY));

END_PROPERTY

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

BEGIN_METHOD(Clipper_Offset, GB_OBJECT polygons; GB_FLOAT delta; GB_INTEGER join; GB_FLOAT limit; GB_BOOLEAN do_not_fix)

	Paths polygons;
	Paths result;
	ClipperOffset co;

	if (to_polygons(polygons, VARG(polygons)))
		return;

	SimplifyPolygons(polygons, result, pftNonZero);
	polygons = result;

	co.AddPaths(polygons, (JoinType)VARGOPT(join, jtSquare), etClosedPolygon);
	co.MiterLimit = VARGOPT(limit, 0.0);
	co.Execute(result, VARG(delta) * SCALE);

	//OffsetPaths(polygons, result, VARG(delta) * SCALE, (JoinType)VARGOPT(join, jtSquare), VARGOPT(limit, 0.0), !VARGOPT(do_not_fix, false));

	GB.ReturnObject(from_polygons(result, true));

END_METHOD


BEGIN_METHOD(Clipper_Inflate, GB_OBJECT polygons; GB_FLOAT delta; GB_INTEGER join; GB_INTEGER cap; GB_FLOAT limit; GB_BOOLEAN outline)

	Paths polygons;
	Paths result;
	Paths polygons_opened;
	Paths polygons_closed;
	uint i;
	JoinType join;
	EndType cap;
	double delta;
	double limit;

	if (to_polygons(polygons, VARG(polygons)))
		return;

	/*SimplifyPolygons(polygons, result, pftNonZero);
	polygons = result;
	result.clear();*/

	join = (JoinType)VARGOPT(join, jtSquare);
	cap = (EndType)VARGOPT(cap, etOpenSquare);
	delta = VARG(delta) * SCALE;
	limit = VARGOPT(limit, 0.0);

	for (i = 0; i < polygons.size(); i++)
	{
		if (is_polygon_almost_closed(polygons[i]))
			polygons_closed.push_back(polygons[i]);
		else
			polygons_opened.push_back(polygons[i]);
	}

	if (polygons_closed.size())
	{
		ClipperOffset co;
		Paths inflated;

		co.AddPaths(polygons_closed, join, VARGOPT(outline, TRUE) ? cap : etClosedPolygon);
		co.MiterLimit = limit;
		co.Execute(inflated, delta);

		result.insert(result.end(), inflated.begin(), inflated.end());
	}

	if (polygons_opened.size())
	{
		ClipperOffset co;
		Paths inflated;

		co.AddPaths(polygons_opened, join, cap);
		co.MiterLimit = limit;
		co.Execute(inflated, delta);

		result.insert(result.end(), inflated.begin(), inflated.end());
	}

	GB.ReturnObject(from_polygons(result, true));

END_METHOD


BEGIN_METHOD(Clipper_Simplify, GB_OBJECT polygons; GB_INTEGER fill)

	Paths polygons;
	Paths result;

	if (to_polygons(polygons, VARG(polygons)))
		return;

	SimplifyPolygons(polygons, result, (PolyFillType)VARGOPT(fill, pftNonZero));

	GB.ReturnObject(from_polygons(result, true));

END_METHOD

BEGIN_METHOD(Clipper_Clean, GB_OBJECT polygons; GB_FLOAT distance)

	Paths polygons;
	Paths result;

	if (to_polygons(polygons, VARG(polygons)))
		return;

	result.resize(polygons.size());

	CleanPolygons(polygons, result, VARGOPT(distance, DEFAULT_CLEAN_DIST));

	GB.ReturnObject(from_polygons(result, true));

END_METHOD

BEGIN_METHOD(Clipper_Trim, GB_OBJECT polygons)

	Paths polygons;
	Paths result;

	if (to_polygons(polygons, VARG(polygons)))
		return;

	result.resize(polygons.size());

	CleanPolygons(polygons, result, DEFAULT_CLEAN_DIST);

	GB.ReturnObject(from_polygons(result, true));

END_METHOD

static void execute(ClipType action, PolyFillType fill, void *subject, void *clip)
{
	Clipper c;
	Paths psubject, pclip, result;
	PolyTree tree;

	if (to_polygons(psubject, subject))
		return;

	if (clip && to_polygons(pclip, clip))
		return;

	c.AddPaths(psubject, ptSubject, true);
	if (clip)
		c.AddPaths(pclip, ptClip, true);

	c.StrictlySimple(true);
	c.Execute(action, tree, fill, fill);
	ClosedPathsFromPolyTree(tree, result);

	GB.ReturnObject(from_polygons(result, true));
}

BEGIN_METHOD(Clipper_Union, GB_OBJECT subject; GB_OBJECT clip; GB_INTEGER fill)

	execute(ctUnion, (PolyFillType)VARGOPT(fill, pftNonZero), VARG(subject), VARGOPT(clip, NULL));

END_METHOD

BEGIN_METHOD(Clipper_Intersection, GB_OBJECT subject; GB_OBJECT clip; GB_INTEGER fill)

	execute(ctIntersection, (PolyFillType)VARGOPT(fill, pftNonZero), VARG(subject), VARGOPT(clip, NULL));

END_METHOD

BEGIN_METHOD(Clipper_Difference, GB_OBJECT subject; GB_OBJECT clip; GB_INTEGER fill)

	execute(ctDifference, (PolyFillType)VARGOPT(fill, pftNonZero), VARG(subject), VARGOPT(clip, NULL));

END_METHOD

BEGIN_METHOD(Clipper_ExclusiveOr, GB_OBJECT subject; GB_OBJECT clip; GB_INTEGER fill)

	execute(ctXor, (PolyFillType)VARGOPT(fill, pftNonZero), VARG(subject), VARGOPT(clip, NULL));

END_METHOD

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

GB_DESC PolygonDesc[] =
{
	GB_DECLARE("Polygon", sizeof(CPOLYGON)),

	GB_METHOD("_new", NULL, Polygon_new, "[(Size)i]"),
	GB_METHOD("_free", NULL, Polygon_free, NULL),

	GB_METHOD("_get", "PointF", Polygon_get, "(Index)i"),
	GB_METHOD("_put", NULL, Polygon_put, "(Point)PointF;(Index)i"),

	GB_PROPERTY_READ("Count", "i", Polygon_Count),
	GB_PROPERTY_READ("Max", "i", Polygon_Max),
	GB_PROPERTY_READ("Area", "f", Polygon_Area),
	GB_PROPERTY_READ("Orientation", "b", Polygon_Orientation),

	GB_METHOD("Reverse", NULL, Polygon_Reverse, NULL),
	GB_METHOD("Simplify", "Polygon[]", Polygon_Simplify, "[(Fill)i]"),
	GB_METHOD("Clean", "Polygon", Polygon_Clean, "[(Distance)f]"),
	GB_METHOD("Trim", "Polygon", Polygon_Trim, NULL),

	GB_METHOD("Add", NULL, Polygon_Add, "(X)f(Y)f"),
	GB_METHOD("AddPoint", NULL, Polygon_AddPoint, "(Point)PointF;"),
	GB_METHOD("Remove", NULL, Polygon_Remove, "(Index)i[(Count)i]"),

	GB_INTERFACE("_convert", &_convert_polygon),

	GB_END_DECLARE
};

GB_DESC ClipperDesc[] =
{
	GB_DECLARE_VIRTUAL("Clipper"),

	GB_CONSTANT("JoinMiter", "i", jtMiter),
	GB_CONSTANT("JoinSquare", "i", jtSquare),
	GB_CONSTANT("JoinBevel", "i", jtSquare),
	GB_CONSTANT("JoinRound", "i", jtRound),

	GB_CONSTANT("FillEvenOdd", "i", pftEvenOdd),
	GB_CONSTANT("FillWinding", "i", pftNonZero),
	GB_CONSTANT("FillNonZero", "i", pftNonZero),
	GB_CONSTANT("FillPositive", "i", pftPositive),
	GB_CONSTANT("FillNegative", "i", pftNegative),

	GB_CONSTANT("CapSquare", "i", etOpenSquare),
	GB_CONSTANT("CapRound", "i", etOpenRound),
	GB_CONSTANT("CapButt", "i", etOpenButt),
	GB_CONSTANT("CapPolygon", "i", etClosedPolygon),
	GB_CONSTANT("CapJoined", "i", etClosedLine),

	GB_STATIC_METHOD("Offset", "Polygon[]", Clipper_Offset, "(Polygons)Polygon[];(Delta)f[(Join)i(Limit)f(DoNotFix)b]"),
	GB_STATIC_METHOD("Simplify", "Polygon[]", Clipper_Simplify, "(Polygons)Polygon[];[(Fill)i]"),
	GB_STATIC_METHOD("Clean", "Polygon[]", Clipper_Clean, "(Polygons)Polygon[];[(Distance)f]"),
	GB_STATIC_METHOD("Trim", "Polygon[]", Clipper_Trim, "(Polygons)Polygon[]"),
	GB_STATIC_METHOD("Inflate", "Polygon[]", Clipper_Inflate, "(Polygons)Polygon[];(Delta)f[(Join)i(Cap)i(Limit)f(Outline)b]"),

	GB_STATIC_METHOD("Union", "Polygon[]", Clipper_Union, "(Polygons)Polygon[];[(Clip)Polygon[];(Fill)i]"),
	GB_STATIC_METHOD("Intersection", "Polygon[]", Clipper_Intersection, "(Polygons)Polygon[];[(Clip)Polygon[];(Fill)i]"),
	GB_STATIC_METHOD("Difference", "Polygon[]", Clipper_Difference, "(Polygons)Polygon[];[(Clip)Polygon[];(Fill)i]"),
	GB_STATIC_METHOD("ExclusiveOr", "Polygon[]", Clipper_ExclusiveOr, "(Polygons)Polygon[];[(Clip)Polygon[];(Fill)i]"),

	GB_END_DECLARE
};


