/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - OrCAD file format support
 *  Copyright (C) 2023,2026 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 Entrust Fund in 2023)
 *
 *  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 <librnd/core/safe_fs.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/misc_util.h>
#include <libcschem/config.h>
#include <libcschem/cnc_grp.h>
#include <libcschem/cnc_text.h>
#include <libcschem/cnc_line.h>
#include <libcschem/cnc_any_obj.h>
#include <libcschem/util_wirenet.h>
#include <libcschem/operation.h>
#include <genht/htip.h>
#include <genht/hash.h>

#include <stdio.h>
#include <librnd/core/error.h>
#include <librnd/core/math_helper.h>
#include <plugins/lib_alien/read_helper.h>
#include <plugins/lib_alien/read_postproc.h>

#include "read.h"
#include "read_parse.h"
#include "read_fio.h"
#include "io_orcad_conf.h"

/* optional trace */
#if 1
#	include <stdio.h>
#	define tprintf printf
#else
	static int tprintf(const char *fmt, ...) { return 0; }
#endif

#define TEXT_HEIGHT 3000
#define TEXT_HEIGHT_ORCAD (TEXT_HEIGHT / rctx->alien.coord_factor)

typedef struct cache_sym_s {
	const char *name;                    /* points into ->root; also key for the hash */
	orcad_xcachesymvariant_node_t *root;
	orcad_symbolpinmapping_node_t *pinmap_root;
	orcad_pinidxmapping_node_t *pinmap;
	csch_cgrp_t *grp_pwr_invis;          /* rendered with per pin power pin invisibility respected */
	csch_cgrp_t *grp_pwr_vis;            /* rendered with per pin power pin invisibility overridden, power pins made visible */
	long width, height; /* actual bbox of the sym, based on info from the file (in alien size) */
	unsigned simple:1; /* see explanation at cache_sym_how_t */
	unsigned slotted:1; /* has the portmap attribute (converted from pinmap) */
	unsigned linked:1; /* used in cross-linking (propagation) during load*/
} cache_sym_t;

/*** generic utility ***/

/* Convert net_id to wirenet group; if net_id doesn't exist in curent page:
   return NULL (alloc==0) or allocate a new wirenet */
static csch_cgrp_t *orcad_wirenet_lookup(io_orcad_rctx_t *rctx, long net_id, int alloc)
{
	csch_cgrp_t *wn = htip_get(&rctx->wirenets, net_id);

	if (wn == NULL) {
		csch_source_arg_t *src;
		csch_sheet_t *sheet = rctx->alien.sheet;

		if (!alloc)
			return NULL;


		wn = csch_cgrp_alloc(sheet, &sheet->direct, csch_oid_new(sheet, &sheet->direct));
		src = csch_attrib_src_c(rctx->fn, 0, 0, NULL);
		csch_cobj_attrib_set(sheet, wn, CSCH_ATP_HARDWIRED, "role", "wire-net", src);
		htip_set(&rctx->wirenets, net_id, wn);
	}

	return wn;
}

/* Set wn's name attribute; if already set, remember new names as alternative
   names */
static void orcad_cgrp_set_name(csch_cgrp_t *dst, const char *name, const char *src_fn, long src_lineno)
{
	csch_source_arg_t *src;

	src = csch_attrib_src_c(src_fn, src_lineno, 0, NULL);

	if (csch_attrib_get_str(&dst->attr, "name") == NULL)
		csch_attrib_set(&dst->attr, CSCH_ATP_USER_DEFAULT, "name", name, src, NULL);
	else
		csch_attrib_append(&dst->attr, CSCH_ATP_USER_DEFAULT, "alt_names", name, src);
}

/* Change drawing area extents to width/height (origin is always at 0;0) */
static void orcad_sheet_set_extents(io_orcad_rctx_t *rctx, long width, long height, rnd_bool grid_ref, long src_lineno, int is_metric)
{
	csch_sheet_t *sheet = rctx->alien.sheet;

	if (is_metric) {
		width = width/254;
		height = height/254;
	}
	else {
		width = width/10;
		height = height/10;
	}

	if (grid_ref) {
		width -= 20;
		height -= 20;
	}

#if 0
	csch_source_arg_t *src;
	char tmp[128];

	/* alternative to drawing the box; this one wouldn't claim 0;0 tho */
	sprintf(tmp, "%ld", csch_alien_coord_x(&rctx->alien, height));
	src = csch_attrib_src_c(rctx->fn, src_lineno, 0, NULL);
	csch_attrib_set(&sheet->direct.attr, CSCH_ATP_USER_DEFAULT, "drawing_min_height", tmp, src, NULL);

	sprintf(tmp, "%ld", csch_alien_coord_x(&rctx->alien, width));
	src = csch_attrib_src_c(rctx->fn, src_lineno, 0, NULL);
	csch_attrib_set(&sheet->direct.attr, CSCH_ATP_USER_DEFAULT, "drawing_min_width", tmp, src, NULL);
#endif

	rctx->alien.oy = height;

	csch_alien_mkline(&rctx->alien, &sheet->direct, 0, 0, width, 0, "sheet-decor");
	csch_alien_mkline(&rctx->alien, &sheet->direct, 0, 0, 0, height, "sheet-decor");
	csch_alien_mkline(&rctx->alien, &sheet->direct, width, height, width, 0, "sheet-decor");
	csch_alien_mkline(&rctx->alien, &sheet->direct, width, height, 0, height, "sheet-decor");
}

/* textstr needs to be strdup()'d by the caller */
static csch_text_t *place_text(io_orcad_rctx_t *rctx, csch_cgrp_t *dst, double x, double y, const char *stroke, char *txtstr)
{
	csch_text_t *t;

	t = (csch_text_t *)csch_alien_mktext(&rctx->alien, dst, x, y, stroke);
	if (t == NULL)
		return NULL;

	t->text = txtstr;
	t->spec_miry = 1;
	return t;
}

static csch_text_t *add_attr_with_text(io_orcad_rctx_t *rctx, csch_cgrp_t *dst, double x, double y, const char *stroke, const char *key, const char *val, int vis_key, int vis_val, int capital, int trot, long src_lineno)
{
	csch_source_arg_t *src;
	csch_text_t *t;

	src = csch_attrib_src_c(rctx->fn, src_lineno, 0, NULL);
	csch_cobj_attrib_set(rctx->alien.sheet, dst, CSCH_ATP_USER_DEFAULT, key, val, src);

	if (vis_key || vis_val) {
		char *templ;
		const char *capa = capital ? "A" : "a";
		double save;

		if (vis_key && vis_val)  templ = rnd_concat(key, " = %../", capa, ".", key, "%", NULL);
		if (vis_key && !vis_val) templ = rnd_strdup(key);
		if (!vis_key && vis_val) templ = rnd_concat("%../", capa, ".", key, "%", NULL);

		/* do not mirror using sheet height whenplacing in a sub-group */
		if (dst != &rctx->alien.sheet->direct) {
			save = rctx->alien.oy;
			rctx->alien.oy = 0;
		}

		t = place_text(rctx, dst, x, y, stroke, templ);
		if (vis_val)
			t->dyntext = 1;

		/* inverse-transform dst coords out from text coords; text coords
		   are specific  */
		switch((int)dst->xform.rot) {
			case 0: break;
			case 90:
				rnd_swap(rnd_coord_t, t->spec1.x, t->spec1.y);
				t->spec_miry = !t->spec_miry;
				t->spec_rot -= dst->xform.rot;
				break;
			case 270:
				rnd_swap(rnd_coord_t, t->spec1.x, t->spec1.y);
				t->spec1.x = -t->spec1.x;
				t->spec1.y = -(dst->hdr.bbox.x2 - dst->hdr.bbox.x1) - t->spec1.x;
				t->spec_rot = 270;
				break;
			case 180:
				t->spec1.x = -t->spec1.x;
				t->spec1.y = -(dst->hdr.bbox.y2 - dst->hdr.bbox.y1) + t->spec1.y;
				t->spec_rot = 180;
				break;
		}

		t->spec_rot += trot;

		/* restore mirroring */
		if (dst != &rctx->alien.sheet->direct)
			rctx->alien.oy = save;
	}

	return t;
}


/*** sym cache and primitives ***/
static csch_chdr_t *render_primitive_in(io_orcad_rctx_t *rctx, csch_cgrp_t *dst, orcad_prim_t *prim, const char *stroke, const char *fill)
{
	switch(prim->type) {
	case ORCAD_PRIMITIVE_RECT:
		{
			orcad_rect_prim_t *rect = (orcad_rect_prim_t *)prim;
			return csch_alien_mkrect(&rctx->alien, dst, rect->x1, rect->y1, rect->x2, rect->y2, stroke, rect->have_fill_style ? fill : NULL);
		}

	case ORCAD_PRIMITIVE_LINE:
		{
			orcad_line_prim_t *line = (orcad_line_prim_t *)prim;
			return csch_alien_mkline(&rctx->alien, dst, line->x1, line->y1, line->x2, line->y2, stroke);
		}

	case ORCAD_PRIMITIVE_ARC:
		{
			orcad_arc_prim_t *arc = (orcad_arc_prim_t *)prim; /* the bbox stored in arc is really the bbox of the whole circle */
			double cx = (double)(arc->x1 + arc->x2) / 2.0, cy = (double)(arc->y1 + arc->y2) / 2.0;
			double rx = (double)(arc->x2 - arc->x1) / 2.0, ry = (double)(arc->y2 - arc->y1) / 2.0;
			double rd = fabs(rx - ry);
			double sa, ea, da;

			sa = atan2(cy - arc->start_y, arc->start_x - cx);
			ea = atan2(cy - arc->end_y, arc->end_x - cx);

			/* normalize angles and calculate delta */
			if (sa < 0) sa += 2*G2D_PI;
			if (ea < 0) ea += 2*G2D_PI;
			da = ea - sa;
			if (da < 0) da += 2*G2D_PI;

			if (rd < 0.1)
				return csch_alien_mkarc(&rctx->alien, dst, cx, cy, (rx+ry)/2.0, sa * RND_RAD_TO_DEG, da * RND_RAD_TO_DEG, stroke);
			else
				return csch_alien_mkearc(&rctx->alien, dst, cx, cy, rx, ry, sa, -da, stroke, NULL);

			TODO("low prio: Elliptical arc endpoints are off");
		}

	case ORCAD_PRIMITIVE_ELLIPSE:
		{
			orcad_ellipse_prim_t *e = (orcad_ellipse_prim_t *)prim;
			return csch_alien_mkearc(&rctx->alien, dst, (e->x1+e->x2)/2, (e->y1+e->y2)/2, (e->x2-e->x1)/2, (e->y2-e->y1)/2, 0, 2*G2D_PI, stroke, NULL);
		}
		break;

	case ORCAD_PRIMITIVE_POLYGON:
		{
			orcad_polygon_prim_t *poly = (orcad_polygon_prim_t *)prim;
			long n;
			csch_chdr_t *p = csch_alien_mkpoly(&rctx->alien, dst, stroke, poly->have_fill_style ? fill : NULL);
			for(n = 0; n < poly->num_points-1; n++)
				csch_alien_append_poly_line(&rctx->alien, p, poly->points[n].x, poly->points[n].y, poly->points[n+1].x, poly->points[n+1].y);
			n = poly->num_points-1;
			csch_alien_append_poly_line(&rctx->alien, p, poly->points[n].x, poly->points[n].y, poly->points[0].x, poly->points[0].y);

			return p;
		}

	case ORCAD_PRIMITIVE_POLYLINE:
		{
			orcad_polyline_prim_t *poly = (orcad_polyline_prim_t *)prim;
			long n;
			csch_chdr_t *p = csch_alien_mkpoly(&rctx->alien, dst, stroke, NULL);
			/* start loop from 1, index backwards */
			for(n = 1; n < poly->num_points; n++)
				csch_alien_append_poly_line(&rctx->alien, p, poly->points[n-1].x, poly->points[n-1].y, poly->points[n].x, poly->points[n].y);

			return p;
		}

	case ORCAD_PRIMITIVE_TEXT:
		{
			orcad_text_prim_t *text = (orcad_text_prim_t *)prim;
			csch_text_t *t = place_text(rctx, dst, text->x, text->y, stroke, rnd_strdup(text->text));
			return &t->hdr;
		}
		break;

	case ORCAD_PRIMITIVE_BITMAP:
		{
			orcad_bitmap_prim_t *rect = (orcad_bitmap_prim_t *)prim;
			return csch_alien_mkrect(&rctx->alien, dst, rect->x, rect->y, rect->x2, rect->y2, stroke, fill);
		}

	case ORCAD_PRIMITIVE_BEZIER:
		{
			csch_sheet_t *sheet = rctx->alien.sheet;
			orcad_bezier_prim_t *bezier = (orcad_bezier_prim_t *)prim;
			csch_cgrp_t *grp;
			long n;
			
			grp = csch_cgrp_alloc(sheet, dst, csch_oid_new(sheet, &sheet->direct));
			for(n = 0; n < bezier->num_segments; n++) {
				orcad_bsegment_t *bs = &bezier->segments[n];
				csch_alien_mkbezier(&rctx->alien, grp,
					bs->p1.x, bs->p1.y, bs->p2.x, bs->p2.y, bs->p3.x, bs->p3.y, bs->p4.x, bs->p4.y,
					stroke);
			}

			return &grp->hdr;
		}
		break;

	default:
		rnd_message(RND_MSG_ERROR, "orcad: unknown primitive 0x%x\n", (unsigned int)prim->type);
		break;
	}

	return NULL;
}

static void render_symbolpin_in(io_orcad_rctx_t *rctx, csch_cgrp_t *dst, orcad_symbolpin_node_t *nd, int vis_disp_name, int vis_name, int rot_name, int force_vis_power)
{
	csch_cgrp_t *pin;
	csch_source_arg_t *src;
	int shape_gr = nd->pin_shape & 0x0e;
	enum {
		PT_INPUT          = 0,
		PT_BIDIR          = 1,
		PT_OUTPUT         = 2,
		PT_OPEN_COLLECTOR = 3,
		PT_PASSIVE        = 4, /* default */
		PT_TRISTATE       = 5,
		PT_OPEN_EMITTER   = 6,
		PT_POWER          = 7
	} ptype = nd->port_type;
	static const char *ptype_names[] = { "input", "bidir", "output", "oc", NULL, NULL, "oe", "power", NULL }; /* coraleda crl002 dir names */
	int dot_r = 3, dx, dy, nx, ny, nogfx = 0, globpin;
	long start_x = nd->start_x, start_y = nd->start_y;

	dx = start_x - nd->hotpt_x;
	dy = start_y - nd->hotpt_y;

	/* "gobal pins" are those found in global symbols usually: gnd, vcc and alike;
	   there's no explicit flag for this, but they are zero-length */
	globpin = ((dx == 0) && (dy == 0));

	/* pin_shape & 128: the normal "invisible power pin" flag from a lib symbol
	   pin_shape & 32:  the lib symbol gets a second copy in the cache, as _0 or _1, etc, with this flag set, to force that one power pin visible again
	   force_vis_power: part instantiation forces all power pins visible
	
	   Related sampels: T07* and opamp*    */
	if ((nd->pin_shape & 128) && !(nd->pin_shape & 32) && !force_vis_power) {
		/* invisible power pin - not creating it would make gnd and vcc fail */
		if (!globpin)
			nogfx = 1; /* a "global pin" does have a zero length sch-rnd graphics so it can be connected; a true invisible pin (e.g. hidden opamp pin) has no graphics */
		start_x = nd->hotpt_x;
		start_y = nd->hotpt_y;
	}

	/* calculate directional vector assuming 90 degree rotations only */
	if (dx > 0) dx = +1;
	else if (dx < 0) dx = -1;
	if (dy > 0) dy = +1;
	else if (dy < 0) dy = -1;

	/* normal vector */
	nx = dy;
	ny = -dx;

	if (shape_gr & 0x04) {
		/* dot outside: make the pin shorter */
		start_x -= dx * dot_r * 2;
		start_y -= dy * dot_r * 2;
	}
	if (ptype == PT_INPUT) {
		/* input arrow outside: make the pin shorter */
		start_x -= dx * dot_r;
		start_y -= dy * dot_r;
	}

	if (!nogfx) {
		src = csch_attrib_src_c(rctx->fn, nd->node.offs, 0, NULL);
		pin = (csch_cgrp_t *)csch_alien_mkpin_line(&rctx->alien, src, dst, nd->hotpt_x, nd->hotpt_y, start_x, start_y);
		src = csch_attrib_src_c(rctx->fn, nd->node.offs, 0, NULL);
		csch_cobj_attrib_set(rctx->alien.sheet, pin, CSCH_ATP_HARDWIRED, "name", nd->pin_name, src);

		if (!((dx == 0) && (dy == 0))) {

			/* add extra graphics */
			if (shape_gr & 0x04) {
				/* dot (circle outside) */
				csch_alien_mkarc(&rctx->alien, pin,
					nd->start_x - dx * dot_r, nd->start_y - dy * dot_r,
					dot_r, 0, 360, "sym-decor");
			}
			if (shape_gr & 0x02) {
				/* clock (triangle inside) */
				csch_alien_mkline(&rctx->alien, pin,
					nd->start_x + nx * dot_r, nd->start_y + ny * dot_r,
					nd->start_x + dx * dot_r, nd->start_y + dy * dot_r,
				 "sym-decor");

				csch_alien_mkline(&rctx->alien, pin,
					nd->start_x - nx * dot_r, nd->start_y - ny * dot_r,
					nd->start_x + dx * dot_r, nd->start_y + dy * dot_r,
				 "sym-decor");
			}

			if (ptype == PT_INPUT) {
				/* clock (triangle inside) */
				csch_alien_mkline(&rctx->alien, pin,
					nd->start_x, nd->start_y,
					nd->start_x - dx * dot_r + nx * dot_r, nd->start_y - dy * dot_r + ny * dot_r,
				 "sym-decor");
				csch_alien_mkline(&rctx->alien, pin,
					nd->start_x, nd->start_y,
					nd->start_x - dx * dot_r - nx * dot_r, nd->start_y - dy * dot_r - ny * dot_r,
				 "sym-decor");
				csch_alien_mkline(&rctx->alien, pin,
					nd->start_x - dx * dot_r - nx * dot_r, nd->start_y - dy * dot_r - ny * dot_r,
					nd->start_x - dx * dot_r + nx * dot_r, nd->start_y - dy * dot_r + ny * dot_r,
				 "sym-decor");
			}
		}

		if ((ptype >= 0) && (ptype < sizeof(ptype_names) / sizeof(ptype_names[0])) && (ptype_names[ptype] != NULL)) {
			src = csch_attrib_src_c(rctx->fn, nd->node.offs, 0, NULL);
			csch_cobj_attrib_set(rctx->alien.sheet, pin, CSCH_ATP_HARDWIRED, "dir", ptype_names[ptype], src);
		}

		/* place pin name */
		if (!nogfx) {
			csch_text_t *t;
		double text_ang;

			if ((dx == 0) && (dy == +1)) {
				/* north */
				nx = -nx;
				ny = -ny;
			}
			else if (dx < 0) {
				/* east */
				nx = -nx;
				ny = -ny;
			}

			/* tune text rotation and placement depending on rot_name */
			if (rot_name) {
				if (dy == 0) {
					text_ang = 0;
					nx = 0;
				}
				else
					text_ang = 90;
			}
			else
				text_ang = 0;

			/* display/name is always printed next to the pin's end */
			if (vis_disp_name) {
				t = (csch_text_t *)csch_alien_mktext(&rctx->alien, pin, nd->start_x - dx * dot_r * 2, nd->start_y - dy * dot_r * 2, "sym-decor");
				if (t != NULL) {
					t->text = rnd_strdup("%../a.display/name%");
					t->dyntext = 1;
					t->spec_rot = text_ang;
					if (dx < 0)
						t->spec_mirx = 1;
				}
			}

			/* this is the pin's original name inside the box */
			if (vis_name) {
				t = (csch_text_t *)csch_alien_mktext(&rctx->alien, pin, nd->start_x + dx * dot_r * 2 - nx * TEXT_HEIGHT_ORCAD/2, nd->start_y + dy * dot_r * 2 - ny * TEXT_HEIGHT_ORCAD/2, "sym-decor");
				if (t != NULL) {
					t->text = rnd_strdup("%../A.name%");
					t->dyntext = 1;
					t->spec_rot = text_ang;
					if (dx < 0)
						t->spec_mirx = 1;
				}
			}
		}
	}
}

static int sym_create_pinmap_in_cache(io_orcad_rctx_t *rctx, cache_sym_t *cs, csch_cgrp_t *grp)
{
	gds_t tmp = {0};
	int p;
	orcad_symbolgraphic_node_t *root = (orcad_symbolgraphic_node_t *)cs->root->obj;
	orcad_pinidxmapping_node_t *map = cs->pinmap;

	if ((map == NULL) || (grp == NULL))
		return 0;

/*		rnd_trace(" unit_ref=%s\n", map->unit_ref);*/
	for(p = 0; (p < map->num_pins) && (p < root->num_symbolpins); p++) {
		csch_source_arg_t *src;
		orcad_pin_t *map_pin = map->pins[p];
		orcad_symbolpin_node_t *sym_pin = root->symbolpins[p];

		if (map_pin == NULL) continue;

		tmp.used = 0;
		if ((map->unit_ref != NULL) && (*map->unit_ref != '\0')) {
			gds_append_str(&tmp, map->unit_ref);
			gds_append(&tmp, '/');
		}
		gds_append_str(&tmp, sym_pin->pin_name);
		gds_append_str(&tmp, "->pcb/pinnum=");
		gds_append_str(&tmp, map_pin->pin_name);

/*			rnd_trace("  [%d] map=%s sym=%s: '%s'\n", p, map_pin->pin_name, sym_pin->pin_name, tmp.array);*/

		src = csch_attrib_src_c(rctx->fn, map->node.offs, 0, NULL);
		csch_attrib_append(&grp->attr, CSCH_ATP_USER_DEFAULT, "+portmap", tmp.array, src);
		cs->slotted = 1;
	}

	gds_uninit(&tmp);
	return 0;
}

/* Look up name for a slot index (->pim_idx in the partinst node). Returns NULL on
   error. */
static const char *cs_slot_name(io_orcad_rctx_t *rctx, cache_sym_t *cs, int slot_idx)
{
	orcad_pinidxmapping_node_t *map;

	if ((slot_idx < 0) || (cs->pinmap_root == NULL) || (slot_idx >= cs->pinmap_root->num_pinidxmappings))
		return NULL;

	map = cs->pinmap_root->pinidxmappings[slot_idx];
	if (map == NULL)
		return NULL;

	if ((map->unit_ref == NULL) || (*map->unit_ref == '\0'))
		return NULL;

	return map->unit_ref;
}

/* Create cs->grp* building all the objects from the tree */
static int render_sym_in_cache(io_orcad_rctx_t *rctx, cache_sym_t *cs, int force_vis_power)
{
	csch_sheet_t *sheet = rctx->alien.sheet;
	csch_source_arg_t *src;
	csch_cgrp_t *grp;

	grp = csch_cgrp_alloc(sheet, &sheet->indirect, csch_oid_new(sheet, &sheet->indirect));
	src = csch_attrib_src_c("orcad sym cache", cs->root->node.offs, 0, NULL);
	csch_cobj_attrib_set(sheet, grp, CSCH_ATP_HARDWIRED, "role", "symbol", src);

	if (force_vis_power)
		cs->grp_pwr_vis = grp;
	else
		cs->grp_pwr_invis = grp;

	switch(cs->root->obj->type) {
		case ORCAD_TYPE_GLOBALSYMBOL:
		case ORCAD_TYPE_PORTSYMBOL:
		case ORCAD_TYPE_OFFPAGECONNSYMBOL:
			/* note: these 3 nodes share the same structure */
			{
				double oy_save;
				unsigned int n;
				orcad_globalsymbol_node_t *root = (orcad_globalsymbol_node_t *)cs->root->obj;

				oy_save = rctx->alien.oy;
				rctx->alien.oy = 0;

				for(n = 0; n < root->num_primitives; n++)
					render_primitive_in(rctx, grp, root->primitives[n], "sym-decor", "sym-decor-fill");
				for(n = 0; n < root->num_symbolpins; n++)
					render_symbolpin_in(rctx, grp, root->symbolpins[n], 0, 0, 0, force_vis_power);

				cs->width = root->symbox_x;
				cs->height = root->symbox_y;

				rctx->alien.oy = oy_save;
			}
			return 0;
		case ORCAD_TYPE_SYMBOLGRAPHIC:
			{
				double oy_save;
				unsigned int n;
				orcad_symbolgraphic_node_t *root = (orcad_symbolgraphic_node_t *)cs->root->obj;

				oy_save = rctx->alien.oy;
				rctx->alien.oy = 0;

				for(n = 0; n < root->num_primitives; n++)
					render_primitive_in(rctx, grp, root->primitives[n], "sym-decor", "sym-decor-fill");
				for(n = 0; n < root->num_symbolpins; n++)
					render_symbolpin_in(rctx, grp, root->symbolpins[n], root->pin_numbers_visible, root->pin_names_visible, root->pin_names_rotate, force_vis_power);

				/* Note: floaters (displayprops) are created only at placment */

				cs->width = root->symbox_x;
				cs->height = root->symbox_y;

				rctx->alien.oy = oy_save;
			}
			return sym_create_pinmap_in_cache(rctx, cs, grp);
		default:
			rnd_message(RND_MSG_ERROR, "orcad cache symbol render: invalid obj type 0x%x\n", cs->root->obj->type);
			return -1;
	}


	return -1;
}

/* Place a symbol from the cahce onto the sheet; render symbol in cache if
   needed. Returns the symbol group just placed (or NULL on error) */
static csch_cgrp_t *place_sym_from_cache(io_orcad_rctx_t *rctx, cache_sym_t *cs, long x_in, long y_in, int rot_in, int in_mirror, int slot_idx, int force_vis_power, int lineno)
{
	csch_sheet_t *sheet = rctx->alien.sheet;
	csch_cgrp_t *placed;

	if (force_vis_power) {
		if (cs->grp_pwr_vis == NULL)
			render_sym_in_cache(rctx, cs, force_vis_power);
	}
	else {
		if (cs->grp_pwr_invis == NULL)
			render_sym_in_cache(rctx, cs, force_vis_power);
	}

	if (force_vis_power) {
		if (cs->grp_pwr_vis == NULL)
			return NULL;
	}
	else {
		if (cs->grp_pwr_invis == NULL)
			return NULL;
	}


	placed = csch_cgrp_dup(sheet, &sheet->direct, force_vis_power ? cs->grp_pwr_vis : cs->grp_pwr_invis, 0);

	/* initial placement wherever, rotate, mirror */
	placed->x = placed->y = 0;
	placed->spec_rot = rot_in * 90;
	placed->mirx = in_mirror;

	csch_cgrp_update(rctx->alien.sheet, placed, 1);

	/* shift coords so that ("bbox without pins") top-left is at 0;0, this
	   compensation depends on rotation and mirror */
	switch (rot_in) {
		case 0:
			if (in_mirror)
				placed->x += csch_alien_coord(&rctx->alien, cs->width);
			break;

		case 1:
			if (in_mirror)
				placed->spec_rot += 180;
			else
				placed->y -= csch_alien_coord(&rctx->alien, cs->width);
			break;

		case 2:
			if (!in_mirror)
				placed->x += csch_alien_coord(&rctx->alien, cs->width);
			placed->y -= csch_alien_coord(&rctx->alien, cs->height);
			break;

		case 3:
			placed->x += csch_alien_coord(&rctx->alien, cs->height);
			if (in_mirror) {
				placed->spec_rot -= 180;
				placed->y -= csch_alien_coord(&rctx->alien, cs->width);
			}
			break;
	}

	/* apply requested placement move, effectively grabbing the top-left corner */
	placed->x += csch_alien_coord_x(&rctx->alien, x_in);
	placed->y += csch_alien_coord_y(&rctx->alien, y_in);

	/* set slot if symbol is slotted */
	if ((slot_idx >= 0) && cs->slotted) {
		const char *slot_name = cs_slot_name(rctx, cs, slot_idx);
		if (slot_name != NULL) {
			csch_source_arg_t *src = csch_attrib_src_c(rctx->fn, lineno, 0, NULL);
			csch_attrib_set(&placed->attr, CSCH_ATP_USER_DEFAULT, "-slot", slot_name, src, NULL);
		}
		else if ((cs->pinmap_root != NULL) && (cs->pinmap_root->num_pinidxmappings > 1))
			rnd_message(RND_MSG_ERROR, "io_orcad: failed to find slot name for pim_idx=%d - please report this bug\n", slot_idx);
	}

	return placed;
}

static const char *lookup_displayprop_name(io_orcad_rctx_t *rctx, unsigned long name_idx)
{
	if ((rctx->library_root == NULL) || (name_idx >= rctx->library_root->num_names))
		return NULL;

	return rctx->library_root->names[name_idx];
}

/* Linear search for name_idx in the ->name_idx fields of nm[] */
static orcad_namemapping_t *lookup_name_mapping(io_orcad_rctx_t *rctx, unsigned int nms_len, orcad_namemapping_t *nms, unsigned long name_idx)
{
	unsigned long n;
	for(n = 0; n < nms_len; n++)
		if (nms[n].name_idx == name_idx)
			return &nms[n];
	return NULL;
}

typedef struct {
	const char *name;
	const char *value;
} floater_map_hardwired_t;

static void place_floaters_from_displayprops(io_orcad_rctx_t *rctx, csch_cgrp_t *dst, unsigned int num_dp, orcad_symboldisplayprop_node_t **dp, unsigned int nm1_len, orcad_namemapping_t *nm1, unsigned int nm2_len, orcad_namemapping_t *nm2, floater_map_hardwired_t *hw, int floaters, const char *stroke_pri, const char *stroke_sec, long src_lineno, const char *name_key)
{
	unsigned int n;
	const char *stroke = stroke_sec;
	csch_text_t *txt;
	enum {
		FMT_VAL_ONLY = 1,
		FMT_BOTH = 2,
		FMT_NAME_ONLY = 3,
		FMT_BOTH_IF_NONEMPTY = 4
	} fmt;

	rnd_trace("place_floaters_from_displayprops():\n");
	for(n = 0; n < num_dp; n++) {
		orcad_symboldisplayprop_node_t *p = dp[n];
		orcad_namemapping_t *m = NULL;
		const char *key = NULL, *val = NULL;
		int capital = 0;

		fmt = p->format;

		/* find the name mapping in the two alternaive maps provided by the
		   caller. Normally nm1 is partinst's mapping (placement), nm2 is the
		   symbolgraphics's mapping (from the Cache lib) */
		if (nm1 != NULL)
			m = lookup_name_mapping(rctx, nm1_len, nm1, p->name_idx);
		if ((m == NULL) && (nm2 != NULL))
			m = lookup_name_mapping(rctx, nm2_len, nm2, p->name_idx);

		/* Fallback mechanism of mapping was not found in either: use name_idx
		   as a direct string index for lookup_displayprop_name();
		   except these values are cached in read_library() */
		if (m == NULL) {
			if (p->name_idx == rctx->hwpropnames.part_ref) {
				key = name_key;
				val = hw->name;
				stroke = stroke_pri;
			}
			else if (p->name_idx == rctx->hwpropnames.name) {
				key = name_key;
				val = hw->name;
				stroke = stroke_pri;
			}
			else if (p->name_idx == rctx->hwpropnames.value) {
				key = "value";
				val = hw->value;
			}
		}

#if 0
		/* trace prints for floater placement */
		rnd_trace(" [%d] at %d %d  format %d  idx %d %p\n", n, p->x, p->y, fmt, p->name_idx, m);
		if (key != NULL) {
			rnd_trace("  '%s' = '%s'\n", key, val);
		}
		else if (m != NULL) {
			rnd_trace("  %ld:'%s' = %ld:'%s'\n",
				(long)m->name_idx, lookup_displayprop_name(rctx, m->name_idx),
				(long)m->value_idx, lookup_displayprop_name(rctx, m->value_idx));
		}
#endif

		/* if key and val are not hardwired and we had a matching map entry, look
		   them string values up */
		if ((key == NULL) && (m != NULL)) {
			key = lookup_displayprop_name(rctx, m->name_idx);
			val = lookup_displayprop_name(rctx, m->value_idx);
		}

		if (key != NULL) {
			int vis_key, vis_val;

			switch(fmt) {
				case FMT_VAL_ONLY:
					vis_key = 0;
					vis_val = 1;
					break;
				case FMT_NAME_ONLY:
					vis_key = 1;
					vis_val = 0;
					break;
				case FMT_BOTH_IF_NONEMPTY:
					if ((val == NULL) || (*val == '\0')) {
						vis_key = 0;
						vis_val = 0;
						break;
					}
					/* else: fall thru */
				case FMT_BOTH:
					vis_key = 1;
					vis_val = 1;
					break;
			}
		rnd_trace(" [%d] at %d %d sym: %d %d | '%s' %d %d %d\n", n, p->x, p->y, dst->x, dst->y, val, p->rotation, p->unknown_0, p->unknown_2);
			txt = add_attr_with_text(rctx, dst, p->x, p->y, stroke, key, val, vis_key, vis_val, capital, -p->rotation*90, src_lineno);
			if (floaters)
				txt->hdr.floater = 1;
		}
	}
}

static void add_forge(io_orcad_rctx_t* const rctx,
	struct orcad_node* const node, csch_cgrp_t* const grp,
	const char* const pin_name, const char* const rail_attrib)
{
	char sub_buf[128];
	char suba_buf[128];

	const char* forge[] =
	{
		"delete,forge/tmp",
		"scalar,forge/tmp",
		NULL, /* sub */
		NULL, /* suba */
		"array,connect",
		"append,connect,forge/tmp",
		NULL
	};

	csch_source_arg_t* const src = csch_attrib_src_c(rctx->fn, node->offs, 0, NULL);

	sprintf(sub_buf, "sub,^,%s:,forge/tmp", pin_name);
	forge[2] = sub_buf;

	sprintf(suba_buf, "suba,$,%s,forge/tmp", rail_attrib);
	forge[3] = suba_buf;

	csch_attrib_set_arr_c(&grp->attr, CSCH_ATP_USER_DEFAULT, "forge",
		forge, src, NULL);
}

/* Read displayprops from the library symbol (cs) to place floaters: partinst variant */
static void place_sym_floaters_pi(io_orcad_rctx_t *rctx, cache_sym_t *cs, csch_cgrp_t *sym, orcad_partinst_node_t *partinst)
{
	orcad_symbolgraphic_node_t *sg = (orcad_symbolgraphic_node_t *)cs->root->obj;
	floater_map_hardwired_t hw = {0};

	if (partinst == NULL)
		return;

	hw.name  = partinst->refdes;
	hw.value = lookup_displayprop_name(rctx, partinst->value_idx);

/* don't do this: it seems partinst has these copied:
	if (sg != NULL)
		place_floaters_from_displayprops(rctx, sym, sg->num_displayprops, sg->displayprops, "name");
*/
	place_floaters_from_displayprops(rctx, sym, partinst->num_displayprops, partinst->displayprops, partinst->node.num_namemappings, partinst->node.namemappings, sg->node.num_namemappings, sg->node.namemappings, &hw, 1, "sym-primary", "sym-secondary", partinst->node.offs, "name");
}

/* Read displayprops from the library symbol (cs) to place floaters: global symbol variant */
static void place_sym_floaters_gs(io_orcad_rctx_t *rctx, cache_sym_t *cs, csch_cgrp_t *sym, orcad_graphicinst_node_t *gr, orcad_uint32_t wire_id)
{
	orcad_globalsymbol_node_t *gs = (orcad_globalsymbol_node_t *)cs->root->obj;
	floater_map_hardwired_t hw = {0};

	const char *name_attrib;

	if (gs->num_symbolpins == 1)
		name_attrib = "rail_name";
	else
		name_attrib = "name";

	hw.name = lookup_displayprop_name(rctx, gr->graphic.instname_idx);

	/* some offpage connectors do not have explicit name, its name is
	   derived from the net it is connected to (however, offpage connectors
	   connected to wires)
	*/
	if (hw.name == NULL) {
		orcad_wire_node_t* wire = htip_get(&rctx->wires, wire_id);

		if (wire != NULL) {
			orcad_xnetalias_node_t* netalias = htip_get(&rctx->netaliases, wire->net_id);

			if (netalias != NULL)
				hw.name = netalias->alias;
		}
	}

/* don't do this: it seems partinst has these copied:
	if (gs != NULL)
		place_floaters_from_displayprops(rctx, sym, gs->num_displayprops, gs->displayprops, NULL, NULL, gs->num_namemappings, gs->namemappings, &hw, 1, "sym-primary", "sym-secondary", 0, name_attrib);
*/
	if (gr != NULL)
		place_floaters_from_displayprops(rctx, sym, gr->graphic.num_displayprops, gr->graphic.displayprops, gr->node.num_namemappings, gr->node.namemappings, gs->node.num_namemappings, gs->node.namemappings, &hw, 0, "sym-primary", "sym-secondary", 0, name_attrib);

	if (gs->num_symbolpins == 1)
		add_forge(rctx, &gs->node, sym, gs->symbolpins[0]->pin_name, name_attrib);
}

/**** high level (tree walk) ****/

typedef enum {
	CHCSYM_SIMPLE, /* simple "graphic" symbols: holds symbol graphics; vcc and gnd are like this, and are directly referenced */
	CHCSYM_PROPS,  /* complex symbols, properties: reference back to one or more symple symbols */
	CHCSYM_MAPS    /* complex symbols, pin mapping: matches the name of a CHCSYM_PROPS */
} cache_sym_how_t;

/* read_cache_sym_dir() for CHCSYM_SIMPLE and CHCSYM_PROPS */
static void read_cache_sym_dir_simple_props(io_orcad_rctx_t *ctx, orcad_xsymbolgroup_node_t *sgrp, cache_sym_how_t how, orcad_xcachesymbol_node_t *snd)
{
	cache_sym_t *cs, *cs2;

	cs = htsp_get(&ctx->syms, snd->symname);

	if (cs != NULL) { /* these are unique */
		rnd_message(RND_MSG_ERROR, "orcad: multiple occurances of cache symbol '%s'\n", snd->symname);
		return;
	}
	cs = calloc(sizeof(cache_sym_t), 1);
	cs->name = snd->symname;
	TODO("low prio: how to decide which variant to use? (using the first one for now)");
	htsp_insert(&ctx->syms, (char *)cs->name, cs);

	/* fill in extra fields */
	switch(how) {
		case CHCSYM_SIMPLE:
			cs->root = snd->variants[0];
			cs->simple = 1;
			break;
		case CHCSYM_PROPS:
			{
				orcad_properties_node_t *pn = (orcad_properties_node_t *)snd->variants[0]->obj;

				if (pn->node.type != ORCAD_TYPE_PROPERTIES) {
					rnd_message(RND_MSG_ERROR, "orcad: prop type complex cache symbol '%s' doesn't have properties\n", snd->symname);
					break;
				}
				if (pn->num_partnames < 1) {
					rnd_message(RND_MSG_ERROR, "orcad: prop type complex cache symbol '%s' doesn't have part names\n", snd->symname);
					break;
				}

				/* link cs to an existing symbol */
				TODO("low prio: this assumes there's only one part - find an example (heterogenous slotting doesn't trigger) - for(p = 0; p < pn->num_partnames; p++)");
				cs2 = htsp_get(&ctx->syms, pn->partnames[0]);
				if (cs2 == NULL) {
					rnd_message(RND_MSG_ERROR, "orcad: prop type complex cache symbol '%s' references non-existing graphics '%s'\n", snd->symname, pn->partnames[0]);
					break;
				}
				if (!cs2->simple)
					rnd_message(RND_MSG_ERROR, "orcad: prop type complex cache symbol '%s' references another complex symbol '%s'\n", snd->symname, pn->partnames[0]);
				cs->root = cs2->root;
			}
			break;
		case CHCSYM_MAPS:
			abort(); /* can't get here - caller already switch()'d */
	}
}

/* read_cache_sym_dir() for CHCSYM_MAPS */
static void read_cache_sym_dir_maps(io_orcad_rctx_t *ctx, orcad_xsymbolgroup_node_t *sgrp, cache_sym_how_t how, orcad_xcachesymbol_node_t *snd, gds_t *tmp)
{
	orcad_symbolpinmapping_node_t *pinmap = (orcad_symbolpinmapping_node_t *)snd->variants[0]->obj;
	int n;

	for(n = 0; n < pinmap->num_pinidxmappings; n++) {
		cache_sym_t *cs;
		orcad_pinidxmapping_node_t *pim = pinmap->pinidxmappings[n];

		/* build full slot name - unit_ref may be duplicate, in which case cs will be NULL but we have a fallback for that */
		tmp->used = 0;
		gds_append_str(tmp, pim->symname);
		gds_append_str(tmp, pim->unit_ref);
		cs = htsp_get(&ctx->syms, tmp->array);

		/* maybe the full slotted name is pim->symname */
		if (cs == NULL) {
			cs = htsp_get(&ctx->syms, pim->symname);
			/* for a non-explicit-slot symbol: do not overwrite the first hit;
			   so a symbol that could be slotted but is instantiated without slotting
			   it's always the first slot we use */
			if (cs->pinmap != NULL)
				continue;
		}

		if (cs == NULL) { /* created at CHCSYM_PROPS */
			rnd_message(RND_MSG_ERROR, "orcad: cache symbol '%s' does not exist for pin mapping\n", snd->symname);
			continue;
		}
		if (cs->simple) { /* if created at CHCSYM_SIMPLE, we can't pinmap it */
			rnd_message(RND_MSG_ERROR, "orcad: cache symbol '%s' is a simple graphic symbol, can't be pinmapped\n", snd->symname);
			continue;
		}

		cs->pinmap = pim;
		cs->pinmap_root = pinmap;
	}
}

/* Do not parse the whole symbol yet, just remember its tree node and name;
   parse and create the group later, on-demand */
static int read_cache_sym_dir(io_orcad_rctx_t *ctx, orcad_xsymbolgroup_node_t *sgrp, cache_sym_how_t how)
{
	long n;
	gds_t tmp = {0};

	for(n = 0; n < sgrp->num_symbols; n++) {
		orcad_xcachesymbol_node_t *snd = sgrp->symbols[n];

		if (snd->num_variants == 0) /* nothing stored */
			continue;

		/* look up or create cs */
		switch(how) {
			case CHCSYM_SIMPLE:
			case CHCSYM_PROPS:
				read_cache_sym_dir_simple_props(ctx, sgrp, how, snd);
				break;
				
			case CHCSYM_MAPS:
				read_cache_sym_dir_maps(ctx, sgrp, how, snd, &tmp);
				break;
		}
	}

	gds_uninit(&tmp);
	return 0;
}

/* propagate pinmaps among linked objects: if multiple cs's are using the
   same root, they should also share pinmap (can't be done in
   read_cache_sym_dir() because props have to be read before pinmaps) */
static void cache_link_propagate(io_orcad_rctx_t *ctx)
{
	htsp_entry_t *e, *f;
	for(e = htsp_first(&ctx->syms); e != NULL; e = htsp_next(&ctx->syms, e)) {
		cache_sym_t *cs = e->value;
		if (cs->linked)
			continue;
		cs->linked = 1;
		if (cs->pinmap == NULL)
			continue;

		for(f = htsp_first(&ctx->syms); f != NULL; f = htsp_next(&ctx->syms, f)) {
			cache_sym_t *cs2 = f->value;
			if ((cs2->root == cs->root) && (cs2->pinmap == NULL)) {
				cs2->pinmap = cs->pinmap;
				cs2->pinmap_root = cs->pinmap_root;
				cs2->linked = 1;
			}
		}
	}
}

static int read_cache_sym_dirs(io_orcad_rctx_t *ctx)
{
	int res;
	
	res = read_cache_sym_dir(ctx, ctx->cache_root->titleblocks, CHCSYM_SIMPLE);
	res |= read_cache_sym_dir(ctx, ctx->cache_root->symbolgraphics, CHCSYM_SIMPLE);
	res |= read_cache_sym_dir(ctx, ctx->cache_root->symbolproperties, CHCSYM_PROPS);
	res |= read_cache_sym_dir(ctx, ctx->cache_root->symbolpinmappings, CHCSYM_MAPS);

	cache_link_propagate(ctx);

	return res;
}

static int read_library(io_orcad_rctx_t *rctx)
{
	long n;

	rctx->hwpropnames.part_ref = -1;
	rctx->hwpropnames.value = -1;
	rctx->hwpropnames.name = -1;

	rctx->force_show_invis_power_pins = (rctx->library_root->flags & 1);

	for(n = 0; n < rctx->library_root->num_names; n++) {
		const char *s = rctx->library_root->names[n];
		if (strcmp(s, "Part Reference") == 0) rctx->hwpropnames.part_ref = n;
		if (strcmp(s, "Value") == 0) rctx->hwpropnames.value = n;
		if (strcmp(s, "Name") == 0) rctx->hwpropnames.name = n;
	}

	return 0;
}


static int read_wire(io_orcad_rctx_t *rctx, orcad_wire_node_t *nd)
{
	csch_cgrp_t *wn = orcad_wirenet_lookup(rctx, nd->net_id, 1);
	csch_alien_mkline(&rctx->alien, wn, nd->start_x, nd->start_y, nd->end_x, nd->end_y, "wire");
	return 0;
}

static int read_xnetalias(io_orcad_rctx_t *rctx, orcad_xnetalias_node_t *nd)
{
	csch_cgrp_t *wn = orcad_wirenet_lookup(rctx, nd->net_id, 0);

	if (wn == NULL) { /* happens on wireless nets, anon-rail symbols's terminal touching a rail symbol's terminal */
		csch_cgrp_t *term = htip_get(&rctx->pinconns, nd->net_id);

		/* draw a tiny 45 degree wirenet line across the terminal's main line and get the wire to connect to a net */
		if (term != NULL) {
			csch_sheet_t *sheet = rctx->alien.sheet;
			csch_source_arg_t *src = csch_attrib_src_c(rctx->fn, nd->node.offs, 0, NULL);
			csch_line_t *termlin = NULL, *line;
			csch_coord_t x, y;
			htip_entry_t *e;

			/* first line in the terminal is what we are going to cross */
			for(e = htip_first(&term->id2obj); e != NULL; e = htip_next(&term->id2obj, e)) {
				csch_chdr_t *h = e->value;
				if (h->type == CSCH_CTYPE_LINE) {
					termlin = (csch_line_t *)h;
					break;
				}
			}
			
			if (termlin != NULL) {
				wn = csch_cgrp_alloc(sheet, &sheet->direct, csch_oid_new(sheet, &sheet->direct));
				csch_cobj_attrib_set(sheet, wn, CSCH_ATP_HARDWIRED, "role", "wire-net", src);

				line = csch_line_alloc(sheet, wn, csch_oid_new(sheet, wn));

				x = (termlin->hdr.bbox.x1 + termlin->hdr.bbox.x2)/2;
				y = (termlin->hdr.bbox.y1 + termlin->hdr.bbox.y2)/2;
				line->spec.p1.x = x - 1000;
				line->spec.p1.y = y - 1000;
				line->spec.p2.x = x + 1000;
				line->spec.p2.y = y + 1000;
				line->hdr.stroke_name = csch_comm_str(sheet, "wire", 1);
			}
			else {
				const char *symname, *termname;

				termname = csch_attrib_get_str(&term->attr, "name");
				symname  = csch_attrib_get_str(&term->hdr.parent->attr, "name");
				rnd_message(RND_MSG_ERROR, "Failed to find terminal line in %s-%s - connection not made to %s\n", symname, termname, nd->alias);
				return 0;
			}
		}
		else {
			rnd_message(RND_MSG_ERROR, "Failed to find net_id %d - connection not made to %s\n", nd->net_id, nd->alias);
			return 0;
		}
	}
	orcad_cgrp_set_name(wn, nd->alias, rctx->fn, nd->node.offs);
	return 0;
}

/* Global/Port/Offpageconn graphic symbol */
static int read_gpo_graphic(io_orcad_rctx_t *rctx, orcad_graphicinst_node_t *gr, long offs, orcad_uint32_t wire_id)
{
	cache_sym_t *cs;
	csch_cgrp_t *sym;

	if (gr == NULL) {
		rnd_message(RND_MSG_ERROR, "orcad: missing graphic in page global/port/offpageconn (symbol placement)\n");
		return -1;
	}

	cs = htsp_get(&rctx->syms, gr->graphic.name);
	if (cs == NULL) {
		rnd_message(RND_MSG_ERROR, "orcad: page global/port/offpageconn (symbol placement) references non-existing cache symbol: '%s'\n", gr->graphic.name);
		return -1;
	}

	sym = place_sym_from_cache(rctx, cs, gr->graphic.x, gr->graphic.y, gr->graphic.rotation, gr->graphic.mirrored, -1, 0, offs);
	place_sym_floaters_gs(rctx, cs, sym, gr, wire_id);

	return 0;
}

static int read_global(io_orcad_rctx_t *rctx, orcad_global_node_t *nd)
{
	return read_gpo_graphic(rctx, (orcad_graphicinst_node_t *)nd, nd->node.offs, nd->wire_id);
}

static int read_port(io_orcad_rctx_t *rctx, orcad_port_node_t *nd)
{
	return read_gpo_graphic(rctx, (orcad_graphicinst_node_t *)nd, nd->node.offs, nd->wire_id);
}

static int read_offpageconn(io_orcad_rctx_t *rctx, orcad_offpageconn_node_t *nd)
{
	return read_gpo_graphic(rctx, (orcad_graphicinst_node_t *)nd, nd->node.offs, nd->wire_id);
}

/* Remember pinconns so xnetalias can be applied later */
static int partinst_load_pinconns(io_orcad_rctx_t *rctx, orcad_partinst_node_t *nd, csch_cgrp_t *sym)
{
	int n;

	for(n = 0; n < nd->num_pinconnections; n++) {
		orcad_pinconnection_node_t *pnc = nd->pinconnections[n];
		htip_entry_t *e, *e2;
		csch_coord_t x, y;

		rnd_trace("PINCONN: idx=%d %d;%d net_id = %d\n", pnc->idx, pnc->x, pnc->y, (int)pnc->net_id);

		if (pnc->net_id == 0)
			continue;

		x = csch_alien_coord_x(&rctx->alien, pnc->x);
		y = csch_alien_coord_y(&rctx->alien, pnc->y);
/*		rnd_trace(" search at %d;%d\n", x, y);*/

		for(e = htip_first(&sym->id2obj); e != NULL; e = htip_next(&sym->id2obj, e)) {
			csch_cgrp_t *grp = e->value;
			if (csch_obj_is_grp(&grp->hdr) && (grp->role == CSCH_ROLE_TERMINAL)) {
				for(e2 = htip_first(&grp->id2obj); e2 != NULL; e2 = htip_next(&grp->id2obj, e2)) {
					csch_line_t *lin = e2->value;
					if (lin->hdr.type == CSCH_CTYPE_LINE) {
						if (((lin->inst.c.p1.x == x) && (lin->inst.c.p1.y == y)) || ((lin->inst.c.p2.x == x) && (lin->inst.c.p2.y == y))) {
							rnd_trace("term line %d -> %p!\n", pnc->net_id, grp);
							htip_set(&rctx->pinconns, pnc->net_id, grp);
						}
					}
				}
			}
		}
	}

	return 0;
}

static int read_partinst(io_orcad_rctx_t *rctx, orcad_partinst_node_t *nd)
{
	cache_sym_t *cs;
	csch_cgrp_t *sym;
	int force_vis_power;

	cs = htsp_get(&rctx->syms, nd->name);
	if (cs == NULL) {
		rnd_message(RND_MSG_ERROR, "orcad: page partinst (symbol placement) references non-existing cache symbol: '%s'\n", nd->name);
		return -1;
	}

	force_vis_power = !!(nd->flags & 0x8000);     /* see T07*: instance overrides power pin visibility to visible */

	if (rctx->force_show_invis_power_pins)        /* design level global override */
		force_vis_power = 1;

	sym = place_sym_from_cache(rctx, cs, nd->x, nd->y, nd->rotation, nd->mirrored, nd->pim_idx, force_vis_power, nd->node.offs);
	if (sym != NULL) {
		csch_source_arg_t *src;
		orcad_cgrp_set_name(sym, nd->refdes, rctx->fn, nd->node.offs);

		/* overwrite symtypename: the cache version contains the long one from
		   the simple symbol while partinst is using the complex symbol */
		src = csch_attrib_src_c(rctx->fn, nd->node.offs, 0, NULL);
		csch_attrib_set(&sym->attr, CSCH_ATP_USER_DEFAULT, "symtypename", nd->symname, src, NULL);

		place_sym_floaters_pi(rctx, cs, sym, nd);
	}

	csch_cgrp_update(rctx->alien.sheet, sym, 1); /* get terminal line coords calculated for pinconn lookup */
	return partinst_load_pinconns(rctx, nd, sym);
}

static int draw_graphic_in(io_orcad_rctx_t *rctx, csch_cgrp_t *parent, orcad_graphic_inline_t *gr, const char *stroke, const char *fill)
{
	if (gr->obj->num_primitives == 1) {
		/* special case: single object, just place it on the sheet; translate
		   are embedded in the primitive, ignore gr->x, gr->y, but rotation matters.  */
		csch_chdr_t *obj = render_primitive_in(rctx, parent, gr->obj->primitives[0], stroke, fill);
		if (gr->rotation != 0) {
			csch_text_t *txt = (csch_text_t *)obj;
			txt->spec_miry = 0;
			if (gr->rotation == 2)
				txt->spec_mirx = 1;
			if (gr->rotation == 1)
				txt->spec_mirx = txt->spec_miry = 1;
			csch_rotate90(rctx->alien.sheet, obj, txt->spec1.x, txt->spec1.y, gr->rotation, 0);
		}
	}
	else {
		/* create a custom user group */
		TODO("low prio: implement me when an example comes up");
		abort();
	}
	return 0;
}

static int read_graphicinst(io_orcad_rctx_t *rctx, orcad_graphicinst_node_t *nd)
{
	draw_graphic_in(rctx, &rctx->alien.sheet->direct, &nd->graphic, "sheet-decor", "sheet-decor-fill");
	return 0;
}


/* Read global page properties (not drawing objects) */
static int read_page_props(io_orcad_rctx_t *rctx)
{
	orcad_page_node_t *pg = rctx->page_root;
	rnd_bool grid_ref = pg->settings.gridref_displayed || pg->settings.gridref_printed;

	orcad_sheet_set_extents(rctx, pg->settings.width, pg->settings.height, grid_ref, pg->node.offs, pg->settings.is_metric);

	return 0;
}

static void cache_wire(io_orcad_rctx_t *rctx, orcad_wire_node_t *wire)
{
	htip_set(&rctx->wires, wire->wire_id, wire);
}

static void cache_netalias(io_orcad_rctx_t *rctx, orcad_xnetalias_node_t *netalias)
{
	htip_set(&rctx->netaliases, netalias->net_id, netalias);
}

/* Cache some page objects for later use */
static void cache_page_objs(io_orcad_rctx_t *rctx)
{
	orcad_page_node_t *pg = rctx->page_root;
	long n;

	for(n = 0; n < pg->num_wires; n++)
		cache_wire(rctx, pg->wires[n]);

	for(n = 0; n < pg->num_netaliases; n++)
		cache_netalias(rctx, pg->netaliases[n]);
}

/* Read drawing objects */
static int read_page_objs(io_orcad_rctx_t *rctx)
{
	orcad_page_node_t *pg = rctx->page_root;
	long n;
	int res = 0;

	for(n = 0; n < pg->num_wires; n++)
		res |= read_wire(rctx, pg->wires[n]);

	for(n = 0; n < pg->num_globals; n++)
		res |= read_global(rctx, pg->globals[n]);

	for(n = 0; n < pg->num_ports; n++)
		res |= read_port(rctx, pg->ports[n]);

	for(n = 0; n < pg->num_offpageconns; n++)
		res |= read_offpageconn(rctx, pg->offpageconns[n]);

	for(n = 0; n < pg->num_partinsts; n++)
		res |= read_partinst(rctx, pg->partinsts[n]);

	for(n = 0; n < pg->num_graphicinsts; n++)
		res |= read_graphicinst(rctx, pg->graphicinsts[n]);

	for(n = 0; n < pg->num_netaliases; n++) /* must be after parts because it depends on terminals */
		res |= read_xnetalias(rctx, pg->netaliases[n]);

	return res;
}

static void recalc_junctions(io_orcad_rctx_t* const ctx)
{
	htip_entry_t* ent;

	for(ent=htip_first(&ctx->wirenets);ent;ent=htip_next(&ctx->wirenets, ent))
	{
		csch_cgrp_t* const wn = (csch_cgrp_t*)ent->value;

		csch_wirenet_recalc_junctions(ctx->alien.sheet, wn);
	}
}


/**** binary ****/

static int cdf_strcmp(const char *cdf_str, const char *c_str)
{
	int cdf_ch;
	int c_ch;

	for(;;)
	{
		cdf_ch = *cdf_str++;
		c_ch = *c_str++;

		if(cdf_ch==0 || c_ch==0)
		{
			break;
		}

		/* translate to CDF character */
		switch(c_ch)
		{
		case '/': c_ch = 0x02; break;
		}

		if(cdf_ch!=c_ch)
		{
			break;
		}
	}

	return cdf_ch - c_ch;
}

ucdf_direntry_t *cdf_path(ucdf_ctx_t *ucdf, const char **path, ucdf_direntry_t *from)
{
	ucdf_direntry_t *de = from;

	if (path[0] == NULL)
		return de;

	if (de == NULL)
		de = ucdf->root->children;
	else
		de = de->children;

	for(; de != NULL; de = de->next)
		if (cdf_strcmp(de->name, path[0]) == 0)
			return cdf_path(ucdf, path+1, de);

	return NULL;
}

void *io_orcad_test_parse_bundled(FILE *f, const char *fn, const char *fmt, csch_plug_io_type_t type)
{
	io_orcad_rctx_t *ctx = calloc(sizeof(io_orcad_rctx_t), 1);
	const char *path[] = { "Views", NULL };
	const char *path2[] = { "Pages", NULL };
	const char *path3[] = { "Schematic", NULL };
	ucdf_direntry_t *de;

	if (ucdf_open(&ctx->ucdf, fn) != 0) {
		if (io_orcad_conf.plugins.io_orcad.debug.trace_test_parse)
			rnd_message(RND_MSG_ERROR, "io_orcad test_parse: failed to open cdf\n");
		free(ctx);
		return NULL;
	}

	de = cdf_path(&ctx->ucdf, path, NULL);
	if (de == NULL) {
		if (io_orcad_conf.plugins.io_orcad.debug.trace_test_parse)
			rnd_message(RND_MSG_ERROR, "io_orcad test_parse: failed to find Views/ in cdf\n");
		free(ctx);
		return NULL;
	}

	if (de->children == NULL) {
		if (io_orcad_conf.plugins.io_orcad.debug.trace_test_parse)
			rnd_message(RND_MSG_ERROR, "io_orcad test_parse: failed to find sheets in Views/\n");
		free(ctx);
		return NULL;
	}

	ctx->pages_de = cdf_path(&ctx->ucdf, path2, de->children);
	if (ctx->pages_de->children == NULL) {
		if (io_orcad_conf.plugins.io_orcad.debug.trace_test_parse)
			rnd_message(RND_MSG_ERROR, "io_orcad test_parse: failed to find sheets in Views/*/Pages/\n");
		free(ctx);
		return NULL;
	}

	ctx->sch_de = cdf_path(&ctx->ucdf, path3, de->children);
	if (ctx->sch_de == NULL) {
		if (io_orcad_conf.plugins.io_orcad.debug.trace_test_parse)
			rnd_message(RND_MSG_ERROR, "io_orcad test_parse: failed to find Schematic in Views/*/\n");
		free(ctx);
		return NULL;
	}

	ctx->alien.fmt_prefix = "io_orcad";
	ctx->alien.flip_y = 1;
	ctx->fn = fn;

	return ctx;
}


int io_orcad_load_cache(io_orcad_rctx_t *ctx)
{
	orcad_node_t *nd;
	ucdf_direntry_t *de;
	const char *path[] = { "Cache", NULL };
	int res = -1;

	rnd_message(RND_MSG_INFO, "io_orcad: reading cache...\n");

	htsp_init(&ctx->syms, strhash, strkeyeq);
	ctx->cache_loaded = 1;


	de = cdf_path(&ctx->ucdf, path, NULL);
	if (de == NULL)
		return 0;

	if (ucdf_fopen(&ctx->ucdf, &ctx->fp, de) != 0)
		return -1;

	ctx->has_fp = 1;

	TODO("low prio: workaround for missing short seek");
	ctx->cheat_buf = malloc(de->size);
	ucdf_fread(&ctx->fp, ctx->cheat_buf, de->size);
	ctx->cheat_offs = 0;
	ctx->cheat_len = de->size;

	nd = orcad_read_cache(ctx);
	if (nd == NULL) {
		rnd_message(RND_MSG_ERROR, "io_orcad: failed to read (symbol) cache\n");
		goto err1;
	}

	if (nd->type != ORCAD_TYPE_X_CACHE) {
		rnd_message(RND_MSG_ERROR, "io_orcad: (symbol) cache root type mismatch: expected %d got %d\n", ORCAD_TYPE_X_CACHE, nd->type);
		goto err1;
	}

	ctx->cache_root = (orcad_xcache_node_t *)nd;

	res = read_cache_sym_dirs(ctx);

	err1:;
	ctx->has_fp = 0;
	free(ctx->cheat_buf);

	return res;
}

int io_orcad_load_sch(io_orcad_rctx_t *ctx)
{
	orcad_node_t *nd;
	ucdf_direntry_t *de;
	int res = -1;

	rnd_message(RND_MSG_INFO, "io_orcad: reading schematic descriptor...\n");

	ctx->sch_loaded = 1;

	de = ctx->sch_de;
	if (de == NULL)
		return 0;

	if (ucdf_fopen(&ctx->ucdf, &ctx->fp, de) != 0)
		return -1;

	ctx->has_fp = 1;

	TODO("low prio: workaround for missing short seek");
	ctx->cheat_buf = malloc(de->size);
	ucdf_fread(&ctx->fp, ctx->cheat_buf, de->size);
	ctx->cheat_offs = 0;
	ctx->cheat_len = de->size;

	nd = orcad_read_schematic(ctx);
	if (nd == NULL) {
		rnd_message(RND_MSG_ERROR, "io_orcad: failed to read schematic descriptor\n");
		goto err1;
	}

	if (nd->type != ORCAD_TYPE_SCHEMATIC) {
		rnd_message(RND_MSG_ERROR, "io_orcad: (symbol) schematic descriptor root type mismatch: expected %d got %d\n", ORCAD_TYPE_SCHEMATIC, nd->type);
		goto err1;
	}

	ctx->schematic_root = (orcad_schematic_node_t *)nd;
	ctx->num_pages_left = ctx->schematic_root->num_pagenames;

	err1:;
	ctx->has_fp = 0;
	free(ctx->cheat_buf);

	return res;
}

int io_orcad_free_cache(io_orcad_rctx_t *ctx)
{
	htsp_entry_t *e;

	assert(ctx->cache_loaded);

	for(e = htsp_first(&ctx->syms); e != NULL; e = htsp_next(&ctx->syms, e)) {
		cache_sym_t *cs = e->value;
		if (cs->grp_pwr_vis != NULL)
			csch_cgrp_free(cs->grp_pwr_vis);
		if (cs->grp_pwr_invis != NULL)
			csch_cgrp_free(cs->grp_pwr_invis);
		/* do not free cs->name or e->key: they are pointing to a field in ctx->cache_root */
		free(cs);
	}

	htsp_uninit(&ctx->syms);
	ctx->cache_loaded = 0;

	if (ctx->cache_root != NULL)
		orcad_free((orcad_node_t *)ctx->cache_root);
	ctx->cache_root = NULL;

	return 0;
}

int io_orcad_load_library(io_orcad_rctx_t *ctx)
{
	orcad_node_t *nd;
	ucdf_direntry_t *de;
	const char *path[] = { "Library", NULL };
	int res = -1;

	rnd_message(RND_MSG_INFO, "io_orcad: reading library...\n");

	ctx->library_loaded = 1;
	de = cdf_path(&ctx->ucdf, path, NULL);
	if (de == NULL)
		return 0;

	if (ucdf_fopen(&ctx->ucdf, &ctx->fp, de) != 0)
		return -1;

	ctx->has_fp = 1;

	TODO("low prio: workaround for missing short seek");
	ctx->cheat_buf = malloc(de->size);
	ucdf_fread(&ctx->fp, ctx->cheat_buf, de->size);
	ctx->cheat_offs = 0;
	ctx->cheat_len = de->size;

	nd = orcad_read_library(ctx);
	if (nd == NULL) {
		rnd_message(RND_MSG_ERROR, "io_orcad: failed to read strings (Library)\n");
		goto err1;
	}

	if (nd->type != ORCAD_TYPE_X_LIBRARY) {
		rnd_message(RND_MSG_ERROR, "io_orcad: strings (Library) root type mismatch: expected %d got %d\n", ORCAD_TYPE_X_LIBRARY, nd->type);
		goto err1;
	}

	ctx->library_root = (orcad_xlibrary_node_t *)nd;

	res = read_library(ctx);

	err1:;
	ctx->has_fp = 0;
	free(ctx->cheat_buf);

	return res;
}

int io_orcad_free_library(io_orcad_rctx_t *ctx)
{
	if (ctx->library_root != NULL)
		orcad_free((orcad_node_t *)ctx->library_root);
	ctx->library_root = NULL;

	return 0;
}

int io_orcad_free_schematic(io_orcad_rctx_t *ctx)
{
	if (ctx->schematic_root != NULL)
		orcad_free((orcad_node_t *)ctx->schematic_root);
	ctx->schematic_root = NULL;

	return 0;
}

static int io_orcad_load_page(io_orcad_rctx_t *ctx, ucdf_direntry_t *de)
{
	int res = -1;
	orcad_node_t *nd;

	TODO("low prio: workaround for missing short seek");
	ctx->cheat_buf = malloc(de->size);
	ucdf_fread(&ctx->fp, ctx->cheat_buf, de->size);
	ctx->cheat_offs = 0;
	ctx->cheat_len = de->size;


	nd = orcad_read(ctx);
	if (nd == NULL) {
		rnd_message(RND_MSG_ERROR, "io_orcad: failed to read page\n");
		goto err1;
	}

	if (nd->type != ORCAD_TYPE_PAGE) {
		rnd_message(RND_MSG_ERROR, "io_orcad: page root type mismatch: expected %d got %d\n", ORCAD_TYPE_PAGE, nd->type);
		goto err1;
	}

	ctx->page_root = (orcad_page_node_t *)nd;

	cache_page_objs(ctx);
	res = read_page_props(ctx) | read_page_objs(ctx);

	/* postprocess sheet */

	csch_alien_postproc_sheet(&ctx->alien);
	csch_cgrp_update(ctx->alien.sheet, &ctx->alien.sheet->direct, 1);
	csch_alien_update_conns(&ctx->alien);

	if(io_orcad_conf.plugins.io_orcad.emulate_text_ang_180)
	{
		/*csch_cgrp_update(ctx->alien.sheet, &ctx->alien.sheet->direct, 1);*/
		csch_alien_postproc_text_autorot(&ctx->alien,
			&ctx->alien.sheet->direct, 1, 1);
	}

	recalc_junctions(ctx);

	/* free temps */
	htip_uninit(&ctx->wirenets);
	htip_uninit(&ctx->pinconns);
	htip_uninit(&ctx->wires);
	htip_uninit(&ctx->netaliases);
	if (ctx->page_root != NULL)
		orcad_free((orcad_node_t *)ctx->page_root);
	ctx->page_root = NULL;
	err1:;
	free(ctx->cheat_buf);

	return res;
}

static char* orcad_translate_sheetname(char* const name)
{
	char* p = name;

	while(*p)
	{
		char ch = *p;

		switch(ch)
		{
		case '/':
			ch = '.';
			break;
		}

		*p++ = ch;
	}

	return name;
}

int io_orcad_load_sheet_bundled(void *cookie, FILE *f, const char *fn, csch_sheet_t *dst)
{
	io_orcad_rctx_t *ctx = cookie;
	ucdf_direntry_t *de;
	const char *pagename;
	const char *path[2];
	int res;

	if (ctx->has_fp)
		ctx->has_fp = 0;

	if (!ctx->library_loaded)
		io_orcad_load_library(ctx);

	if (!ctx->cache_loaded)
		io_orcad_load_cache(ctx);

	if (!ctx->sch_loaded) {
		io_orcad_load_sch(ctx);

		if (ctx->num_pages_left == 0) {
			rnd_message(RND_MSG_ERROR, "io_orcad: there are no pages in the schematic!\n");
			return -1;
		}
	}

	pagename = ctx->schematic_root->pagenames[--ctx->num_pages_left];

	rnd_message(RND_MSG_INFO, "io_orcad: reading page %s...\n", pagename);

	path[0] = pagename;
	path[1] = NULL;

	de = cdf_path(&ctx->ucdf, path, ctx->pages_de);
	if (de == NULL) {
		rnd_message(RND_MSG_ERROR, "io_orcad: failed to locate sheet file in Views/*/Pages/\n");
		return -1;
	}

	ctx->alien.sheet = dst;
	ctx->alien.sheet->hidlib.loadname = orcad_translate_sheetname(rnd_strdup(pagename));

	ctx->alien.coord_factor = io_orcad_conf.plugins.io_orcad.coord_mult;
	htip_init(&ctx->wirenets, longhash, longkeyeq);
	htip_init(&ctx->pinconns, longhash, longkeyeq);
	htip_init(&ctx->wires, longhash, longkeyeq);
	htip_init(&ctx->netaliases, longhash, longkeyeq);
	csch_alien_sheet_setup(&ctx->alien, 1);

	if (ucdf_fopen(&ctx->ucdf, &ctx->fp, de) != 0)
		return -1;

	ctx->has_fp = 1;

	res = io_orcad_load_page(ctx, de);


/*	ctx->alien.sheet->hidlib.fullpath = rnd_strdup_printf("%s_%ld.rs", fn, ++ctx->sheetno);
	ctx->alien.sheet = NULL;*/

	if (res == 0) {
		if (ctx->num_pages_left == 0) {
			io_orcad_free_cache(ctx);
			return 1; /* no more pages */
		}
		return 0; /* proceed to read next page */
	}

	io_orcad_free_cache(ctx);
	io_orcad_free_library(ctx);
	io_orcad_free_schematic(ctx);
	return -1; /* error */
}

void io_orcad_end_bundled(void *cookie, const char *fn)
{
	io_orcad_rctx_t *ctx = cookie;

	if (ctx->has_fp)
		ctx->has_fp = 0;

	ucdf_close(&ctx->ucdf);
	free(ctx);
}
