/*
 *                            COPYRIGHT
 *
 *  libpolybool, 2D polygon bool operations
 *  Copyright (C) 2024 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 Entrust in 2024)
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.*
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 31 Milk Street, # 960789 Boston, MA 02196 USA
 *
 *  Contact:
 *    Project page: http://www.repo.hu/projects/librnd
 *    lead developer: http://www.repo.hu/projects/librnd/contact.html
 *    mailing list: pcb-rnd (at) list.repo.hu (send "subscribe")
 *
 */

#include <assert.h>
#include "polygon1_gen.h"
#include "pa_config.h"
#include "pa_math.c"

TODO("arc: upgrade this code to deal with arcs in input")

/* when defined: save each rim step dump svg set in I???/ */
#define OFFSET3_SAVE_RIM_DUMPS

int rnd_vertices_are_coaxial(rnd_vnode_t *node);
void pa_dump_pl(const rnd_pline_t *pl, const char *fn);

long rnd_polybool_dump_offset_round = DEBUG_DUMP_OFFSET_ROUND;

#define Vequ2(a,b)       (memcmp((a),   (b),   sizeof(rnd_vector_t)) == 0)

#define Vsub2(r, a, b) \
	do { \
		(r)[0] = (a)[0] - (b)[0]; \
		(r)[1] = (a)[1] - (b)[1]; \
	} while(0)

/* Corner case handler: when p1..p2 and q1..q2 are parallel */
RND_INLINE int rnd_iline_inters2_par(rnd_vector_t p1, rnd_vector_t p2, rnd_vector_t q1, rnd_vector_t q2)
{
	rnd_vector_t q1p1, q1q2;

	Vsub2(q1p1, q1, p1);
	Vsub2(q1q2, q1, q2);

	/* check if p1..p2 and q1..q2 are not on same line (no intersections then) */
	if (rnd_vect_det2(q1p1, q1q2) != 0)
		return 0;

	return -1; /* coaxial lines */
}

/* same as pa_vect_inters2 but for infinitely extended lines, but return
   value differs:
    -1: coaxial lines (infinite number of intersections; Sout is not filled in)
     0: no intersection (prallel lines; Sout is not filled in)
     1: single intersection (Sout filled in) */
RND_INLINE int pa_iline_inters2(rnd_vector_t p1, rnd_vector_t p2, rnd_vector_t q1, rnd_vector_t q2, rnd_vector_t Sout)
{
	double t, divider;
	double pdx, pdy, qdx, qdy;

	/* calculate and cache delta_x/delta_y for p and q */
	pdx = p2[0] - p1[0]; pdy = p2[1] - p1[1];
	qdx = q2[0] - q1[0]; qdy = q2[1] - q1[1];

	/* for the actual algorithm see comments in pa_vect_inters2() */
	divider = pdy * qdx - pdx * qdy;
	if (divider == 0) /* divider is zero if the lines are parallel */
		return rnd_iline_inters2_par(p1, p2, q1, q2);

	/* degenerate cases: endpoint on the other line */
	if (Vequ2(q1, p1) || Vequ2(q1, p2)) {
		Sout[0] = q1[0]; Sout[1] = q1[1];
		return 1;
	}
	if (Vequ2(q2, p1) || Vequ2(q2, p2)) {
		Sout[0] = q2[0]; Sout[1] = q2[1];
		return 1;
	}

	t = (pdy * (p1[0] - q1[0]) + pdx * (q1[1] - p1[1])) / divider;
	Sout[0] = q1[0] + PA_ROUND(t * qdx);
	Sout[1] = q1[1] + PA_ROUND(t * qdy);
	return 1;
}



RND_INLINE rnd_vnode_t *pl_append_xy(rnd_pline_t *dst, rnd_coord_t x, rnd_coord_t y)
{
	rnd_vnode_t *vn = calloc(sizeof(rnd_vnode_t), 1);

	vn->point[0] = x;
	vn->point[1] = y;

	if (dst->head != NULL) {
		vn->next = dst->head;
		vn->prev = dst->head->prev;
		dst->head->prev->next = vn;
		dst->head->prev = vn;
	}
	else {
		vn->next = vn->prev = vn;
		dst->head = vn;
	}

	return vn;
}

RND_INLINE rnd_vnode_t *pl_insert_after_xy(rnd_vnode_t *dst, rnd_coord_t x, rnd_coord_t y)
{
	rnd_vnode_t *prev = dst, *next = dst->next, *vn = calloc(sizeof(rnd_vnode_t), 1);

	vn->point[0] = x;
	vn->point[1] = y;

	vn->next = next;
	vn->prev = prev;
	prev->next = vn;
	next->prev = vn;

	return vn;
}

/* how to handle conerns where the two offseted edges don't intersect */
typedef enum {
	RND_PLINE_CORNER_ROUND = 1,
	RND_PLINE_CORNER_SHARP,     /* sharp corner with only the intersection point of the two edges */
	RND_PLINE_CORNER_SHARP2,    /* semi-sharp corner with the offseted original endpoints and the intersection point of the two edges; this keeps edge slopes intact but introduces 2 extra points */
	RND_PLINE_CORNER_FLAT       /* keep the offseted original endpoints and connect them with a straight line */
} rnd_pline_corner_type;

typedef enum corner_cvx_e {
	CONVEX = 1,
	CONCAVE,
	COAXIAL
} corner_cvx_t;

RND_INLINE corner_cvx_t corner_convexity_ll(const rnd_vnode_t *vn)
{
	rnd_coord_t lx1 = vn->prev->point[0], ly1 = vn->prev->point[1];
	rnd_coord_t lx2 = vn->point[0], ly2 = vn->point[1];
	rnd_coord_t px = vn->next->point[0], py = vn->next->point[1];
	double crossp;

	crossp = (double)(lx2 - lx1) * (double)(py - ly1) - (double)(ly2 - ly1) * (double)(px - lx1);

	if (crossp == 0) return COAXIAL;
	if (crossp > 0) return CONCAVE; /* p is left of the line (curving out) */
	return CONVEX;  /* p is right of the line (curving in) */
}

RND_INLINE corner_cvx_t corner_convexity(const rnd_vnode_t *vn)
{
	TODO("arc: this fails on any arc input; if vn is arc and is CW (adir==1), it's a convex corner");
	return corner_convexity_ll(vn);
}


/* offset each edge also keeping the original corners but marked; returns
   whether the whole pline is CONCAVE or CONVEX */
RND_INLINE corner_cvx_t offset_pline_raw(rnd_pline_t *dst, const rnd_pline_t *src, rnd_coord_t offs)
{
	rnd_vnode_t *curr, *start;
	rnd_vnode_t *vnew, *next;
	corner_cvx_t cvy, res = CONVEX;

	/* clean up while making a copy: remove redundant and coaxial nodes */
	curr = src->head;
	do {
		if ((curr->point[0] == curr->next->point[0]) && (curr->point[1] == curr->next->point[1]))
			continue;

		cvy = corner_convexity(curr);
		if (cvy == COAXIAL)
			continue;
		if (cvy == CONCAVE)
			res = CONCAVE;

		vnew = pl_append_xy(dst, curr->point[0], curr->point[1]);
		vnew->flg.mark = 1;

	} while((curr = curr->next) != src->head);

	/* add orthogonally extended nodes */
	curr = start = dst->head;
	do {
		double vx, vy, nx, ny, len;
		rnd_vnode_t *vnew;

		next = curr->next;
		vx = next->point[0] - curr->point[0];
		vy = next->point[1] - curr->point[1];
		len = sqrt(vx*vx + vy*vy);
		vx /= len;
		vy /= len;
		nx = (-vy) * -(double)offs;
		ny = (vx) * -(double)offs;
		vnew = pl_insert_after_xy(curr, curr->point[0] + nx, curr->point[1] + ny);
		vnew = pl_insert_after_xy(vnew, next->point[0] + nx, next->point[1] + ny);
	} while((curr = next) != start);

	return res;
}

#ifdef OFFSET3

/* new code, area-based, works with arcs */
#include "pb2.h"
#include <librnd/core/safe_fs.h>

/* these are not part of the official API but offset2 is a separate compilation unit */
void pb2_offset3_rim_incremental(pb2_ctx_t *ctx, double offs2);

RND_INLINE void map_seg(pb2_ctx_t *ctx, rnd_vnode_t *vn, char poly_id, int reverse)
{
	switch(vn->flg.curve_type) {
		case RND_VNODE_ARC:
			if (reverse)
				pb2_1_map_seg_arc(ctx, vn->next->point, vn->point, vn->curve.arc.center, !vn->curve.arc.adir, poly_id);
			else
				pb2_1_map_seg_arc(ctx, vn->point, vn->next->point, vn->curve.arc.center, vn->curve.arc.adir, poly_id);
			break;
		case RND_VNODE_LINE:
		default:
			if (reverse)
				pb2_1_map_seg_line(ctx, vn->next->point, vn->point, poly_id);
			else
				pb2_1_map_seg_line(ctx, vn->point, vn->next->point, poly_id);
			break;
	}
}

/* Offset rim with offs > 0 has a large hole in the middle that in theory
   follows the contour of the original pline precisely. In practice, due
   to integer rounding errors the rim inner countour may deviate a tiny bit
   from the original contour. When the original contour and the rim union is
   computed, this would result in tiny, narrow holes. See test case
   off_rnd_05f.
   This code takes the resulting pline and verifies each hole: if a hole
   touches (or is very close to) the original contour, it must be a hole
   created the way described above because any valid hole (e.g. a C shaped
   input that is closed by the rim) has at least offs separation from the
   original contour. This function removes accidental holes. */
static void pb2_offset3_workaround_rounding_holes(rnd_pline_t *res, const rnd_pline_t *orig)
{
	rnd_pline_t *hole = res->next, *hnext, *hprev = res;

/*	fprintf(stderr, "pb2_offset3_workaround_rounding_holes %p\n", hole);*/

	for(; hole != NULL; hole = hnext) {
		rnd_vnode_t *n;
		int del_hole = 0;

		hnext = hole->next;

/*		fprintf(stderr, " holeface pline %p prev=%p next=%p\n", hole, hprev, hnext);*/
		n = hole->head;
		do {
			if (rnd_pline_isect_circ(orig, n->point[0], n->point[1], 4)) {
				del_hole = 1;
				break;
			}
		} while((n = n->next) != hole->head);

		if (del_hole) {
/*			fprintf(stderr, "   remove hole because of %ld %ld\n", n->point[0], n->point[1]);*/
			hprev->next = hnext;
			
			hole->next = NULL;
			pa_pline_free(&hole);
		}
		else
			hprev = hole;
	}
}

#define PTL_NEED_ARC 2

static int icnt = 0;

static rnd_pline_t *pline_dup_with_offset_corner(const rnd_pline_t *src, rnd_coord_t offs, rnd_pline_corner_type ct)
{
	pb2_ctx_t ctx = {0};
	rnd_vnode_t *next, *curr;
	rnd_pline_t *dst = calloc(sizeof(rnd_pline_t), 1);
	pb2_seg_t *seg;
	char dumpfn[128];
	rnd_polyarea_t *res = NULL;
	int cr, op_sub, ctx_valid = 0;
	double offs2 = (double)offs / 2.0;


	if (dst == NULL)
		return NULL;

	rnd_rtree_init(&ctx.seg_tree);
	ctx_valid = 1;

	if (rnd_polybool_dump_offset_round) {
		rnd_polybool_dump_offset_round++;
		sprintf(dumpfn, "off3_%06ld.I.poly", rnd_polybool_dump_offset_round);
		pa_dump_pl(src, dumpfn);
	}

	offset_pline_raw(dst, src, offs);

	/* map which nodes are concave and trim back concave extensions */
	curr = dst->head;
	do {
		int is_convex, need_arc;

		next = curr->next;
		if (!curr->flg.mark) continue; /* deal with original (marked) nodes only */


		is_convex = (corner_convexity(curr) != CONCAVE);

		if (offs > 0)
			need_arc = is_convex;
		else
			need_arc = !is_convex;

		if (need_arc)
			curr->flg.plabel |= PTL_NEED_ARC;

		if (!need_arc) {
			double offs[2];
			rnd_vector_t ip[2];
			int ni;

			ni = pb2_iscp_cline_cline_o(curr->prev->prev->point, curr->prev->point, curr->next->point, curr->next->next->point, ip, offs, 1);
			if (ni == 1) {
				rnd_coord_t ix = ip[0][0], iy = ip[0][1];
/*				curr->point[0] = ix;
				curr->point[1] = iy;*/

				curr->next->point[0] = ix;
				curr->next->point[1] = iy;

				curr->prev->point[0] = ix;
				curr->prev->point[1] = iy;
			}
		}
	} while((curr = next) != dst->head);


	/* compute bloat strip as 'B'; this has to be done without the original
	   contour to avoid premature/intermediate intersections that introduce
	   rounding errors resulting in excess narrow holes (see off_rnd_03) */
	ctx.has_B = 1;
	ctx.rule = PB2_RULE_NON0;
/*	ctx.rule = PB2_RULE_EVEN_ODD;*/
	ctx.op = RND_PBO_UNITE;
	ctx.inhibit_edge_tree = 0;

	/* visit original corners (marked) construct bloat shapes in polygon B and
	   incrementally add or subtract them */
	curr = dst->head;
	do {
		int need_arc = curr->flg.plabel & PTL_NEED_ARC;

		next = curr->next;
		if (!curr->flg.mark) continue; /* deal with original (marked) nodes only */

		/* construct edge offset "rectangle" */
		pb2_1_map_seg_line(&ctx, curr->point, curr->next->point, 'B'); /* side going out */
		pb2_1_map_seg_line(&ctx, curr->next->point, curr->next->next->point, 'B'); /* outer edge */ TODO("arc: this might be an arc if input edge is arc");
		pb2_1_map_seg_line(&ctx, curr->next->next->point, curr->next->next->next->point, 'B'); /* side coming back in */
		pb2_1_map_seg_line(&ctx, curr->next->next->next->point, curr->point, 'B'); /* original edge */

		rndo_trace("curr: ", Pint(curr->point[0]), ";", Pint(curr->point[1]), "\n", 0);
		rndo_trace("out1: ", Pint(curr->next->point[0]), ";", Pint(curr->next->point[1]), "\n", 0);
		rndo_trace("out2: ", Pint(curr->next->next->point[0]), ";", Pint(curr->next->next->point[1]), "\n", 0);
		rndo_trace("back: ", Pint(curr->next->next->next->point[0]), ";", Pint(curr->next->next->next->point[1]), "\n", 0);

		/* put an arc "pie segment" in the corner */
		if (need_arc) {
			/* round corner, CW */
TODO("arc: do this only on line-line");
			pb2_1_map_seg_line(&ctx, curr->point, curr->prev->point, 'B'); /* side going out */
			pb2_1_map_seg_arc(&ctx, curr->prev->point, curr->next->point, curr->point, offs > 0 ? 1 : 0, 'B');
			pb2_1_map_seg_line(&ctx, curr->next->point, curr->point, 'B'); /* side coming back in */
		}

		rndo_trace("icnt=", Pint(icnt), " ----------------\n", 0);
#ifdef OFFSET3_SAVE_RIM_DUMPS
		{
			FILE *f = rnd_fopen(NULL, "rim.ctx", "w");
			pb2_dump_ctx(f, &ctx);
			fclose(f);
		}
#endif


		pb2_offset3_rim_incremental(&ctx, offs2);

#ifdef OFFSET3_SAVE_RIM_DUMPS
		if (rnd_polybool_dump_offset_round) {
			char cmd[256];
			icnt++;
			sprintf(cmd, "mkdir -p i%03d; mv step*.svg rim.ctx  i%03d", icnt, icnt);
			rnd_system(NULL, cmd);
		}
#endif
	} while((curr = next) != dst->head);

	op_sub = (offs < 0);

	/* make sure the offset rim is all in 'B' polygon, so that the original
	   contour would be 'A', just in case our operation is A-B (when op_sub==1) */
	for(seg = ctx.all_segs; seg != NULL; seg = seg->next_all) {
/*		fprintf(stderr, "RIM: S%ld A=%d B=%d\n", seg->uid, seg->cntA, seg->cntB);*/
		seg->cntB = seg->cntA;
		seg->non0B = seg->non0A;
		seg->cntA = 0;
		seg->non0A = 0;
	}

	/* add original pline as 'A' */
	curr = src->head;
	do {
		map_seg(&ctx, curr, 'A', 0);
	} while((curr = curr->next) != src->head);

	rndo_trace("Offset3 final: ", (op_sub ? "sub" : "unite"), "\n", 0);


	/* combine the original contour with the bloat strip */
	ctx.op = op_sub ? RND_PBO_SUB : RND_PBO_UNITE;
	cr = pb2_exec_incremental_finish(&ctx, &res);
	ctx_valid = 0;
	if (cr != 0) {
		rndo_trace("ooops incremental exec 3\n", 0);
		goto error;
	}

	if (offs > 6)
		pb2_offset3_workaround_rounding_holes(res->contours, src);

	pa_pline_free(&dst);
	if (res != NULL) {
		TODO("this is where only the first island is kept when offs<0 and multiple islands are formed; we can't return multiple islands as pline");
		dst = res->contours;
		res->contours = NULL;
		rnd_polyarea_free(&res);
	}

	if (rnd_polybool_dump_offset_round) {
		sprintf(dumpfn, "off3_%06ld.O.poly", rnd_polybool_dump_offset_round);
		pa_dump_pl(dst, dumpfn);
	}

	return dst;

	error:;
	if (ctx_valid)
		pb2_exec_incremental_abort(&ctx);
	pa_pline_free(&dst);
	return NULL;
}


#else
/* old code, purely contour-based - fails when offset is too large */
static rnd_pline_t *pline_dup_with_offset_corner(const rnd_pline_t *src, rnd_coord_t offs, rnd_pline_corner_type ct)
{
	const rnd_vnode_t *curr, *next;
	rnd_vnode_t *vnew, *discard;
	rnd_pline_t *dst = calloc(sizeof(rnd_pline_t), 1);
	rnd_vector_t isc1, isc2;
	char dumpfn[128];
	double offs2 = -1;

	if (dst == NULL)
		return NULL;

	if (rnd_polybool_dump_offset_round) {
		rnd_polybool_dump_offset_round++;
		sprintf(dumpfn, "off_%06ld.I.poly", rnd_polybool_dump_offset_round);
		pa_dump_pl(src, dumpfn);
	}

	offset_pline_raw(dst, src, offs);

	/* the next section doesn't work correctly when starting on a marked node
	   (because of all the node removals) */
	if (dst->head->flg.mark)
		dst->head = dst->head->next;

	/* visit original corners (marked) and check if they are now the center of
	   a round outer corner (convex) or an external point on an inverted corner
	   (concave) */
	curr = dst->head;
	do {
		next = curr->next;

		if (curr->flg.mark) {
			if (rnd_vect_inters2(curr->prev->prev->point, curr->prev->point, curr->next->point, curr->next->next->point, isc1, isc2) == 1) {
				/* prev and next seg intersect -> concave */
				discard = curr->prev;
				rnd_poly_vertex_exclude(dst, (rnd_vnode_t *)curr);
				rnd_poly_vertex_exclude(dst, (rnd_vnode_t *)discard);
				curr->next->point[0] = isc1[0];
				curr->next->point[1] = isc1[1];
				free((rnd_vnode_t *)curr); /* curr is from dst now so it's safe to free it */
				free(discard);
			}
			else {
				rnd_coord_t cx = curr->point[0], cy = curr->point[1];
				rnd_vector_t isc;
				int ir, skip;

				/* convex: add a corner */
				switch(ct) {
					case RND_PLINE_CORNER_ROUND:
						rnd_poly_vertex_exclude(dst, (rnd_vnode_t *)curr);
						rnd_poly_frac_circle_to(dst, next->prev, cx, cy, next->prev->point, next->point, RND_POLY_FCT_SKIP_TINY | ((offs >= 0) ? 0 : RND_POLY_FCT_REVERSE));
						free((rnd_vnode_t *)curr); /* curr is from dst now so it's safe to free it */
						break;
					case RND_PLINE_CORNER_FLAT:
						rnd_poly_vertex_exclude(dst, (rnd_vnode_t *)curr);
						break;
					case RND_PLINE_CORNER_SHARP:
					case RND_PLINE_CORNER_SHARP2:
						ir = pa_iline_inters2(curr->prev->prev->point, curr->prev->point, curr->next->point, curr->next->next->point, isc);

						/* special case: see offset03: almost parallel edges with offset so
						   large that the intersection point gets too far away. The isc
						   point can not be further from any offseted endpoint than offs */
						if (offs2 < 0)
							offs2 = (double)offs * (double)offs;
						skip = 1;
						if (rnd_vect_dist2(isc, curr->prev->point) < offs2) skip = 0;
						if (skip && (rnd_vect_dist2(isc, curr->next->point) < offs2)) skip = 0;

						if (!skip) {
							if (ct == RND_PLINE_CORNER_SHARP) {
								/* remove original offseted ends, close to the isc, to preserve number of corners */
								rnd_poly_vertex_exclude(dst, (rnd_vnode_t *)curr->prev);
								rnd_poly_vertex_exclude(dst, (rnd_vnode_t *)curr->next);
							}

							rnd_poly_vertex_include_force(curr->prev, rnd_poly_node_create(isc));
						}

						rnd_poly_vertex_exclude(dst, (rnd_vnode_t *)curr);
/*						rndo_trace(" ISC iline: ", Pint(ir), " ", Pvect(isc), "\n", 0);*/
						break;
					default:
						assert(!"pline_dup_with_offset_corner(): invalid corner type");
						abort();
				}
			}
		}

	} while((curr = next) != dst->head);


	pa_pline_update(dst, 1);
	if (rnd_polybool_dump_offset_round) {
		sprintf(dumpfn, "off_%06ld.O.poly", rnd_polybool_dump_offset_round);
		pa_dump_pl(dst, dumpfn);
	}


	return dst;
}
#endif

rnd_pline_t *rnd_pline_dup_with_offset_round(const rnd_pline_t *src, rnd_coord_t offs)
{
	return pline_dup_with_offset_corner(src, offs, RND_PLINE_CORNER_ROUND);
}

