/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - libcschem (core library)
 *  Copyright (C) 2020,2022,2024 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 PET Fund in 2022)
 *
 *  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/rnd_printf.h>
#include <librnd/core/error.h>

#include "concrete.h"
#include <gengeo2d/xform.h>

#include "event.h"
#include "cnc_obj.h"
#include "cnc_grp.h"
#include "cnc_grp_child.h"
#include "cnc_conn.h"
#include "cnc_any_obj.h"
#include "operation.h"
#include "search.h"
#include "op_common.h"
#include "undo.h"
#include "util_wirenet.h"

extern const csch_ops_t csch_ops_line;
extern const csch_ops_t csch_ops_arc;
extern const csch_ops_t csch_ops_poly;
extern const csch_ops_t csch_ops_text;
extern const csch_ops_t csch_ops_conn;
extern const csch_ops_t csch_ops_cgrp;
extern const csch_ops_t csch_ops_cgrp_ref;
extern const csch_ops_t csch_ops_pen;

static const csch_ops_t *ops[CSCH_CTYPE_max] = {
	NULL, /* invalid */
	&csch_ops_line,
	&csch_ops_arc,
	&csch_ops_poly,
	&csch_ops_text,
	NULL, /* CSCH_CTYPE_BITMAP */
	&csch_ops_conn,
	&csch_ops_cgrp,
	&csch_ops_cgrp_ref,
	&csch_ops_pen
};


const csch_ops_t *csch_op_get(csch_ctype_t type)
{
	if ((type < 0) || (type >= CSCH_CTYPE_max))
		return NULL;
	return ops[type];
}

RND_INLINE csch_chdr_t *csch_cnc_remove_(csch_sheet_t *sheet, csch_chdr_t *obj, csch_cnc_common_remove_t how)
{
	const csch_ops_t *ops = csch_op_get(obj->type);
	csch_undo_remove_t *u, utmp;
	csch_cgrp_t *parent = obj->parent;

	assert(ops != NULL);
	assert(ops->remove_redo != NULL);

	if (how & CSCH_REM_UNDOABLE)
		u = csch_op_remove_alloc_undo(sheet);
	else
		u = &utmp;

	u->obj = obj;
	u->sheet = sheet;

	uundo_freeze_serial(&sheet->undo); /* remove_alloc() may have side effects: undoable changes to other related objects; bundle them */
	ops->remove_alloc(u);

	u->side_effects = 1;
	ops->remove_redo(u);
	u->side_effects = 0;

	if (how & CSCH_REM_UNDOABLE)
		csch_conn_auto_recalc(sheet, obj);

	if ((parent != NULL) && (parent->role == CSCH_ROLE_WIRE_NET))
		csch_wirenet_post_remove_checks(sheet, parent, (how & CSCH_REM_UNDOABLE));

	if ((how & CSCH_REM_DEL_EMPTY_PARENT) && (parent != NULL) && (parent->id2obj.used == 0) && (!csch_obj_is_deleted(&parent->hdr)))
		csch_cnc_remove_(sheet, &parent->hdr, how);

	uundo_unfreeze_serial(&sheet->undo);
	csch_undo_inc_serial(sheet);

	csch_sheet_set_changed(sheet, 1);

	return obj;
}

csch_chdr_t *csch_op_remove(csch_sheet_t *sheet, csch_chdr_t *obj)
{
	return csch_cnc_remove_(sheet, obj, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT | CSCH_REM_UNDOABLE | CSCH_REM_DEL_EMPTY_PARENT);
}

csch_chdr_t *csch_cnc_remove(csch_sheet_t *sheet, csch_chdr_t *obj)
{
	return csch_cnc_remove_(sheet, obj, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT);
}

csch_chdr_t *csch_op_create(csch_sheet_t *sheet, csch_cgrp_t *parent, csch_ctype_t type)
{
	const csch_ops_t *op = csch_op_get(type);
	csch_chdr_t *obj;

	if ((op == NULL) || (op->create == NULL))
		return NULL;

	obj = op->create(sheet, parent);
	if (obj != NULL)
		csch_op_inserted(sheet, parent, obj);

	return obj;
}


csch_chdr_t *csch_op_copy_into(csch_sheet_t *sheet, csch_cgrp_t *dst, csch_chdr_t *src)
{
	csch_chdr_t *newo;

TODO("undo problem: dup doesn't preserve oid and that may need to be undone");
	newo = csch_cobj_dup(sheet, dst, src, 0, 1);
	csch_cobj_update(sheet, newo, 0);
	csch_op_inserted(sheet, dst, newo);
	return newo;
}

csch_chdr_t *csch_op_move_into(csch_sheet_t *sheet, csch_cgrp_t *dst, csch_chdr_t *src, int restore_conns, int undoable_conns)
{
	csch_chdr_t *newo = NULL;
	vtp0_t conn_save;
	long n;

	/* save original connections before removing the object */
	if (restore_conns) {
		conn_save = src->conn;
		src->conn.used = src->conn.alloced = 0;
		src->conn.array = NULL;
	}

	csch_op_remove(sheet, src);

TODO("undo problem: dup doesn't preserve oid and that may need to be undone");
	newo = csch_cobj_dup(sheet, dst, src, 0, 1);
	csch_cobj_update(sheet, newo, 0);
	csch_op_inserted(sheet, dst, newo);

	/* restore connections: remove old object and add new object to the same conns */
	if (restore_conns) {

		for(n = 0; n < conn_save.used; n++) {
			csch_conn_t *orig_conn = conn_save.array[n];
			csch_conn_t *existing = csch_conn_find_mergable(sheet, orig_conn, newo);

			if (existing) {
				/* corer case: removing the wire from orig_conn may have oprhaned
				   orig_conn; this call takes care of removing that conn in an
				   undoable manner */
				csch_conn_auto_del_obj(sheet, orig_conn, src);

				/* if there is an existing "overlapping" conn in dst group, reuse that
				   instead of our original conn; this happens when a wire from wirenet A
				   is moved into wirenet B and the wire creates a new connection to
				   a terminal that wirenet B already connected. In this case the wire
				   should be removed from its own connection (orig_conn) and should be
				   added to the existing connection of wirenet B. */
				csch_conn_add_obj(sheet, existing, newo, undoable_conns);
			}
			else {
				/* replace object in conn: remove old object (src) and add new
				   object (newo); this won't change the number of objects in orig_conn,
				   it can't get orphaned */
				csch_conn_del_obj(sheet, orig_conn, src, undoable_conns);
				csch_conn_add_obj(sheet, orig_conn, newo, undoable_conns);
			}
		}
		vtp0_uninit(&conn_save);
	}
	return newo;
}

void csch_op_merge_into(csch_sheet_t *sheet, csch_cgrp_t *dst, csch_cgrp_t *src)
{
	htip_entry_t *e;

	/* always move the first, don't rely on htip_next() as items are being removed from src */
	for(e = htip_first(&src->id2obj); e != NULL; e = htip_first(&src->id2obj))
		csch_op_move_into(sheet, dst, e->value, 1, 1);

	csch_attrib_copy_all_undoable(sheet, dst, &dst->attr, &src->attr, 1);
}

void csch_op_merge_into_remember(csch_sheet_t *sheet, csch_cgrp_t *dst, csch_cgrp_t *src, vtp0_t *dst_news)
{
	htip_entry_t *e;

	/* always move the first, don't rely on htip_next() as items are being removed from src */
	for(e = htip_first(&src->id2obj); e != NULL; e = htip_first(&src->id2obj)) {
		void *newo = csch_op_move_into(sheet, dst, e->value, 1, 1);
		if (newo != NULL)
			vtp0_append(dst_news, newo);
	}

	csch_attrib_copy_all_undoable(sheet, dst, &dst->attr, &src->attr, 1);
}


int csch_isc_with_box(csch_chdr_t *obj, csch_rtree_box_t *box)
{
	const csch_ops_t *op = csch_op_get(obj->type);
	if ((op == NULL) || (op->isc_with_box == NULL))
		return 0;
	return op->isc_with_box(obj, box);
}

/* When moving or copying a floater: change dx and dy from sheet coords to
   parent group coords */
static void move_copy_floater_inverse(const csch_chdr_t *obj, csch_coord_t *dx, csch_coord_t *dy)
{
	g2d_xform_t mx;
	g2d_vect_t v;

	/* optimization: noop on top objects */
	if (obj->parent == &obj->sheet->direct)
		return;

	/* if there's no parent, there's no transformation possible (when copying to buffer) */
	if (obj->parent == NULL)
		return;

	v.x = *dx; v.y = *dy;
	csch_cgrp_inverse_matrix(&mx, obj->parent);

	mx.v[2] = mx.v[5] = 0; /* drop translation, we are interested only in mirror/rot */

	v = g2d_xform_vect2vect(mx, v);
	*dx = v.x; *dy = v.y;
}

void csch_move(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t dx, csch_coord_t dy, int undoable)
{
	if ((dx != 0) || (dy != 0)) {
		const csch_ops_t *op = csch_op_get(obj->type);
		if ((op != NULL) && (op->move != NULL)) {
			uundo_freeze_serial(&sheet->undo);
			move_copy_floater_inverse(obj, &dx, &dy);

			if (!csch_grp_ref_child_moved(sheet, obj, dx, dy, undoable))
				op->move(sheet, obj, dx, dy, undoable);

			if (undoable) {
				csch_conn_auto_recalc(sheet, obj);
				if (csch_obj_is_grp(obj)) {
					csch_cgrp_t *grp = (csch_cgrp_t *)obj;
					if (grp->role == CSCH_ROLE_WIRE_NET)
						csch_wirenet_recalc_junctions(sheet, grp);
				}
			}
			uundo_unfreeze_serial(&sheet->undo);
			uundo_inc_serial(&sheet->undo);


			if (undoable)
				csch_sheet_set_changed(sheet, 1);
		}
	}
}

void csch_inst2spec(csch_sheet_t *sheet, csch_chdr_t *obj, const csch_chdr_t *inst_from, int undoable)
{
	const csch_ops_t *op = csch_op_get(obj->type);
	if ((op != NULL) && (op->inst2spec != NULL)) {
		uundo_freeze_serial(&sheet->undo);
		op->inst2spec(sheet, obj, inst_from, undoable);
		if (undoable)
			csch_conn_auto_recalc(sheet, obj);
		uundo_unfreeze_serial(&sheet->undo);
		uundo_inc_serial(&sheet->undo);
	}
}


void csch_copy(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t dx, csch_coord_t dy, int undoable)
{
	if ((dx != 0) || (dy != 0)) {
		const csch_ops_t *op = csch_op_get(obj->type);
		if ((op != NULL) && (op->copy != NULL)) {
/*			csch_cobj_t *newo;*/

			if (csch_obj_is_grp_ref_child(sheet, obj)) {
				rnd_message(RND_MSG_ERROR, "Can't copy group ref children (would change the referenced group)\n");
				return;
			}

			uundo_freeze_serial(&sheet->undo);
			move_copy_floater_inverse(obj, &dx, &dy);
/*			newo = */
			op->copy(sheet, obj, dx, dy, undoable);
/*			if (undoable && (newo != NULL))
				csch_conn_auto_recalc(sheet, newo);*/
			uundo_unfreeze_serial(&sheet->undo);
			uundo_inc_serial(&sheet->undo);
		}
	}
}

static void floater_origin(const csch_chdr_t *obj, csch_coord_t *x, csch_coord_t *y)
{
	g2d_xform_t mx;
	g2d_vect_t v;

	/* optimization: noop on top objects */
	if (obj->parent == &obj->sheet->direct)
		return;

	v.x = *x; v.y = *y;
	csch_cgrp_inverse_matrix(&mx, obj->parent);
	v = g2d_xform_vect2vect(mx, v);
	*x = v.x; *y = v.y;
}

void csch_rotate(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t rcx, csch_coord_t rcy, double da, int undoable)
{
	da = fmod(da, 360);
	if (da != 0) {
		const csch_ops_t *op = csch_op_get(obj->type);
		if ((op != NULL) && (op->rotate != NULL)) {
			uundo_freeze_serial(&sheet->undo);
			floater_origin(obj, &rcx, &rcy);
			if (!csch_grp_ref_child_rotated(sheet, obj, rcx, rcy, da, undoable))
				op->rotate(sheet, obj, rcx, rcy, da, undoable);
			if (undoable)
				csch_conn_auto_recalc(sheet, obj);
			uundo_unfreeze_serial(&sheet->undo);
			uundo_inc_serial(&sheet->undo);
		}
	}
}

void csch_rotate90(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t rcx, csch_coord_t rcy, int n, int undoable)
{
	n = n & 0x3;
	if (n != 0) {
		const csch_ops_t *op = csch_op_get(obj->type);
		if ((op != NULL) && (op->rotate90 != NULL)) {
			uundo_freeze_serial(&sheet->undo);
			floater_origin(obj, &rcx, &rcy);
			if (!csch_grp_ref_child_rotated90(sheet, obj, rcx, rcy, n, undoable))
				op->rotate90(sheet, obj, rcx, rcy, n, undoable);
			if (undoable)
				csch_conn_auto_recalc(sheet, obj);
			uundo_unfreeze_serial(&sheet->undo);
			uundo_inc_serial(&sheet->undo);
		}
	}
}

void csch_mirror(csch_sheet_t *sheet, csch_chdr_t *obj, csch_coord_t mcx, csch_coord_t mcy, int mirx, int miry, int undoable)
{
	if (mirx || miry) {
		const csch_ops_t *op = csch_op_get(obj->type);
		if ((op != NULL) && (op->mirror != NULL)) {
			uundo_freeze_serial(&sheet->undo);
			floater_origin(obj, &mcx, &mcy);
			if (!csch_grp_ref_child_mirrored(sheet, obj, mcx, mcy, mirx, miry, undoable))
				op->mirror(sheet, obj, mcx, mcy, mirx, miry, undoable);
			if (undoable)
				csch_conn_auto_recalc(sheet, obj);
			uundo_unfreeze_serial(&sheet->undo);
			uundo_inc_serial(&sheet->undo);
		}
	}
}

