/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - lihata format support
 *  Copyright (C) 2018,2022 Tibor 'Igor2' Palinkas
 *  (test parse code imported from pcb-rnd by the same author)
 *
 *  (Supported by NLnet NGI0 PET Fund in 2022)
 *
 *  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
 */

#include <libcschem/config.h>

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

#include <liblihata/dom.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/conf.h>

#include <libcschem/cnc_pen.h>
#include <libcschem/cnc_grp.h>
#include <libcschem/cnc_grp_child.h>
#include <libcschem/cnc_line.h>
#include <libcschem/cnc_poly.h>
#include <libcschem/cnc_text.h>
#include <libcschem/cnc_conn.h>
#include <libcschem/project.h>
#include <libcschem/util_parse.h>

#include <sch-rnd/util_sheet.h>

#include "read.h"



#define error(node, args) \
	do { \
		rnd_message(RND_MSG_ERROR, "Lihata parse error at %s:%d.%d:\n", node->file_name, node->line, node->col); \
		rnd_msg_error args; \
	} while(0)

typedef struct read_ctx_s {
	lht_doc_t *doc;
	const char *fn;
	int ver;            /* file version */
	csch_sheet_t *sheet;
	csch_project_t *proj;
	rnd_conf_role_t cfg_dest;
} read_ctx_t;

/*** low level field parsing ***/

/* wrapper around lht_dom_hash_get() that throws an error on missing node */
static lht_node_t *hash_must_get(lht_node_t *parent, const char *name)
{
	lht_node_t *res = lht_dom_hash_get(parent, name);
	if (res == NULL)
		error(parent, ("Missing lihata subtree: %s\n", name));
	return res;
}

static void attr_error(void *ectx, lht_node_t *n, const char *msg)
{
/*	read_ctx_t *ctx = ectx;*/
	error(n, ("%s\n", msg));
}

static int parse_attribs(read_ctx_t *ctx, csch_chdr_t *dsth, csch_attribs_t *dsta, lht_node_t *subtree)
{
	if (subtree == NULL)
		return 0;
	return csch_lht_parse_attribs_(ctx->sheet, dsth, dsta, subtree, attr_error, ctx);
}

/* Convert an oid from string, making sure there's no duplicate */
static csch_oid_t load_oid(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *nd, const char *oids)
{
	csch_oid_t oid;
	char *end;

	oid = strtol(oids, &end, 10);
	if (*end != '\0') {
		error(nd, ("invalid ID appendix (not an integer: %s)\n", oids));
		return 0;
	}

	if (htip_get(&parent->id2obj, oid) != NULL) {
		error(nd, ("duplicate oid %lu\n", oid));
		return 0;
	}

	if (parent->hdr.type == CSCH_CTYPE_GRP) {
		if (oid >= parent->data.grp.next_id)
			parent->data.grp.next_id = oid+1;
	}

	return oid;
}

/* Make sure node name matches the expected name and node type matches
   the expected type. On match, parse or invent the oid and return it. On
   error return 0. */
csch_oid_t check_type_oid_(read_ctx_t *ctx, csch_cgrp_t *grp, lht_node_t *nd, const char *name, int namelen, lht_node_type_t type)
{
	const char *oids;

	if (nd->type != type) {
		error(nd, ("%s must be a hash\n", name));
		return 0;
	}
	if (strncmp(nd->name, name, namelen) != 0) {
		error(nd, ("expected node %s, got %s\n", name, nd->name));
		return 0;
	}

	oids = nd->name + namelen;
TODO("delayed ID allocation!");
	if (*oids == '\0') {
		return -1;
/*		return ctx->sheet->next_id++;*/
	}
	if (*oids != '.') {
		error(nd, ("invalid ID appendix or wrong node name (expected %s.<integer> got %s)\n", name, nd->name));
		return 0;
	}

	return load_oid(ctx, grp, nd, oids+1);
}

/* shorthand with compile-time string length */
#define check_type_oid(ctx, grp, nd, name, type) \
	check_type_oid_(ctx, grp, nd, #name, strlen(#name), type)

TODO("revise whether this is needed at all")
#if 0
/* Parse an object 'name.oid' or 'name' (automatic ID allocation) */
csch_oid_t get_name_oid(read_ctx_t *ctx, lht_node_t *nd, char *name, int namelen)
{
	int len;
	const char *sep = strchr(nd->name, '.');
	csch_oid_t oid;

	if (sep != NULL) {
		len = sep - nd->name;
		oid = load_oid(ctx, nd, sep+1);
		if (oid == 0)
			return 0;
	}
	else {
		len = strlen(name)+1;
TODO("delayed ID allocation!");
		oid = ctx->sheet->next_id++;
	}

	if (len >= namelen-1) {
		error(nd, ("invalid object name '%s': too long\n", nd->name));
		return 0;
	}

	memcpy(name, nd->name, len);
	name[len] = '\0';
	return oid;
}
#endif

typedef struct dispatch_s {
	const char *prefix;
	int (*handler)(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *nd, csch_oid_t oid);
} dispatch_t;

static int dispatch_node(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *nd, const dispatch_t *tbl)
{
	char *sep = strchr(nd->name, '.');
	int len;
	const dispatch_t *t;
	csch_oid_t oid = 0;

	if (sep != NULL) {
		len = sep - nd->name;
		if (strncmp(nd->name, "pen.", 4) != 0) {
			oid = load_oid(ctx, parent, nd, sep+1);
			if (oid == 0)
				return -1;
		}
	}
	else
		len = strlen(nd->name);

	for(t = tbl; t->prefix != NULL; t++)
		if (strncmp(nd->name, t->prefix, len) == 0)
			return t->handler(ctx, parent, nd, oid);

	error(nd, ("invalid object name '%s': unknown under node %s\n", nd->name, nd->parent->name));
	return -1;
}

static int dispatch_subtree(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *parent_tree, lht_node_t *subtree, const dispatch_t *tbl, int allow_empty)
{
	lht_node_t *n;
	lht_dom_iterator_t it;

	if (subtree == NULL) {
		if (allow_empty)
			return 0;
		error(parent_tree, ("the subtree of '%s' shall not be empty\n", parent_tree->name));
		return -1;
	}

	for(n = lht_dom_first(&it, subtree); n != NULL; n = lht_dom_next(&it))
		if (dispatch_node(ctx, parent, n, tbl))
			return -1;

	return 0;
}

static int parse_coord_(read_ctx_t *ctx, csch_coord_t *dst, const lht_node_t *src, int silent)
{
	char *end;
	long l;

	if (src == NULL) {
		if (silent)
			return -1;
		*dst = 0;
		return 0;
	}

	if (src->type != LHT_TEXT) {
		if (!silent)
			error(src, ("coordinate value must be text\n"));
		return -1;
	}
	l = strtol(src->data.text.value, &end, 10);
	if (*end != '\0') {
		if (!silent)
			error(src, ("coordinate value: invalid integer '%s'\n", src->data.text.value));
		return -1;
	}
	*dst = l;
	return 0;
}

static int parse_coord(read_ctx_t *ctx, csch_coord_t *dst, const lht_node_t *src)
{
	return parse_coord_(ctx, dst, src, 0);
}

static int parse_coord_maybe(read_ctx_t *ctx, csch_coord_t *dst, const lht_node_t *src)
{
	if (src == NULL)
		return 0;
	return parse_coord_(ctx, dst, src, 0);
}

static int parse_angle(read_ctx_t *ctx, g2d_angle_t *dst, const lht_node_t *src)
{
	char *end;
	double d;

	if (src == NULL) {
		*dst = 0;
		return 0;
	}

	if (src->type != LHT_TEXT) {
		error(src, ("angle value must be text\n"));
		return -1;
	}
	d = strtod(src->data.text.value, &end);
	if (*end != '\0') {
		error(src, ("angle value: invalid decimal '%s'\n", src->data.text.value));
		return -1;
	}
	*dst = d;
	return 0;
}

static int parse_dash(read_ctx_t *ctx, unsigned short *dst, const lht_node_t *src)
{
	char *s, *end;
	long l;

	if (src == NULL) {
		*dst = 0;
		return 0;
	}

	if (src->type != LHT_TEXT) {
		error(src, ("dash value must be text\n"));
		return -1;
	}
	s = src->data.text.value;
	if ((s[0] == '0') && (s[1] == 'x')) s += 2;
	l = strtol(s, &end, 16);
	if (*end != '\0') {
		error(src, ("dash value: invalid hex numeric '%s'\n", src->data.text.value));
		return -1;
	}

	if ((l < 0) || (l > 0xFFFF)) {
		error(src, ("dash value: out of range: '%s' (must be between 0 and 0xFFFF)\n", src->data.text.value));
		return -1;
	}

	*dst = l;
	return 0;
}

static int parse_oid(read_ctx_t *ctx, csch_oid_t *dst, const lht_node_t *src, int silent)
{
	char *end;
	long l;

	if (src == NULL) {
		if (!silent)
			error(src, ("missing oid\n"));
		return -1;
	}

	if (src->type != LHT_TEXT) {
		if (!silent)
			error(src, ("oid value must be text\n"));
		return -1;
	}
	l = strtol(src->data.text.value, &end, 10);
	if (*end != '\0') {
		if (!silent)
			error(src, ("oid value: invalid integer '%s'\n", src->data.text.value));
		return -1;
	}
	if (l < 0) {
		if (!silent)
			error(src, ("oid value: must be positive: '%s'\n", src->data.text.value));
		return -1;
	}
	*dst = l;
	return 0;
}

static int parse_oidpath(read_ctx_t *ctx, char **dst, const lht_node_t *src)
{
	if ((src == NULL) || (src->type != LHT_TEXT))
		return -1;
	*dst = rnd_strdup(src->data.text.value);
	return 0;
}

/* defaults to 0 if src is NULL */
static int parse_double(read_ctx_t *ctx, double *dst, const lht_node_t *src)
{
	char *end;
	double d;

	if (src == NULL) {
		*dst = 0;
		return 0;
	}

	if (src->type != LHT_TEXT) {
		error(src, ("oid value must be text\n"));
		return -1;
	}
	d = strtod(src->data.text.value, &end);
	if (*end != '\0') {
		error(src, ("oid value: invalid integer '%s'\n", src->data.text.value));
		return -1;
	}
	*dst = d;
	return 0;
}

/* does not overwrite dst if src is NULL */
static int parse_double_maybe(read_ctx_t *ctx, double *dst, const lht_node_t *src)
{
	if (src == NULL) return 0;
	return parse_double(ctx, dst, src);
}


static int parse_halign(read_ctx_t *ctx, csch_halign_t *dst, const lht_node_t *src)
{
	if (src == NULL) {
		*dst = CSCH_HALIGN_START;
		return 0;
	}
	if (src->type != LHT_TEXT) {
		error(src, ("halign value must be text\n"));
		return -1;
	}

	*dst = csch_str2halign(src->data.text.value);
	if (*dst == CSCH_HALIGN_invalid) {
		error(src, ("halign value is unkown/invalid: %s\n", src->data.text.value));
		return -1;
	}
	return 0;
}

static int parse_bool(read_ctx_t *ctx, int *dst, const lht_node_t *src, const lht_node_t *parent, int defval)
{
	if (src == NULL) {
		if (defval < 0) {
			error(parent, ("missing mandatory bool field\n"));
			return defval;
		}
		*dst = defval;
		return 0;
	}
	if (src->type != LHT_TEXT) {
		error(src, ("bool value must be text\n"));
		return -1;
	}

	*dst = rnd_istrue(src->data.text.value);
	return 0;
}

static int parse_uuid(read_ctx_t *ctx, minuid_bin_t dst, const lht_node_t *src)
{
	static minuid_bin_t uuid0 = {0};

	if (src == NULL) {
		minuid_cpy(dst, uuid0);
		return 0;
	}

	if (src->type != LHT_TEXT) {
		error(src, ("uuid must be text\n"));
		return -1;
	}

	if (strlen(src->data.text.value) != (MINUID_TXT_LEN-1)) {
		error(src, ("invalid uuid format; wrong length\n"));
		return -1;
	}

	if (minuid_str2bin(dst, src->data.text.value) != 0) {
		error(src, ("invalid uuid format; parse error\n"));
		return -1;
	}

	return 0;
}

/*** atomic object parsing ***/

static int parse_color(read_ctx_t *ctx, rnd_color_t *dst, const lht_node_t *src)
{
	char *end;
	unsigned long l;

	if (src->type != LHT_TEXT) {
		error(src, ("color value must be text\n"));
		return -1;
	}
	if (*src->data.text.value != '#') {
		error(src, ("color value must start with '#' (wrong value: '%s')\n", src->data.text.value));
		return -1;
	}
	if (strlen(src->data.text.value+1) < 6) {
		error(src, ("color value: invalid integer length '%s' - must be 24 bit hexadecimap\n", src->data.text.value+1));
		return -1;
	}
	l = strtol(src->data.text.value+1, &end, 16);
	if ((*end != '\0') && (!isspace(*end))) {
		error(src, ("color value: invalid integer '%s' - must be 24 bit hexadecimap\n", src->data.text.value+1));
		return -1;
	}
	if ((l < 0) || (l > 0xffffff)) {
		error(src, ("color value: invalid integer '%s' - out of bounds\n", src->data.text.value+1));
		return -1;
	}
	l = l << 8;
	return rnd_color_load_packed(dst, l);
}

static int parse_pen_shape(read_ctx_t *ctx, csch_pen_shape_t *dst, const lht_node_t *src)
{
	const char *val;
	if (src->type != LHT_TEXT) {
		error(src, ("pen shape value must be text\n"));
		return -1;
	}
	val = src->data.text.value;
	if ((strncmp(val, "circle", 6) == 0) || (strncmp(val, "round", 5) == 0)) {
		*dst = CSCH_PSHP_ROUND;
		return 0;
	}
	if (strncmp(val, "square", 6) == 0) {
		*dst = CSCH_PSHP_SQUARE;
		return 0;
	}
	return -1;
}

static int parse_common_properties(read_ctx_t *ctx, csch_chdr_t *obj, lht_node_t *subtree)
{
	int tmp;

	if (parse_bool(ctx, &tmp, lht_dom_hash_get(subtree, "lock"), subtree, 0) != 0)
		return -1;
	obj->lock = tmp;

	if (parse_bool(ctx, &tmp, lht_dom_hash_get(subtree, "floater"), subtree, 0) != 0)
		return -1;
	obj->floater = tmp;

	return 0;
}

static int parse_pen(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_cpen_t *pen;
	const char *name = NULL;
	lht_node_t *n;

	if (subtree == NULL)
		return 0;

	name = subtree->name + 4;

	pen = csch_pen_alloc(ctx->sheet, parent, name);
	if (pen == NULL) {
		error(subtree, ("failed to allocate pen\n"));
		return -1;
	}

	if (parse_common_properties(ctx, &pen->hdr, subtree) != 0)
		return -1;
	if (parse_coord(ctx, &pen->size, lht_dom_hash_get(subtree, "size")) != 0)
		return -1;
	if (parse_color(ctx, &pen->color, lht_dom_hash_get(subtree, "color")) != 0)
		return -1;
	if (parse_pen_shape(ctx, &pen->shape, lht_dom_hash_get(subtree, "shape")) != 0)
		return -1;
	if (parse_attribs(ctx, &pen->hdr, NULL, lht_dom_hash_get(subtree, "attrib")) != 0)
		return -1;

	if (parse_dash(ctx, &pen->dash, lht_dom_hash_get(subtree, "dash")) != 0)
		return -1;
	if (parse_coord(ctx, &pen->dash_period, lht_dom_hash_get(subtree, "dash_period")) != 0)
		return -1;

	if (parse_coord(ctx, &pen->font_height, lht_dom_hash_get(subtree, "font_height")) != 0)
		return -1;

	n = lht_dom_hash_get(subtree, "font_family");
	if (n != NULL) {
		if (n->type != LHT_TEXT) {
			error(n, ("font_family is not a text node\n"));
			return -1;
		}
		pen->font_family = rnd_strdup(n->data.text.value);
	}

	n = lht_dom_hash_get(subtree, "font_style");
	if (n != NULL) {
		if (n->type != LHT_TEXT) {
			error(n, ("font_style is not a text node\n"));
			return -1;
		}
		pen->font_style = rnd_strdup(n->data.text.value);
	}

	if (parse_coord(ctx, &pen->font_height, lht_dom_hash_get(subtree, "font_height")) != 0)
		return -1;

	return 0;
}

static int parse_pen_ref_(read_ctx_t *ctx, csch_chdr_t *obj, lht_node_t *subtree, int silent, int is_stroke)
{
	const lht_node_t *pn = lht_dom_hash_get(subtree, (is_stroke ? "stroke" : "fill"));
	if (pn == NULL) {
		if (!silent)
			error(subtree, ("missing pen\n"));
		return -1;
	}
	if (is_stroke)
		obj->stroke_name = csch_comm_str(ctx->sheet, pn->data.text.value, 1);
	else
		obj->fill_name = csch_comm_str(ctx->sheet, pn->data.text.value, 1);
	return 0;
}

static int parse_stroke(read_ctx_t *ctx, csch_chdr_t *obj, lht_node_t *subtree, int silent)
{
	return parse_pen_ref_(ctx, obj, subtree, silent, 1);
}

static int parse_fill(read_ctx_t *ctx, csch_chdr_t *obj, lht_node_t *subtree, int silent)
{
	return parse_pen_ref_(ctx, obj, subtree, silent, 0);
}

static int parse_line_fields(read_ctx_t *ctx, csch_line_t *line, lht_node_t *subtree)
{
	if (parse_common_properties(ctx, &line->hdr, subtree) != 0)
		return -1;
	if (parse_coord(ctx, &line->spec.p1.x, lht_dom_hash_get(subtree, "x1")) != 0)
		return -1;
	if (parse_coord(ctx, &line->spec.p1.y, lht_dom_hash_get(subtree, "y1")) != 0)
		return -1;
	if (parse_coord(ctx, &line->spec.p2.x, lht_dom_hash_get(subtree, "x2")) != 0)
		return -1;
	if (parse_coord(ctx, &line->spec.p2.y, lht_dom_hash_get(subtree, "y2")) != 0)
		return -1;
	return 0;
}

static int parse_line(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_line_t *line;

	if (subtree == NULL)
		return 0;

	if (subtree->type != LHT_HASH) {
		error(subtree, ("line must be a hash\n"));
		return -1;
	}

	line = csch_line_alloc(ctx->sheet, parent, oid);

	if (parse_line_fields(ctx, line, subtree) != 0)
		return -1;

	return parse_stroke(ctx, &line->hdr, subtree, 0);
}

static int parse_arc_fields(read_ctx_t *ctx, csch_arc_t *arc, lht_node_t *subtree)
{
	int has_x, has_y;

	if (parse_common_properties(ctx, &arc->hdr, subtree) != 0)
		return -1;
	if (parse_coord(ctx, &arc->spec.c.x, lht_dom_hash_get(subtree, "cx")) != 0)
		return -1;
	if (parse_coord(ctx, &arc->spec.c.y, lht_dom_hash_get(subtree, "cy")) != 0)
		return -1;
	if (parse_coord(ctx, &arc->spec.r, lht_dom_hash_get(subtree, "r")) != 0)
		return -1;
	if (parse_angle(ctx, &arc->spec.start, lht_dom_hash_get(subtree, "sang")) != 0)
		return -1;
	if (parse_angle(ctx, &arc->spec.delta, lht_dom_hash_get(subtree, "dang")) != 0)
		return -1;

	arc->spec.start /= RND_RAD_TO_DEG;
	arc->spec.delta /= RND_RAD_TO_DEG;

	/* optional endpoint fields */
	has_x = (parse_coord_(ctx, &arc->spec_sx, lht_dom_hash_get(subtree, "sx"), 1) == 0);
	has_y = (parse_coord_(ctx, &arc->spec_sy, lht_dom_hash_get(subtree, "sy"), 1) == 0);
	arc->svalid = has_x && has_y;

	has_x = (parse_coord_(ctx, &arc->spec_ex, lht_dom_hash_get(subtree, "ex"), 1) == 0);
	has_y = (parse_coord_(ctx, &arc->spec_ey, lht_dom_hash_get(subtree, "ey"), 1) == 0);
	arc->evalid = has_x && has_y;

	return 0;
}

static int parse_arc(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_arc_t *arc;

	if (subtree == NULL)
		return 0;

	if (subtree->type != LHT_HASH) {
		error(subtree, ("arc must be a hash\n"));
		return -1;
	}

	arc = csch_arc_alloc(ctx->sheet, parent, oid);

	if (parse_arc_fields(ctx, arc, subtree) != 0)
		return -1;

	return parse_stroke(ctx, &arc->hdr, subtree, 0);
}


static int parse_poly(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_cpoly_t *poly;
	lht_node_t *outline, *o;

	if (subtree == NULL)
		return 0;

	if (subtree->type != LHT_HASH) {
		error(subtree, ("polygon must be a hash\n"));
		return -1;
	}
	outline = lht_dom_hash_get(subtree, "outline");
	if (outline == NULL) {
		error(outline, ("polygon outline does not exist\n"));
		return -1;
	}
	if (outline->type != LHT_LIST) {
		error(outline, ("polygon outline must be a list\n"));
		return -1;
	}

	poly = csch_cpoly_alloc(ctx->sheet, parent, oid);

	for(o = outline->data.list.first; o != NULL; o = o->next) {
		if (o->type != LHT_HASH) {
			error(o, ("polygon outline list must contain hash items\n"));
			return -1;
		}

		if (strncmp(o->name, "line", 4) == 0) {
			csch_coutline_t *dst = csch_vtcoutline_alloc_append(&poly->outline, 1);
			dst->hdr = poly->hdr;
			dst->hdr.type = CSCH_CTYPE_LINE;
			if (parse_line_fields(ctx, &dst->line, o) != 0)
				return -1;
		}
		else if (strncmp(o->name, "arc", 3) == 0) {
			csch_coutline_t *dst = csch_vtcoutline_alloc_append(&poly->outline, 1);
			dst->hdr = poly->hdr;
			dst->hdr.type = CSCH_CTYPE_ARC;
			if (parse_arc_fields(ctx, &dst->arc, o) != 0)
				return -1;
		}
		else {
			error(o, ("polygon outline: invalid/unsupported object type\n"));
			return -1;
		}
	}

	/* common has to be after the outline loop so that outline objects don't
	   inherit common properties of the parent poly */
	if (parse_common_properties(ctx, &poly->hdr, subtree) != 0)
		return -1;

	if (parse_fill(ctx, &poly->hdr, subtree, 1) == 0)
		poly->has_fill = 1;

	if (parse_stroke(ctx, &poly->hdr, subtree, 1) == 0)
		poly->has_stroke = 1;

	if (!poly->has_stroke && !poly->has_fill)
		error(subtree, ("invisible polygon: no stroke, no fill; this is probably an error in your data\n"));

	return 0;
}

static int parse_text(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_text_t *text;
	lht_node_t *ntext, *nx2, *ny2;
	int itmp;

	if (subtree == NULL)
		return 0;

	if (subtree->type != LHT_HASH) {
		error(subtree, ("text must be a hash\n"));
		return -1;
	}

	ntext = lht_dom_hash_get(subtree, "text");
	if (ntext == NULL) {
		error(subtree, ("text.text is missing\n"));
		return -1;
	}
	if (ntext->type != LHT_TEXT) {
		error(ntext, ("text.text is not a text node\n"));
		return -1;
	}

	text = csch_text_alloc(ctx->sheet, parent, oid);

	if (parse_common_properties(ctx, &text->hdr, subtree) != 0)
		return -1;
	if (parse_coord(ctx, &text->spec1.x, lht_dom_hash_get(subtree, "x1")) != 0)
		return -1;
	if (parse_coord(ctx, &text->spec1.y, lht_dom_hash_get(subtree, "y1")) != 0)
		return -1;

	nx2 = lht_dom_hash_get(subtree, "x2");
	ny2 = lht_dom_hash_get(subtree, "y2");
	if ((nx2 != NULL) && (ny2 != NULL)) {
		if (parse_coord(ctx, &text->spec2.x, nx2) != 0)
			return -1;
		if (parse_coord(ctx, &text->spec2.y, ny2) != 0)
			return -1;
		text->has_bbox = 1;
	}
	else
		text->has_bbox = 0;



	if (parse_angle(ctx, &text->spec_rot, lht_dom_hash_get(subtree, "rot")) != 0)
		return -1;

	if (parse_bool(ctx, &itmp, lht_dom_hash_get(subtree, "mirx"), subtree, 0) != 0)
		return -1;
	text->spec_mirx = itmp;
	if (parse_bool(ctx, &itmp, lht_dom_hash_get(subtree, "miry"), subtree, 0) != 0)
		return -1;
	text->spec_miry = itmp;

	if (parse_halign(ctx, &text->halign, lht_dom_hash_get(subtree, "halign")) != 0) {
		error(subtree, ("wrong halign in text object\n"));
		return -1;
	}

	text->text = rnd_strdup(ntext->data.text.value);

	if (parse_bool(ctx, &itmp, lht_dom_hash_get(subtree, "dyntext"), subtree, 0) != 0)
		return -1;
	text->dyntext = itmp;


	return parse_stroke(ctx, &text->hdr, subtree, 0);
}

static int parse_connection(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_conn_t *conn;
	lht_node_t *n, *nconn;

	if (subtree == NULL)
		return 0;

	if (subtree->type != LHT_HASH) {
		error(subtree, ("connection must be a hash\n"));
		return -1;
	}

	nconn = lht_dom_hash_get(subtree, "conn");
	if (nconn == NULL) {
		error(subtree, ("connection without conn list\n"));
		return -1;
	}

	conn = csch_conn_alloc(ctx->sheet, parent, oid);

	for(n = nconn->data.list.first; n != NULL; n = n->next) {
		csch_oidpath_t *oidp = csch_vtoidpath_alloc_append(&conn->conn_path, 1);
		if (csch_oidpath_parse(oidp, n->data.text.value) != 0) {
			error(subtree, ("invalid oid path in conn\n"));
			return -1;
		}
	}

	return 0;
}

/*** composite object parsing: groups, object lists */

static int parse_group(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid);
static int parse_group_ref(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid);

static const dispatch_t objs_tbl[] = {
	{"group",     parse_group},
	{"group_ref", parse_group_ref},
	{"line",      parse_line},
	{"text",      parse_text},
	{"arc",       parse_arc},
	{"polygon",   parse_poly},
	{"pen",       parse_pen},
	{"connection",parse_connection},
	{NULL,        NULL}
};

static int parse_group_(read_ctx_t *ctx, csch_cgrp_t *grp, lht_node_t *subtree)
{
	int res, itmp;
	lht_node_t *nd;

	if (subtree == NULL)
		return -1;

	if (parse_uuid(ctx, (unsigned char *)&grp->uuid, lht_dom_hash_get(subtree, "uuid")) != 0)
		return -1;
	if (grp->hdr.type == CSCH_CTYPE_GRP) {
		if (parse_uuid(ctx, (unsigned char *)&grp->data.grp.src_uuid, lht_dom_hash_get(subtree, "src_uuid")) != 0)
			return -1;
	}

	if (parse_attribs(ctx, &grp->hdr, NULL, lht_dom_hash_get(subtree, "attrib")) != 0)
		return -1;
	if (parse_common_properties(ctx, &grp->hdr, subtree) != 0)
		return -1;

	nd = lht_dom_hash_get(subtree, "loclib_name");
	if (nd != NULL) {
		if (nd->type != LHT_TEXT) {
			error(nd, ("loclib_name needs to be a text node\n"));
			return -1;
		}
		grp->loclib_name = rnd_strdup(nd->data.text.value);
	}

	if (parse_coord(ctx, &grp->x, lht_dom_hash_get(subtree, "x")) != 0)
		return -1;
	if (parse_coord(ctx, &grp->y, lht_dom_hash_get(subtree, "y")) != 0)
		return -1;
	if (parse_double(ctx, &grp->spec_rot, lht_dom_hash_get(subtree, "rot")) != 0)
		return -1;

	if (parse_bool(ctx, &itmp, lht_dom_hash_get(subtree, "mirx"), subtree, 0) != 0)
		return -1;
	grp->mirx = itmp;

	if (parse_bool(ctx, &itmp, lht_dom_hash_get(subtree, "miry"), subtree, 0) != 0)
		return -1;
	grp->miry = itmp;

	csch_cgrp_xform_update(ctx->sheet, grp); /* set up the matrix for object transformations */
	csch_cgrp_role_update(ctx->sheet, grp); /* set up display layer before parsing children (they will depend on this) */

	res = dispatch_subtree(ctx, grp, subtree, lht_dom_hash_get(subtree, "objects"), objs_tbl, 0);

	csch_cgrp_bbox_update(ctx->sheet, grp);

	return res;
}

static int parse_group(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_cgrp_t *grp = csch_cgrp_alloc(ctx->sheet, parent, oid);
	if (grp == NULL) {
		error(subtree, ("failed to allocate group\n"));
		return -1;
	}
	return parse_group_(ctx, grp, subtree);
}

static int parse_root_group(read_ctx_t *ctx, csch_cgrp_t *grp, lht_node_t *subtree, csch_oid_t oid)
{
	if ((grp != &ctx->sheet->direct) && (grp != &ctx->sheet->indirect))
		csch_cgrp_init(ctx->sheet, grp, NULL, oid);
	return parse_group_(ctx, grp, subtree);
}

static int parse_child_xform_entry(read_ctx_t *ctx, csch_cgrp_t *grp, lht_node_t *subtree, csch_child_xform_t *cx)
{
	int mirx = 0, miry = 0, remv = 0;

	if (subtree->type != LHT_HASH) {
		error(subtree, ("group ref child_xform list entries need to be hashes\n"));
		return -1;
	}

	if (csch_oidpath_parse_relative(&cx->path, subtree->name) != 0) {
		error(subtree, ("group ref child_xform: syntax error in path\n"));
		rnd_trace("name=%s\n", subtree->name);
		return -1;
	}

	if (parse_coord_maybe(ctx, &cx->movex, lht_dom_hash_get(subtree, "movex")) != 0) return -1;
	if (parse_coord_maybe(ctx, &cx->movey, lht_dom_hash_get(subtree, "movey")) != 0) return -1;
	if (parse_double_maybe(ctx, &cx->rot, lht_dom_hash_get(subtree, "rot")) != 0) return -1;
	if (parse_bool(ctx, &mirx, lht_dom_hash_get(subtree, "mirx"), subtree, 0) != 0) return -1;
	if (parse_bool(ctx, &miry, lht_dom_hash_get(subtree, "miry"), subtree, 0) != 0) return -1;
	if (parse_bool(ctx, &remv, lht_dom_hash_get(subtree, "remove"), subtree, 0) != 0) return -1;

	cx->mirx = mirx;
	cx->miry = miry;
	cx->remove = remv;

	return 0;
}

static int parse_child_xform(read_ctx_t *ctx, csch_cgrp_t *grpref, lht_node_t *subtree)
{
	lht_node_t *n;

	if (subtree == NULL)
		return 0; /* optional */

	if (subtree->type != LHT_LIST) {
		error(subtree, ("group ref child_xform needs to be a list\n"));
		return -1;
	}

	for(n = subtree->data.list.first; n != NULL; n = n->next) {
		csch_child_xform_t *cx = calloc(sizeof(csch_child_xform_t), 1);
		vtp0_append(&grpref->data.ref.child_xform, cx);
		if (parse_child_xform_entry(ctx, grpref, n, cx) != 0)
			return -1;
	}
	return 0;
}

static int parse_group_ref(read_ctx_t *ctx, csch_cgrp_t *parent, lht_node_t *subtree, csch_oid_t oid)
{
	csch_cgrp_t *grp = csch_cgrp_ref_alloc(ctx->sheet, parent, oid);

	if (grp == NULL) {
		error(subtree, ("failed to allocate group ref\n"));
		return -1;
	}

	if (parse_attribs(ctx, &grp->hdr, NULL, lht_dom_hash_get(subtree, "attrib")) != 0)
		return -1;
	if (parse_common_properties(ctx, &grp->hdr, subtree) != 0)
		return -1;

	if (parse_coord(ctx, &grp->x, lht_dom_hash_get(subtree, "x")) != 0)
		return -1;
	if (parse_coord(ctx, &grp->y, lht_dom_hash_get(subtree, "y")) != 0)
		return -1;
	if (parse_double(ctx, &grp->spec_rot, lht_dom_hash_get(subtree, "rot")) != 0)
		return -1;
	if (parse_oidpath(ctx, &grp->data.ref.ref_str, lht_dom_hash_get(subtree, "ref")) != 0)
		return -1;
	if (parse_child_xform(ctx, grp, lht_dom_hash_get(subtree, "child_xform")) != 0)
		return -1;

	return 0;
}

static int parse_conf(read_ctx_t *ctx, lht_node_t *sub)
{
	if ((sub == NULL) || (ctx->cfg_dest == RND_CFR_invalid))
		return 0;
	if (rnd_conf_insert_tree_as(ctx->cfg_dest, sub) != 0) {
		rnd_message(RND_MSG_ERROR, "Failed to insert the config subtree '%s' found in %s\n", sub->name, ctx->sheet->hidlib.fullpath);
		return -1;
	}
	else
		rnd_conf_update(NULL, -1);
	return 0;
}

static int parse_sheet(read_ctx_t *ctx, lht_node_t *subtree, int load_conf)
{
	if ((ctx->ver < 1) || (ctx->ver > 1)) {
		error(subtree, ("Unknown sheet version (too old or too new)\n"));
		return -1;
	}

	if (parse_root_group(ctx, &ctx->sheet->indirect, hash_must_get(subtree, "obj_indirect.1"), 1) != 0)
		return -1;
	if (parse_root_group(ctx, &ctx->sheet->direct, hash_must_get(subtree, "obj_direct.2"), 2) != 0)
		return -1;
	if (load_conf && (parse_conf(ctx, lht_dom_hash_get(subtree, "sch-rnd-conf-v1")) != 0))
		return -1;
	return 0;
}

/* Copy only standard, symbol-related pen names from the default sheet */
static int sym_as_sheet_chk_copy_pen(void *udata, csch_sheet_t *dst, csch_cpen_t *p)
{
/*	read_ctx_t *ctx = udata;*/
	if (strncmp(p->name.str, "busterm-", 8) == 0) return 1;
	if (strncmp(p->name.str, "term-", 5) == 0) return 1;
	if (strncmp(p->name.str, "sym-", 4) == 0) return 1;
	return 0;
}

static int parse_sym_as_sheet(read_ctx_t *ctx, lht_node_t *root_hash, int load_conf)
{
	int res;
	lht_node_t *grp1;

	/* ctx->sheet->indirect and ctx->sheet->direct are initialized and empty */
	rnd_trace("SYM LOAD\n");

	grp1 = lht_dom_hash_get(root_hash, "group.1");
	if (grp1 == NULL) {
		rnd_message(RND_MSG_ERROR, "Symbol-as-sheet load: group.1 doesn't exist\n");
		return -1;
	}
	if (grp1->type != LHT_HASH) {
		rnd_message(RND_MSG_ERROR, "Symbol-as-sheet load: group.1 is not a hash\n");
		return -1;
	}



	res = parse_group_(ctx, &ctx->sheet->direct, grp1);
	if (res == 0) {
		ctx->sheet->is_symbol = 1;
		sch_rnd_sheet_setup(ctx->sheet, SCH_RND_SSC_PENS | SCH_RND_SSC_PEN_MARK_DEFAULT, sym_as_sheet_chk_copy_pen, ctx);
	}

	return res;
}

/*** file level parsing and entry points ***/

static int io_lihata_load_sheet_or_buff_or_sym(FILE *f, const char *fn, const char *fmt, csch_sheet_t *dst, const char *root_prefix, int root_prefix_len, int load_conf, int allow_sym)
{
	int res = -1;
	char *errmsg = NULL;
	read_ctx_t ctx;

	ctx.fn = fn;
	ctx.sheet = dst;
	ctx.doc = lht_dom_load(fn, &errmsg);
	if (ctx.doc == NULL) {
		rnd_message(RND_MSG_ERROR, "Error loading '%s':\n%s\n", fn, errmsg);
		free(errmsg);
		return -1;
	}

	TODO("This should be coming from an arg for loading file into buffer: , rnd_conf_role_t settings_dest");
	ctx.cfg_dest = RND_CFR_DESIGN;

	if ((ctx.doc->root->type == LHT_HASH) && (strncmp(ctx.doc->root->name, root_prefix, root_prefix_len) == 0)) {
		char *end;
		ctx.ver = strtol(ctx.doc->root->name + root_prefix_len, &end, 10);
		if (*end != '\0')
			error(ctx.doc->root, ("invalid sheet version (not an integer)\n"));
		else {
			res = parse_sheet(&ctx, ctx.doc->root, load_conf);
			if (res == 0)
				csch_sheet_bbox_update(dst);
		}
	}
	else if (allow_sym && (ctx.doc->root->type == LHT_HASH) && (strncmp(ctx.doc->root->name, "cschem-group-v", 14) == 0)) {
		/* special case: load sym as sheet for direct editing */
		res = parse_sym_as_sheet(&ctx, ctx.doc->root, load_conf);
		if (res == 0)
			csch_sheet_bbox_update(dst);
	}
	else {
		rnd_message(RND_MSG_ERROR, "Error loading '%s': not a %s\n", fn, root_prefix);
		res = -1;
	}

	lht_dom_uninit(ctx.doc);
	free(errmsg);
	return res;
}

int io_lihata_load_sheet(FILE *f, const char *fn, const char *fmt, csch_sheet_t *dst)
{
	return io_lihata_load_sheet_or_buff_or_sym(f, fn, fmt, dst, "cschem-sheet-v", 14, 1, 1);
}

int io_lihata_load_buffer(FILE *f, const char *fn, const char *fmt, csch_sheet_t *dst)
{
	return io_lihata_load_sheet_or_buff_or_sym(f, fn, fmt, dst, "cschem-buffer-v", 15, 0, 0);
}

int io_lihata_load_project(FILE *f, const char *fn, const char *fmt, csch_project_t *dst, int with_sheets)
{
	int res = -1;
	char *errmsg = NULL;
	read_ctx_t ctx;

	ctx.fn = fn;
	ctx.proj = dst;
	ctx.doc = lht_dom_load(fn, &errmsg);
	if (ctx.doc == NULL) {
		rnd_message(RND_MSG_ERROR, "Error loading '%s':\n%s\n", fn, errmsg);
		free(errmsg);
		return -1;
	}

	if ((ctx.doc->root->type == LHT_HASH) && (strncmp(ctx.doc->root->name, "coraleda-project-v", 18) == 0)) {
		char *end;
		ctx.ver = strtol(ctx.doc->root->name+18, &end, 10);
		if (*end == '\0') {
			free(dst->hdr.loadname);
			dst->hdr.loadname = rnd_strdup(fn);
			csch_proj_update_filename(dst);
			dst->hdr.root = ctx.doc;
			ctx.doc = NULL;
			res = 0;
		}
		else
			error(ctx.doc->root, ("invalid project version (not an integer)\n"));
	}
	else {
		rnd_message(RND_MSG_ERROR, "Error loading '%s': not a project\n", fn);
		res = -1;
	}

	if (ctx.doc != NULL)
		lht_dom_uninit(ctx.doc);
	free(errmsg);
	return res;
}

csch_cgrp_t *io_lihata_load_grp(FILE *f, const char *fn, const char *fmt, const char *subsymname, csch_sheet_t *sheet)
{
	csch_cgrp_t *resgrp = NULL;
	char *errmsg = NULL;
	read_ctx_t ctx;

	ctx.fn = fn;
	ctx.sheet = sheet;
	ctx.doc = lht_dom_load(fn, &errmsg);
	if (ctx.doc == NULL) {
		rnd_message(RND_MSG_ERROR, "Error loading '%s':\n%s\n", fn, errmsg);
		free(errmsg);
		return NULL;
	}

	TODO("code dup with sheet load");

	if ((ctx.doc->root != NULL) && (ctx.doc->root->type == LHT_HASH) && (strncmp(ctx.doc->root->name, "cschem-group-v", 14) == 0)) {
		char *end;
		ctx.ver = strtol(ctx.doc->root->name+14, &end, 10);
		if (*end != '\0')
			error(ctx.doc->root, ("invalid sheet version (not an integer)\n"));
		else {
			lht_node_t *nd = lht_dom_hash_get(ctx.doc->root, "group.1");

			if (nd != NULL) {
				if (htip_get(&sheet->direct.id2obj, 1) == NULL) {
					int rv = parse_group(&ctx, &sheet->direct, nd, 1);
					if (rv == 0) {
						resgrp = htip_get(&sheet->direct.id2obj, 1);
						csch_cgrp_update(sheet, resgrp, 1);
						csch_sheet_bbox_update(sheet);
						if (sheet->direct.data.grp.next_id < 2)
							sheet->direct.data.grp.next_id = 2;
					}
				}
				else
					rnd_message(RND_MSG_ERROR, "Error loading '%s': there's already a group1 in destination sheet\n", fn);
			}
		}
	}
	else
		rnd_message(RND_MSG_ERROR, "Error loading '%s': not a group\n", fn);

	lht_dom_uninit(ctx.doc);
	free(errmsg);
	return resgrp;
}

typedef struct {
	csch_plug_io_type_t expect, have;
	enum {
		TPS_UNDECIDED,
		TPS_GOOD,
		TPS_BAD
	} state;
} test_parse_t;

/* expect root to be a ha:pcb-rnd-board-v* */
void test_parse_ev(lht_parse_t *ctx, lht_event_t ev, lht_node_type_t nt, const char *name, const char *value)
{
	test_parse_t *tp = ctx->user_data;
	if (ev == LHT_OPEN) {
		if ((nt == LHT_HASH) && (strncmp(name, "cschem-sheet-v", 14) == 0))
			tp->have = CSCH_IOTYP_SHEET;
		else if ((nt == LHT_HASH) && (strncmp(name, "cschem-buffer-v", 15) == 0))
			tp->have = CSCH_IOTYP_BUFFER;
		else if ((nt == LHT_HASH) && (strncmp(name, "cschem-group-v", 14) == 0))
			tp->have = CSCH_IOTYP_GROUP;
		else if ((nt == LHT_HASH) && (strncmp(name, "coraleda-project-v", 18) == 0))
			tp->have = CSCH_IOTYP_PROJECT;
		else if ((nt == LHT_LIST) && (strncmp(name, "sch-rnd-attbl-v", 15) == 0))
			tp->have = CSCH_IOTYP_ATTBL;
		else
			tp->have = 0;

		if ((tp->have == CSCH_IOTYP_GROUP) && (tp->expect == CSCH_IOTYP_SHEET))
			tp->state = TPS_GOOD; /* special case: loading a symbol group as a sheet for direct editing */
		else
			tp->state = (tp->have & tp->expect) ? TPS_GOOD : TPS_BAD;
	}
}


/* run an event parser for the first 32k of the file; accept the file if it
   has a valid looking root; refuse if:
    - no root in the first 32k (or till eof)
    - not a valid lihata doc (parser error)
    - lihata, but the wrong root
*/
int io_lihata_test_parse(FILE *f, const char *fn, const char *fmt, csch_plug_io_type_t type)
{
	lht_parse_t ctx;
	int count;
	test_parse_t tp;

	tp.expect = type;
	tp.state = TPS_UNDECIDED;

	lht_parser_init(&ctx);
	ctx.event = test_parse_ev;
	ctx.user_data = &tp;

	for(count = 0; count < 32768; count++) {
		int c = fgetc(f);
		if (lht_parser_char(&ctx, c) != LHTE_SUCCESS) {
			/* parse error or end */
			tp.state = TPS_BAD;
			break;
		}
		if (tp.state != TPS_UNDECIDED)
			break;
	}
	lht_parser_uninit(&ctx);
	return (tp.state == TPS_GOOD) ? 0 : -1;
}
