/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - easyeda file format support
 *  Copyright (C) 2024 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 Entrust Fund in 2024)
 *
 *  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/sch-rnd
 *    contact lead developer: http://www.repo.hu/projects/sch-rnd/contact.html
 *    mailing list: http://www.repo.hu/projects/sch-rnd/contact.html
 */

/* high level for the 'std' format - included from read.c; drawing objects */

#define REQ_ARGC_(nd, op, num, errstr, errstmt) \
do { \
	if (nd->type != GDOM_ARRAY) { \
		error_at(ctx, nd, ("%s: object node is not an array\n", errstr)); \
		errstmt; \
	} \
	if (nd->value.array.used op num) { \
		error_at(ctx, nd, ("%s: not enough fields: need at least %ld, got %ld\n", errstr, (long)num, nd->value.array.used)); \
		errstmt; \
	} \
} while(0)

#define REQ_ARGC_GTE(nd, num, errstr, errstmt) REQ_ARGC_((nd), <, (num), (errstr), errstmt)
#define REQ_ARGC_EQ(nd, num, errstr, errstmt)  REQ_ARGC_((nd), !=, (num), (errstr), errstmt)

/* call these only after a REQ_ARGC_* as it won't do bound check */
#define GET_ARG_STR(dst, nd, num, errstr, errstmt) \
do { \
	gdom_node_t *__tmp__ = nd->value.array.child[num]; \
	if ((__tmp__->type == GDOM_DOUBLE) && (__tmp__->value.dbl == -1)) {\
		dst = NULL; \
	} \
	else { \
		if (__tmp__->type != GDOM_STRING) { \
			error_at(ctx, nd, ("%s: wrong argument type for arg #%ld (expected string)\n", errstr, (long)num)); \
			errstmt; \
		} \
		dst = __tmp__->value.str; \
	} \
} while(0)

#define GET_ARG_DBL(dst, nd, num, errstr, errstmt) \
do { \
	gdom_node_t *__tmp__ = nd->value.array.child[num]; \
	if (__tmp__->type != GDOM_DOUBLE) { \
		error_at(ctx, nd, ("%s: wrong argument type for arg #%ld (expected double)\n", errstr, (long)num)); \
		errstmt; \
	} \
	dst = __tmp__->value.dbl; \
} while(0)

#define GET_ARG_HASH(dst, nd, num, errstr, errstmt) \
do { \
	gdom_node_t *__tmp__ = nd->value.array.child[num]; \
	if (__tmp__->type != GDOM_HASH) { \
		error_at(ctx, nd, ("%s: wrong argument type for arg #%ld; expected a hash\n", errstr, (long)num)); \
		errstmt; \
	} \
	dst = __tmp__; \
} while(0)

#define GET_ARG_ARRAY(dst, nd, num, errstr, errstmt) \
do { \
	gdom_node_t *__tmp__ = nd->value.array.child[num]; \
	if (__tmp__->type != GDOM_ARRAY) { \
		error_at(ctx, nd, ("%s: wrong argument type for arg #%ld; expected an array\n", errstr, (long)num)); \
		errstmt; \
	} \
	dst = __tmp__; \
} while(0)

#define CHK_ARG_KW(nd, num, kwval, errstr, errstmt) \
do { \
	const char *__str__;\
	GET_ARG_STR(__str__, nd, num, errstr, errstmt); \
	if (strcmp(__str__, kwval) != 0) { \
		error_at(ctx, nd, ("%s: arg #%ld must be '%s' but is '%s'\n", errstr, (long)num, kwval, __str__)); \
		errstmt; \
	}\
} while(0)

/* Look up style by name and return whether the given style fills */
static int easypro_style_filled(read_ctx_t *ctx, gdom_node_t *obj, const char *sty)
{
	htsi_entry_t *e = htsi_getentry(&ctx->pro_pen_fill, sty);
	if (e == NULL) {
		error_at(ctx, obj, ("easypro_style_filled: undefined style '%s'\n", sty));
		return 0;
	}
	return e->value;
}

/*** parse obj subtrees (lines in the file) ***/

/* Add new symbol (e.g. new slot (PART) of a symbol) in symtab and
   create the symbol group for child objects */
static void easypro_add_sym(read_ctx_t *ctx, const char *name, csch_cgrp_t **parent)
{
		csch_source_arg_t *src;

		*parent = csch_cgrp_alloc(ctx->sheet, &ctx->sheet->direct, csch_oid_new(ctx->sheet, &ctx->sheet->direct));
		src = csch_attrib_src_c(ctx->fn, 0, 0, NULL); /* whole-file context, no need to set location */
		csch_cobj_attrib_set(ctx->sheet, *parent, CSCH_ATP_HARDWIRED, "role", "symbol", src);

		if (name != NULL)
			htsp_insert(ctx->pro_symtab, rnd_strdup(name), *parent);

		if (ctx->symtab_key != NULL) {
			/* also save a hash-like name link, 1.0 sheets sometime refer using that */
			htsp_insert(ctx->pro_symtab, rnd_strdup(ctx->symtab_key), *parent);
			ctx->symtab_key = NULL;
		}

	ctx->pro_last = *parent;
}

/* Announces a slot (even for single-slot parts); slots are full copies */
static int easypro_parse_part(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t **parent)
{
	gdom_node_t *bbox, *coords;
	REQ_ARGC_GTE(obj, 3, "easypro_parse_part", return -1);
	GET_ARG_HASH(bbox, obj, 2, "easypro_parse_part: bbox", return -1);

	ctx->pro_slot++;
	if (ctx->pro_symtab != NULL) { /* create a new symbol group for the new part */
		const char *name;
		GET_ARG_STR(name, obj, 1, "easypro_parse_part: name", return -1);
		easypro_add_sym(ctx, name, parent);
	}
	else if (ctx->pro_want_slot == -1) { /* create all slots, offseted by bbox */
		double x1, x2/*, y1, y2*/;

		coords = gdom_hash_get(bbox, easy_BBOX);
		if (coords->type != GDOM_ARRAY) {
			error_at(ctx, coords, ("PART/BBOX needs to be an array\n"));
			return -1;
		}

		REQ_ARGC_GTE(coords, 4, "easypro_parse_part PART/BBOX", return -1);
		GET_ARG_DBL(x1, coords, 0, "easypro_parse_part PART/BBOX: x1", return -1);
/*		GET_ARG_DBL(y1, coords, 1, "easypro_parse_part PART/BBOX: y1", return -1);*/
		GET_ARG_DBL(x2, coords, 2, "easypro_parse_part PART/BBOX: x2", return -1);
/*		GET_ARG_DBL(y2, coords, 3, "easypro_parse_part PART/BBOX: y2", return -1);*/

		/* when reading all slots they would badly overlap - shift by bbox */
		if (ctx->pro_slot > 1)
			ctx->alien.ox += (x2-x1)*2;
	}
	else if (ctx->pro_slot > ctx->pro_want_slot) /* read only the first */
		ctx->pro_stop = 1; /* don't read any more slots */

	ctx->pro_last = *parent;
	return 0;
}

/* PIN, e19, 1, null, 40, 0, 20, 180, null, 0,  0
   obj  ID   ?  ?     x   y  len rot  ?    gfx  ?
   rot: 0   = left *---   (* is the startpoint)
        90  = down
        180 = right ---*
        270 = up
   gfx: 2 = "not" (circle r=3 on the start side)
*/
static int easypro_parse_pin(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	double x, y, len, rot, gfx;
	double sx, sy, ex, ey, dx = 0, dy = 0; /* start, end, direction */
	int want_circ = 0, gfxi;
	static const double notr = 3;
	csch_source_arg_t *src;
	csch_cgrp_t *term;

	REQ_ARGC_GTE(obj, 11, "easypro_parse_pin", return -1);

	GET_ARG_DBL(x,   obj, 4,   "easypro_parse_pin: coord x", return -1);
	GET_ARG_DBL(y,   obj, 5,   "easypro_parse_pin: coord y", return -1);
	GET_ARG_DBL(len, obj, 6,   "easypro_parse_pin: length", return -1);
	GET_ARG_DBL(rot, obj, 7,   "easypro_parse_pin: rotation", return -1);
	GET_ARG_DBL(gfx, obj, 9,   "easypro_parse_pin: gfx", return -1);

	/* figure rotation and direction */
	switch((int)rot) {
		case -1: /* null */
		case 360:
		case 0:   dx = +1; break;
		case 90:  dy = +1; break;
		case 180: dx = -1; break;
		case 270: dy = -1; break;
		default:
			error_at(ctx, obj, ("easypro_parse_pin: invalid rotation angle %f\n", rot));
			return -1;
	}

	/* figure graphics */
	gfxi = gfx;
	if (gfxi & 2) { want_circ = 1; gfxi &= ~2; }
	if (gfxi != 0)
		error_at(ctx, obj, ("easypro_parse_pin: gfx bits unhandled: %x\n(please report this bug among with the file)\n", gfxi));

	/* figure endpoints */
	sx = x; sy = y;
	ex = sx + dx * len; ey = sy + dy * len;

	if (want_circ) {
		ex -= dx * notr*2;
		ey -= dy * notr*2;
	}

	/* create the pin */
	src = easyeda_attrib_src_c(ctx, obj, NULL);
	term = (csch_cgrp_t *)csch_alien_mkpin_line(&ctx->alien, src, parent, sx, sy, ex, ey);

	if (want_circ)
		csch_alien_mkarc(&ctx->alien, term, ex+dx*notr, ey+dy*notr, notr, 0, 360, "term-decor");

	ctx->pro_last = term;

	return 0;
}


/* "ATTR", "e51", "", "Device", "",  1,     1,     0, -60, 0,     "st1", 0]
    kw      id     ?   key      val keyvis valvis  x   y  rotdeg   pen   ?
    rotdeg: 0   = horiz
            90  = vert, read from the right
*/
static int easypro_parse_attr(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t **parent)
{
	csch_source_arg_t *src;
	double x, y, rot, kvis, vvis;
	const char *key, *val;

	REQ_ARGC_GTE(obj, 12, "easypro_parse_attr", return -1);

	GET_ARG_STR(key,  obj, 3,   "easypro_parse_attr: key", return -1);
	GET_ARG_STR(val,  obj, 4,   "easypro_parse_attr: val", return -1);
	GET_ARG_DBL(kvis, obj, 5,   "easypro_parse_attr: key visibility", return -1);
	GET_ARG_DBL(vvis, obj, 6,   "easypro_parse_attr: value visibility", return -1);
	GET_ARG_DBL(x,    obj, 7,   "easypro_parse_attr: coord x", return -1);
	GET_ARG_DBL(y,    obj, 8,   "easypro_parse_attr: coord y", return -1);
	GET_ARG_DBL(rot,  obj, 9,   "easypro_parse_attr: rotation", return -1);

	if (ctx->pro_last == NULL) {
		if (ctx->version >= 1.1) {
			error_at(ctx, obj, ("easypro_parse_attr: ATTR without previously created group object\n"));
			return -1;
		}
		else {
			/* implicit PART; some 1.0 symbols with a single slot do not have
			   PART at all */
			easypro_add_sym(ctx, NULL, parent);
		}
	}


	src = easyeda_attrib_src_c(ctx, obj, NULL);
	csch_attrib_set(&ctx->pro_last->attr, CSCH_ATP_USER_DEFAULT, key, val, src, NULL);

	/* create a floater if anything is visible */
	if (kvis || vvis) {
		const char *penname = DECOR_PEN_NAME(ctx->pro_last);
		csch_coord_t tx, ty;
		csch_text_t *txt;

		txt = (csch_text_t *)csch_alien_mktext(&ctx->alien, ctx->pro_last, 0, 0, penname);
		txt->dyntext = 1;
		if (ctx->pro_last->role == CSCH_ROLE_SYMBOL)
			txt->hdr.floater = 1;

		if (kvis && vvis) txt->text = rnd_concat(key, "=", "%../a.", key, "%", NULL);
		else if (kvis) txt->text = rnd_strdup(key);
		else if (vvis) txt->text = rnd_concat("%../a.", key, "%", NULL);

		switch((int)rot) {
			case -1: /* null */
			case 360:
			case 0: break;
			case 90:
			case 180:
			case 270:
				txt->spec_rot = (int)rot;
				break;
			default:
				error_at(ctx, obj, ("easypro_parse_attr: invalid rotation angle %f\n", rot));
				return -1;
		}

		/* inverse-transform sheet coords specified in the file to parent group
		   relative coords that the cschem model requires */
		tx = csch_alien_coord_x(&ctx->alien, x);
		ty = csch_alien_coord_y(&ctx->alien, y);
		csch_cgrp_inverse_xform(ctx->pro_last, &tx, &ty, 1);
		txt->spec1.x = tx;
		txt->spec1.y = ty;
		txt->spec_rot -= ctx->pro_last->spec_rot;
		if (ctx->pro_last->mirx) {
			txt->spec_mirx ^= 1;
			if ((ctx->pro_last->spec_rot == 90) || (ctx->pro_last->spec_rot == 270))
				txt->spec_rot += 180;
		}
	}

	return 0;
}

/* "POLY","e253",[758,10,758,70],false,"st6",0 
     kw    id     coords          ?     sty  ?  */
static int easypro_parse_poly(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *penname = DECOR_PEN_NAME(parent), *sty;
	gdom_node_t *coords;
	double x, y, lx, ly;
	long n, v;
	csch_chdr_t *poly;

	REQ_ARGC_GTE(obj, 6, "easypro_parse_poly", return -1);

	GET_ARG_ARRAY(coords, obj, 2, "easypro_parse_poly: coords", return -1);
	GET_ARG_STR(sty,   obj, 4,   "easypro_parse_circle: style name", return -1);


	v = coords->value.array.used;
	if (v < 4) {
		error_at(ctx, coords, ("easypro_parse_poly: too few coords\n"));
		return -1;
	}
	if ((v % 2) != 0) {
		error_at(ctx, coords, ("easypro_parse_poly: odd number of coords\n"));
		return -1;
	}

	if (v > 4) {
		int filled = easypro_style_filled(ctx, obj, sty);
		poly = csch_alien_mkpoly(&ctx->alien, parent, penname, filled ? penname : NULL);
	}
	else
		poly = NULL;

	for(n = 0; n < v; n+=2) {
		GET_ARG_DBL(x, coords, n,   "easypro_parse_poly: coord x", return -1);
		GET_ARG_DBL(y, coords, n+1, "easypro_parse_poly: coord y", return -1);
		if (n > 0) {
			if (poly != NULL)
				csch_alien_append_poly_line(&ctx->alien, poly, lx, ly, x, y);
			else
				csch_alien_mkline(&ctx->alien, parent, lx, ly, x, y, penname);
		}
		lx = x;
		ly = y;
	}

	return 0;
}

/* absolute value of the difference between two angles in radian */
static double angle_err(double a1, double a2)
{
	/* normalize the angles */
	if (a1 < 0) a1 += M_PI*2;
	if (a1 > M_PI*2) a1 -= M_PI*2;
	if (a2 < 0) a2 += M_PI*2;
	if (a2 > M_PI*2) a2 -= M_PI*2;

	/* now they are in the same range of 0..M_PI*2 */
	return fabs(a1-a2);
}

/* "ARC","e11", -16.879,0.06024, -12.89446,4.35448, -8.92106,0.04999, "st4",     0
                    start x y        middle x y         end x y      linetype */
static int easypro_parse_arc(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *penname = DECOR_PEN_NAME(parent), *sty;
	double sx, sy, mx, my, ex, ey;
	double sx2, sy2, mx2, my2, ex2, ey2;
	double A, B, C, D;
	double cx, cy, r, sa, da, ea, ma, da1, da2;
	int filled;

	REQ_ARGC_GTE(obj, 10, "easypro_parse_arc", return -1);

	GET_ARG_DBL(sx,    obj, 2,   "easypro_parse_arc: coord sx", return -1);
	GET_ARG_DBL(sy,    obj, 3,   "easypro_parse_arc: coord sy", return -1);
	GET_ARG_DBL(mx,    obj, 4,   "easypro_parse_arc: coord mx", return -1);
	GET_ARG_DBL(my,    obj, 5,   "easypro_parse_arc: coord my", return -1);
	GET_ARG_DBL(ex,    obj, 6,   "easypro_parse_arc: coord ex", return -1);
	GET_ARG_DBL(ey,    obj, 7,   "easypro_parse_arc: coord ey", return -1);
	GET_ARG_STR(sty,   obj, 8,   "easypro_parse_arc: style name", return -1);

	/* figure the center and radius, see https://math.stackexchange.com/q/4000949 */
	sx2 = sx*sx; sy2 = sy*sy;
	mx2 = mx*mx; my2 = my*my;
	ex2 = ex*ex; ey2 = ey*ey;

	A = sx*(my-ey) - sy*(mx-ex) + mx*ey - ex*my;
	B = (sx2+sy2)*(ey-my) + (mx2+my2)*(sy-ey) + (ex2+ey2)*(my-sy);
	C = (sx2+sy2)*(mx-ex) + (mx2+my2)*(ex-sx) + (ex2+ey2)*(sx-mx);
	D = (sx2+sy2)*(ex*my-mx*ey) + (mx2+my2)*(sx*ey-ex*sy) + (ex2+ey2)*(mx*sy-sx*my);

	if (A == 0) {
		error_at(ctx, obj, ("easypro_parse_arc: the three coords are colinear\n"));
		return -1;
	}

	cx = -B/(2*A);
	cy = -C/(2*A);
	r  = (B*B+C*C-4*A*D)/(4*A*A);
	if (r <= 0) {
		error_at(ctx, obj, ("easypro_parse_arc: invalid radius\n"));
		return -1;
	}
	r = sqrt(r);

	/* figure angles */
	sa = atan2(cy - sy, cx - sx);
	ma = atan2(cy - my, cx - mx);
	ea = atan2(cy - ey, cx - ex);

	da1 = ea - sa;
	da2 = sa - ea;
	da = (angle_err(ma, sa+da1/2) < angle_err(ma, sa+da2/2)) ? da1 : da2;

	/* convert to deg */
	sa *= RND_RAD_TO_DEG;
	da *= RND_RAD_TO_DEG;

	/* y flip */
	sa += 180;

	filled = easypro_style_filled(ctx, obj, sty);
	if (filled) {
		csch_chdr_t *poly = csch_alien_mkpoly(&ctx->alien, parent, penname, penname);
		csch_alien_append_poly_arc(&ctx->alien, poly, cx, cy, r, sa, da);
	}
	else
		csch_alien_mkarc(&ctx->alien, parent, cx, cy, r, sa, da, penname);

	return 0;
}

/* "CIRCLE",  "e42",  40,10, 10, "st5", 0
                       x y   r    sty */
static int easypro_parse_circle(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *penname = DECOR_PEN_NAME(parent), *sty;
	double cx, cy, r;
	int filled;

	REQ_ARGC_GTE(obj, 7, "easypro_parse_circle", return -1);

	GET_ARG_DBL(cx,    obj, 2,   "easypro_parse_circle: coord cx", return -1);
	GET_ARG_DBL(cy,    obj, 3,   "easypro_parse_circle: coord cy", return -1);
	GET_ARG_DBL(r,     obj, 4,   "easypro_parse_circle: coord r", return -1);
	GET_ARG_STR(sty,   obj, 5,   "easypro_parse_circle: style name", return -1);

	if (r <= 0) {
		error_at(ctx, obj, ("easypro_parse_circle: invalid radius\n"));
		return -1;
	}

	filled = easypro_style_filled(ctx, obj, sty);

	if (filled) {
		csch_chdr_t *poly = csch_alien_mkpoly(&ctx->alien, parent, penname, penname);
		csch_alien_append_poly_arc(&ctx->alien, poly, cx, cy, r, 0, 360);
	}
	else
		csch_alien_mkarc(&ctx->alien, parent, cx, cy, r, 0, 360, penname);

	return 0;
}

/* "ELLIPSE","e43",  -10,30,  20,10,  0,"st5",0
                       x y    rx ,ry  ?  pen  ? */
static int easypro_parse_ellipse(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *penname = DECOR_PEN_NAME(parent), *sty;
	double cx, cy, rx, ry, ex, ey;
	path_ctx_t pctx;
	int filled;

	REQ_ARGC_GTE(obj, 7, "easypro_parse_ellipse", return -1);

	GET_ARG_DBL(cx,    obj, 2,   "easypro_parse_ellipse: coord cx", return -1);
	GET_ARG_DBL(cy,    obj, 3,   "easypro_parse_ellipse: coord cy", return -1);
	GET_ARG_DBL(rx,    obj, 4,   "easypro_parse_ellipse: coord r", return -1);
	GET_ARG_DBL(ry,    obj, 5,   "easypro_parse_ellipse: coord r", return -1);
	GET_ARG_STR(sty,   obj, 7,   "easypro_parse_ellipse: style name", return -1);

	if ((rx <= 0) || (ry <= 0)) {
		error_at(ctx, obj, ("easypro_parse_ellipse: invalid radius\n"));
		return -1;
	}

	filled = easypro_style_filled(ctx, obj, sty);

	easyeda_svgpath_setup();

	pctx.ctx = ctx;
	pctx.nd = obj;
	pctx.penname = penname;
	pctx.in_poly = csch_alien_mkpoly(&ctx->alien, parent, penname, filled ? penname : NULL);
	pctx.parent = parent;

	ex = cx + rx;
	ey = cy;
	svgpath_approx_earc(&pathcfg, &pctx, ex, ey, cx, cy, rx, ry, 0, 2*M_PI, 0, ex, ey, pathcfg.curve_approx_seglen*pathcfg.curve_approx_seglen);

	return 0;
}

/* "RECT","e44", 30,30, 40,20, 0,0,     0,"st5",0
                 x1 y1  x2 y2  rx,ry    ?  pen  ? */
static int easypro_parse_rect(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *penname = DECOR_PEN_NAME(parent),*sty;
	double x1, y1, x2, y2, rx, ry, r;
	int filled;

	REQ_ARGC_GTE(obj, 11, "easypro_parse_rect", return -1);

	GET_ARG_DBL(x1,    obj, 2,   "easypro_parse_rect: coord x1", return -1);
	GET_ARG_DBL(y1,    obj, 3,   "easypro_parse_rect: coord y1", return -1);
	GET_ARG_DBL(x2,    obj, 4,   "easypro_parse_rect: coord x2", return -1);
	GET_ARG_DBL(y2,    obj, 5,   "easypro_parse_rect: coord y2", return -1);
	GET_ARG_DBL(rx,    obj, 6,   "easypro_parse_rect: coord rx", return -1);
	GET_ARG_DBL(ry,    obj, 7,   "easypro_parse_rect: coord ry", return -1);
	GET_ARG_STR(sty,   obj, 9,   "easypro_parse_rect: style name", return -1);

	if ((rx < 0) || (ry < 0)) {
		error_at(ctx, obj, ("easypro_parse_rect: invalid radius\n"));
		return -1;
	}

	if (x1 > x2) rnd_swap(double, x1, x2);
	if (y1 > y2) rnd_swap(double, y1, y2);

	filled = easypro_style_filled(ctx, obj, sty);

	easyeda_svgpath_setup();

	r = (rx+ry)/2.0;

	parent = (csch_cgrp_t *)csch_alien_mkpoly(&ctx->alien, parent, penname, filled ? penname : NULL);

	if ((r > 0) && (rx != ry))
		error_at(ctx, obj, ("round rect: elliptical rounding not supported, using circular with average radius\n"));

	if (r > 0) csch_alien_append_poly_arc(&ctx->alien, &parent->hdr, x1+r, y1+r, r, -90, -90);
	csch_alien_append_poly_line(&ctx->alien, &parent->hdr, x1+r, y1,     x2-r, y1);
	if (r > 0) csch_alien_append_poly_arc(&ctx->alien, &parent->hdr, x2-r, y1+r, r, 0, -90);
	csch_alien_append_poly_line(&ctx->alien, &parent->hdr, x2, y1+r,   x2, y2-r);
	if (r > 0) csch_alien_append_poly_arc(&ctx->alien, &parent->hdr, x1+r, y2-r, r, 90, +90);
	csch_alien_append_poly_line(&ctx->alien, &parent->hdr, x2-r, y2, x1+r, y2);
	if (r > 0) csch_alien_append_poly_arc(&ctx->alien, &parent->hdr, x2-r, y2-r, r, 0, +90);
	csch_alien_append_poly_line(&ctx->alien, &parent->hdr, x1, y2-r,   x1, y1+r);

	return 0;
}

/* "BEZIER","e41", [-20,10, 0,20, 10,10, 30,20]  ,"st5",0
             id      start  ctrl1 ctrl2  end            ? */
static int easypro_parse_bezier(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *penname = DECOR_PEN_NAME(parent), *sty;
	double sx, sy, ex, ey, c1x, c1y, c2x, c2y;
	path_ctx_t pctx;
	gdom_node_t *coords;
	int filled;

	REQ_ARGC_GTE(obj, 5, "easypro_parse_bezier", return -1);

	GET_ARG_ARRAY(coords, obj, 2, "easypro_parse_bezier: coords array", return -1);
	GET_ARG_STR(sty,      obj, 3, "easypro_parse_bezier: style name", return -1);

	REQ_ARGC_GTE(coords, 6, "easypro_parse_bezier: coords", return -1);
	GET_ARG_DBL(sx,    coords, 0,   "easypro_parse_bezier: coord sx", return -1);
	GET_ARG_DBL(sy,    coords, 1,   "easypro_parse_bezier: coord sy", return -1);
	GET_ARG_DBL(c1x,   coords, 2,   "easypro_parse_bezier: coord c1x", return -1);
	GET_ARG_DBL(c1y,   coords, 3,   "easypro_parse_bezier: coord c1y", return -1);
	GET_ARG_DBL(c2x,   coords, 4,   "easypro_parse_bezier: coord c2x", return -1);
	GET_ARG_DBL(c2y,   coords, 5,   "easypro_parse_bezier: coord c2y", return -1);
	GET_ARG_DBL(ex,    coords, 6,   "easypro_parse_bezier: coord ex", return -1);
	GET_ARG_DBL(ey,    coords, 7,   "easypro_parse_bezier: coord ey", return -1);

	filled = easypro_style_filled(ctx, obj, sty);

	/* svgpath happens to implement an approximator, reuse that */

	easyeda_svgpath_setup();

	pctx.ctx = ctx;
	pctx.nd = coords;
	pctx.penname = penname;
	pctx.in_poly = csch_alien_mkpoly(&ctx->alien, parent, penname, filled ? penname : NULL);
	pctx.parent = parent;

	svgpath_approx_bezier_cubic(&pathcfg, &pctx, sx, sy, c1x, c1y, c2x, c2y, ex, ey, pathcfg.curve_approx_seglen*pathcfg.curve_approx_seglen);

	return 0;
}

/* "TEXT","e45",  -10,-20, 0,   "str",  "st6",0
            id     x   y   rot          pen     */
static int easypro_parse_text(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *penname = DECOR_PEN_NAME(parent);
	double x, y, rot;
	const char *str;
	csch_text_t *txt;
	int roti;

	REQ_ARGC_GTE(obj, 7, "easypro_parse_text", return -1);

	GET_ARG_DBL(x,    obj, 2,   "easypro_parse_text: coord x", return -1);
	GET_ARG_DBL(y,    obj, 3,   "easypro_parse_text: coord y", return -1);
	GET_ARG_DBL(rot,  obj, 4,   "easypro_parse_text: coord rot", return -1);
	GET_ARG_STR(str,  obj, 5,   "easypro_parse_attr: text string", return -1);

	roti = rot;
	switch(roti) {
		case -1: case 0: case 90: case 180: case 270: case 360: break;
		default:
			error_at(ctx, obj, ("easypro_parse_text: invalid rotation angle %f\n", rot));
			return -1;
	}

	txt = (csch_text_t *)csch_alien_mktext(&ctx->alien, parent, x, y, penname);
	txt->text = rnd_strdup(str);
	txt->spec_rot = roti;

	return 0;
}

/* "GROUP",1,0,"border",["e607","e608","e609","e611","e613","e615","e617","e619","e621","e623","e625","e627","e629","e631","e633","e635","e637","e639","e641","e643","e645","e647","e649","e650","e651","e652","e653","e654","e655","e656","e657","e658","e659","e660","e661","e662","e663","e664"]
           ? ?    ?      array of IDs */
static int easypro_parse_group(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
#if 0
	gdom_node_t *ids;

	REQ_ARGC_GTE(obj, 5, "easypro_parse_group", return -1);
	GET_ARG_ARRAY(ids, obj, 4, "easypro_parse_group: ids", return -1);

	/* No-op: groups happen within symbols (e.g. A4 frame); actualy grouping
	   them won't help sym editing and won't make any difference when placed
	   on a sheet */
#endif

	return 0;
}

/* "OBJ","e674","",494,43,155,30,0,0,"data:image/svg+xml;base64,PD94b...==",0
    kw     id    ?  x   y  w   h ? ?  image                                 ?
    spotted in version 1.1 files */
static int easypro_parse_obj_1_1(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	double x, y, w, h;

	REQ_ARGC_GTE(obj, 11, "easypro_parse_obj (1.1)", return -1);
	GET_ARG_DBL(x,    obj, 3,   "easypro_parse_obj (1.1): coord x", return -1);
	GET_ARG_DBL(y,    obj, 4,   "easypro_parse_obj (1.1): coord y", return -1);
	GET_ARG_DBL(w,    obj, 5,   "easypro_parse_obj (1.1): coord w", return -1);
	GET_ARG_DBL(h,    obj, 6,   "easypro_parse_obj (1.1): coord h", return -1);

	easyeda_mkimage_sym(ctx, parent, obj, x, y-h, w, h);

	return 0;
}

/* "OBJ","e367","","image/svg+xml;base64",620,70,300,61,0,"PD94b...",0
    kw     id    ?  data_type              x   y  w   h ?  image     ?
    spotted in version 1.0 files */
static int easypro_parse_obj_1_0(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	double x, y, w, h;

	REQ_ARGC_GTE(obj, 11, "easypro_parse_obj (1.0)", return -1);
	GET_ARG_DBL(x,    obj, 4,   "easypro_parse_obj (1.0): coord x", return -1);
	GET_ARG_DBL(y,    obj, 5,   "easypro_parse_obj (1.0): coord y", return -1);
	GET_ARG_DBL(w,    obj, 6,   "easypro_parse_obj (1.0): coord w", return -1);
	GET_ARG_DBL(h,    obj, 7,   "easypro_parse_obj (1.0): coord h", return -1);

	easyeda_mkimage_sym(ctx, parent, obj, x, y-h, w, h);

	return 0;
}

/* There seem to be two different OBJ variants; detect and dispatch */
static int easypro_parse_obj(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	REQ_ARGC_GTE(obj, 4, "easypro_parse_obj", return -1);

	if (obj->value.array.child[3]->type == GDOM_DOUBLE)
		return easypro_parse_obj_1_1(ctx, obj, parent);

	if (obj->value.array.child[3]->type == GDOM_STRING)
		return easypro_parse_obj_1_0(ctx, obj, parent);

	error_at(ctx, obj, ("easypro_parse_obj: unrecognized OBJ format\n"));
	return -1;
}


/* "LINESTYLE","st12",null,null,"#99CCFF",null
     kw         name   ?    ?     fill     ?   */
static int easypro_parse_linestyle(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *name, *fill;
	int is_filled;

	REQ_ARGC_GTE(obj, 6, "easypro_parse_linestyle", return -1);

	GET_ARG_STR(name,  obj, 1,   "easypro_parse_linestyle: name", return -1);
	GET_ARG_STR(fill,  obj, 4,   "easypro_parse_linestyle: name", return -1);

	is_filled = ((fill != NULL) && (*fill == '#'));

	if (!htsi_has(&ctx->pro_pen_fill, name))
		htsi_insert(&ctx->pro_pen_fill, rnd_strdup(name), is_filled);
	else
		error_at(ctx, obj, ("easypro_parse_linestyle: style '%s' redefined (ignoring)\n", name));

	return 0;
}

/* Read ATTR subtrees following the current COMPONENT (obj) to find the
   first one that has key Symbol; the value is the hash-like name of
   the symbol to use. This is useful when the COMPONENT line doesn't
   have a symbol name */
static const char *easypro_find_symname(read_ctx_t *ctx, gdom_node_t *obj, long objidx)
{
	gdom_node_t *parent = obj->parent, *attr, *key;
	long n;
	const char *device;

	assert(parent->type == GDOM_ARRAY);

	/* Look up Symbol and Device ATTRs; if Symbol is found, return that
	   immediately. Otherwise if Device is found, do a lookup through the
	   project file. If all else fails, return NULL */
	for(n = objidx+1; n < parent->value.array.used; n++) {
		attr = parent->value.array.child[n];
		if ((attr->type != GDOM_ARRAY) || (attr->value.array.used < 1))
			break;

		key = attr->value.array.child[0];
		if ((key->type == GDOM_STRING) && (strcmp(key->value.str, "COMPONENT") == 0))
			break;

		if ((key->type == GDOM_STRING) && (strcmp(key->value.str, "ATTR") == 0)) {
			if ((attr->value.array.used >= 5) && (strcmp(attr->value.array.child[3]->value.str, "Symbol") == 0) && (attr->value.array.child[4]->type == GDOM_STRING) && (attr->value.array.child[4]->value.str != NULL) && (*attr->value.array.child[4]->value.str != '\0'))
				return attr->value.array.child[4]->value.str;
			if ((attr->value.array.used >= 5) && (strcmp(attr->value.array.child[3]->value.str, "Device") == 0) && (attr->value.array.child[4]->type == GDOM_STRING))
				device = attr->value.array.child[4]->value.str;
		}
	}

	if (device != NULL) {
		const char *symname;

		/* Device ATTR is set, look up via the project file */
		symname = htss_get(ctx->pro_devmap, device);
		if (symname == NULL)
			error_at(ctx, obj, ("easypro_find_symname: component references device '%s' that is not found in the devmap (in project.json) or has no symbol attribute\n", device));
		return symname;
	}

	return NULL;
}


/* "COMPONENT","e152","LM358.1",190,360,0,    0,      {},0
     kw          id    symname  x   y   rot   mirx    ? ? */
static int easypro_parse_component(read_ctx_t *ctx, gdom_node_t *obj, long objidx, csch_cgrp_t *parent)
{
	const char *symname;
	double x, y, rot, mirx;
	const csch_cgrp_t *src;
	csch_cgrp_t *dst;

	if (ctx->pro_symtab == NULL) {
		error_at(ctx, obj, ("easypro_parse_component:  no symtab in context - component within a symbol?!\n"));
		return -1;
	}

	REQ_ARGC_GTE(obj, 9, "easypro_parse_component", return -1);

	GET_ARG_STR(symname,  obj, 2,   "easypro_parse_component: symname", return -1);
	GET_ARG_DBL(x,        obj, 3,   "easypro_parse_component: coord x", return -1);
	GET_ARG_DBL(y,        obj, 4,   "easypro_parse_component: coord y", return -1);
	GET_ARG_DBL(rot,      obj, 5,   "easypro_parse_component: rotation", return -1);
	GET_ARG_DBL(mirx,     obj, 6,   "easypro_parse_component: mirror-x", return -1);

	/* if the COMPONENT line doesn't specify a symbol name, pick it out from
	   the hash-like name from Symbol ATTR following the COMPONENT */
	if ((symname == NULL) || (*symname == '\0')) {
		symname = easypro_find_symname(ctx, obj, objidx);
		if (symname == NULL) {
			error_at(ctx, obj, ("easypro_parse_component: failed to find symbol name from ATTR\n"));
			return -1;
		}
	}

	src = htsp_get(ctx->pro_symtab, symname);
	if (src == NULL) {
		error_at(ctx, obj, ("easypro_parse_component: failed to find symbol '%s' in symtab - not in the project file?\n", symname));
		return -1;
	}

	/* always embed from symhash */
	dst = csch_cgrp_dup(ctx->sheet, &ctx->sheet->direct, src, 0);
	if (dst == NULL) {
		error_at(ctx, obj, ("easypro_parse_component: failed to copy symbol '%s' from the symtab to the sheet\n", symname));
		return -1;
	}
	dst->x = csch_alien_coord_x(&ctx->alien, x);
	dst->y = csch_alien_coord_y(&ctx->alien, y);
	dst->spec_rot = rot;
	if (mirx)
		dst->mirx = 1;

	csch_cgrp_xform_update(ctx->sheet, dst); /* update the matrix so that children attribute transformations are done correctly */

	ctx->pro_last = dst;
	return 0;
}

/* "WIRE","e1", [[80,65,80,245],[80,245,150,245],[150,245,150,350]] ,"st3",0
     kw    id     x1 y1 x2 y2                                         sty  ?  */
static int easypro_parse_wire(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	gdom_node_t *segs, *coords;
	long n;

	REQ_ARGC_GTE(obj, 5, "easypro_parse_wire", return -1);
	GET_ARG_ARRAY(segs, obj, 2, "easypro_parse_wire: segments", return -1);

	for(n = 0; n < segs->value.array.used; n++) {
		double x1, y1, x2, y2;

		GET_ARG_ARRAY(coords, segs, n, "easypro_parse_wire: coords", return -1);
		REQ_ARGC_GTE(coords, 4, "easypro_parse_wire coords", return -1);

		GET_ARG_DBL(x1,   coords, 0,   "easypro_parse_wire: coord x1", return -1);
		GET_ARG_DBL(y1,   coords, 1,   "easypro_parse_wire: coord y1", return -1);
		GET_ARG_DBL(x2,   coords, 2,   "easypro_parse_wire: coord x2", return -1);
		GET_ARG_DBL(y2,   coords, 3,   "easypro_parse_wire: coord y2", return -1);

		csch_alien_mknet(&ctx->alien, &ctx->sheet->direct, x1, y1, x2, y2);
	}

	return 0;
}

static void easypro_warn_bus(read_ctx_t *ctx)
{
	if (ctx->warned_bus)
		return;

	ctx->warned_bus = 1;
	rnd_message(RND_MSG_ERROR, "io_easyeda: buses are loaded as purely graphical features for now; expect broken netlist output\n");
}

/* "BUS","e1994",[[535,755,485,755],[485,755,485,735]],"st8",0]
     kw    id     x1   y1  x2  y2                       sty  ?  */
static int easypro_parse_bus(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	gdom_node_t *segs, *coords;
	long n;

	REQ_ARGC_GTE(obj, 5, "easypro_parse_bus", return -1);
	GET_ARG_ARRAY(segs, obj, 2, "easypro_parse_bus: segments", return -1);

	easypro_warn_bus(ctx);

	for(n = 0; n < segs->value.array.used; n++) {
		double x1, y1, x2, y2;

		GET_ARG_ARRAY(coords, segs, n, "easypro_parse_bus: coords", return -1);
		REQ_ARGC_GTE(coords, 4, "easypro_parse_bus coords", return -1);

		GET_ARG_DBL(x1,   coords, 0,   "easypro_parse_bus: coord x1", return -1);
		GET_ARG_DBL(y1,   coords, 1,   "easypro_parse_bus: coord y1", return -1);
		GET_ARG_DBL(x2,   coords, 2,   "easypro_parse_bus: coord x2", return -1);
		GET_ARG_DBL(y2,   coords, 3,   "easypro_parse_bus: coord y2", return -1);

		csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x1, y1, x2, y2, "bus");
	}

	return 0;
}

/* "BUSENTRY","e2114","e1994",   0  ,475,755,0]
     kw        id       id       idx  x   y  ? */
static int easypro_parse_busentry(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	double idx, x, y;

	REQ_ARGC_GTE(obj, 7, "easypro_parse_busentry", return -1);
	GET_ARG_DBL(idx, obj, 3,   "easypro_parse_busentry: index", return -1);
	GET_ARG_DBL(x,   obj, 4,   "easypro_parse_busentry: coord x", return -1);
	GET_ARG_DBL(y,   obj, 5,   "easypro_parse_busentry: coord y", return -1);

	easypro_warn_bus(ctx);

	csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x, y, x+10, y, "busterm-primary");

	return 0;
}

typedef struct easypro_tab_s {
	vtd0_t rowy, colx; /* top left coords of each cell; both arrays are one element longer than the original table so the width/height of the last cell can be computed */
} easypro_tab_t;

static void easypro_tab_free(easypro_tab_t *tab)
{
	vtd0_uninit(&tab->rowy);
	vtd0_uninit(&tab->colx);
	free(tab);
}

/* "TABLE","e4459",  25,334,  [22,22,22,22,22],[200,42.8,42.8],[0,0,0,0,0],[0,0,0]
     kw        id    top x;y    row heights      col widths */
static int easypro_parse_table(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	double x, y, x2, y2, crd;
	const char *id;
	long n;
	int res = 0;
	gdom_node_t *rowhs, *colws;
	easypro_tab_t *tab;

	REQ_ARGC_GTE(obj, 8,          "easypro_parse_table", return -1);
	GET_ARG_STR(id, obj, 1,       "easypro_parse_table: id", return -1);
	GET_ARG_DBL(x, obj, 2,        "easypro_parse_table: x", return -1);
	GET_ARG_DBL(y, obj, 3,        "easypro_parse_table: y", return -1);
	GET_ARG_ARRAY(rowhs, obj, 4,  "easypro_parse_table: row heights", return -1);
	GET_ARG_ARRAY(colws, obj, 5, "easypro_parse_table: col widths", return -1);


	tab = calloc(sizeof(easypro_tab_t), 1);

	for(n = 0, crd = y; n < rowhs->value.array.used; n++) {
		double h;
		GET_ARG_DBL(h,    rowhs, n,   "easypro_parse_table: row height", res = -1;continue);
		vtd0_append(&tab->rowy, crd);
		crd -= h;
	}
	y2 = crd;
	vtd0_append(&tab->rowy, crd);

	for(n = 0, crd = x; n < colws->value.array.used; n++) {
		double w;
		GET_ARG_DBL(w,    colws, n,   "easypro_parse_table: col width", res = -1;continue);
		vtd0_append(&tab->colx, crd);
		crd += w;
	}
	x2 = crd;
	vtd0_append(&tab->colx, crd);

	if (ctx->pro_tabtab == NULL)
		ctx->pro_tabtab = htsp_alloc(strhash, strkeyeq);

	htsp_insert(ctx->pro_tabtab, rnd_strdup(id), tab);

	/* draw outer frame */
	csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x, y, x2, y, "sheet-decor");
	csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x2, y, x2, y2, "sheet-decor");
	csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x, y, x, y2, "sheet-decor");
	csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x, y2, x2, y2, "sheet-decor");

	return res;
}

/* "TABEL_CELL", "e17781","e4459", "GD32F407Vx",  2,   0,   1,        1,          "st21","st21","st21","st21","st22",null]
     kw          id       tbl_id    text          row  col  rowspan   colspan                                              
   Note: the keyword has a typo in it */
static int easypro_parse_table_cell(read_ctx_t *ctx, gdom_node_t *obj, csch_cgrp_t *parent)
{
	const char *tid, *text;
	double row, col, rowspan, colspan, x1, y1, x2, y2, tx, ty;
	easypro_tab_t *tab;
	csch_text_t *txt;

	REQ_ARGC_GTE(obj, 8,          "easypro_parse_table_cell", return -1);
	GET_ARG_STR(tid, obj, 2,      "easypro_parse_table_cell: id", return -1);
	GET_ARG_STR(text, obj, 3,     "easypro_parse_table_cell: id", return -1);
	GET_ARG_DBL(row, obj, 4,      "easypro_parse_table_cell: row", return -1);
	GET_ARG_DBL(col, obj, 5,      "easypro_parse_table_cell: col", return -1);
	GET_ARG_DBL(rowspan, obj, 6,  "easypro_parse_table_cell: rowspan", return -1);
	GET_ARG_DBL(colspan, obj, 7,  "easypro_parse_table_cell: colspan", return -1);

	if (ctx->pro_tabtab == NULL) {
		error_at(ctx, obj, ("table_cell without table\n"));
		return -1;
	}

	if ((colspan < 1) || (rowspan < 1) || (*text == '\0'))
		return 0;


	tab = htsp_get(ctx->pro_tabtab, rnd_strdup(tid));
	if (tab == NULL) {
		error_at(ctx, obj, ("table_cell referencing table '%s' which is not created\n", tid));
		return -1;
	}

	if ((row < 0) || (row+rowspan > tab->rowy.used)) {
		error_at(ctx, obj, ("table_cell row is out of bounds: %f..%f in a table with %ld rows\n", row, row+rowspan, tab->rowy.used-1));
		return -1;
	}

	if ((col < 0) || (col+colspan > tab->colx.used)) {
		error_at(ctx, obj, ("table_cell col is out of bounds: %f..%f in a table with %ld cols\n", col, col+colspan, tab->colx.used-1));
		return -1;
	}

	x1 = tab->colx.array[(long)col];
	x2 = tab->colx.array[(long)(col+colspan)];
	y1 = tab->rowy.array[(long)row];
	y2 = tab->rowy.array[(long)(row+rowspan)];

	if (row > 0)
		csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x1, y1, x2, y1, "sheet-decor");
	if (col > 0)
		csch_alien_mkline(&ctx->alien, &ctx->sheet->direct, x1, y1, x1, y2, "sheet-decor");

	tx = x1;
	ty = tab->rowy.array[(long)row+1];

	txt = (csch_text_t *)csch_alien_mktext(&ctx->alien, &ctx->sheet->direct, tx, ty, "sheet-decor");
	txt->text = rnd_strdup(text);

	return 0;
}



/* object dispatcher */
static int easypro_parse_any_obj(read_ctx_t *ctx, gdom_node_t *obj, long objidx, csch_cgrp_t **parent)
{
	char *cmds;
	int cmdi;

	REQ_ARGC_GTE(obj, 1, "easypro_parse_obj: need type of the object", return -1);
	GET_ARG_STR(cmds, obj, 0, "easypro_parse_obj: first arg must be a string", return -1);
	cmdi = easy_sphash(cmds);

	switch(cmdi) {
		case easy_PART: return easypro_parse_part(ctx, obj, parent);
		case easy_PIN: return easypro_parse_pin(ctx, obj, *parent);
		case easy_ATTR: return easypro_parse_attr(ctx, obj, parent);
		case easy_POLY: return easypro_parse_poly(ctx, obj, *parent);
		case easy_ARC: return easypro_parse_arc(ctx, obj, *parent);
		case easy_CIRCLE: return easypro_parse_circle(ctx, obj, *parent);
		case easy_ELLIPSE: return easypro_parse_ellipse(ctx, obj, *parent);
		case easy_RECT: return easypro_parse_rect(ctx, obj, *parent);
		case easy_BEZIER: return easypro_parse_bezier(ctx, obj, *parent);
		case easy_TEXT: return easypro_parse_text(ctx, obj, *parent);
		case easy_GROUP: return easypro_parse_group(ctx, obj, *parent);
		case easy_OBJ: return easypro_parse_obj(ctx, obj, *parent);
		case easy_LINESTYLE: return easypro_parse_linestyle(ctx, obj, *parent);
		case easy_COMPONENT: return easypro_parse_component(ctx, obj, objidx, *parent);
		case easy_WIRE: return easypro_parse_wire(ctx, obj, *parent);
		case easy_BUS: return easypro_parse_bus(ctx, obj, *parent);
		case easy_BUSENTRY: return easypro_parse_busentry(ctx, obj, *parent);
		case easy_TABLE: return easypro_parse_table(ctx, obj, *parent);
		case easy_TABEL_CELL: return easypro_parse_table_cell(ctx, obj, *parent); /* the keyword has a typo in it */
		case easy_FONTSTYLE: return 0; /* ignore: sch-rnd uses pens */

		case -1:
		default:
			error_at(ctx, obj, ("easypro_parse_any_obj: unrecognized obj %d '%s'\n", cmdi, cmds));
			return -1;
	}

	return -1; /* can't get here */
}

/* On success returns start idx within the root array where the first object
   node is; on error returns negative */
static int easypro_verify_header(read_ctx_t *ctx, int is_sym)
{
	gdom_node_t *hdr, *head;
	char *vers;

	if (ctx->root->type != GDOM_ARRAY) {
		error_at(ctx, ctx->root, ("easypro_verify_header: internal error: root must be an array\n"));
		return -1;
	}

	if (ctx->root->value.array.used < 2) {
		error_at(ctx, ctx->root, ("easypro_verify_header: root must have at least two items (headers)\n"));
		return -1;
	}

	/* first line: DOCTYPE */
	hdr = ctx->root->value.array.child[0];
	REQ_ARGC_GTE(hdr, 3, "easypro_verify_header: DOCTYPE", return -1);
	CHK_ARG_KW(hdr, 0, "DOCTYPE", "easypro_verify_header: DOCTYPE", return -1);
	if (is_sym)
		CHK_ARG_KW(hdr, 1, "SYMBOL", "easypro_verify_header: DOCTYPE", return -1);
	else
		CHK_ARG_KW(hdr, 1, "SCH", "easypro_verify_header: DOCTYPE", return -1);

	GET_ARG_STR(vers, hdr, 2, "easypro_verify_header: version", return -1);
	ctx->version = strtod(vers, NULL);
	if (ctx->version != 1.1)
		rnd_message(RND_MSG_ERROR, "easyeda pro: file version is %s; the version that works the best with this plugin is 1.1\n", vers);

	/* second line: HEAD */
	hdr = ctx->root->value.array.child[1];
	REQ_ARGC_GTE(hdr, 2, "easypro_verify_header: HEAD", return -1);
	CHK_ARG_KW(hdr, 0, "HEAD", "easypro_verify_header: HEAD", return -1);
	GET_ARG_HASH(head, hdr, 1, "easypro_verify_header: HEAD", return -1);

	/* second line: HEAD's hash */
	(void)head;
	/* There's nothing useful in head; origin could be, but sheet bbox is
	   calculated automatically anyway */

	return 2;
}

static void easypro_init_ctx(read_ctx_t *ctx)
{
	ctx->pro_last = NULL;
	htsi_init(&ctx->pro_pen_fill, strhash, strkeyeq);

	/* for whatever reason symbols seem to be y-mirrored */
	ctx->alien.flip_y = 0;
}

static void easypro_uninit_ctx(read_ctx_t *ctx)
{
	htsi_entry_t *e;
	for(e = htsi_first(&ctx->pro_pen_fill); e != NULL; e = htsi_next(&ctx->pro_pen_fill, e))
		free(e->key);
	htsi_uninit(&ctx->pro_pen_fill);

	TODO("uninit pro_symtab");

	if (ctx->pro_tabtab != NULL) {
		htsp_entry_t *e;
		for(e = htsp_first(ctx->pro_tabtab); e != NULL; e = htsp_next(ctx->pro_tabtab, e)) {
			easypro_tab_free(e->value);
			free(e->key);
		}
		htsp_uninit(ctx->pro_tabtab);
		free(ctx->pro_tabtab);
	}

	ctx->alien.flip_y = 1;
}

