/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - suite level features
 *  Copyright (C) 2025 Tibor 'Igor2' Palinkas
 *
 *  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/pcb-rnd
 *    lead developer: http://repo.hu/projects/pcb-rnd/contact.html
 *    mailing list: pcb-rnd (at) list.repo.hu (send "subscribe")
 */

#include <libcschem/config.h>

#include <genvector/gds_char.h>
#include <genht/htpi.h>
#include <genht/hash.h>
#include <librnd/core/plugins.h>
#include <librnd/core/actions.h>
#include <librnd/core/compat_misc.h>
#include <libcschem/project.h>
#include <libcschem/search.h>
#include <libcschem/cnc_any_obj.h>
#include <libcschem/actions_csch.h>
#include <sch-rnd/funchash_core.h>
#include <sch-rnd/crosshair.h>
#include <sch-rnd/search.h>
#include <sch-rnd/draw.h>
#include <sch-rnd/select.h>
#include <plugins/lib_netlist_exp/lib_netlist_exp.h>

#include "suite_conf.h"

static conf_suite_t conf_suite;

#include "conf_internal.c"

#include <librnd/hid/hid_menu.h>
#include "menu_internal.c"

static gds_t atmp, ntmp;
static vtp0_t hits;

/* append "str" to dst and replace \ with \\ and " with \" in str */
static void append_quoted(gds_t *dst, const char *str)
{
	const char *s;

	gds_append(dst, '"');
	for(s = str; *s != '\0'; s++) {
		if ((*s == '\\') || (*s == '"'))
			gds_append(dst, '\\');
		gds_append(dst, *s);
	}

	gds_append(dst, '"');
}

/* Send out XProveSend(cmd, a2, [a3]) request to the remote controller */
static void xprobe_send3(rnd_design_t *dsg, const char *cmd, const char *a2, const char *a3)
{
	atmp.used = 0;

	gds_append_str(&atmp, "XProbeSend(");
	gds_append_str(&atmp, cmd);
	gds_append(&atmp, ',');
	append_quoted(&atmp, a2);
	if (a3 != NULL) {
		gds_append(&atmp, ',');
		append_quoted(&atmp, a3);
	}
	gds_append_str(&atmp, ")\n");

/*	rnd_trace("suite req: '%s'\n", atmp.array);*/
	rnd_actionva(dsg, "remctrl_tcp_req", atmp.array, NULL);
}

/* Send out XProveSend(<cmd>, <grp>, [a3]) request to the remote controller;
   cmd and grp are set automatically from the grp argument. Returs 0
   on success (request sent) */
static int xprobe_send_grp(rnd_design_t *dsg, csch_cgrp_t *grp, const char *a3)
{
	const char *cmd, *a2, *cname, *pname;
	csch_ahdr_t *aobj;
	csch_acomp_t *acomp;
	csch_project_t *prj = (csch_project_t *)dsg->project;

	switch(grp->role) {
		case CSCH_ROLE_SYMBOL:
			if (grp->aid.used == 0)
				break;

			cmd = "comp";
			aobj = htip_get(&prj->abst->aid2obj, grp->aid.array[0]);
			if ((aobj == NULL) || (aobj->type != CSCH_ATYPE_COMP))
				return -1;
			a2 = sch_nle_get_refdes((csch_acomp_t *)aobj);
			break;

		case CSCH_ROLE_TERMINAL:
			if (grp->aid.used == 0)
				break;

			cmd = "term";
			aobj = htip_get(&prj->abst->aid2obj, grp->aid.array[0]);
			if ((aobj == NULL) || (aobj->type != CSCH_ATYPE_PORT))
				return -1;

			acomp = ((csch_aport_t *)aobj)->parent;
			if ((acomp == NULL) || (acomp->hdr.type != CSCH_ATYPE_COMP))
				return -1;

			cname = sch_nle_get_refdes(acomp);
			pname = sch_nle_get_pinnum((csch_aport_t *)aobj);

			if ((cname == NULL) || (pname == NULL))
				return -1;

			ntmp.used = 0;
			gds_append_str(&ntmp, cname);
			gds_append(&ntmp, '-');
			gds_append_str(&ntmp, pname);
			a2 = ntmp.array;
			break;
		case CSCH_ROLE_WIRE_NET:
			if (grp->aid.used == 0)
				break;

			cmd = "net";
			aobj = htip_get(&prj->abst->aid2obj, grp->aid.array[0]);
			if ((aobj == NULL) || (aobj->type != CSCH_ATYPE_NET))
				return -1;
			a2 = sch_nle_get_netname(((csch_anet_t *)aobj));
			break;
		default:
			return -1;
	}

	if (a2 == NULL)
		return -1;

	xprobe_send3(dsg, cmd, a2, a3);
	return 0;
}

static const char csch_acts_XProbeSend[] =
	"XProbeSend(object|selection, [Show|Select|Unselect])\n"
	"XProbeSend(comp|net|term, name, [Show|Select|Unselect)\n"
	"XProbeSend(All, All, [Unselect])"
	;
static const char csch_acth_XProbeSend[] = "Send a cross-probe request to other software in the suite";
static fgw_error_t csch_act_XProbeSend(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	rnd_design_t *dsg = RND_ACT_DESIGN;
	csch_sheet_t *sheet = (csch_sheet_t *)dsg;
	csch_project_t *prj = (csch_project_t *)dsg->project;
	int op;
	char *a2 = NULL, *a3 = NULL;

	RND_ACT_CONVARG(1, FGW_KEYWORD, XProbeSend, op = fgw_keyword(&argv[1]));
	RND_ACT_MAY_CONVARG(2, FGW_STR, XProbeSend, a2 = argv[2].val.str);
	RND_ACT_MAY_CONVARG(3, FGW_STR, XProbeSend, a3 = argv[3].val.str);

	/* don't attempt any object lookup if there's no abstract model */
	switch(op) {
		case F_Object:
		case F_Selected: case F_Selection:
			if (prj->abst == NULL) {
				rnd_message(RND_MSG_ERROR, "XProbeSend(): can't cross-probe object without an abstract model - compile first!\n");
				RND_ACT_IRES(-1);
				return 0;
			}
		default: break;
	}

	/* Object lookup or manual send */
	switch(op) {
		case F_Object:
			{
				csch_chdr_t *obj;
				csch_coord_t x, y;
				csch_cgrp_t *grp;

				if (sch_rnd_get_coords("Click on oject to cross-probe", &x, &y, 0) != 0)
					break;
				obj = sch_rnd_search_first_gui_inspect(sheet, C2P(x), C2P(y));
				if (obj == NULL) {
					rnd_message(RND_MSG_ERROR, "XProbeSend(): no object under cursor\n");
					break;
				}
				if (csch_obj_is_grp(obj))
					grp = (csch_cgrp_t *)obj;
				else
					grp = obj->parent;

				if (xprobe_send_grp(dsg, grp, a2) != 0) {
					rnd_message(RND_MSG_ERROR, "XProbeSend(): object type not suitable for cross-probe\n");
					break;
				}
			}
			break;

		case F_Selected: case F_Selection:
			{
				vtp0_t objs = {0};

				if ((a3 == NULL) || (rnd_strcasecmp(a3, "select") == 0))
					xprobe_send3(dsg, "All", "All", "Unselect");

				if (csch_search_all_selected(sheet, NULL, &objs, 0) > 0) {
					long n;
					int found = 0;
					htpi_t ht = {0};
					htpi_entry_t *e;

					htpi_init(&ht, ptrhash, ptrkeyeq);

					for(n = 0; n < objs.used; n++) {
						csch_chdr_t *obj = objs.array[n];
						csch_cgrp_t *grp;

						if (csch_obj_is_grp(obj))
							grp = (csch_cgrp_t *)obj;
						else
							grp = obj->parent;

						htpi_set(&ht, grp, 1);
					}

					for(e = htpi_first(&ht); e != NULL; e = htpi_next(&ht, e))
						if (xprobe_send_grp(dsg, e->key, a2) == 0)
							found = 1;

					htpi_uninit(&ht);

					if (!found)
						rnd_message(RND_MSG_ERROR, "XProbeSend(): nothing suitable selected\n");
				}
				else
					rnd_message(RND_MSG_ERROR, "XProbeSend(): nothing selected\n");
			}
			break;

		case F_Comp: case F_Component: xprobe_send3(dsg, "comp", a2, a3); break;
		case F_Net: case F_Network:    xprobe_send3(dsg, "net", a2, a3); break;
		case F_Term: case F_Terminal:  xprobe_send3(dsg, "term", a2, a3); break;
			break;
		default:
			rnd_message(RND_MSG_ERROR, "Invalid first argument to XProbeSend()\n");
			return FGW_ERR_ARG_CONV;
	}

	RND_ACT_IRES(0);
	return 0;
}

#include "abst_cache.c"

/* Look up key in the abstract cache and append all source concrete objects
   to hits. Returns whether anything is added */
static int list_by_key(vtp0_t *hits, csch_project_t *prj, htea_key_t key, int abst_listing)
{
	csch_ahdr_t *a = htea_get(&abst_cache, key);
	long n;

	if ((a == NULL) || (a->srcs.used == 0))
		return 0;

	if (abst_listing) {
		vtp0_append(hits, a);
	}
	else {
		for(n = 0; n < a->srcs.used; n++)
			vtp0_append(hits, a->srcs.array[n]);
	}

	return 1;
}

/* List all objects that match op:name into hits; in abst_listing mode append
   abstract objects to hits, otherwise append all contributing concrete objects */
static void list_by_name(vtp0_t *hits, csch_project_t *prj, int op, char *name, int abst_listing)
{
	htea_key_t key;
	char *sep;

	abst_cache_update(prj->abst);

	switch(op) {
		case F_Comp: case F_Component:
			key.type = CSCH_ATYPE_COMP;
			key.name1 = name;
			key.name2 = NULL;
			if (list_by_key(hits, prj, key, abst_listing) == 0)
				rnd_message(RND_MSG_ERROR, "XProbeRecv(): Failed to find component '%s' for cross probing\n", name);
			break;
		case F_Net: case F_Network:
			key.type = CSCH_ATYPE_NET;
			key.name1 = name;
			key.name2 = NULL;
			if (list_by_key(hits, prj, key, abst_listing) == 0)
				rnd_message(RND_MSG_ERROR, "XProbeRecv(): Failed to find network '%s' for cross probing\n", name);
			break;
		case F_Term: case F_Terminal:
			sep = strchr(name, '-');
			if (sep == NULL) {
				rnd_message(RND_MSG_ERROR, "XProbeRecv(): Invalid terminal name '%s': needs a dash\n", name);
				return;
			}
			*sep = '\0';
			key.type = CSCH_ATYPE_PORT;
			key.name1 = name;
			key.name2 = sep+1;
			if (list_by_key(hits, prj, key, abst_listing) == 0)
				rnd_message(RND_MSG_ERROR, "XProbeRecv(): Failed to find terminal '%s-%s' for cross probing\n", key.name1, key.name2);
			*sep = '-';
			break;

		default: break;
	}
}

/* Open a TreeDialog to show a set of concrete objects in a tree view */
static void obj_list_show_tree(vtp0_t *hits, rnd_design_t *dsg)
{
	fgw_func_t *ffgw = NULL;
	fgw_arg_t res = {0}, args[3];

	rnd_find_action("TreeDialog", &ffgw);
	if (ffgw == NULL) {
		rnd_message(RND_MSG_ERROR, "XProbeRecv(): failed to open tree view dialog: action TreeDialog() is not registered\n");
		return;
	}

	args[1].type = FGW_STR;
	args[1].val.cstr = "objarr";
	fgw_ptr_reg(&rnd_fgw, &args[2], CSCH_PTR_DOMAIN_COBJ_ARR, FGW_PTR | FGW_STRUCT, (void *)hits);
	rnd_actionv_bin(dsg, "TreeDialog", &res, 3, args);
	fgw_ptr_unreg(&rnd_fgw, &args[2], CSCH_PTR_DOMAIN_COBJ_ARR);
}

/* Open an AbstractDialog to show abstract objects */
static void obj_list_show_abst(vtp0_t *hits, rnd_design_t *dsg)
{
	fgw_func_t *ffgw = NULL;
	fgw_arg_t res = {0}, args[3];
	long n;

	rnd_find_action("AbstractDialog", &ffgw);
	if (ffgw == NULL) {
		rnd_message(RND_MSG_ERROR, "XProbeRecv(): failed to open tree view dialog: action TreeDialog() is not registered\n");
		return;
	}

	for(n = 0; n < hits->used; n++) {
		csch_ahdr_t *a = hits->array[n];
		args[1].type = FGW_INT;
		args[1].val.nat_int = a->aid;
		rnd_actionv_bin(dsg, "AbstractDialog", &res, 2, args);
	}
}

/* Show listed objects, either in abstract dialog or tree view dialog */
static void obj_list_show(vtp0_t *hits, rnd_design_t *dsg, int abst_dialog)
{
	if (abst_dialog)
		obj_list_show_abst(hits, dsg);
	else
		obj_list_show_tree(hits, dsg);
}


/* Select (sel==1) or unselect (sel==0) listed objects */
static void obj_list_select(vtp0_t *hits, rnd_design_t *dsg, int sel)
{
	long n;
	for(n = 0; n < hits->used; n++) {
		csch_chdr_t *obj = hits->array[n];
		if (sel)
			csch_cobj_select(obj->sheet, obj);
		else
			csch_cobj_unselect(obj->sheet, obj);
	}
}


static const char csch_acts_XProbeRecv[] =
	"XProbeRecv(comp|net|term, name, [Show|Select|Unselect)\n"
	"XProbeRecv(All, All, Unselect)"
	;
static const char csch_acth_XProbeRecv[] = "Receive a cross-probe request from other software in the suite";
static fgw_error_t csch_act_XProbeRecv(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	rnd_design_t *dsg = RND_ACT_DESIGN;
	csch_project_t *prj = (csch_project_t *)dsg->project;
	int op, how = F_Select;
	int abst_listing = !conf_suite.plugins.suite.show_in_tree_view;
	char *name;

	RND_ACT_CONVARG(1, FGW_KEYWORD, XProbeRecv, op = fgw_keyword(&argv[1]));
	RND_ACT_CONVARG(2, FGW_STR, XProbeRecv, name = argv[2].val.str);
	RND_ACT_MAY_CONVARG(3, FGW_KEYWORD, XProbeRecv, how = fgw_keyword(&argv[3]));

	/* special case: (all, all, unselect) won't list objects jsut unselect all */
	if ((op == F_All) && (how == F_Unselect)) {
		long n;
		for(n = 0; n < prj->hdr.designs.used; n++)
			sch_rnd_unselect_all((csch_sheet_t *)prj->hdr.designs.array[n]);
		RND_ACT_IRES(0);
		return 0;
	}

	/* need to have a compiled abstract model */
	if (prj->abst == NULL)
		rnd_actionva(dsg, "CompileProject", NULL);

	if (prj->abst == NULL) {
		rnd_message(RND_MSG_ERROR, "XProbeRecv(): can't cross-probe object without an abstract model - make the project compilable first!\n");
		RND_ACT_IRES(-1);
		return 0;
	}

	/* some modes require concrete objects, turn on abst_listing for those */
	switch(how) {
		case F_Show:
			break;
		case F_Select:
		case F_Unselect:
			abst_listing = 0;
			break;
	}

	/* list requested objects */
	hits.used = 0;
	list_by_name(&hits, prj, op, name, abst_listing);
	if (hits.used == 0) {
		rnd_message(RND_MSG_ERROR, "XProbeRecv(): can't find %s for cross-probing\n", name);
		RND_ACT_IRES(-1);
		return 0;
	}

	switch(how) {
		case F_Show:     obj_list_show(&hits, dsg, abst_listing); break;
		case F_Select:   obj_list_select(&hits, dsg, 1); break;
		case F_Unselect: obj_list_select(&hits, dsg, 0); break;
	}


	RND_ACT_IRES(0);
	return 0;
}


static rnd_action_t suite_action_list[] = {
	{"XProbeSend", csch_act_XProbeSend, csch_acth_XProbeSend, csch_acts_XProbeSend},
	{"XProbeRecv", csch_act_XProbeRecv, csch_acth_XProbeRecv, csch_acts_XProbeRecv}
};

char *suite_cookie = "suite plugin";

int pplg_check_ver_suite(int ver_needed) { return 0; }

void pplg_uninit_suite(void)
{
	rnd_remove_actions_by_cookie(suite_cookie);
	gds_uninit(&atmp);
	gds_uninit(&ntmp);
	vtp0_uninit(&hits);
	abst_cache_uninit();
	rnd_conf_plug_unreg("plugins/suite/", suite_conf_internal, suite_cookie);
	rnd_hid_menu_unload(rnd_gui, suite_cookie);
}

int pplg_init_suite(void)
{
	RND_API_CHK_VER;
	RND_REGISTER_ACTIONS(suite_action_list, suite_cookie);

	rnd_conf_plug_reg(conf_suite, suite_conf_internal, suite_cookie);
#define conf_reg(field,isarray,type_name,cpath,cname,desc,flags) \
	rnd_conf_reg_field(conf_suite, field,isarray,type_name,cpath,cname,desc,flags);
#include "suite_conf_fields.h"

	rnd_hid_menu_load(rnd_gui, NULL, suite_cookie, 153, NULL, 0, suite_menu, "plugin: suite");

	return 0;
}
