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

/* Paste buffers for copy/paste */

#include <libcschem/config.h>

#include <librnd/core/compat_misc.h>

#include <libcschem/project.h>
#include <libcschem/search.h>
#include <libcschem/cnc_any_obj.h>
#include <libcschem/cnc_grp.h>
#include <libcschem/cnc_text.h>
#include <libcschem/cnc_obj.h>
#include <libcschem/cnc_conn.h>
#include <libcschem/operation.h>
#include <libcschem/op_common.h>
#include <libcschem/plug_io.h>
#include <libcschem/event.h>
#include <libcschem/util_wirenet.h>
#include <libcschem/util_grp.h>

#include "conf_core.h"
#include "crosshair.h"
#include "draw.h"
#include "search.h"
#include "funchash_core.h"

#include "buffer.h"

csch_sheet_t *sch_rnd_buffer[SCH_RND_BUFFER_MAX];
csch_project_t sch_rnd_buffer_prj;

RND_INLINE void get_obj_parent_sel(csch_sheet_t *sheet, csch_chdr_t *obj, int *skip, int *orphan)
{
	csch_cgrp_t *grp;

	*skip = 0;
	*orphan = 0;

	if (obj->parent == &sheet->direct)
		return; /* first level selected is always good to copy */

	/* skip if any parent group is selected (will be copied by copying that group) */
	for(grp = obj->parent; grp != &sheet->direct; grp = grp->hdr.parent) {
		if ((grp == NULL) || (grp->hdr.selected)) {
			*skip = 1;
			return;
		}
	}

	/* group-part object selected, but group is not selected */
	*orphan = 1;
}

/* Create a new wirenet in the buffer for each wirenet on the sheet we are
   copying wire object for */
static csch_cgrp_t *buffer_wirenet(csch_sheet_t *buffer, csch_cgrp_t *sheet_wn, int alloc)
{
	htip_entry_t *e;
	csch_cgrp_t *buff_wn;
	csch_source_arg_t *src;

	for(e = htip_first(&buffer->direct.id2obj); e != NULL; e = htip_next(&buffer->direct.id2obj, e)) {
		buff_wn = e->value;
		if ((buff_wn->hdr.type == CSCH_CTYPE_GRP) && (buff_wn->role == CSCH_ROLE_WIRE_NET) && (buff_wn->hdr.mark_ptr == sheet_wn))
			return buff_wn;
	}

	if (!alloc)
		return NULL;

	buff_wn = csch_cgrp_alloc(buffer, &buffer->direct, csch_oid_new(buffer, &buffer->direct));
	src = csch_attrib_src_c(NULL, 0, 0, "wirenet copy to buffer");
	csch_attrib_set(&buff_wn->attr, 0, "role", "wire-net", src, NULL);
	buff_wn->role = CSCH_ROLE_WIRE_NET;
	buff_wn->hdr.mark_ptr = sheet_wn;
	return buff_wn;
}

/* Copy src attributes if dst doesn't have attributes yet. Both src and
   dst are wirenets */
static void maybe_copy_wirenet_attrs(csch_cgrp_t *dst, csch_cgrp_t *src)
{
	if (dst->attr.used > 1)
		return; /* already copied */
	csch_attrib_copy_all(&dst->attr, &src->attr);
}

int sch_rnd_buffer_copy_selected(csch_sheet_t *sheet, csch_sheet_t *buffer, csch_coord_t dx, csch_coord_t dy, int del)
{
	long n;
	vtp0_t objs = {0};

	if (csch_search_all_selected(sheet, NULL, &objs, 1) == 0) {
		rnd_message(RND_MSG_ERROR, "Can't copy to buffer: nothing selected\n");
		return -1;
	}

	csch_cobj_redraw_freeze(sheet);
	uundo_freeze_serial(&sheet->undo);
	uundo_freeze_add(&buffer->undo);
	csch_wirenet_recalc_freeze(sheet);
	csch_wirenet_recalc_freeze(buffer);

	for(n = 0; n < objs.used; n++) {
		csch_chdr_t *obj = objs.array[n], *newo;
		int skip, orphan, need_norm;

		get_obj_parent_sel(sheet, obj, &skip, &orphan);
		if (skip) continue;

		newo = obj;
		need_norm = 0;
		rnd_event(&sheet->hidlib, CSCH_EVENT_BUFFER_COPY_CUSTOM, "pp", &newo, buffer);
		if (newo == obj) {
			/* nobody handled it: do a plain copy */
			if (obj->type == CSCH_CTYPE_CONN) /* do not copy conns, let the code recalc them */
				continue;
			if (obj->type == CSCH_CTYPE_GRP_REF) {
				rnd_message(RND_MSG_ERROR, "Can't copy group ref object to buffer\n");
				continue;
			}
			if (obj->parent->role == CSCH_ROLE_WIRE_NET) { /* need to put the object in the "same" wirenet within the buffer so the result is a wire not just a line */
				csch_cgrp_t *buff_wn = buffer_wirenet(buffer, obj->parent, 1);
				newo = csch_cobj_dup(buffer, buff_wn, obj, 0, 0);
				need_norm = 1;
				if ((obj->type == CSCH_CTYPE_TEXT) && (((csch_text_t *)obj)->dyntext)) {
					/* if wirenet dyntext is copied, assume all attributes should be copied too */
					maybe_copy_wirenet_attrs(buff_wn, obj->parent);
				}
			}
			else
				newo = csch_cobj_dup(buffer, &buffer->direct, obj, 0, 0);
		}

		if (newo->floater || need_norm)
			csch_inst2spec(sheet, newo, obj, 0); /* transform from parent to absolute */

		if ((newo != NULL) && ((dx != 0) || (dy != 0)))
			csch_move(buffer, newo, -dx, -dy, 0);

		if (del)
			csch_op_remove(sheet, objs.array[n]);
	}

	csch_wirenet_recalc_unfreeze(buffer);
	csch_wirenet_recalc_unfreeze(sheet);
	uundo_unfreeze_add(&buffer->undo);
	uundo_unfreeze_serial(&sheet->undo);
	uundo_inc_serial(&sheet->undo);
	csch_cobj_redraw_unfreeze(sheet);

	vtp0_uninit(&objs);
	return 0;
}

void sch_rnd_buffer_paste(csch_sheet_t *sheet, csch_sheet_t *buffer, csch_coord_t dx, csch_coord_t dy)
{
	int warned = 0;
	htip_entry_t *e;

	csch_cobj_redraw_freeze(sheet);
	uundo_freeze_serial(&sheet->undo);
	csch_wirenet_recalc_freeze(sheet);

	/* copy only the first level of the tree: groups will implicitly bring their sub-objects */
	for(e = htip_first(&buffer->direct.id2obj); e != NULL; e = htip_next(&buffer->direct.id2obj, e)) {
		csch_chdr_t *obj = e->value, *newo;
		int need_conn_recalc = 0;

		if ((obj == &buffer->direct.hdr) || (obj == &buffer->indirect.hdr))
			continue;

		newo = obj;
		rnd_event(&sheet->hidlib, CSCH_EVENT_BUFFER_PASTE_CUSTOM, "p", &newo);
		if (newo == obj) {
			/* nobody handled it: do a plain copy */

			if (obj->type == CSCH_CTYPE_CONN) /* rather let the code recalculate them */
				continue;

			if (obj->type == CSCH_CTYPE_GRP_REF) {
				rnd_message(RND_MSG_ERROR, "Can't paste group ref object from buffer\n");
				continue;
			}
			newo = csch_cobj_dup(sheet, &sheet->direct, obj, 0, 0);
			if (csch_obj_is_grp(newo)) {
				csch_cgrp_t *newg = ((csch_cgrp_t *)newo);
				newg->sym_prefer_loclib = 0; /* lib buffer paste related special casing */

				if ((newg->role == CSCH_ROLE_SYMBOL) || (newg->role == CSCH_ROLE_TERMINAL) || (newg->role == CSCH_ROLE_WIRE_NET))
					need_conn_recalc = 1; /* recalc later, after the final coords are known */
			}
		}

		if (newo != NULL) {
			csch_cgrp_t *grp;

			if ((dx != 0) || (dy != 0))
				csch_move(sheet, newo, dx, dy, 0);
			else if (csch_obj_is_grp(newo))
				csch_cgrp_update(sheet, (csch_cgrp_t *)newo, 1);
			csch_op_inserted(sheet, newo->parent, newo);

			/* wirenet side effects */
			grp = (csch_cgrp_t *)newo;
			if (csch_obj_is_grp(newo) && (grp->role == CSCH_ROLE_WIRE_NET)) {
				TODO("merge#31: also put grp on a list that attemtps to merge lines using csch_wirenet_recalc_merges in unfreeze(); test case: draw wire, copy it to extend itself -> should be merged into a single line, pcb-rnd does that too");
				csch_wirenet_recalc_junctions(sheet, grp); /* we are under freeze; this really just adds the wn to the merge-recalc-list for the unfreeze */
			}
		
			if (need_conn_recalc)
				csch_conn_auto_recalc(sheet, newo);
		}
		else {
			if (!warned) {
				rnd_message(RND_MSG_ERROR, "Failed to paste some of the buffer objects\n");
				warned = 1;
			}
		}
	}

	csch_wirenet_recalc_unfreeze(sheet);
	uundo_unfreeze_serial(&sheet->undo);
	uundo_inc_serial(&sheet->undo);
	csch_cobj_redraw_unfreeze(sheet);
}

int sch_rnd_buffer_clear(csch_sheet_t *buffer)
{
	csch_chdr_t *obj, *next;

	uundo_freeze_add(&buffer->undo);
	for(obj = gdl_first(&buffer->active); obj != NULL; obj = next) {
		next = gdl_next(&buffer->active, obj);
		if ((obj != &buffer->direct.hdr) && (obj != &buffer->indirect.hdr))
			csch_cobj_uninit(obj);
	}
	uundo_unfreeze_add(&buffer->undo);

	return 0;
}

static const char csch_acts_BufferPaste[] = "BufferPaste([x, y])";
static const char csch_acth_BufferPaste[] = "Paste selection from active paste buffer to the board at crosshair or x;y.\n";
fgw_error_t csch_act_BufferPaste(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = (csch_sheet_t *)RND_ACT_DESIGN;
	csch_coord_t x = P2C(sch_rnd_crosshair_x), y = P2C(sch_rnd_crosshair_y);

	RND_ACT_MAY_CONVARG(1, FGW_COORD, BufferPaste, x = fgw_coord(&(argv[1])));
	RND_ACT_MAY_CONVARG(2, FGW_COORD, BufferPaste, y = fgw_coord(&(argv[2])));

	sch_rnd_buffer_paste(sheet, SCH_RND_PASTEBUFFER, x, y);
	RND_ACT_IRES(0);
	return 0;
}

static const char csch_acts_BufferCopy[] = "BufferCopy([x, y])";
static const char csch_acth_BufferCopy[] = "Copy selection to currently active paste buffer.\n";
fgw_error_t csch_act_BufferCopy(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = (csch_sheet_t *)RND_ACT_DESIGN;
	csch_coord_t x = P2C(sch_rnd_crosshair_x), y = P2C(sch_rnd_crosshair_y);

	RND_ACT_MAY_CONVARG(1, FGW_COORD, BufferCopy, x = fgw_coord(&(argv[1])));
	RND_ACT_MAY_CONVARG(2, FGW_COORD, BufferCopy, y = fgw_coord(&(argv[2])));

	RND_ACT_IRES(sch_rnd_buffer_copy_selected(sheet, SCH_RND_PASTEBUFFER, x, y, 0));
	return 0;
}

static const char csch_acts_BufferCut[] = "BufferCut()";
static const char csch_acth_BufferCut[] = "Cut selection to currently active paste buffer: copy objects then remove them from the sheet.\n";
fgw_error_t csch_act_BufferCut(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = (csch_sheet_t *)RND_ACT_DESIGN;
	RND_ACT_IRES(sch_rnd_buffer_copy_selected(sheet, SCH_RND_PASTEBUFFER, P2C(sch_rnd_crosshair_x), P2C(sch_rnd_crosshair_y), 1));
	return 0;
}

static const char csch_acts_BufferClear[] = "BufferClear()";
static const char csch_acth_BufferClear[] = "Empty currently active paste buffer.\n";
fgw_error_t csch_act_BufferClear(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	RND_ACT_IRES(sch_rnd_buffer_clear(SCH_RND_PASTEBUFFER));
	return 0;
}

static const char csch_acts_BufferSave[] = "BufferSave([group|symbol|all, [filename], [fmt])";
static const char csch_acth_BufferSave[] = "Save content of buffer; if first arg is symbol, the first symbol group is saved, if sheet the whole buffer is saved as sheet\n";
fgw_error_t csch_act_BufferSave(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	int rv = -1, cmd, anygrp = 0;
	char *fn = NULL;
	const char *fmt = "lihata";

	RND_ACT_CONVARG(1, FGW_KEYWORD, BufferSave, cmd = fgw_keyword(&argv[1]));
	RND_ACT_MAY_CONVARG(2, FGW_STR, BufferSave, fn = argv[2].val.str);
	RND_ACT_MAY_CONVARG(3, FGW_STR, BufferSave, fmt = argv[3].val.str);

	if (fn == NULL) {
		static char *default_file = NULL;
		const char *ext = ".*";
		const char *title, *expl;

		if (cmd == F_Symbol) {
			default_file = rnd_strdup("sym.ry");
			ext = ".ry";
		}

		switch(cmd) {
			case F_Symbol:
				title = "Save symbol from buffer...";
				expl = "Choose a file to save the symbol (group) to,\nfrom the paste buffer.\n";
				break;

			case F_All:
				title = "Save whole buffer...";
				expl = "Choose a file to save the whole paste buffer to.\n";
				break;

			case F_Group:
			default:
				title = "Save group from buffer...";
				expl = "Choose a file to save the group to,\nfrom the paste buffer.\n";
				break;
		}

		fn = rnd_hid_fileselect(rnd_gui, title, expl, default_file, ext, NULL, "grp", 0, NULL);
		if (default_file != NULL) {
			free(default_file);
			default_file = NULL;
		}
		if (fn == NULL) { /* cancel */
			RND_ACT_IRES(-1);
			return 0;
		}
		default_file = fn;
	}

	switch(cmd) {
		case F_Group:
			anygrp = 1;
			/* fall-thru */
		case F_Symbol:
			{
				csch_cgrp_t *grp = NULL, *dir = &(SCH_RND_PASTEBUFFER->direct);
				htip_entry_t *e;

				for(e = htip_first(&dir->id2obj); e != NULL; e = htip_next(&dir->id2obj, e)) {
					csch_cgrp_t *o = e->value;
					if ((o->hdr.type == CSCH_CTYPE_GRP) && (anygrp || (o->role == CSCH_ROLE_SYMBOL))) {
						grp = o;
						break;
					}
				}
				if (grp != NULL)
					rv = csch_save_grp(grp, fn, fmt, 1);
			}
			break;
		case F_All:
			rv = csch_save_buffer(SCH_RND_PASTEBUFFER, fn, fmt);
			break;
		default:
			rnd_message(RND_MSG_ERROR, "BufferSave: invalid first arg\n");
			break;
	}

	RND_ACT_IRES(rv);
	return 0;
}

static const char csch_acts_BufferLoad[] = "BufferLoad([group|symbol|all, [filename], [fmt])";
static const char csch_acth_BufferLoad[] = "Load content of buffer; if first arg is symbol, the first symbol group is saved, if sheet the whole buffer is saved as sheet\n";
fgw_error_t csch_act_BufferLoad(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	int rv = -1, cmd = F_Sheet;
	char *fn = NULL;
	const char *fmt = "lihata";

	RND_ACT_MAY_CONVARG(1, FGW_KEYWORD, BufferSave, cmd = fgw_keyword(&argv[1]));
	RND_ACT_MAY_CONVARG(2, FGW_STR, BufferSave, fn = argv[2].val.str);

	if (fn == NULL) {
		static char *default_file = NULL;
		fn = rnd_hid_fileselect(rnd_gui, "Load group into buffer...",
			"Choose a file to load the group from\ninto the paste buffer.\n",
			default_file, ".*", NULL, "grp", RND_HID_FSD_READ, NULL);
		if (default_file != NULL) {
			free(default_file);
			default_file = NULL;
		}
		if (fn == NULL) { /* cancel */
			RND_ACT_IRES(-1);
			return 0;
		}
		default_file = fn;
	}

	switch(cmd) {
		case F_Symbol:
		case F_Group:
			if (csch_load_grp(SCH_RND_PASTEBUFFER, fn, fmt) != NULL)
				rv = 0;
			break;
		case F_All:
			rv = csch_load_buffer(SCH_RND_PASTEBUFFER, fn, fmt);
			break;
		default:
			rnd_message(RND_MSG_ERROR, "BufferLoad: invalid first arg\n");
			break;
	}

	RND_ACT_IRES(rv);
	return 0;
}

static const char csch_acts_BufferChk[] = "BufferChk([group|symbol|sheet, [filename], [fmt])";
static const char csch_acth_BufferChk[] = "Chk content of buffer; if first arg is symbol, the first symbol group is saved, if sheet the whole buffer is saved as sheet\n";
fgw_error_t csch_act_BufferChk(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	int expected;
	RND_ACT_CONVARG(1, FGW_INT, BufferChk, expected = argv[1].val.nat_int);
	RND_ACT_IRES((conf_core.editor.buffer_number + 1) == expected);
	return 0;
}

static const char csch_acts_BufferSwitch[] = "BufferSwitch([group|symbol|sheet, [filename], [fmt])";
static const char csch_acth_BufferSwitch[] = "Switch content of buffer; if first arg is symbol, the first symbol group is saved, if sheet the whole buffer is saved as sheet\n";
fgw_error_t csch_act_BufferSwitch(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	int bn;
	RND_ACT_CONVARG(1, FGW_INT, BufferSwitch, bn = argv[1].val.nat_int);

	rnd_conf_set_design("editor/buffer_number", "%d", bn-1);
	rnd_gui->invalidate_all(rnd_gui);

	RND_ACT_IRES(0);
	return 0;
}

static const char csch_acts_ReplaceSymbol[] = "ReplaceSymbol([object])";
static const char csch_acth_ReplaceSymbol[] = "Replace target symbol with the first symbol from the buffer\n";
fgw_error_t csch_act_ReplaceSymbol(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = (csch_sheet_t *)RND_ACT_DESIGN;
	int op = F_Object;
	csch_chdr_t *dst;
	csch_cgrp_t *dst_grp, *src_grp;
	csch_coord_t x, y;
	htip_entry_t *e;

	RND_ACT_MAY_CONVARG(1, FGW_KEYWORD, ReplaceSymbol, op = fgw_keyword(&argv[1]));

	e = htip_first(&SCH_RND_PASTEBUFFER->direct.id2obj);
	if (e == NULL) {
		rnd_message(RND_MSG_ERROR, "ReplaceSymbol(): buffer is empty, should contain a single symbol\n");
		goto error;
	}
	src_grp = e->value;
	if (!csch_obj_is_grp(&src_grp->hdr) || (src_grp->role != CSCH_ROLE_SYMBOL)) {
		rnd_message(RND_MSG_ERROR, "ReplaceSymbol(): buffer should contain a single symbol\n");
		goto error;
	}

	switch(op) {
		case F_Object:
			if (sch_rnd_get_coords("Click on symbol to replace", &x, &y, 0) != 0)
				goto error;
			dst = sch_rnd_search_obj_at(sheet, x, y, sch_rnd_slop);
			if (dst == NULL) {
				rnd_message(RND_MSG_ERROR, "ReplaceSymbol(): no object under cursor\n");
				goto error;
			}
			dst_grp = (csch_cgrp_t *)dst;
			if (!csch_obj_is_grp(dst) || (dst_grp->role != CSCH_ROLE_SYMBOL))  {
				rnd_message(RND_MSG_ERROR, "ReplaceSymbol(): object under the cursor is not a symbol\n");
				goto error;
			}
			if (csch_cgrp_replace(dst->sheet, dst_grp, src_grp, CSCH_REPLGRP_KEEP_DST_ATTR | CSCH_REPLGRP_PREFER_DST_ATTR) != 0) {
				rnd_message(RND_MSG_ERROR, "ReplaceSymbol(): failed to replace symbol\n");
				goto error;
			}
			break;
		default:
			rnd_message(RND_MSG_ERROR, "ReplaceSymbol(): invalid target argument\n");
			return FGW_ERR_ARG_CONV;
	}

	RND_ACT_IRES(0);
	return 0;
	error:;
	RND_ACT_IRES(-1);
	return 0;
}

static rnd_action_t buffer_action_list[] = {
	{"BufferPaste", csch_act_BufferPaste, csch_acth_BufferPaste, csch_acts_BufferPaste},
	{"BufferCopy", csch_act_BufferCopy, csch_acth_BufferCopy, csch_acts_BufferCopy},
	{"BufferCut", csch_act_BufferCut, csch_acth_BufferCut, csch_acts_BufferCut},
	{"BufferClear", csch_act_BufferClear, csch_acth_BufferClear, csch_acts_BufferClear},
	{"BufferSave", csch_act_BufferSave, csch_acth_BufferSave, csch_acts_BufferSave},
	{"BufferLoad", csch_act_BufferLoad, csch_acth_BufferLoad, csch_acts_BufferLoad},
	{"BufferChk", csch_act_BufferChk, csch_acth_BufferChk, csch_acts_BufferChk},
	{"BufferSwitch", csch_act_BufferSwitch, csch_acth_BufferSwitch, csch_acts_BufferSwitch},
	{"ReplaceSymbol", csch_act_ReplaceSymbol, csch_acth_ReplaceSymbol, csch_acts_ReplaceSymbol}
};


void sch_rnd_buffer_init2(void)
{
	int n;
	for(n = 0; n < SCH_RND_BUFFER_MAX; n++)
		sch_rnd_buffer[n] = csch_sheet_alloc(&sch_rnd_buffer_prj);
	RND_REGISTER_ACTIONS(buffer_action_list, NULL);
}

void sch_rnd_buffer_uninit(void)
{
	int n;
	for(n = 0; n < SCH_RND_BUFFER_MAX; n++)
		csch_sheet_free(sch_rnd_buffer[n]);
}



