/*
 *                            COPYRIGHT
 *
 *  camv-rnd - electronics-related CAM viewer
 *  Copyright (C) 2019 Tibor 'Igor2' Palinkas
 *
 *  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 of the License, 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., 31 Milk Street, # 960789 Boston, MA 02196 USA.
 *
 *  Contact:
 *    Project page: http://repo.hu/projects/camv-rnd
 *    lead developer: http://repo.hu/projects/camv-rnd/contact.html
 *    mailing list: pcb-rnd (at) list.repo.hu (send "subscribe")
 */

#include <librnd/config.h>

#include <stdio.h>
#include <ctype.h>

#include <librnd/core/error.h>
#include <librnd/core/safe_fs.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/rotate.h>
#include <librnd/core/math_helper.h>
#include <librnd/poly/polygon1_gen.h>
#include <librnd/poly/polyarea.h>

#include "data.h"
#include "plug_io.h"
#include "obj_any.h"

#include "geparse.h"
#include "gedraw.h"

/*** low level draw ***/

/* assumes dst is zero sized */
static void pline2camv(camv_poly_t *dst, rnd_pline_t *src)
{
	rnd_pline_t *l;
	rnd_vnode_t *v;
	long n;

	n = 0;
	for(l = src; l != NULL; l = l->next) {
		v = l->head;
#ifdef DONT_TRUST_COUNT
		do {
			n++;
			v = v->next;
		} while(v != &l->head);
#else
		n += l->Count;
#endif
/*		printf("l cont=%d n=%ld\n", l->Count, n);*/
	}


	camv_poly_allocpts(dst, n);
	n = 0;
	for(l = src; l != NULL; l = l->next) {
		v = l->head;
		do {
			dst->x[n] = v->point[0];
			dst->y[n] = v->point[1];
			n++;
			v = v->next;
		} while(v != l->head);
	}
}

static void emit_build(rnd_pline_t *pl, void *ud)
{
	camv_grp_t *dst = ud;
	camv_poly_t *opoly = (camv_poly_t *)&dst->objs[dst->len];
	camv_poly_init(opoly);
	pline2camv(opoly, pl);
	dst->len++;
}

/* Returns a polygon or a group of polygons, free's src*/
static camv_any_obj_t *polyarea2camv(gedraw_ctx_t *ctx, rnd_polyarea_t *src)
{
	rnd_pline_t *l;
	long holes = 0, islands = 0;
	rnd_polyarea_t *pa;

	pa = src;
	do {
		for(l = pa->contours->next; l != NULL; l = l->next) holes++;
		islands++;
		pa = pa->f;
	} while(pa != src);

	if (holes > 0) { /* has holes, dice and return a group of objects */
		camv_grp_t *grp = camv_grp_new();
		grp->len = 0;
		grp->loc.line = ctx->line; grp->loc.col = ctx->col;
		grp->objs = calloc(sizeof(camv_any_obj_t), (holes+2)*2);
		rnd_polyarea_no_holes_dicer(src, 0, 0, 0, 0, emit_build, grp); /* handles islands */
		return (camv_any_obj_t *)grp;
	}
	else {
		if (islands != 1) { /* multiple islands, no holes: need to create a group of camv polygons */
			long i = 0;
			camv_grp_t *grp = camv_grp_new();

			grp->loc.line = ctx->line; grp->loc.col = ctx->col;
			grp->len = islands;
			grp->objs = malloc(sizeof(camv_any_obj_t) * grp->len);
			pa = src;
			do {
				camv_poly_t *dst = &grp->objs[i].poly;
				camv_poly_init(dst);
				pline2camv(dst, pa->contours);
				i++;
				pa = pa->f;
			} while(pa != src);
			rnd_polyarea_free(&src);
			return (camv_any_obj_t *)grp;
		}
		else { /* single island, no holes: return a sinlge camv poly */
			camv_poly_t *dst = camv_poly_new();
			dst->loc.line = ctx->line; dst->loc.col = ctx->col;
			pline2camv(dst, src->contours);
			rnd_polyarea_free(&src);
			return (camv_any_obj_t *)dst;
		}
	}
}

/* Convert an oblong aperture into a camv polygon around 0;0 */
static camv_any_obj_t *aper_oblong2poly(gedraw_ctx_t *ctx, ge_aper_t *aper, double rot)
{
	rnd_coord_t xs = aper->data.oblong.xs, ys = aper->data.oblong.ys, xh, yh;
	rnd_polyarea_t *pa, *ph, *ptmp;
	camv_any_obj_t *o;

	TODO("figure if we need rotation");

	/* real thin oblong is sometimes made to be 0 in size that would be a
	   self intersecting poly; fix that up */
	if (xs < 2) xs = 2;
	if (ys < 2) ys = 2;

	xh = rnd_round((double)xs / 2.0);
	yh = rnd_round((double)ys / 2.0);
	if (xh > yh)
		pa = rnd_poly_from_line(-xh+yh, 0, +xh-yh, 0, ys, 0);
	else
		pa = rnd_poly_from_line(0, -yh+xh, 0, +yh-xh, xs, 0);

	/* subtract the hole if there's one */
	if (aper->hole > 0) {
		ph = rnd_poly_from_circle(0, 0, aper->hole/2);
		rnd_polyarea_boolean_free(pa, ph, &ptmp, RND_PBO_SUB);
		pa = ptmp;
	}

	o = polyarea2camv(ctx, pa);
	return o;
}

/* Convert a regular polygon with no hole aperture into a camv polygon around 0;0 */
static camv_any_obj_t *aper_poly2poly_nohole(gedraw_ctx_t *ctx, ge_aper_t *aper, double rot)
{
	camv_any_obj_t *o;
	double r, a, as;
	int n;

	o = (camv_any_obj_t *)camv_poly_new();
	o->proto.loc.line = ctx->line; o->proto.loc.col = ctx->col;
	camv_poly_allocpts(&o->poly, aper->data.poly.corners);
	as = (360.0 / (double)aper->data.poly.corners) / RND_RAD_TO_DEG;
	r = (double)aper->data.poly.dia / 2.0;
	for(n = 0, a = (rot + aper->data.poly.rot) / RND_RAD_TO_DEG; n < aper->data.poly.corners; n++,a+=as) {
		o->poly.x[n] = rnd_round(r * cos(a));
		o->poly.y[n] = rnd_round(r * sin(a));
	}
	return o;
}

static rnd_polyarea_t *pa_regpoly(rnd_coord_t cx, rnd_coord_t cy, rnd_cardinal_t corners, rnd_coord_t dia, double rot)
{
	rnd_pline_t *contour;
	rnd_vector_t v;
	double r, a, as;
	int n;

	as = (360.0 / (double)corners) / RND_RAD_TO_DEG;
	r = (double)dia / 2.0;

	for(n = 0, a = rot / RND_RAD_TO_DEG; n < corners; n++, a+=as) {
		v[0] = cx + rnd_round(r * cos(a));
		v[1] = cy + rnd_round(r * sin(a));
		if (n == 0)
			contour = rnd_poly_contour_new(v);
		else
			rnd_poly_vertex_include(contour->head->prev, rnd_poly_node_create(v));
	}

	return rnd_poly_from_contour_autoinv(contour);
}

/* Convert a regular polygon with hole aperture into a camv polygon around 0;0 */
static camv_any_obj_t *aper_poly2poly_hole(gedraw_ctx_t *ctx, ge_aper_t *aper, double rot)
{
	rnd_polyarea_t *ps, *ph, *pres;

	ps = pa_regpoly(0, 0, aper->data.poly.corners, aper->data.poly.dia, rot + aper->data.poly.rot);
	ph = rnd_poly_from_circle(0, 0, aper->hole/2);
	rnd_polyarea_boolean_free(ps, ph, &pres, RND_PBO_SUB);

	return polyarea2camv(ctx, pres);
}

/* idx is counted from 1 for code clarity */
static int param_eval_int(const ge_macro_line_t *l, int paridx, vtd0_t *param)
{
	double res;
	ge_expr_prg_t **prg = (ge_expr_prg_t **)vtp0_get((vtp0_t *)&l->operand, paridx-1, 0);
	if (prg == NULL) return 0;
	gex_eval(*prg, param, &res);
	return rnd_round(res);
}

/* idx is counted from 1 for code clarity */
static double param_eval_dbl(const ge_macro_line_t *l, int paridx, vtd0_t *param)
{
	double res;
	ge_expr_prg_t **prg = (ge_expr_prg_t **)vtp0_get((vtp0_t *)&l->operand, paridx-1, 0);
	if (prg == NULL) return 0;
	gex_eval(*prg, param, &res);
	return res;
}

/* idx is counted from 1 for code clarity */
static rnd_coord_t param_eval_crd(gedraw_ctx_t *ctx, const ge_macro_line_t *l, int paridx, vtd0_t *param)
{
	double res;
	ge_expr_prg_t **prg = (ge_expr_prg_t **)vtp0_get((vtp0_t *)&l->operand, paridx-1, 0);
	if (prg == NULL) return 0;
	gex_eval(*prg, param, &res);

	if (ctx->aper_unit == GEU_INCH)
		res *= 25.4;
	/* else we are in mm already */

	return (rnd_coord_t)RND_MM_TO_COORD(res);
}

/* merge pb into dst (pol=0: clear, pol=1: draw) */
static void macro_line_render_merge(rnd_polyarea_t **dst, rnd_polyarea_t *pb, int pol)
{
	rnd_polyarea_t *pa = *dst;

	if (pa == NULL) {
		if ((pb == NULL) || (!pol))
			return;
		*dst = pb;
	}
	else {
		*dst = NULL;
		rnd_polyarea_boolean_free(pa, pb, dst, pol ? RND_PBO_UNITE : RND_PBO_SUB);
	}
}

static rnd_polyarea_t *pa_rect(rnd_coord_t x1, rnd_coord_t y1, rnd_coord_t x2, rnd_coord_t y2, rnd_coord_t x3, rnd_coord_t y3, rnd_coord_t x4, rnd_coord_t y4)
{
	rnd_vector_t v;
	rnd_pline_t *pl;

	v[0] = x1;
	v[1] = y1;
	pl = rnd_poly_contour_new(v);

	v[0] = x2;
	v[1] = y2;
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	v[0] = x3;
	v[1] = y3;
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	v[0] = x4;
	v[1] = y4;
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	return rnd_poly_from_contour_autoinv(pl);
}

static rnd_polyarea_t *pa_rect_rot(rnd_coord_t x1, rnd_coord_t y1, rnd_coord_t x2, rnd_coord_t y2, rnd_coord_t x3, rnd_coord_t y3, rnd_coord_t x4, rnd_coord_t y4, double cs, double sn)
{
	rnd_vector_t v;
	rnd_pline_t *pl;

	v[0] = x1;
	v[1] = y1;
	rnd_rotate(&v[0], &v[1], 0, 0, cs, sn);
	pl = rnd_poly_contour_new(v);

	v[0] = x2;
	v[1] = y2;
	rnd_rotate(&v[0], &v[1], 0, 0, cs, sn);
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	v[0] = x3;
	v[1] = y3;
	rnd_rotate(&v[0], &v[1], 0, 0, cs, sn);
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	v[0] = x4;
	v[1] = y4;
	rnd_rotate(&v[0], &v[1], 0, 0, cs, sn);
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	return rnd_poly_from_contour_autoinv(pl);
}



static rnd_polyarea_t *pa_ring(rnd_coord_t cx, rnd_coord_t cy, rnd_coord_t d_outer, rnd_coord_t d_inner)
{
	rnd_polyarea_t *pbig, *psmall, *pout;

	pbig = rnd_poly_from_circle(cx, cy, d_outer/2);
	if (d_inner < 100)
		return pbig;
	psmall = rnd_poly_from_circle(cx, cy, d_inner/2);
	rnd_polyarea_boolean_free(pbig, psmall, &pout, RND_PBO_SUB);
	return pout;
}

/* Render a single line (primitive) of a macro into an accumulating polyarea dst */
static int macro_line_render(gedraw_ctx_t *ctx, rnd_polyarea_t **dst, const ge_macro_line_t *l, vtd0_t *param, double rot)
{
	int pol, corners, n, maxrng;
	rnd_coord_t x, y, d, th, x1, y1, x2, y2, od, id, rth, chth, chd, gap, w, h;
	double r, vx, vy, nx, ny, len, cs, sn;
	rnd_polyarea_t *pb, *pc, *pd;
	rnd_vector_t v;
	rnd_pline_t *contour;

	switch(l->op) {
		case GEMO_CIRC:
			pol = param_eval_int(l, 1, param);
			d = param_eval_crd(ctx, l, 2, param);
			x = param_eval_crd(ctx, l, 3, param);
			y = param_eval_crd(ctx, l, 4, param);
			r = param_eval_dbl(l, 5, param) + rot;
			if (r != 0)
				rnd_rotate(&x, &y, 0, 0, cos(-r / RND_RAD_TO_DEG), sin(-r / RND_RAD_TO_DEG));
			pb = rnd_poly_from_circle(x, y, d/2);
			macro_line_render_merge(dst, pb, pol);
			return 0;
		case GEMO_POLY:
			pol = param_eval_int(l, 1, param);
			corners = param_eval_int(l, 2, param);
			r = param_eval_dbl(l, l->operand.used, param) + rot;
			cs = cos(-r / RND_RAD_TO_DEG);
			sn = sin(-r / RND_RAD_TO_DEG);
			v[0] = param_eval_crd(ctx, l, 3, param);
			v[1] = param_eval_crd(ctx, l, 4, param);
			rnd_rotate(&v[0], &v[1], 0, 0, cs, sn);
			contour = rnd_poly_contour_new(v);
			for(n = 1; n < corners; n++) {
				v[0] = param_eval_crd(ctx, l, n*2+3, param);
				v[1] = param_eval_crd(ctx, l, n*2+4, param);
				rnd_rotate(&v[0], &v[1], 0, 0, cs, sn);
				rnd_poly_vertex_include(contour->head->prev, rnd_poly_node_create(v));
			}
			pb = rnd_poly_from_contour_autoinv(contour);
			macro_line_render_merge(dst, pb, pol);
			return 0;
		case GEMO_REGPOLY:
			pol = param_eval_int(l, 1, param);
			corners = param_eval_int(l, 2, param);
			x = param_eval_crd(ctx, l, 3, param);
			y = param_eval_crd(ctx, l, 4, param);
			d = param_eval_crd(ctx, l, 5, param);
			r = param_eval_dbl(l, 6, param) + rot;
			if (r != 0)
				rnd_rotate(&x, &y, 0, 0, cos(-r / RND_RAD_TO_DEG), sin(-r / RND_RAD_TO_DEG));
			pb = pa_regpoly(x, y, corners, d, r);
			macro_line_render_merge(dst, pb, pol);
			return 0;
		case GEMO_MOIRE:
			x = x1 = param_eval_crd(ctx, l, 1, param);
			y = y1 = param_eval_crd(ctx, l, 2, param);
			od = param_eval_crd(ctx, l, 3, param);
			rth = param_eval_crd(ctx, l, 4, param);
			gap = param_eval_crd(ctx, l, 5, param);
			maxrng = param_eval_int(l, 6, param);
			chth = param_eval_crd(ctx, l, 7, param);
			chd = param_eval_crd(ctx, l, 8, param);
			r = param_eval_dbl(l, 9, param) + rot;
			cs = cos(-r / RND_RAD_TO_DEG);
			sn = sin(-r / RND_RAD_TO_DEG);
			if (r != 0) {
				rnd_rotate(&x1, &y1, 0, 0, cs, sn);
				if ((x != 0) || (y != 0))
					rnd_message(RND_MSG_WARNING, "gerb: some gerber viewers (e.g. gerbv) won't render correctly rotated aperture macro primitive Moire when center is not 0;0\n");
			}

			/* crosshair */
			chth /= 2;
			chd /= 2;
			pb = pa_rect_rot(x-chd, y-chth, x+chd, y-chth, x+chd, y+chth, x-chd, y+chth, cs, sn); /* horizontal */
			pc = pa_rect_rot(x-chth, y-chd, x+chth, y-chd, x+chth, y+chd, x-chth, y+chd, cs, sn); /* vertical */
			rnd_polyarea_boolean_free(pb, pc, &pd, RND_PBO_UNITE);

			/* accumulate rings in pd */
			for(n = 0; (n < maxrng) && (od > 100); od -= gap*2 + rth*2, n++) {
				pb = pa_ring(x1, y1, od, od-rth*2);
				pc = pd;
				pd = NULL;
				rnd_polyarea_boolean_free(pb, pc, &pd, RND_PBO_UNITE);
			}

			macro_line_render_merge(dst, pd, 1);
			return 0;
		case GEMO_THERM:
			x = x1 = param_eval_crd(ctx, l, 1, param);
			y = y1 = param_eval_crd(ctx, l, 2, param);
			od = param_eval_crd(ctx, l, 3, param);
			id = param_eval_crd(ctx, l, 4, param);
			th = param_eval_crd(ctx, l, 5, param);
			r = param_eval_dbl(l, 6, param) + rot;

			cs = cos(-r / RND_RAD_TO_DEG);
			sn = sin(-r / RND_RAD_TO_DEG);
			if (r != 0) {
				rnd_rotate(&x1, &y1, 0, 0, cs, sn);
				if ((x != 0) || (y != 0))
					rnd_message(RND_MSG_WARNING, "gerb: some gerber viewers (e.g. gerbv) won't render correctly rotated aperture macro primitive therm when center is not 0;0\n");
			}
			pb = pa_ring(x1, y1, od, id);

			th /= 2;
			pc = pa_rect_rot(x-od, y-th, x+od, y-th, x+od, y+th, x-od, y+th, cs, sn); /* horizontal */
			rnd_polyarea_boolean_free(pb, pc, &pd, RND_PBO_SUB);
			
			pc = pa_rect_rot(x-th, y-od, x+th, y-od, x+th, y+od, x-th, y+od, cs, sn); /* vertical */
			rnd_polyarea_boolean_free(pd, pc, &pb, RND_PBO_SUB);

			macro_line_render_merge(dst, pb, 1);
			return 0;
		case GEMO_LINE_XY:
			pol = param_eval_int(l, 1, param);
			th = param_eval_crd(ctx, l, 2, param);
			x1 = param_eval_crd(ctx, l, 3, param);
			y1 = param_eval_crd(ctx, l, 4, param);
			x2 = param_eval_crd(ctx, l, 5, param);
			y2 = param_eval_crd(ctx, l, 6, param);
			r = param_eval_dbl(l, 7, param) + rot;

			line_xy:;
			if (r != 0) {
				r = -r;
				rnd_rotate(&x1, &y1, 0, 0, cos(r / RND_RAD_TO_DEG), sin(r / RND_RAD_TO_DEG));
				rnd_rotate(&x2, &y2, 0, 0, cos(r / RND_RAD_TO_DEG), sin(r / RND_RAD_TO_DEG));
			}
			vx = x2 - x1;
			vy = y2 - y1;
			nx = -vy;
			ny = vx;
			len = rnd_distance(x1, y1, x2, y2);
			if (len == 0)
				len = 1;
			nx /= len;
			ny /= len;
			pb = pa_rect(
				rnd_round(x1 - nx*th/2.0), rnd_round(y1 - ny*th/2.0),
				rnd_round(x2 - nx*th/2.0), rnd_round(y2 - ny*th/2.0),
				rnd_round(x2 + nx*th/2.0), rnd_round(y2 + ny*th/2.0),
				rnd_round(x1 + nx*th/2.0), rnd_round(y1 + ny*th/2.0));
			macro_line_render_merge(dst, pb, pol);
			return 0;
		case GEMO_LINE_WH:
			pol = param_eval_int(l, 1, param);
			w = param_eval_crd(ctx, l, 2, param);
			h = param_eval_crd(ctx, l, 3, param);
			x = param_eval_crd(ctx, l, 4, param);
			y = param_eval_crd(ctx, l, 5, param);
			r = param_eval_dbl(l, 6, param) + rot;
			x1 = x - w/2; x2 = x + w/2;
			y1 = y2 = y;
			th = h;
			goto line_xy;
		case GEMO_SET:
			n = l->idx;
			r = param_eval_dbl(l, 1, param);
			vtd0_set(param, n-1, r);
			return 0;
	}
	
	return -1;
}

/* Render a macro aperture using a macro def into a polygon */
static camv_any_obj_t *aper_macro_render(ge_aper_t *aper, gedraw_ctx_t *ctx, double rot)
{
	rnd_polyarea_t *pa;
	const ge_aper_macro_t *am = aper->data.macro.am;
	const ge_macro_line_t *l;
	vtd0_t param;

	/* need a copy of the parameters because they may change ($n=... in macro) */
	vtd0_init(&param);
	vtd0_copy(&param, 0, &aper->data.macro.param, 0, aper->data.macro.param.used);

	pa = NULL;
	for(l = am->line1; l != NULL; l = l->next)
		macro_line_render(ctx, &pa, l, &param, rot);

	if (pa == NULL) {
		rnd_message(RND_MSG_WARNING, "aperture macro results in empty render\n");
		return NULL;
	}
	return polyarea2camv(ctx, pa);
}


static camv_any_obj_t *gedraw_render_aper_nohole(camv_design_t *camv, gedraw_ctx_t *ctx, ge_aper_t *aper, rnd_coord_t ox, rnd_coord_t oy, double rot)
{
	camv_any_obj_t *o;
	double rx, ry;
	int n;

	switch(aper->shape) {
		case GEA_CIRC:
			o = (camv_any_obj_t *)camv_line_new();
			o->proto.loc.line = ctx->line; o->proto.loc.col = ctx->col;
			o->line.x1 = o->line.x2 = ox;
			o->line.y1 = o->line.y2 = oy;
			o->line.thick = aper->data.circ.dia;
			return o;

		case GEA_RECT:
			o = (camv_any_obj_t *)camv_poly_new();
			o->proto.loc.line = ctx->line; o->proto.loc.col = ctx->col;
			camv_poly_allocpts(&o->poly, 4);

			rx = ((double)aper->data.rect.xs / 2.0);
			ry = ((double)aper->data.rect.ys / 2.0);

			o->poly.x[3] = rnd_round((double)ox - rx);
			o->poly.y[3] = rnd_round((double)oy - ry);
			o->poly.x[2] = rnd_round((double)ox + rx);
			o->poly.y[2] = rnd_round((double)oy - ry);
			o->poly.x[1] = rnd_round((double)ox + rx);
			o->poly.y[1] = rnd_round((double)oy + ry);
			o->poly.x[0] = rnd_round((double)ox - rx);
			o->poly.y[0] = rnd_round((double)oy + ry);
			if (rot != 0) {
				rot = rot / RND_RAD_TO_DEG;
				for(n = 0; n < 4; n++)
					rnd_rotate(&o->poly.x[n], &o->poly.y[n], ox, oy, cos(rot), sin(rot));
			}

			return o;

		case GEA_OBLONG:
			if (aper->cached == NULL)
				aper->cached = aper_oblong2poly(ctx, aper, rot);
			o = camv_obj_dup(aper->cached);
			o->proto.calls->move(o, ox, oy);
			return o;

		case GEA_POLY:
			if (aper->cached == NULL)
				aper->cached = aper_poly2poly_nohole(ctx, aper, rot);
			o = camv_obj_dup(aper->cached);
			o->proto.calls->move(o, ox, oy);
			return o;

		case GEA_MACRO:
			if (aper->cached == NULL)
				aper->cached = aper_macro_render(aper, ctx, rot);
			if (aper->cached == NULL) /* aperture creation may fail on empty aper */
				return NULL;
			o = camv_obj_dup(aper->cached);
			o->proto.calls->move(o, ox, oy);
			return o;
	}
	return NULL;
}

static camv_any_obj_t *gedraw_render_aper_hole(camv_design_t *camv, gedraw_ctx_t *ctx, ge_aper_t *aper, rnd_coord_t ox, rnd_coord_t oy, double rot)
{
	camv_any_obj_t *o;
	rnd_polyarea_t *pdiff, *ps, *ph;
	double rx, ry;

TODO("Generate and cache these and dup with offset instead of reconstructing all the time");
	switch(aper->shape) {
		case GEA_CIRC:
			if (aper->cached == NULL) {
				ps = rnd_poly_from_circle(0, 0, aper->data.circ.dia/2);
				ph = rnd_poly_from_circle(0, 0, aper->hole/2);
				rnd_polyarea_boolean_free(ps, ph, &pdiff, RND_PBO_SUB);
				aper->cached = polyarea2camv(ctx, pdiff);
			}
			o = camv_obj_dup(aper->cached);
			o->proto.calls->move(o, ox, oy);
			return o;


		case GEA_RECT:
			if (aper->cached == NULL) {
				rx = ((double)aper->data.rect.xs / 2.0);
				ry = ((double)aper->data.rect.ys / 2.0);
				ps = rnd_poly_from_rect(-rx, +rx, -ry, +ry);
				ph = rnd_poly_from_circle(0, 0, aper->hole/2);
				rnd_polyarea_boolean_free(ps, ph, &pdiff, RND_PBO_SUB);
				aper->cached = polyarea2camv(ctx, pdiff);
			}
			o = camv_obj_dup(aper->cached);
			o->proto.calls->move(o, ox, oy);
			return o;

		case GEA_OBLONG:
			if (aper->cached == NULL)
				aper->cached = aper_oblong2poly(ctx, aper, rot); /* handles the hole */
			o = camv_obj_dup(aper->cached);
			o->proto.calls->move(o, ox, oy);
			return o;

		case GEA_POLY:
			if (aper->cached == NULL)
				aper->cached = aper_poly2poly_hole(ctx, aper, rot);
			o = camv_obj_dup(aper->cached);
			o->proto.calls->move(o, ox, oy);
			return o;

		case GEA_MACRO:
			return gedraw_render_aper_nohole(camv, ctx, aper, ox, oy, rot);
	}

	return NULL;
}

/* render an aperture at x;y into a camv drawing primitive (may be a grp).
   Rot is in deg in gerb notation; x;y are in gerber notation. */
static camv_any_obj_t *gedraw_render_aper(camv_design_t *camv, gedraw_ctx_t *ctx, ge_aper_t *aper, rnd_coord_t ox, rnd_coord_t oy, double rot)
{
	if (aper->hole == 0)
		return gedraw_render_aper_nohole(camv, ctx, aper, ox, oy, rot);
	else
		return gedraw_render_aper_hole(camv, ctx, aper, ox, oy, rot);
}


/*** gerber state machine ***/

static int gedraw_aper_store(gedraw_ctx_t *ctx, long aid, const ge_aper_t *aper)
{
	if (!ctx->aper_inited) {
		htip_init(&ctx->aper, longhash, longkeyeq);
		ctx->aper_inited = 1;
	}

	if (htip_has(&ctx->aper, aid)) { 
		static int explained = 0;
		rnd_message(RND_MSG_WARNING, "gedraw_aper_store: aperture %ld is re-defined\n", aid);
		if (!explained) {
			rnd_message(RND_MSG_WARNING, "This is not necessarily an error, but is probably bad\npractice and is dangerous: some gerber viewers will break on this\n", aid);
			explained = 1;
		}
	}

	htip_set(&ctx->aper, aid, (ge_aper_t *)aper); /* can remember by pointer because it's coming from the VM code */
	return 0;
}

static ge_aper_t *gedraw_aper_get(gedraw_ctx_t *ctx, long aid)
{
	if (!ctx->aper_inited)
		return NULL;
	return htip_get(&ctx->aper, aid);
}

typedef struct {
	rnd_coord_t x, y, i, j;
	gedraw_cmd_t op;
	long aper;
	ge_interp_t interp;
	ge_quadr_t quadr;
	long sets; /* number of set instructions within the block */
	long crds; /* number of coord instructions within the block */
	unsigned relcrd:1;
	unsigned relat:1;
	unsigned poly:1;
	unsigned clearing:1;
} gerbst_t;


static void gedraw_poly_close(camv_design_t *camv, gedraw_ctx_t *ctx, camv_layer_t *ly)
{
	rnd_cardinal_t n, i;
	camv_any_obj_t *o;

	if (ctx->contour.used == 0)
		return;
	if (ctx->contour.used < 6) {
		rnd_message(RND_MSG_ERROR, "gedraw_poly_close: contour with too few vertices (%d)\n", ctx->contour.used/2);
		ctx->contour.used = 0;
		return;
	}
	if (!ctx->poly_closed)
		rnd_message(RND_MSG_ERROR, "gedraw_poly_close: contour is not closed\n");

	o = (camv_any_obj_t *)camv_poly_new();
	o->proto.loc.line = ctx->line; o->proto.loc.col = ctx->col;
	camv_poly_allocpts(&o->poly, ctx->contour.used/2);
	for(n = 0, i = 0; i < ctx->contour.used; n++) {
		o->poly.x[n] = ctx->contour.array[i++];
		o->poly.y[n] = ctx->contour.array[i++];
	}
	camv_obj_add_to_layer(ly, o);
	ctx->contour.used = 0;
	ctx->poly_closed = 0;
}

static void gedraw_poly_append(gedraw_ctx_t *ctx, rnd_coord_t x, rnd_coord_t y)
{
	if ((ctx->contour.used > 5) && (ctx->contour.array[0] == x) && (ctx->contour.array[1] == y)) {
		/* arrived back at the start, close and avoid dupliocate */
		ctx->poly_closed = 1;
		return;
	}

	if ((ctx->contour.used > 1) && (ctx->contour.array[ctx->contour.used-2] == x) && (ctx->contour.array[ctx->contour.used-1] == y)) {
		/* new point is the same as last: skip */
		return;
	}

	ctx->poly_closed = 0;
	vtc0_append(&ctx->contour, x);
	vtc0_append(&ctx->contour, y);
}

#define SETCRD(dst, src) \
	do { \
		if (curr->relcrd) (dst) += (src); \
		else (dst) = (src); \
	} while(0)

static void gedraw_do_pol_(camv_design_t *camv, const camv_layer_t *main_layer, camv_layer_t **ly, int clearing)
{
	*ly = camv_layer_new();
	(*ly)->sub = 1;
	(*ly)->clearing = clearing;
	(*ly)->color = main_layer->color;
	camv->loader.ly = camv_sublayer_append_after(camv, *ly, camv->loader.ly);
}

static void gedraw_do_pol(camv_design_t *camv, const camv_layer_t *main_layer, camv_layer_t **ly, gerbst_t *curr, gerbst_t *last)
{
	if (curr->clearing == last->clearing)
		return; /* optimization: do not create a new layer with the same polarity */
	gedraw_do_pol_(camv, main_layer, ly, curr->clearing);
}

static rnd_polyarea_t *pa_rect_side(gerbst_t *c1, gerbst_t *c2, double dx1, double dy1, double dx2, double dy2)
{
	rnd_vector_t v;
	rnd_pline_t *pl;

	v[0] = rnd_round((double)c1->x+dx1);
	v[1] = rnd_round((double)c1->y+dy1);
	pl = rnd_poly_contour_new(v);

	v[0] = rnd_round((double)c2->x+dx1);
	v[1] = rnd_round((double)c2->y+dy1);
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	v[0] = rnd_round((double)c2->x+dx2);
	v[1] = rnd_round((double)c2->y+dy2);
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	v[0] = rnd_round((double)c1->x+dx2);
	v[1] = rnd_round((double)c1->y+dy2);
	rnd_poly_vertex_include(pl->head->prev, rnd_poly_node_create(v));

	return rnd_poly_from_contour_autoinv(pl);
}

static void gedraw_do_draw_line(camv_design_t *camv, gedraw_ctx_t *ctx, camv_layer_t *ly, gerbst_t *curr, gerbst_t *last)
{
	camv_any_obj_t *o;
	ge_aper_t *aper;
	rnd_polyarea_t *slice, *sum, *pres;
	double rx, ry;

	if (curr->poly) {
		gedraw_poly_append(ctx, ctx->ox + last->x, ctx->oy + last->y);
		gedraw_poly_append(ctx, ctx->ox + curr->x, ctx->oy + curr->y);
		return;
	}

	aper = gedraw_aper_get(ctx, curr->aper);
	if (aper == NULL) {
		rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW line: no aperture\n");
		return;
	}

	switch(aper->shape) {
		case GEA_CIRC:
			o = (camv_any_obj_t *)camv_line_new();
			o->proto.loc.line = ctx->line; o->proto.loc.col = ctx->col;
			o->line.x1 = ctx->ox + last->x; o->line.y1 = ctx->oy + last->y;
			o->line.x2 = ctx->ox + curr->x; o->line.y2 = ctx->oy + curr->y;
			o->line.thick = aper->data.circ.dia;
			camv_obj_add_to_layer(ly, o);
			break;
		case GEA_RECT:
			rx = ((double)aper->data.rect.xs / 2.0);
			ry = ((double)aper->data.rect.ys / 2.0);

			if ((last->x == curr->x) || (last->y == curr->y)) { /* cheap corner case: axis aligned line or zero length line */
				double x1, y1, x2, y2;

				x1 = MIN(last->x, curr->x);
				x2 = MAX(last->x, curr->x);
				y1 = MIN(last->y, curr->y);
				y2 = MAX(last->y, curr->y);

				o = (camv_any_obj_t *)camv_poly_new();
				o->proto.loc.line = ctx->line; o->proto.loc.col = ctx->col;
				camv_poly_allocpts(&o->poly, 4);
				o->poly.x[0] = rnd_round(ctx->ox + x1 - rx);
				o->poly.y[0] = rnd_round(ctx->oy + y1 - ry);
				o->poly.x[1] = rnd_round(ctx->ox + x2 + rx);
				o->poly.y[1] = rnd_round(ctx->oy + y1 - ry);
				o->poly.x[2] = rnd_round(ctx->ox + x2 + rx);
				o->poly.y[2] = rnd_round(ctx->oy + y2 + ry);
				o->poly.x[3] = rnd_round(ctx->ox + x1 - rx);
				o->poly.y[3] = rnd_round(ctx->oy + y2 + ry);
				camv_obj_add_to_layer(ly, o);
			}
			else {
				sum = rnd_poly_from_rect(ctx->ox + last->x-rx, ctx->ox + last->x+rx, ctx->oy + last->y-ry, ctx->oy + last->y+ry);
				slice = rnd_poly_from_rect(ctx->ox + curr->x-rx, ctx->ox +  curr->x+rx, ctx->oy + curr->y-ry, ctx->oy + curr->y+ry);
				rnd_polyarea_boolean_free(sum, slice, &pres, RND_PBO_UNITE); sum = pres;

				slice = pa_rect_side(last, curr, ctx->ox-rx, ctx->oy-ry, ctx->ox+rx, ctx->oy-ry); /* top horiz */
				rnd_polyarea_boolean_free(sum, slice, &pres, RND_PBO_UNITE); sum = pres;

				slice = pa_rect_side(last, curr, ctx->ox+rx, ctx->oy-ry, ctx->ox+rx, ctx->oy+ry); /* right vert */
				rnd_polyarea_boolean_free(sum, slice, &pres, RND_PBO_UNITE); sum = pres;

				slice = pa_rect_side(last, curr, ctx->ox-rx, ctx->oy+ry, ctx->ox+rx, ctx->oy+ry); /* bottom horiz */
				rnd_polyarea_boolean_free(sum, slice, &pres, RND_PBO_UNITE); sum = pres;

				slice = pa_rect_side(last, curr, ctx->ox-rx, ctx->oy-ry, ctx->ox-rx, ctx->oy+ry); /* left vert */
				rnd_polyarea_boolean_free(sum, slice, &pres, RND_PBO_UNITE); sum = pres;

				o = polyarea2camv(ctx, sum);
				camv_obj_add_to_layer(ly, o);
			}
			break;
		default:
			rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW line: linear interpolation is permitted only using round or rectangle apertures\n");
	}

}

static void gedraw_do_draw_arc(camv_design_t *camv, gedraw_ctx_t *ctx, camv_layer_t *ly, gerbst_t *curr, gerbst_t *last, int is_cw)
{
	camv_arc_t *arc;
	ge_aper_t *aper;
	rnd_coord_t cx, cy, r1, r2;
	double ang1, ang2, delta;

	if (!curr->poly) {
		aper = gedraw_aper_get(ctx, curr->aper);
		if (aper == NULL) {
			rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW arc: invalid aperture %ld\n", curr->aper);
			return;
		}

		if (aper->shape != GEA_CIRC) {
			rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW arc: circular interpolation is permitted only with filled circle aperture\n");
			return;
		}
	}
	else
		aper = NULL; /* in poly mode no need to use the aperture */

	if (curr->quadr == GEQ_SINGLE) {
		rnd_coord_t i = curr->i, j = curr->j;
		/* it seems I;J sign is ignored and reconstructed from last->curr movement */
		if (i < 0) {
			rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW arc: single quadrant I should be positive\n");
			i = -i;
		}
		if (j < 0) {
			rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW arc: single quadrant J should be positive\n");
			j = -j;
		}
		if (curr->x < last->x) i = -i;
		if (curr->y < last->y) j = -j;
		cx = last->x + i;
		cy = last->y + j;
	}
	else {
		/* in multi-quadrant mode I and J are just signed */
		cx = last->x + curr->i;
		cy = last->y + curr->j;
	}

	ang1 = atan2(last->y - cy, last->x - cx) * RND_RAD_TO_DEG;
	ang2 = atan2(curr->y - cy, curr->x - cx) * RND_RAD_TO_DEG;

	if (ang1 < 0) ang1 += 360;
	if (ang2 < 0) ang2 += 360;

	if (curr->quadr == GEQ_SINGLE) {
		if (is_cw) {
			delta = ang2 - ang1;
			if (delta > 90)
				delta -= 360;
/*
TODO: test this code when the error below triggers
			if (delta < 0.0)
				delta = -delta;
*/
			if ((delta < 0.0) || (delta > 90.0))
				rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW arc: single quadrant arc with angle span out of range (CW)\n");
		}
		else {
			delta = ang1 - ang2;
			if (delta < -90)
				delta += 360;
			if (delta > 90)
				delta = -(360 - delta);
/*			else if (delta > 0)
				delta = -delta; TODO: figure what to do here */
			if ((delta < -90.0) || (delta > 0.0))
				rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW arc: single quadrant arc with angle span out of range (CCW)\n");
			delta = -delta;
		}
	}
	else {
		if (is_cw) { /* CW */
			delta = ang1 - ang2;
			if (delta <= 0) {
				delta = 360 + delta;
			}
			delta = -delta;
		}
		else { /* CCW */
			delta = ang2 - ang1;
			if (delta <= 0)
				delta = 360 + delta;
		}
	}

	r1 = rnd_distance(last->x, last->y, cx, cy);
	r2 = rnd_distance(curr->x, curr->y, cx, cy);
	if (RND_ABS(r1 - r2) > 2*ctx->acceptable_error)
		rnd_message(RND_MSG_ERROR, "gedraw_do: DRAW arc: not circular: radius %.6mm vs. %.6mm\n", (rnd_coord_t)rnd_round(r1), (rnd_coord_t)rnd_round(r2));

	if (curr->poly) {
		double a, a1, a2, dlt, step;
		
		if (delta == 0) /* zero length arc means no modification to a poly contour */
			return;

		if ((delta < 0) && (ang2 > ang1)) ang2 -= 360;
		if ((delta > 0) && (ang1 > ang2)) ang1 -= 360;

		dlt = delta / RND_RAD_TO_DEG;
		a1 = ang1 / RND_RAD_TO_DEG;
		a2 = ang2 / RND_RAD_TO_DEG;
		step = (RND_COORD_TO_MM(r1)*8.0);
		if (step < 4)
			step = 4;
		step = dlt / step;
		for(a = a1; (step < 0) ? (a > a2) : (a < a2); a += step)
			gedraw_poly_append(ctx, rnd_round(ctx->ox + cx + cos(a) * r1), rnd_round(ctx->oy + cy + sin(a) * r1));

		gedraw_poly_append(ctx, ctx->ox + curr->x, ctx->oy + curr->y); /* make sure we arrive at the endpoint requested */
		return;
	}

	arc = camv_arc_new();
	arc->loc.line = ctx->line; arc->loc.col = ctx->col;
	arc->cx = ctx->ox + cx; arc->cy = ctx->oy + cy; arc->r = r1;
	arc->thick = aper == NULL ? 0 : aper->data.circ.dia;
	arc->start = 180 - ang1;
	arc->delta = -delta;
	camv_obj_add_to_layer(ly, (camv_any_obj_t *)arc);
}

static void gedraw_do_draw(camv_design_t *camv, gedraw_ctx_t *ctx, camv_layer_t *ly, gerbst_t *curr, gerbst_t *last)
{
	/* DO after interpolation set shall draw only if there was any coord command; test case: tests/l3*.gbr */
	if (curr->crds == 0)
		return;

	switch(curr->interp) {
		case GEI_LIN: gedraw_do_draw_line(camv, ctx, ly, curr, last); break;
		case GEI_CW:  gedraw_do_draw_arc(camv, ctx, ly, curr, last, 1); break;
		case GEI_CCW: gedraw_do_draw_arc(camv, ctx, ly, curr, last, 0); break;
	}
	curr->i = curr->j = 0;
}

static void gedraw_do_move(camv_design_t *camv, gedraw_ctx_t *ctx, camv_layer_t *ly, gerbst_t *curr, gerbst_t *last)
{
	/* auto-close poly contour on _real_ move; a move that stats in place is not
	   really a move and shouldn't break the contour - this is how the reference
	   viewer v2018.11b works */
	if ((curr->poly) &&  ((curr->x != last->x) || (curr->y != last->y))) {
		if (ctx->contour.used > 2)
			gedraw_poly_close(camv, ctx, ly);
		else
			ctx->contour.used = 0; /* do not throw warnings in gedraw_poly_close() but make sure the poly is clean */
		return;
	}
	/* normal draw: do nothing, coords are getting updated on the curr-to-last copy */
}

static void gedraw_do_flash(camv_design_t *camv, gedraw_ctx_t *ctx, camv_layer_t *ly, gerbst_t *curr, gerbst_t *last)
{
	camv_any_obj_t *o;
	ge_aper_t *aper = gedraw_aper_get(ctx, curr->aper);

	if (aper == NULL)
		return;
	o = gedraw_render_aper(camv, ctx, aper, curr->x + ctx->ox, curr->y + ctx->oy, 0);
	if (o != NULL)
		camv_obj_add_to_layer(ly, o);
	else
		rnd_message(RND_MSG_ERROR, "gedraw_do: failed to FLASH aperture %ld\n", curr->aper);
}

static void gedraw_do(camv_design_t *camv, gedraw_ctx_t *ctx, const camv_layer_t *main_layer, camv_layer_t **ly, gerbst_t *curr, gerbst_t *last)
{
	switch(curr->op) {
		case GEC_SET_POLCLR: gedraw_do_pol(camv, main_layer, ly, curr, last); break;
		case GEC_DRAW:       gedraw_do_draw(camv, ctx, *ly, curr, last); break;
		case GEC_MOVE:       gedraw_do_move(camv, ctx, *ly, curr, last); break;
		case GEC_FLASH:      gedraw_do_flash(camv, ctx, *ly, curr, last); break;

		case GEC_invalid:
			if (!curr->sets) {
				/* Empty command is normal in case of Orcad Layout Plus 9.2 exported gerbers */
				/*rnd_message(RND_MSG_WARNING, "gedraw_do: invalid opcode %d (uninitialized #1; probably an empty command sequence terminated by a '*')\n", curr->op);*/
			}
			/* else the command sequence did set some states, so it is okay */
			break;
		default:
			rnd_message(RND_MSG_ERROR, "gedraw_do: invalid opcode %d (unrecognized #1)\n", curr->op);
	}
	memcpy(last, curr, sizeof(gerbst_t));
	curr->sets = curr->crds = 0;
}

static int gedraw_camv_(camv_design_t *camv, gedraw_ctx_t *ctx, const camv_layer_t *main_layer, camv_layer_t **layer, size_t *from, int in_sr, gerbst_t *curr, gerbst_t *last)
{
	size_t n;
	camv_layer_t *ly = *layer;

	for(n = *from; n < ctx->code.used; n++) {
		gedraw_inst_t *i = &ctx->code.array[n]; /* need to retrieve it each iteration because n may jump after SR */
		ctx->line = i->line; ctx->col = i->col;
		switch(i->cmd) {
			case GEC_MACRO_DEF:  break; /* shouldn't happen */

			case GEC_SET_X:      SETCRD(curr->x, i->data.coord); curr->sets++; curr->crds++; break;
			case GEC_SET_Y:      SETCRD(curr->y, i->data.coord); curr->sets++; curr->crds++; break;
			case GEC_SET_I:      SETCRD(curr->i, i->data.coord); curr->sets++; curr->crds++; break;
			case GEC_SET_J:      SETCRD(curr->j, i->data.coord); curr->sets++; curr->crds++; break;
			case GEC_SET_RELAT:  curr->relat = i->data.on; curr->sets++; break;
			case GEC_SET_RELCRD: curr->relcrd = i->data.on; curr->sets++; break;
			case GEC_SET_INTERP: curr->interp = i->data.interp; curr->sets++; break;
			case GEC_SET_QUADR:  curr->quadr = i->data.quadr; curr->sets++; break;

			case GEC_SET_POLY:
				if ((curr->poly) && (!i->data.on))
					gedraw_poly_close(camv, ctx, ly);
				curr->poly = i->data.on;
				curr->sets++;
				break;

			case GEC_APER_DEF:
				if (gedraw_aper_store(ctx, i->data.aper.id, &i->data.aper) != 0)
					return -1;
				break;

			case GEC_APER_SEL:
				curr->aper = i->data.id;
				if (gedraw_aper_get(ctx, curr->aper) == NULL) {
					rnd_message(RND_MSG_ERROR, "gedraw_camv: selected invalid (undefined) aperture %ld\n", curr->aper);
					return -1;
				}
				break;

			case GEC_SET_POLCLR: curr->op = i->cmd; curr->clearing = i->data.on; break;

			case GEC_DRAW:
			case GEC_MOVE:
			case GEC_FLASH:      curr->op = i->cmd; break;

			case GEC_DO:         gedraw_do(camv, ctx, main_layer, &ly, curr, last); break;

			case GEC_invalid:
				rnd_message(RND_MSG_ERROR, "gedraw_camv: invalid opcode %d (uninitialized #2) \n", i->cmd);
				break;

			case GEC_STEPREP:
				if (!in_sr) {
					int nx, ny, vx, vy, was_clearing;
					size_t srn;

					was_clearing = ly->clearing;
					vx = i->data.steprep.x;
					vy = i->data.steprep.y;
					if ((vx < 1) && (vy < 1)) {
						n++;
						goto quit_sr; /* a new SR terminates an already open one */
					}

					if (vx < 1) {
						rnd_message(RND_MSG_ERROR, "gedraw_camv: invalid SR x\n");
						return -1;
					}
					if (vy < 1) {
						rnd_message(RND_MSG_ERROR, "gedraw_camv: invalid SR y\n");
						return -1;
					}
					for(nx = 0; nx < vx; nx++) {
						for(ny = 0; ny < vy; ny++) {
							gedraw_ctx_t srctx;
							gerbst_t srcurr, srlast;

							memcpy(&srctx, ctx, sizeof(gedraw_ctx_t));
							memcpy(&srcurr, curr, sizeof(gerbst_t));
							memcpy(&srlast, last, sizeof(gerbst_t));
							srctx.ox = ctx->ox + nx * i->data.steprep.i;
							srctx.oy = ctx->oy + ny * i->data.steprep.j;
							srn = n+1;
							if (gedraw_camv_(camv, &srctx, main_layer, &ly, &srn, 1, &srcurr, &srlast) != 0)
								return -1;
							if (was_clearing != ly->clearing) {
								/* if last layer doesn't match initial polarity, the only way to restore the original polarity is to create a new sublayer */
								gedraw_do_pol_(camv, main_layer, &ly, was_clearing);
							}
						}
					}
					n = srn-1; /* continue with the next instruction after the block */
				}
				else {
					if ((i->data.steprep.x <= 1) && (i->data.steprep.y <= 1))
						n++; /* closing SR, won't start a loop, skip it */
					goto quit_sr; /* a new SR terminates an already open one */
				}
		}
	}

	quit_sr:;
	*layer = ly;
	*from = n;
	return 0;
}

static int gedraw_camv(camv_design_t *camv, gedraw_ctx_t *ctx, const char *name)
{
	size_t from = 0;
	camv_layer_t *ly = camv_layer_new();
	gerbst_t curr, last;
	int res;

	ly->name = rnd_strdup(name);
	camv_layer_invent_color(camv, ly); /* gerber files do not have layer color */
	camv->loader.ly = camv_layer_append_to_design(camv, ly);

	memset(&curr, 0, sizeof(curr));
	curr.interp = GEI_LIN;
	curr.quadr = GEQ_INVALID;
	memcpy(&last, &curr, sizeof(last));

	res = gedraw_camv_(camv, ctx, ly, &ly, &from, 0, &curr, &last);
	ctx->line = ctx->col = -1;
	return res;
}

static int ge_getchar(geparse_ctx_t *ctx)
{
	return fgetc((FILE *)ctx->user_data);
}

int camv_gerb_load(camv_design_t *camv, const char *fn, FILE *f)
{
	geparse_ctx_t ctx;
	ge_parse_res_t res;
	int retv;

	memset(&ctx, 0, sizeof(ctx));
	ctx.get_char = ge_getchar;
	ctx.user_data = f;
	while((res = geparse(&ctx)) == GEP_NEXT) ;
	if (res == GEP_ERROR) {
		rnd_message(RND_MSG_ERROR, "parse error at %ld:%ld: %s\n", ctx.line, ctx.col, ctx.errmsg);
		return -1;
	}

	if (ctx.unit == GEU_INCH)
		ctx.draw.acceptable_error = RND_INCH_TO_COORD((double)pow(0.1, ctx.cfmt_fra));
	else
		ctx.draw.acceptable_error = RND_MM_TO_COORD((double)pow(0.1, ctx.cfmt_fra));

	ctx.draw.aper_unit = ctx.unit;
	retv = gedraw_camv(camv, &ctx.draw, fn);

	if (ctx.draw.aper_inited) {
		htip_entry_t *e;
		for(e = htip_first(&ctx.draw.aper); e != NULL; e = htip_next(&ctx.draw.aper, e)) {
			ge_aper_t *aper = e->value;
			if (aper->cached != NULL)
				camv_obj_free(aper->cached);
		}
		htip_uninit(&ctx.draw.aper);
	}

	geparse_free(&ctx);
	return retv;
}

static int camv_gerb_test_load(camv_design_t *camv, const char *fn, FILE *f)
{
	char *line, line_[1024];
	int bad = 0;
	
	while((line = fgets(line_, sizeof(line_), f)) != NULL) {
		while(isspace(*line)) line++;
		if (strncmp(line, "M02*", 4) == 0)
			return 1;
		if (strncmp(line, "%MOIN", 5) == 0)
			return 1;
		if (strncmp(line, "%MOMM", 5) == 0)
			return 1;
		if ((strncmp(line, "%ADD", 4) == 0) && (isdigit(line[4])))
			return 1;
		bad++;
		if (bad > 64)
			return 0;
	}
	return 0;
}

static camv_io_t io_gerb = {
	"gerber", 90,
	camv_gerb_test_load,
	camv_gerb_load,
	NULL,
};

int pplg_check_ver_import_gerb(int ver_needed) { return 0; }

void pplg_uninit_import_gerb(void)
{
	camv_io_unreg(&io_gerb);
}

int pplg_init_import_gerb(void)
{
	camv_io_reg(&io_gerb);
	return 0;
}
