// 7 september 2015
#include "uipriv_windows.hpp"
#include "draw.hpp"

// TODO
// - write a test for transform followed by clip and clip followed by transform to make sure they work the same as on gtk+ and cocoa
// - write a test for nested transforms for gtk+

struct uiDrawPath {
	ID2D1PathGeometry *path;
	ID2D1GeometrySink *sink;
	BOOL inFigure;
};

uiDrawPath *uiDrawNewPath(uiDrawFillMode fillmode)
{
	uiDrawPath *p;
	HRESULT hr;

	p = uiprivNew(uiDrawPath);
	hr = d2dfactory->CreatePathGeometry(&(p->path));
	if (hr != S_OK)
		logHRESULT(L"error creating path", hr);
	hr = p->path->Open(&(p->sink));
	if (hr != S_OK)
		logHRESULT(L"error opening path", hr);
	switch (fillmode) {
	case uiDrawFillModeWinding:
		p->sink->SetFillMode(D2D1_FILL_MODE_WINDING);
		break;
	case uiDrawFillModeAlternate:
		p->sink->SetFillMode(D2D1_FILL_MODE_ALTERNATE);
		break;
	}
	return p;
}

void uiDrawFreePath(uiDrawPath *p)
{
	if (p->inFigure)
		p->sink->EndFigure(D2D1_FIGURE_END_OPEN);
	if (p->sink != NULL)
		// TODO close sink first?
		p->sink->Release();
	p->path->Release();
	uiprivFree(p);
}

void uiDrawPathNewFigure(uiDrawPath *p, double x, double y)
{
	D2D1_POINT_2F pt;

	if (p->sink == NULL)
		uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);

	if (p->inFigure)
		p->sink->EndFigure(D2D1_FIGURE_END_OPEN);
	pt.x = x;
	pt.y = y;
	p->sink->BeginFigure(pt, D2D1_FIGURE_BEGIN_FILLED);
	p->inFigure = TRUE;
}

// Direct2D arcs require a little explanation.
// An arc in Direct2D is defined by the chord between the endpoints.
// There are four possible arcs with the same two endpoints that you can draw this way.
// See https://www.youtube.com/watch?v=ATS0ANW1UxQ for a demonstration.
// There is a property rotationAngle which deals with the rotation /of the entire ellipse that forms an ellpitical arc/ - it's effectively a transformation on the arc.
// That is to say, it's NOT THE SWEEP.
// The sweep is defined by the start and end points and whether the arc is "large".
// As a result, this design does not allow for full circles or ellipses with a single arc; they have to be simulated with two.
// TODO https://github.com/Microsoft/WinObjC/blob/develop/Frameworks/CoreGraphics/CGPath.mm#L313

struct arc {
	double xCenter;
	double yCenter;
	double radius;
	double startAngle;
	double sweep;
	int negative;
};

// this is used for the comparison below
// if it falls apart it can be changed later
#define aerMax 6 * DBL_EPSILON

static void drawArc(uiDrawPath *p, struct arc *a, void (*startFunction)(uiDrawPath *, double, double))
{
	double sinx, cosx;
	double startX, startY;
	double endX, endY;
	D2D1_ARC_SEGMENT as;
	BOOL fullCircle;
	double absSweep;

	// as above, we can't do a full circle with one arc
	// simulate it with two half-circles
	// of course, we have a dragon: equality on floating-point values!
	// I've chosen to do the AlmostEqualRelative() technique in https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
	fullCircle = FALSE;
	// use the absolute value to tackle both ≥2π and ≤-2π at the same time
	absSweep = fabs(a->sweep);
	if (absSweep > (2 * uiPi))		// this part is easy
		fullCircle = TRUE;
	else {
		double aerDiff;

		aerDiff = fabs(absSweep - (2 * uiPi));
		// if we got here then we know a->sweep is larger (or the same!)
		fullCircle = aerDiff <= absSweep * aerMax;
	}
	// TODO make sure this works right for the negative direction
	if (fullCircle) {
		a->sweep = uiPi;
		drawArc(p, a, startFunction);
		a->startAngle += uiPi;
		drawArc(p, a, NULL);
		return;
	}

	// first, figure out the arc's endpoints
	// unfortunately D2D1SinCos() is only defined on Windows 8 and newer
	// the MSDN page doesn't say this, but says it requires d2d1_1.h, which is listed as only supported on Windows 8 and newer elsewhere on MSDN
	// so we must use sin() and cos() and hope it's right...
	sinx = sin(a->startAngle);
	cosx = cos(a->startAngle);
	startX = a->xCenter + a->radius * cosx;
	startY = a->yCenter + a->radius * sinx;
	sinx = sin(a->startAngle + a->sweep);
	cosx = cos(a->startAngle + a->sweep);
	endX = a->xCenter + a->radius * cosx;
	endY = a->yCenter + a->radius * sinx;

	// now do the initial step to get the current point to be the start point
	// this is either creating a new figure, drawing a line, or (in the case of our full circle code above) doing nothing
	if (startFunction != NULL)
		(*startFunction)(p, startX, startY);

	// now we can draw the arc
	as.point.x = endX;
	as.point.y = endY;
	as.size.width = a->radius;
	as.size.height = a->radius;
	as.rotationAngle = 0;		// as above, not relevant for circles
	if (a->negative)
		as.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE;
	else
		as.sweepDirection = D2D1_SWEEP_DIRECTION_CLOCKWISE;
	// TODO explain the outer if
	if (!a->negative)
		if (a->sweep > uiPi)
			as.arcSize = D2D1_ARC_SIZE_LARGE;
		else
			as.arcSize = D2D1_ARC_SIZE_SMALL;
	else
		// TODO especially this part
		if (a->sweep > uiPi)
			as.arcSize = D2D1_ARC_SIZE_SMALL;
		else
			as.arcSize = D2D1_ARC_SIZE_LARGE;
	p->sink->AddArc(&as);
}

void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
{
	struct arc a;

	if (p->sink == NULL)
		uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);

	a.xCenter = xCenter;
	a.yCenter = yCenter;
	a.radius = radius;
	a.startAngle = startAngle;
	a.sweep = sweep;
	a.negative = negative;
	drawArc(p, &a, uiDrawPathNewFigure);
}

void uiDrawPathLineTo(uiDrawPath *p, double x, double y)
{
	D2D1_POINT_2F pt;

	if (p->sink == NULL)
		uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);

	pt.x = x;
	pt.y = y;
	p->sink->AddLine(pt);
}

void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative)
{
	struct arc a;

	if (p->sink == NULL)
		uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);

	a.xCenter = xCenter;
	a.yCenter = yCenter;
	a.radius = radius;
	a.startAngle = startAngle;
	a.sweep = sweep;
	a.negative = negative;
	drawArc(p, &a, uiDrawPathLineTo);
}

void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY)
{
	D2D1_BEZIER_SEGMENT s;

	if (p->sink == NULL)
		uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);

	s.point1.x = c1x;
	s.point1.y = c1y;
	s.point2.x = c2x;
	s.point2.y = c2y;
	s.point3.x = endX;
	s.point3.y = endY;
	p->sink->AddBezier(&s);
}

void uiDrawPathCloseFigure(uiDrawPath *p)
{
	if (p->sink == NULL)
		uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);

	p->sink->EndFigure(D2D1_FIGURE_END_CLOSED);
	p->inFigure = FALSE;
}

void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height)
{
	if (p->sink == NULL)
		uiprivUserBug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p);

	// this is the same algorithm used by cairo and Core Graphics, according to their documentations
	uiDrawPathNewFigure(p, x, y);
	uiDrawPathLineTo(p, x + width, y);
	uiDrawPathLineTo(p, x + width, y + height);
	uiDrawPathLineTo(p, x, y + height);
	uiDrawPathCloseFigure(p);
}

void uiDrawPathEnd(uiDrawPath *p)
{
	HRESULT hr;

	if (p->inFigure) {
		p->sink->EndFigure(D2D1_FIGURE_END_OPEN);
		// needed for uiDrawFreePath()
		p->inFigure = FALSE;
	}
	hr = p->sink->Close();
	if (hr != S_OK)
		logHRESULT(L"error closing path", hr);
	p->sink->Release();
	// also needed for uiDrawFreePath()
	p->sink = NULL;
}

int uiDrawPathEnded(uiDrawPath *p)
{
	return p->sink == NULL ? 1 : 0;
}

ID2D1PathGeometry *pathGeometry(uiDrawPath *p)
{
	if (p->sink != NULL)
		uiprivUserBug("You cannot draw with a uiDrawPath that was not ended. (path: %p)", p);
	return p->path;
}
