/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - libcschem (core library)
 *  Copyright (C) 2018,2022,2023 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 PET Fund in 2022, Entrust in 2023)
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.*
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  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 "config.h"

#include <librnd/core/error.h>
#include <librnd/core/rnd_printf.h>

#include "event.h"
#include "concrete.h"
#include "cnc_obj.h"
#include "cnc_conn.h"
#include "cnc_line.h"
#include "cnc_arc.h"
#include "op_common.h"
#include "operation.h"
#include "intersect.h"
#include "undo.h"

#include <gengeo2d/cline.h>
#include <gengeo2d/carc.h>
#include <gengeo2d/intersect.h>

csch_conn_t *csch_conn_alloc(csch_sheet_t *sheet, csch_cgrp_t *parent, csch_oid_t oid)
{
	csch_conn_t *conn;

	conn = htip_get(&parent->id2obj, oid);
	if (conn != NULL)
		return NULL;
	conn = calloc(sizeof(csch_conn_t), 1);

	csch_cobj_init(&conn->hdr, sheet, parent, oid, CSCH_CTYPE_CONN);
	return conn;
}

void csch_conn_free(csch_conn_t *conn)
{
	long n;

	for(n = 0; n < conn->conn_path.used; n++)
		csch_oidpath_free(&conn->conn_path.array[n]);

	vtp0_uninit(&conn->conn);
	vtl0_uninit(&conn->coords);
	csch_vtoidpath_uninit(&conn->conn_path);
	csch_cobj_uninit(&conn->hdr);
	free(conn);
}

csch_conn_t *csch_conn_get(csch_sheet_t *sheet, csch_oid_t oid)
{
	csch_conn_t *conn = htip_get(&sheet->direct.id2obj, oid);
	if ((conn != NULL) && (conn->hdr.type != CSCH_CTYPE_CONN))
		return NULL;
	return conn;
}


void csch_conn_text2ptr(csch_sheet_t *sheet, csch_conn_t *conn)
{
	long n;

	vtp0_enlarge(&conn->conn, conn->conn_path.used);
	conn->conn.used = 0;
	for(n = 0; n < conn->conn_path.used; n++) {
		csch_chdr_t *obj = csch_oidpath_resolve(sheet, &conn->conn_path.array[n]);
		csch_chdr_t *grp;
		if (obj != NULL) {
			grp = (csch_chdr_t *)obj->parent;
			if (csch_obj_is_grp(grp))
				csch_conn_add_obj(sheet, conn, obj, 0);
			else
				rnd_msg_error("Invalid oid path in connection %ld: not a group or group-ref\n", conn->hdr.oid);
		}
		else
			rnd_msg_error("Can not resolve oid path in connection %ld\n", conn->hdr.oid);

		csch_oidpath_free(&conn->conn_path.array[n]);
	}
	csch_vtoidpath_truncate(&conn->conn_path, 0);
}

/* append one or more connection points of o to tmp in x;y form */
RND_INLINE void conn_append_pts(csch_conn_t *conn, csch_chdr_t *o, vtl0_t *tmp)
{
	switch(o->type) {
		case CSCH_CTYPE_LINE:
			{
				csch_line_t *l = (csch_line_t *)o;
				vtl0_append(tmp, l->inst.c.p1.x);
				vtl0_append(tmp, l->inst.c.p1.y);
				vtl0_append(tmp, l->inst.c.p2.x);
				vtl0_append(tmp, l->inst.c.p2.y);
				vtl0_append(tmp, (l->inst.c.p1.x + l->inst.c.p2.x)/2);
				vtl0_append(tmp, (l->inst.c.p1.y + l->inst.c.p2.y)/2);
			}
			break;
		case CSCH_CTYPE_ARC:
			{
				csch_arc_t *a = (csch_arc_t *)o;
				g2d_vect_t v;
				
				v = g2d_carc_offs(&a->inst.c, 0);
				vtl0_append(tmp, v.x); vtl0_append(tmp, v.y);
				if (a->inst.c.delta < 1.8 * G2D_PI) {
					v = g2d_carc_offs(&a->inst.c, 1);
					vtl0_append(tmp, v.x); vtl0_append(tmp, v.y);
				}
				if (a->inst.c.delta > 0.5*G2D_PI) {
					v = g2d_carc_offs(&a->inst.c, 0.5);
					vtl0_append(tmp, v.x); vtl0_append(tmp, v.y);
				}
			}
			break;
		default:
			/* fallback: bbox center */
			vtl0_append(tmp, (o->bbox.x1 + o->bbox.x2)/2);
			vtl0_append(tmp, (o->bbox.y1 + o->bbox.y2)/2);
			break;
	}
}

/* append a single line of shortest connection between o1 and o2 notable points
   to tmp in x1;y1;x2;y2 line form */
RND_INLINE void conn_recalc_add_line_npts(csch_conn_t *conn, csch_chdr_t *o1, csch_chdr_t *o2, vtl0_t *tmp)
{
	int o2_start, besti1 = -1, besti2 = -1, i1, i2;
	double best = (double)CSCH_COORD_MAX * (double)CSCH_COORD_MAX;

	/* collect o1 and o2 points in tmp */
	tmp->used = 0;
	conn_append_pts(conn, o1, tmp);
	o2_start = tmp->used;
	conn_append_pts(conn, o2, tmp);

	/* find shortest line */
	for(i1 = 0; i1 < o2_start; i1+=2) {
		csch_coord_t x1 = tmp->array[i1], y1 = tmp->array[i1+1];

		for(i2 = o2_start; i2 < tmp->used; i2+=2) {
			csch_coord_t x2 = tmp->array[i2], y2 = tmp->array[i2+1];
			double dx = x2 - x1, dy = y2 - y1, dist = dx*dx + dy*dy;

			if (dist < best) {
				best = dist;
				besti1 = i1;
				besti2 = i2;
			}
		}
	}

	/* append results */
	assert(besti1 >= 0);
	assert(besti2 > besti1);
	vtl0_append(&conn->coords, tmp->array[besti1]);
	vtl0_append(&conn->coords, tmp->array[besti1+1]);
	vtl0_append(&conn->coords, tmp->array[besti2]);
	vtl0_append(&conn->coords, tmp->array[besti2+1]);

	csch_bbox_bump(&conn->hdr.bbox, x, tmp->array[besti1]);
	csch_bbox_bump(&conn->hdr.bbox, y, tmp->array[besti1+1]);
	csch_bbox_bump(&conn->hdr.bbox, x, tmp->array[besti2]);
	csch_bbox_bump(&conn->hdr.bbox, y, tmp->array[besti2+1]);
}

/* Search for centerline intersections between o1 and o2, create zero-length
   conn lines in those points and return the number of points found. */
RND_INLINE int conn_recalc_add_line_cent_isc(csch_conn_t *conn, csch_chdr_t *o1, csch_chdr_t *o2, vtl0_t *tmp)
{
	csch_sheet_t *sheet = o1->sheet;
	g2d_vect_t iscp[32];
	int maxlen = sizeof(iscp) / sizeof(iscp[0]);
	long n, len;

	if (o1->parent == o2->parent)
		return 0;

	len = csch_obj_intersect_obj(sheet, o1, o2, iscp, maxlen);
	if (len > maxlen)
		len = maxlen;

	for(n = 0; n < len; n++) {
		vtl0_append(&conn->coords, iscp[n].x);
		vtl0_append(&conn->coords, iscp[n].y);
		vtl0_append(&conn->coords, iscp[n].x);
		vtl0_append(&conn->coords, iscp[n].y);
		csch_bbox_bump(&conn->hdr.bbox, x, iscp[n].x);
		csch_bbox_bump(&conn->hdr.bbox, y, iscp[n].y);
	}

	return len;
}

/* Append new connection line(s) between o1 and o2; prefer zero-length conns at
   center-line intersections, fall back to non-zero-length conns between
   notable points when there's no centerline intersection */
RND_INLINE void conn_recalc_add_line(csch_conn_t *conn, csch_chdr_t *o1, csch_chdr_t *o2, vtl0_t *tmp)
{
	if (o1->parent == o2->parent)
		return; /* corner case: never indicate connection within the same group */
	if (conn_recalc_add_line_cent_isc(conn, o1, o2, tmp) == 0)
		conn_recalc_add_line_npts(conn, o1, o2, tmp); /* fallback */
}

void csch_conn_recalc_coords(csch_conn_t *conn)
{
	int i1, i2, readd_rtree;
	vtl0_t tmp = {0};
	csch_sheet_t *sheet = conn->hdr.sheet;

	/* try removing the object from its rtree; if it succeeds, add it back after the recalculation */
	readd_rtree = (sheet != NULL) && (csch_cobj_rtree_del(sheet, &conn->hdr) == 0);

	csch_bbox_reset(&conn->hdr.bbox);
	conn->coords.used = 0;
	for(i1 = 0; i1 < conn->conn.used; i1++)
		for(i2 = i1+1; i2 < conn->conn.used; i2++)
			conn_recalc_add_line(conn, conn->conn.array[i1], conn->conn.array[i2], &tmp);

	if (readd_rtree)
		csch_cobj_rtree_add(sheet, &conn->hdr);

	vtl0_uninit(&tmp);
}

void csch_conn_update(csch_sheet_t *sheet, csch_conn_t *conn, int do_xform)
{
	/* live data has pointers, not oid paths */
	if ((conn->conn_path.used > 0) && (conn->conn.used == 0)) {
		csch_conn_text2ptr(sheet, conn);
	}

	csch_cobj_rtree_del(sheet, &conn->hdr);
	csch_conn_recalc_coords(conn);
	csch_cobj_rtree_add(sheet, &conn->hdr);

	csch_cobj_bbox_changed(sheet, &conn->hdr);
	conn->dirty = 0;
}

static long find_obj_in_conn(const csch_chdr_t *obj, const csch_conn_t *conn)
{
	long n;
	for(n = 0; n < conn->conn.used; n++)
		if (conn->conn.array[n] == obj)
			return n;
	return -1;
}

static long find_conn_in_obj(const csch_conn_t *conn, const csch_chdr_t *obj)
{
	long n;
	for(n = 0; n < obj->conn.used; n++)
		if (obj->conn.array[n] == conn)
			return n;
	return -1;
}

RND_INLINE long csch_conn_add_obj_(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, long *cidx)
{
	long ci, gi;

	ci = find_obj_in_conn(obj, conn);
	if (ci >= 0) {
		if (cidx != NULL)
			*cidx = -1;
		return -1;
	}
	gi = find_conn_in_obj(conn, obj);
	if (gi >= 0) {
		if (cidx != NULL)
			*cidx = gi;
		return -1;
	}

	vtp0_append(&conn->conn, obj);
	vtp0_append(&obj->conn, conn);
	if (cidx != NULL)
		*cidx = obj->conn.used - 1;
	return 0;
}


RND_INLINE int csch_conn_del_obj_(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj)
{
	long ci, gi;
	
	ci = find_obj_in_conn(obj, conn);
	gi = find_conn_in_obj(conn, obj);
	if (ci >= 0) vtp0_remove(&conn->conn, ci, 1);
	if (gi >= 0) vtp0_remove(&obj->conn, gi, 1);
	if ((ci < 0) || (gi < 0))
		return -1;
	return 0;
}

void csch_conn_ptr2text(csch_sheet_t *sheet, csch_conn_t *conn)
{
	long n;

	csch_vtoidpath_enlarge(&conn->conn_path, conn->conn.used);
	conn->conn_path.used = conn->conn.used;
	for(n = 0; n < conn->conn.used; n++)
		csch_oidpath_from_obj(&conn->conn_path.array[n], conn->conn.array[n]);

	/* this changes conn->conn, so go backward */
	for(n = conn->conn.used-1; n >= 0; n--)
		csch_conn_del_obj_(sheet, conn, conn->conn.array[n]);

	conn->conn.used = 0; /* just in case... to make sure the code resolves the oidpaths */
	conn->dirty = 1;
}

/*** undoable conn-obj add/del ***/
typedef struct {
	csch_sheet_t *sheet;
	csch_conn_t *conn;
	csch_chdr_t *obj;
	long last_cidx;
	unsigned add:1; /* if 1, swap should add, else swap should del (swap always inverts afterwards) */
} undo_conn_obj_t;

static int undo_conn_obj_swap_(void *udata, int update)
{
	undo_conn_obj_t *u = udata;

	if (u->add)
		csch_conn_add_obj_(u->sheet, u->conn, u->obj, &u->last_cidx);
	else
		csch_conn_del_obj_(u->sheet, u->conn, u->obj);

	if (update) /* recalc coords (the graphics) when called from undo/redo */
		csch_conn_update(u->sheet, u->conn, 0);

	u->add = !u->add;

	return 0;
}

static int undo_conn_obj_swap(void *udata)
{
	return undo_conn_obj_swap_(udata, 1);
}

static void undo_conn_obj_print(void *udata, char *dst, size_t dst_len)
{
	undo_conn_obj_t *u = udata;
	gds_t tmp;
	csch_oidpath_t oidp = {0};


	tmp.used = 0;
	tmp.alloced = dst_len;
	tmp.array = dst;
	tmp.no_realloc = 1;

	csch_oidpath_from_obj(&oidp, u->obj);

	rnd_append_printf(&tmp, "%s object ", u->add ? "add" : "del");
	csch_oidpath_to_str_append(&tmp, &oidp);
	rnd_append_printf(&tmp, " in conn #%ld", (long)u->conn->hdr.oid);

	csch_oidpath_free(&oidp);
}

static const char core_conn_cookie[] = "libcschem/core/cnc_conn.c";

static const uundo_oper_t undo_conn_obj_modify = {
	core_conn_cookie,
	NULL,
	undo_conn_obj_swap,
	undo_conn_obj_swap,
	undo_conn_obj_print
};


RND_INLINE int csch_conn_do_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int add, long *cidx, int undoable)
{
	int res;
	undo_conn_obj_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_conn_obj_modify, sizeof(undo_conn_obj_t));

	u->sheet = sheet;
	u->conn = conn;
	u->obj = obj;
	u->add = add;

	res = undo_conn_obj_swap_(u, 0); /* update 0: expect the caller to do the update */
	if (undoable) csch_undo_inc_serial(sheet);
	if (cidx != NULL)
		*cidx = u->last_cidx;

	return res;
}

int csch_conn_del_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int undoable)
{
	return csch_conn_do_obj(sheet, conn, obj, 0, NULL, undoable);
}

int csch_conn_add_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int undoable)
{
	return csch_conn_do_obj(sheet, conn, obj, 1, NULL, undoable);
}

/* returns conn index in obj or -1 on error */
long csch_conn_add_obji(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj, int undoable)
{
	long res;
	csch_conn_do_obj(sheet, conn, obj, 1, &res, undoable);
	return res;
}

/* Return conn where:
   obj1 is in the connection and another object from obj2's parent is in too
   (or return NULL if not found) */
RND_INLINE csch_conn_t *conn_find_common(csch_chdr_t *obj1, csch_chdr_t *obj2, long *o1i, csch_conn_t *ignore)
{
	long n, m;
	csch_conn_t *conn;
	csch_chdr_t *o;

	for(n = 0; n < obj1->conn.used; n++) {
		conn = obj1->conn.array[n];
		if (conn == ignore)
			continue;
		for(m = 0; m < conn->conn.used; m++) {
			o = conn->conn.array[m];
			if (o->parent == obj2->parent) {
				if (o1i != NULL)
					*o1i = n;
				return conn;
			}
		}
	}

	if (o1i != NULL)
		*o1i = -1;
	return NULL;
}

csch_conn_t *csch_conn_find_mergable(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *owner)
{
	long n;
	
	for(n = 0; n < conn->conn.used; n++) {
		csch_chdr_t *co = conn->conn.array[n];
		csch_conn_t *res = conn_find_common(co, owner, NULL, conn);
		if (res != NULL)
			return res;
	}
	return NULL;
}

RND_INLINE csch_conn_t *csch_conn_auto_add_(csch_sheet_t *sheet, csch_chdr_t *obj1, csch_chdr_t *obj2, long *obj1i)
{
	csch_conn_t *conn;

	/* check if there's an existing connection we can extend; requirement is:
	   obj1/2 is in the connection and another object from obj2/1's parent is in too */
	conn = conn_find_common(obj1, obj2, obj1i, NULL);
	if (conn != NULL) {
		if (find_obj_in_conn(obj2, conn) < 0) {
			csch_conn_add_obj(sheet, conn, obj2, 1);
			csch_conn_update(sheet, conn, 1);
		}
		return conn;
	}

	conn = conn_find_common(obj2, obj1, NULL, NULL);
	if (conn != NULL) {
		*obj1i = find_obj_in_conn(obj1, conn);
		if (*obj1i < 0) {
			*obj1i = csch_conn_add_obji(sheet, conn, obj1, 1);
			csch_conn_update(sheet, conn, 1);
		}
		return conn;
	}


	/* there was no common conn, create a new one */
	conn = (csch_conn_t *)csch_op_create(sheet, &sheet->direct, CSCH_CTYPE_CONN);
	*obj1i = csch_conn_add_obji(sheet, conn, obj1, 1);
	csch_conn_add_obj(sheet, conn, obj2, 1);
	csch_conn_update(sheet, conn, 1);
	return conn;
}

csch_conn_t *csch_conn_auto_add(csch_sheet_t *sheet, csch_chdr_t *obj1, csch_chdr_t *obj2)
{
	long tmp;
	return csch_conn_auto_add_(sheet, obj1, obj2, &tmp);
}

long csch_conn_auto_addi(csch_sheet_t *sheet, csch_chdr_t *obj1, csch_chdr_t *obj2)
{
	long tmp = -1;
	csch_conn_auto_add_(sheet, obj1, obj2, &tmp);

	return tmp;
}

/* Return 1 if the connection lists at least 2 objects with different parent
   groups (so that the connection is valid) */
static int conn_has_multiple_groups(csch_sheet_t *sheet, csch_conn_t *conn)
{
	long n;
	csch_chdr_t *obj;
	csch_cgrp_t *par;

	if (conn->conn.used < 2)
		return 0;

	obj = conn->conn.array[0];
	par = obj->parent;
	for(n = 1; n < conn->conn.used; n++) {
		obj = conn->conn.array[n];
		if (obj->parent != par)
			return 1;
	}

	return 0;
}

int csch_conn_auto_del_obj(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *obj)
{
	int res;

	TODO("This may not be undoable; check with 3 objects crossing");
	res = csch_conn_del_obj(sheet, conn, obj, 1);
	if (res != 0)
		return res;

	/* if connection doesn't connect two groups anymore, remove it */
	if (!conn_has_multiple_groups(sheet, conn)) {
		long n;

		res = 0;
		for(n = conn->conn.used-1; n >= 0; n--)
			res |= csch_conn_del_obj(sheet, conn, conn->conn.array[n], 1);
		if (res != 0)
			return res;
	}

	if (conn->conn.used == 0)
		csch_op_remove(sheet, &conn->hdr);
	else
		csch_conn_update(sheet, conn, 1);
	return 0;
}


int csch_conn_auto_del_obj_all(csch_sheet_t *sheet, csch_chdr_t *obj)
{
	long n;
	int res = 0;

	for(n = obj->conn.used-1; n >= 0; n--)
		res |= csch_conn_auto_del_obj(sheet, obj->conn.array[n], obj);

	return res;
}



static csch_chdr_t *conn_create(csch_sheet_t *sheet, csch_cgrp_t *parent)
{
	csch_conn_t *conn = csch_conn_alloc(sheet, parent, csch_oid_new(sheet, parent));
	if (conn == NULL) return NULL;
	return &conn->hdr;
}

static void conn_remove_alloc(csch_undo_remove_t *slot)
{
}

static void conn_remove_redo(csch_undo_remove_t *slot)
{
	csch_cnc_common_remove_redo(slot, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT);
}

static void conn_remove_undo(csch_undo_remove_t *slot)
{
	csch_cnc_common_remove_undo(slot, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT);
}

static int conn_isc_with_box(csch_chdr_t *obj, csch_rtree_box_t *box)
{
	return 0; /* a connection is a purely logical concept, has no geometry */
}

const csch_ops_t csch_ops_conn = {
	conn_create, conn_remove_alloc, conn_remove_redo, conn_remove_undo,
	conn_isc_with_box
};


/*** conn recalc ***/

typedef struct {
	csch_sheet_t *sheet;
	csch_chdr_t *wobj;
	gds_t keep;
} find_conn_t;

static csch_rtree_dir_t find_conn_cb(void *ctx_, void *obj_, const csch_rtree_box_t *box)
{
	find_conn_t *ctx = ctx_;
	csch_chdr_t *obj = obj_;
	long ci;
	char *keep;

	if ((obj == ctx->wobj) || (obj->parent == ctx->wobj->parent))
		return csch_RTREE_DIR_NOT_FOUND_CONT;

	if (csch_obj_is_grp(obj))
		return csch_RTREE_DIR_NOT_FOUND_CONT;

	if (csch_obj_intersect_obj(obj->sheet, ctx->wobj, obj, NULL, 0) <= 0)
		return csch_RTREE_DIR_NOT_FOUND_CONT;

	ci = csch_conn_auto_addi(obj->sheet, ctx->wobj, obj);

	keep = gds_get(&ctx->keep, ci, 1);
	*keep = 1;

	return csch_RTREE_DIR_FOUND_CONT;
}

/* Remove objects from conn that are not intersecting with main_obj */
static void csch_conn_remove_disconnected_objs(csch_sheet_t *sheet, csch_conn_t *conn, csch_chdr_t *main_obj)
{
	long n;
	for(n = conn->conn.used-1; n >= 0; n--) {
		csch_chdr_t *obj = conn->conn.array[n];
		if (csch_obj_intersect_obj(obj->sheet, main_obj, obj, NULL, 0) <= 0)
			csch_conn_auto_del_obj(sheet, conn, obj);
	}
}

void csch_recalc_obj_conn(csch_sheet_t *sheet, csch_chdr_t *wobj, csch_displayer_t target, csch_displayer_t target2)
{
	find_conn_t ctx = {0};
	long n;

	ctx.sheet = sheet;
	ctx.wobj = (csch_chdr_t *)wobj;


	if (wobj->conn.used != 0) {
		gds_enlarge(&ctx.keep, wobj->conn.used+4);
		memset(ctx.keep.array, 0, wobj->conn.used+4);
		ctx.keep.used = wobj->conn.used;
	}

	/* we may be called on deleted objects to get their conns removed */
	if (!csch_obj_is_deleted(wobj)) {
		csch_rtree_search_obj(&sheet->dsply[target], &wobj->bbox, find_conn_cb, &ctx);
		if (target2 != CSCH_DSPLY_invalid)
			csch_rtree_search_obj(&sheet->dsply[target2], &wobj->bbox, find_conn_cb, &ctx);
	}

	for(n = ctx.keep.used-1; n >= 0; n--) {
		if (ctx.keep.array[n]) {
			csch_conn_remove_disconnected_objs(sheet, wobj->conn.array[n], wobj);
			if (wobj->conn.used > n)
				csch_conn_update(sheet, wobj->conn.array[n], 1);
			continue;
		}
		rnd_trace("! remove conn %ld\n", n);
		csch_conn_auto_del_obj(sheet, wobj->conn.array[n], wobj);
	}
}


static void csch_conn_auto_recalc_(csch_sheet_t *sheet, csch_chdr_t *obj, int in_term, int is_deleted)
{
	if (csch_obj_is_grp(obj)) {
		csch_cgrp_t *grp = (csch_cgrp_t *)obj;
		htip_entry_t *e;

		if (grp->role == CSCH_ROLE_TERMINAL)
			in_term = 1;

		for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e))
			csch_conn_auto_recalc_(sheet, e->value, in_term, is_deleted);
	}
	else {
		if ((in_term) || ((obj->parent != NULL) && (obj->parent->role == CSCH_ROLE_WIRE_NET))) {
			if (is_deleted)
				csch_conn_auto_del_obj_all(sheet, obj);
			else
				csch_recalc_obj_conn(sheet, obj, in_term ? CSCH_DSPLY_WIRE : CSCH_DSPLY_HUBTERM, in_term ? CSCH_DSPLY_HUBTERM : CSCH_DSPLY_invalid);
		}
	}
}

void csch_conn_auto_recalc(csch_sheet_t *sheet, csch_chdr_t *obj)
{
	csch_conn_auto_recalc_(sheet, obj, 0, csch_obj_is_deleted(obj));
}


void csch_conn_auto_recalc_geo(csch_sheet_t *sheet, csch_chdr_t *obj)
{
	long n;
	for(n = 0; n < obj->conn.used; n++)
		csch_conn_recalc_coords((csch_conn_t *)obj->conn.array[n]);

	/* recurse into children */
	if (csch_obj_is_grp(obj)) {
		csch_cgrp_t *grp = (csch_cgrp_t *)obj;
		htip_entry_t *e;
		for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e))
			csch_conn_auto_recalc_geo(sheet, e->value);
	}
	else if (obj->type == CSCH_CTYPE_CONN)
		csch_conn_recalc_coords((csch_conn_t *)obj);
}

void csch_conn_update_dirties(csch_sheet_t *sheet, csch_cgrp_t *grp, int do_xform)
{
	htip_entry_t *e;

	for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) {
		csch_cgrp_t *g = e->value;
		csch_conn_t *c = e->value;
		if ((c->hdr.type == CSCH_CTYPE_CONN) && c->dirty)
			csch_conn_update(sheet, c, do_xform);
		else if (csch_obj_is_grp(&g->hdr))
			csch_conn_update_dirties(sheet, g, do_xform);
	}
}
