/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - libcschem (core library)
 *  Copyright (C) 2018..2020,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 <stdarg.h>

#include "concrete.h"

#include <librnd/core/misc_util.h>
#include <librnd/core/rnd_printf.h>
#include <librnd/core/math_helper.h>
#include <librnd/core/error.h>
#include <libminuid/libminuid.h>
#include <gengeo2d/xform.h>
#include <gengeo2d/box.h>

#include "event.h"
#include "cnc_grp.h"
#include "cnc_grp_child.h"
#include "cnc_conn.h"
#include "cnc_obj.h"
#include "cnc_any_obj.h"
#include "cnc_loop.h"
#include "op_common.h"
#include "operation.h"
#include "undo.h"
#include "util_grp.h"
#include "op_common.h"
#include "rotate.h"
#include "project.h"
#include "abstract.h"
#include "libcschem.h"

RND_INLINE csch_role_t csch_role_str2role(const char *str)
{
	if ((str == NULL) || (*str == '\0'))  return CSCH_ROLE_empty;
	if (strcmp(str, "bus-net") == 0)      return CSCH_ROLE_BUS_NET;
	if (strcmp(str, "bus-terminal") == 0) return CSCH_ROLE_BUS_TERMINAL;
	if (strcmp(str, "hub-point") == 0)    return CSCH_ROLE_HUB_POINT;
	if (strcmp(str, "symbol") == 0)       return CSCH_ROLE_SYMBOL;
	if (strcmp(str, "terminal") == 0)     return CSCH_ROLE_TERMINAL;
	if (strcmp(str, "wire-net") == 0)     return CSCH_ROLE_WIRE_NET;
	if (strcmp(str, "junction") == 0)     return CSCH_ROLE_JUNCTION;
	return CSCH_ROLE_invalid;
}

RND_INLINE void csch_role_set(csch_cgrp_t *grp, const char *val)
{
	grp->srole = val;
	grp->role = csch_role_str2role(val);
}


csch_cgrp_t *csch_cgrp_init(csch_sheet_t *sheet, csch_cgrp_t *grp, csch_cgrp_t *parent, csch_oid_t oid)
{
	csch_cobj_init(&grp->hdr, sheet, parent, oid, CSCH_CTYPE_GRP);
	csch_attrib_init(&grp->attr);
	htip_init(&grp->id2obj, longhash, longkeyeq);
	htsp_init(&grp->name2pen, strhash, strkeyeq);
	csch_cgrp_xform_update(sheet, grp);
	grp->role = csch_role_str2role(NULL); /* role is explicit empty on start */
	return grp;
}


csch_cgrp_t *csch_cgrp_alloc(csch_sheet_t *sheet, csch_cgrp_t *parent, csch_oid_t oid)
{
	csch_cgrp_t *grp;

	if (parent != NULL) {
		grp = htip_get(&parent->id2obj, oid);
		if (grp != NULL)
			return NULL;
	}
	grp = calloc(sizeof(csch_cgrp_t), 1);
	grp->data.grp.next_id = 1;
	minuid_gen(&csch_minuid, grp->uuid);
	/* src_uuid is all-zero for new groups */

	return csch_cgrp_init(sheet, grp, parent, oid);
}

csch_cgrp_t *csch_cgrp_dup_into(csch_sheet_t *sheet, csch_cgrp_t *dst, const csch_cgrp_t *src, int copy_attr)
{
	htip_entry_t *e;

	if (dst->hdr.type == CSCH_CTYPE_GRP) {
		if (minuid_is_nil(src->data.grp.src_uuid))
			minuid_cpy(dst->data.grp.src_uuid, src->uuid);
		else
			minuid_cpy(dst->data.grp.src_uuid, src->data.grp.src_uuid); /* share original source if available */
	}

	for(e = htip_first(&src->id2obj); e != NULL; e = htip_next(&src->id2obj, e))
		csch_cobj_dup(sheet, dst, (csch_chdr_t *)e->value, 1, 0);

	if (copy_attr)
		csch_cobj_attrib_copy_all(sheet, dst, src);

	return dst;
}

csch_cgrp_t *csch_cgrp_dup(csch_sheet_t *sheet, csch_cgrp_t *parent, const csch_cgrp_t *src, int keep_id)
{
	csch_cgrp_t *dst;
	if (src->hdr.type == CSCH_CTYPE_GRP) {
		dst = csch_cgrp_alloc(sheet, parent, CSCH_KEEP_OID(parent, src->hdr.oid));
		if (dst == NULL)
			return NULL;
		dst->data.grp = src->data.grp;
	}
	else {
		dst = csch_cgrp_ref_alloc(sheet, parent, CSCH_KEEP_OID(parent, src->hdr.oid));
		dst->data.ref = src->data.ref;
	}
	dst->x = src->x;
	dst->y = src->y;
	dst->spec_rot = src->spec_rot;
	dst->mirx = src->mirx;
	dst->miry = src->miry;
	dst->sym_prefer_loclib = src->sym_prefer_loclib;
	dst->loclib_name = (src->loclib_name == NULL) ? NULL : rnd_strdup(src->loclib_name);
	dst->file_name = (src->file_name == NULL) ? NULL : rnd_strdup(src->file_name);

	return csch_cgrp_dup_into(sheet, dst, src, 1);
}

void csch_cgrp_clear(csch_cgrp_t *grp)
{
	htip_entry_t *e;
	for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e))
		csch_cobj_free(e->value);
	htip_clear(&grp->id2obj);
}

void csch_cgrp_uninit(csch_cgrp_t *grp)
{
	csch_cgrp_clear(grp);
	htip_uninit(&grp->id2obj);
	htsp_uninit(&grp->name2pen);
	vtl0_uninit(&grp->aid);
	csch_cobj_uninit(&grp->hdr);
	csch_attrib_uninit(&grp->attr);
	free(grp->loclib_name);
	free(grp->file_name);
}


void csch_cgrp_free(csch_cgrp_t *grp)
{
	csch_cgrp_uninit(grp);
	free(grp);
}

csch_cgrp_t *csch_cgrp_get(csch_sheet_t *sheet, csch_cgrp_t *parent, csch_oid_t oid)
{
	csch_cgrp_t *grp = htip_get(&parent->id2obj, oid);
	if ((grp != NULL) && (grp->hdr.type != CSCH_CTYPE_GRP))
		return NULL;
	return grp;
}


void csch_cgrp_xform_update(csch_sheet_t *sheet, csch_cgrp_t *grp)
{
	static const g2d_xform_t ind = G2D_XFORM_IDENT;
	csch_child_xform_t cx0 = {0}, *cx = &cx0;

	if (grp->hdr.parent == NULL) {
		grp->xform.mx = ind;
		grp->xform.rot = 0;
		grp->xform.x = 0;
		grp->xform.y = 0;
		grp->xform.mirx = 0;
		grp->xform.miry = 0;
	}
	else {
		csch_grp_ref_parent_mx(&grp->hdr, &cx);
		grp->xform = grp->hdr.parent->xform;
	}

	if (((grp->x + cx->movex) != 0) || ((grp->y + cx->movey) != 0)) {
		grp->xform.mx = g2d_xform_translate(grp->xform.mx, (grp->x + cx->movex), (grp->y + cx->movey));
		grp->xform.x += grp->x + cx->movex;
		grp->xform.y += grp->y + cx->movey;
	}
	if (grp->mirx ^ cx->mirx) {
		grp->xform.mx = g2d_xform_mirrory(grp->xform.mx);
		grp->xform.mirx = !grp->xform.mirx;
	}
	if (grp->miry ^ cx->miry) {
		grp->xform.mx = g2d_xform_mirrorx(grp->xform.mx);
		grp->xform.miry = !grp->xform.miry;
	}

	if ((grp->spec_rot + cx->rot) != 0) {
		grp->xform.mx = g2d_xform_rotate(grp->xform.mx, -(grp->spec_rot + cx->rot) / RND_RAD_TO_DEG);
		if (grp->xform.mirx ^ grp->xform.miry)
			grp->xform.rot -= (grp->spec_rot + cx->rot);
		else
			grp->xform.rot += (grp->spec_rot + cx->rot);
	}
}

void csch_cgrp_role_update(csch_sheet_t *sheet, csch_cgrp_t *grp)
{
	grp->hdr.dsply = csch_cobj_dsply(&grp->hdr);
}


void csch_cgrp_bbox_update(csch_sheet_t *sheet, csch_cgrp_t *grp)
{
	csch_chdr_t *h;
	csch_grpo_iter_t it;

	csch_bbox_reset(&grp->hdr.bbox);
	csch_bbox_reset(&grp->bbox_flt);
	for(h = csch_grpo_first(&it, sheet, grp); h != NULL; h = csch_grpo_next(&it)) {
		if (csch_bbox_is_invalid(&h->bbox))
			continue;

		if (csch_obj_is_grp(h)) { /* group child: should use the child's bbox that includes floaters */
			csch_cgrp_t *hg = (csch_cgrp_t *)h;
			assert(hg->bbox_flt.x1 != CSCH_COORD_INV); /* floater bbox of child can not be invalid if the above bbox validity check passed because normal bbx and flt bbox are updated in parallel */
			csch_bbox_bump(&grp->bbox_flt, x, hg->bbox_flt.x1);
			csch_bbox_bump(&grp->bbox_flt, x, hg->bbox_flt.x2);
			csch_bbox_bump(&grp->bbox_flt, y, hg->bbox_flt.y1);
			csch_bbox_bump(&grp->bbox_flt, y, hg->bbox_flt.y2);
		}
		else {
			csch_bbox_bump(&grp->bbox_flt, x, h->bbox.x1);
			csch_bbox_bump(&grp->bbox_flt, x, h->bbox.x2);
			csch_bbox_bump(&grp->bbox_flt, y, h->bbox.y1);
			csch_bbox_bump(&grp->bbox_flt, y, h->bbox.y2);
		}

		if (!h->floater) {
			csch_bbox_bump(&grp->hdr.bbox, x, h->bbox.x1);
			csch_bbox_bump(&grp->hdr.bbox, x, h->bbox.x2);
			csch_bbox_bump(&grp->hdr.bbox, y, h->bbox.y1);
			csch_bbox_bump(&grp->hdr.bbox, y, h->bbox.y2);
		}
	}
}

void csch_cgrp_update(csch_sheet_t *sheet, csch_cgrp_t *grp, int do_xform)
{
	htip_entry_t *e;
	int has_conn = 0;

	if (do_xform)
		csch_cgrp_xform_update(sheet, grp);

	for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) {
		csch_chdr_t *obj = e->value;
		if (obj->type != CSCH_CTYPE_CONN)
			csch_cobj_update(sheet, obj, do_xform);
		else
			has_conn = 1;
	}

	if (has_conn) {
		/* need to udpate connections in a second pass because they depend on other (referenced) objects already updated */
		for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) {
			csch_chdr_t *obj = e->value;
			if (obj->type == CSCH_CTYPE_CONN)
				csch_cobj_update(sheet, obj, do_xform);
		}
	}

	csch_cobj_rtree_del(sheet, &grp->hdr);
	csch_cgrp_bbox_update(sheet, grp);
	csch_cobj_rtree_add(sheet, &grp->hdr);

	csch_cobj_bbox_changed(sheet, &grp->hdr);
}

void csch_cgrp_render(csch_sheet_t *sheet, csch_cgrp_t *grp)
{
	csch_cgrp_update(sheet, grp, 1);
}

void csch_cgrp_render_all(csch_sheet_t *sheet, csch_cgrp_t *grp)
{
	htip_entry_t *e;
	for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) {
		csch_cgrp_t *g = e->value;
		if (g->hdr.type == CSCH_CTYPE_GRP_REF)
			csch_cgrp_ref_render(sheet, g); /* this is recursive */
		else if (g->hdr.type == CSCH_CTYPE_GRP)
			csch_cgrp_render_all(sheet, g); /* need to recurse to find sub-group-refs */
	}

	csch_conn_update_dirties(sheet, grp, 0);
}

void csch_cgrp_attrib_update(csch_sheet_t *sheet, csch_cgrp_t *grp, int prio, const char *key, const char *val)
{
	if (strcmp(key, "role") == 0) {
		csch_role_set(grp, val);
		csch_cgrp_update_dsply_recurse(sheet, grp); /* need to move all opjects to the wire layer */
	}
}

csch_ahdr_t *csch_cgrp_get_abstract(csch_sheet_t *sheet, const csch_cgrp_t *grp, long idx)
{
	csch_project_t *prj = (csch_project_t *)sheet->hidlib.project;

	if ((prj == NULL) || (prj->abst == NULL))
		return NULL;
	if ((idx < 0) || (idx >= grp->aid.used))
		return NULL;

	return htip_get(&prj->abst->aid2obj, grp->aid.array[idx]);
}

unsigned csch_cgrp_hash_(csch_cgrp_t *grp, csch_hash_ignore_t ignore)
{
	htip_entry_t *e;
	unsigned res;

	if (grp->hdr.type == CSCH_CTYPE_GRP_REF) {
		if (grp->data.ref.grp == NULL)
			if (csch_cgrp_ref_text2ptr(grp->hdr.sheet, grp) != 0)
				return 0;
		grp = grp->data.ref.grp;
	}

	res = csch_chdr_hash(&grp->hdr);
	if (!(ignore & CSCH_HIGN_GRP_ATTRIB))
		res ^= csch_attrib_hash(&grp->attr);
	if (!(ignore & CSCH_HIGN_GRP_PLACEMENT)) {
		res ^= csch_angle_hash(grp->spec_rot);
		res ^= csch_coord_hash(grp->x);
		res ^= csch_coord_hash(grp->y);
		res ^= ((unsigned)grp->mirx) << 9;
		res ^= ((unsigned)grp->miry) << 10;
	}

	/* ->loclib_name is omitted on purpose: a lib vs. instance match would
	   be impossible with that */

	for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) {
		csch_chdr_t *obj = e->value;
		res ^= csch_cobj_hash_(obj, (ignore & CSCH_HIGN_FLOATER_GEO));
	}

	return res;
}

int csch_cgrp_keyeq_(csch_cgrp_t *g1, csch_cgrp_t *g2, csch_hash_ignore_t ignore)
{
	htip_entry_t *e;

	if (g1->hdr.type == CSCH_CTYPE_GRP_REF) {
		if (g1->data.ref.grp == NULL)
			if (csch_cgrp_ref_text2ptr(g1->hdr.sheet, g1) != 0)
				return 0;
		g1 = g1->data.ref.grp;
	}
	if (g2->hdr.type == CSCH_CTYPE_GRP_REF) {
		if (g2->data.ref.grp == NULL)
			if (csch_cgrp_ref_text2ptr(g2->hdr.sheet, g2) != 0)
				return 0;
		g2 = g2->data.ref.grp;
	}

	if (!csch_chdr_eq(&g1->hdr, &g2->hdr)) return 0;
	if (!(ignore & CSCH_HIGN_GRP_PLACEMENT)) {
		if (!csch_angle_eq(g1->spec_rot, g2->spec_rot)) return 0;
		if (g1->x != g2->x) return 0;
		if (g1->y != g2->y) return 0;
		if (g1->mirx != g2->mirx) return 0;
		if (g1->miry != g2->miry) return 0;
	}
	if (htip_length(&g1->id2obj) != htip_length(&g2->id2obj)) return 0;

	/* ->loclib_name is omitted on purpose: a lib vs. instance match would
	   be impossible with that */

	/* expensive tests */
	if (!(ignore & CSCH_HIGN_GRP_ATTRIB)) {
		if (!csch_attrib_eq(&g1->attr, &g2->attr)) return 0;
	}

	for(e = htip_first(&g1->id2obj); e != NULL; e = htip_next(&g1->id2obj, e)) {
		csch_chdr_t *o2 = htip_get(&g2->id2obj, e->key), *o1 = e->value;
		if (o2 == NULL) return 0;
		if (!csch_cobj_keyeq_(o1, o2, (ignore & CSCH_HIGN_FLOATER_GEO))) return 0;
	}

	return 1;
}

#include "cnc_grp_ref.c"

static csch_chdr_t *cgrp_create(csch_sheet_t *sheet, csch_cgrp_t *parent)
{
	csch_cgrp_t *grp = csch_cgrp_alloc(sheet, parent, csch_oid_new(sheet, parent));
	if (grp == NULL) return NULL;
	return &grp->hdr;
}


/* recursively remove an object from the rtree */
static void csch_rtree_remove_recursive(csch_sheet_t *sheet, csch_chdr_t *obj)
{
	csch_cobj_rtree_del(obj->sheet, obj);
	if (csch_obj_is_grp(obj)) { /* remove all part-objects from rtree */
		htip_entry_t *e;
		csch_cgrp_t *grp = (csch_cgrp_t *)obj;
		for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e))
			csch_rtree_remove_recursive(sheet, e->value);
	}
}

static void cgrp_remove_alloc(csch_undo_remove_t *slot)
{

}

static void cgrp_remove_redo(csch_undo_remove_t *slot)
{
	csch_rtree_remove_recursive(slot->sheet, slot->obj); /* need to manually remove from rtree because children are also added */
	csch_cnc_common_remove_redo(slot, CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT);
}

static void cgrp_remove_undo(csch_undo_remove_t *slot)
{
	csch_cnc_common_remove_undo(slot, CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT);
	csch_cgrp_update(slot->sheet, (csch_cgrp_t *)slot->obj, 1); /* need to redo transformations for text object update */
}

static int cgrp_isc_with_box(csch_chdr_t *obj, csch_rtree_box_t *box_)
{
	g2d_box_t obb, box;
	box.p1.x = box_->x1; box.p1.y = box_->y1;
	box.p2.x = box_->x2; box.p2.y = box_->y2;
	obb.p1.x = obj->bbox.x1; obb.p1.y = obj->bbox.y1;
	obb.p2.x = obj->bbox.x2; obb.p2.y = obj->bbox.y2;
	return g2d_isc_box_box(&obb, &box);
}

static void cgrp_move(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t dx, csch_coord_t dy, int undoable)
{
	csch_cgrp_t *cgrp = (csch_cgrp_t *)obj;
	csch_cgrp_modify(sheet, cgrp, &dx, &dy, NULL, NULL, NULL, undoable, 1);
}

static void cgrp_copy(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t dx, csch_coord_t dy, int undoable)
{
	csch_cgrp_t *cgrp = csch_cgrp_dup(sheet, obj->parent, (csch_cgrp_t *)obj, 0);
	if (cgrp != NULL) {
		cgrp->x += dx; cgrp->y += dy;
		csch_cgrp_update(sheet, cgrp, 1);
		csch_op_inserted(sheet, obj->parent, &cgrp->hdr);
	}
}

RND_INLINE double cgrp_rotdir(csch_cgrp_t *cgrp)
{
	return cgrp->mirx ^ cgrp->miry ? -1.0 : 1.0;
}

static void cgrp_rotate(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t rcx, csch_coord_t rcy, double da, int undoable)
{
	csch_cgrp_t *cgrp = (csch_cgrp_t *)obj;
	csch_coord_t nx = cgrp->x, ny = cgrp->y;
	double nrot, cs, sn, rad;

	nrot = cgrp->spec_rot + da * cgrp_rotdir(cgrp);
	rad = -da / RND_RAD_TO_DEG;
	cs = cos(rad); sn = sin(rad);

	csch_rotate_pt(&nx, &ny, rcx, rcy, cs, sn);

	csch_cgrp_modify(sheet, cgrp, &nx, &ny, &nrot, NULL, NULL, undoable, 0);
}

static void cgrp_rotate90(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t rcx, csch_coord_t rcy, int n, int undoable)
{
	csch_cgrp_t *cgrp = (csch_cgrp_t *)obj;
	csch_coord_t nx = cgrp->x, ny = cgrp->y;
	double nrot;

	nrot = fmod(cgrp->spec_rot + (double)n * 90.0 * cgrp_rotdir(cgrp), 360.0);
	csch_rotate90_pt(&nx, &ny, rcx, rcy, n);

	csch_cgrp_modify(sheet, cgrp, &nx, &ny, &nrot, NULL, NULL, undoable, 0);
}

static void cgrp_mirror(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t mcx, csch_coord_t mcy, int mirx, int miry, int undoable)
{
	csch_cgrp_t *cgrp = (csch_cgrp_t *)obj;
	csch_coord_t nx = cgrp->x, ny = cgrp->y;
	int nmirx = cgrp->mirx, nmiry = cgrp->miry;

	csch_mirror_pt(&nx, &ny, mcx, mcy, mirx, miry);
	if (mirx) nmirx = !nmirx;
	if (miry) nmiry = !nmiry;

	csch_cgrp_modify(sheet, cgrp, &nx, &ny, NULL, &nmirx, &nmiry, undoable, 0);
}

static g2d_calc_t mx33det(g2d_xform_t mx)
{
	double det = 0;
	det += mx.v[0] * (mx.v[4] * mx.v[8] - mx.v[5] * mx.v[7]);
	det -= mx.v[1] * (mx.v[3] * mx.v[8] - mx.v[5] * mx.v[6]);
	det += mx.v[2] * (mx.v[3] * mx.v[7] - mx.v[4] * mx.v[6]);
	return det;
}

static g2d_xform_t mx33adj(g2d_xform_t mx)
{
	g2d_xform_t res;
	res.v[0] = (mx.v[4] * mx.v[8] - mx.v[5] * mx.v[7]);
	res.v[3] = (mx.v[3] * mx.v[8] - mx.v[5] * mx.v[6]);
	res.v[6] = (mx.v[3] * mx.v[7] - mx.v[4] * mx.v[6]);
	res.v[1] = (mx.v[1] * mx.v[8] - mx.v[2] * mx.v[7]);
	res.v[4] = (mx.v[0] * mx.v[8] - mx.v[2] * mx.v[6]);
	res.v[7] = (mx.v[0] * mx.v[7] - mx.v[1] * mx.v[6]);
	res.v[2] = (mx.v[1] * mx.v[5] - mx.v[2] * mx.v[4]);
	res.v[5] = (mx.v[0] * mx.v[5] - mx.v[2] * mx.v[3]);
	res.v[8] = (mx.v[0] * mx.v[4] - mx.v[1] * mx.v[3]);
	return res;
}

RND_INLINE void csch_cgrp_inverse_matrix_(g2d_xform_t *dst, const csch_cgrp_t *cgrp)
{
	double det = mx33det(cgrp->xform.mx);
	g2d_xform_t mx = mx33adj(cgrp->xform.mx);
	int n;

	for(n = 0; n < 9; n++) {
		mx.v[n] = mx.v[n] / det;
		if ((n % 2) == 1)
			mx.v[n] = -mx.v[n];
	}

	*dst = mx;
}

void csch_cgrp_inverse_matrix(g2d_xform_t *dst, const csch_cgrp_t *cgrp)
{
	csch_cgrp_inverse_matrix_(dst, cgrp);
}


static void cgrp_inst2spec(csch_sheet_t *sheet, csch_chdr_t *obj, const csch_chdr_t *in, int undoable)
{
	const csch_cgrp_t *cgrp = (const csch_cgrp_t *)in;
	csch_coord_t nx = cgrp->hdr.parent->xform.x, ny = cgrp->hdr.parent->xform.y;
	int nmirx = cgrp->xform.mirx, nmiry = cgrp->xform.miry;
	double nrot = cgrp->xform.rot;
	g2d_vect_t v;

	v.x = cgrp->x; v.y = cgrp->y;
	v = g2d_xform_vect2vect(cgrp->hdr.parent->xform.mx, v);
	nx = v.x;
	ny = v.y;
	if (nmirx ^ nmiry)
		nrot = -nrot;

	csch_cgrp_modify(sheet, (csch_cgrp_t *)obj, &nx, &ny, &nrot, &nmirx, &nmiry, undoable, 0);
}


const csch_ops_t csch_ops_cgrp = {
	cgrp_create, cgrp_remove_alloc, cgrp_remove_redo, cgrp_remove_undo,
	cgrp_isc_with_box,
	cgrp_move, cgrp_copy, cgrp_rotate, cgrp_rotate90, cgrp_mirror,
	cgrp_inst2spec
};


const csch_ops_t csch_ops_cgrp_ref = {
	NULL, cgrp_remove_alloc, cgrp_remove_redo, cgrp_remove_undo,
	cgrp_isc_with_box,
	cgrp_move, cgrp_copy, cgrp_rotate, cgrp_rotate90, cgrp_mirror
};



/*** Modify ***/
typedef struct {
	csch_cgrp_t *grp; /* it is safe to save the object pointer because it is persistent (through the removed object list) */

	unsigned mirx:1, miry:1;
	double spec_rot;
	csch_coord_t x, y;
} undo_cgrp_modify_t;


static int undo_cgrp_modify_swap(void *udata)
{
	undo_cgrp_modify_t *u = udata;

	csch_cobj_redraw(u->grp);

	rnd_swap(csch_coord_t, u->x, u->grp->x);
	rnd_swap(csch_coord_t, u->y, u->grp->y);
	rnd_swap(int, u->mirx, u->grp->mirx);
	rnd_swap(int, u->miry, u->grp->miry);
	rnd_swap(double, u->spec_rot, u->grp->spec_rot);

	csch_cgrp_update(u->grp->hdr.sheet, u->grp, 1);
	csch_conn_auto_recalc_geo(u->grp->hdr.sheet, &u->grp->hdr);
	csch_sheet_set_changed(u->grp->hdr.sheet, 1);

	csch_cobj_redraw(u->grp);
	return 0;
}

static void undo_cgrp_modify_print(void *udata, char *dst, size_t dst_len)
{
	rnd_snprintf(dst, dst_len, "cgrp geometry change");
}

static const char core_cgrp_cookie[] = "libcschem/core/cnc_cgrp.c";

static const uundo_oper_t undo_cgrp_modify = {
	core_cgrp_cookie,
	NULL,
	undo_cgrp_modify_swap,
	undo_cgrp_modify_swap,
	undo_cgrp_modify_print
};


void csch_cgrp_modify(csch_sheet_t *sheet, csch_cgrp_t *grp, csch_coord_t *x, csch_coord_t *y, double *spec_rot, int *mirx, int *miry, int undoable, int relative)
{
	undo_cgrp_modify_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_cgrp_modify, sizeof(undo_cgrp_modify_t));

	u->grp = grp;
	u->x = CSCH_UNDO_RMODIFY(grp, x);
	u->y = CSCH_UNDO_RMODIFY(grp, y);
	u->spec_rot = CSCH_UNDO_RMODIFY(grp, spec_rot);
	u->mirx = CSCH_UNDO_MODIFY(grp, mirx);
	u->miry = CSCH_UNDO_MODIFY(grp, miry);

	undo_cgrp_modify_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}


typedef struct {
	csch_cgrp_t *grp; /* it is safe to save the object pointer because it is persistent (through the removed object list) */
	minuid_bin_t uuid, src_uuid;
} undo_cgrp_modify_uuid_t;


static int undo_cgrp_modify_uuid_swap(void *udata)
{
	undo_cgrp_modify_uuid_t *u = udata;

	minuid_swap(u->uuid, u->grp->uuid);
	if (u->grp->hdr.type == CSCH_CTYPE_GRP)
		minuid_swap(u->src_uuid, u->grp->data.grp.src_uuid);

	csch_cgrp_update(u->grp->hdr.sheet, u->grp, 1);
	csch_sheet_set_changed(u->grp->hdr.sheet, 1);

	return 0;
}

static void undo_cgrp_modify_uuid_print(void *udata, char *dst, size_t dst_len)
{
	rnd_snprintf(dst, dst_len, "cgrp uuid change");
}

static const uundo_oper_t undo_cgrp_modify_uuid = {
	core_cgrp_cookie,
	NULL,
	undo_cgrp_modify_uuid_swap,
	undo_cgrp_modify_uuid_swap,
	undo_cgrp_modify_uuid_print
};


void csch_cgrp_modify_uuid(csch_sheet_t *sheet, csch_cgrp_t *grp, minuid_bin_t *uuid, minuid_bin_t *src_uuid, int undoable)
{
	undo_cgrp_modify_uuid_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_cgrp_modify_uuid, sizeof(undo_cgrp_modify_uuid_t));

	u->grp = grp;
	minuid_cpy(u->uuid, (uuid != NULL) ? *uuid : grp->uuid);
	if (u->grp->hdr.type == CSCH_CTYPE_GRP)
	minuid_cpy(u->src_uuid, (src_uuid != NULL) ? *src_uuid : grp->data.grp.src_uuid);

	undo_cgrp_modify_uuid_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}


/*** transformations ***/
void csch_cgrp_inverse_xform(csch_cgrp_t *parent, csch_coord_t *x, csch_coord_t *y, long num_crds)
{
	g2d_xform_t mx;

	csch_cgrp_inverse_matrix(&mx, parent);

	for(; num_crds > 0; num_crds--,x++,y++) {
		g2d_vect_t v1;
		v1.x = *x; v1.y = *y;
		v1 = g2d_xform_vect2vect(mx, v1);
		*x = v1.x; *y = v1.y;
	}
}

void csch_cgrp_vinverse_xform(csch_cgrp_t *parent, ...)
{
	g2d_xform_t mx;
	va_list ap;

	csch_cgrp_inverse_matrix(&mx, parent);
	va_start(ap, parent);

	for(;;) {
		g2d_vect_t v1;
		csch_coord_t *x, *y;

		x = va_arg(ap, csch_coord_t *); if (x == NULL) break;
		y = va_arg(ap, csch_coord_t *); if (y == NULL) break;

		v1.x = *x; v1.y = *y;
		v1 = g2d_xform_vect2vect(mx, v1);
		*x = v1.x; *y = v1.y;
	}

	va_end(ap);
}

/* based on https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix/13165#13165 */
double csch_cgrp_extract_rot(g2d_xform_t mx)
{
	return atan2(mx.v[3], mx.v[4]) * RND_RAD_TO_DEG;
}

void csch_cgrp_extract_scale(g2d_xform_t mx, double *sx, double *sy)
{
	*sx = sqrt(mx.v[0] * mx.v[0] + mx.v[1] * mx.v[1]);
	*sy = sqrt(mx.v[3] * mx.v[3] + mx.v[4] * mx.v[4]);

	/* This is needed for mirroring */
	if (mx.v[0] < 0) *sx = -(*sx);
	if (mx.v[4] < 0) *sy = -(*sy);
}
