/*
 *                            COPYRIGHT
 *
 *  pcb-rnd, interactive printed circuit board design
 *  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 "config.h"
#include "board.h"
#include <genvector/gds_char.h>
#include <librnd/hid/hid.h>
#include <librnd/core/plugins.h>
#include <librnd/core/actions.h>
#include <librnd/core/rnd_printf.h>

#include "actions_pcb.h"
#include "data_it.h"
#include "funchash_core.h"
#include "obj_subc_parent.h"
#include "obj_common.h"
#include "obj_term.h"
#include "netlist.h"
#include "find.h"
#include "search.h"
#include "select.h"
#include "view.h"

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

static gds_t tmp, atmp;
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 XProbeSend(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);
}

/* If the object found is a terminal figure whether it is connected to a net
   and if it is, return net name through ctx->user_data and terminate the search */
static int xprobe_find_net_for_obj(pcb_find_t *ctx, pcb_any_obj_t *new_obj, pcb_any_obj_t *arrived_from, pcb_found_conn_type_t ctype)
{
	pcb_net_term_t *t;
	pcb_net_t *net;
	const char **dst = ctx->user_data;

	if (new_obj->term == NULL)
		return 0; /* not a terminal: continue the search */

	t = pcb_net_find_by_obj(&ctx->pcb->netlist[PCB_NETLIST_EDITED], new_obj);
	if (t == NULL)
		return 0; /* not a named terminal(?): continue the search */

	net = t->parent.net;
	if ((net == NULL) || (net->name == NULL))
		return 0; /* not on net: continue the search */

	*dst = net->name;
	return 1; /* got a netname: abort the search */
}

/* Send out XProbeSend(<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_obj(rnd_design_t *dsg, pcb_any_obj_t *obj, const char *a3)
{
	const char *cmd, *a2 = NULL;

	if (obj->term != NULL) {
		pcb_subc_t *subc = pcb_gobj_parent_subc(obj->parent_type, &obj->parent);
		if (subc != NULL) {
			tmp.used = 0;
			gds_append_str(&tmp, subc->refdes);
			gds_append(&tmp, '-');
			gds_append_str(&tmp, obj->term);
			
			cmd = "term";
			a2 = tmp.array;
			goto done;
		}
	}

	if (obj->type == PCB_OBJ_SUBC) {
		cmd = "comp";
		a2 = ((pcb_subc_t *)obj)->refdes;
		goto done;
	}

	/* find connected terminal to figure net */
	{
		pcb_board_t *pcb = (pcb_board_t *)dsg;
		pcb_find_t fctx = {0};

		fctx.found_cb = xprobe_find_net_for_obj;
		fctx.user_data = &a2;
		pcb_find_from_obj(&fctx, pcb->Data, obj);

		pcb_find_free(&fctx);

		cmd = "net";
	}

	done:;

	if (a2 == NULL)
		return -1;

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

static const char pcb_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 pcb_acth_XProbeSend[] = "Send a cross-probe request to other software in the suite";
static fgw_error_t pcb_act_XProbeSend(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	pcb_board_t *pcb = PCB_ACT_BOARD;
	rnd_design_t *dsg = &pcb->hidlib;
	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);

	/* Object lookup or manual send */
	switch(op) {
		case F_Object:
			{
				void *p1, *p2, *p3;
				rnd_coord_t x, y;
				int type;

				if (rnd_hid_get_coords("Click on oject to cross-probe", &x, &y, 0) != 0)
					break;

				TODO("subc-in-subc: this assumes subc is a component");
				/* custom, cross-probe specific search logics */
				/* start with the most likely terminal: subc padstack */
				type = pcb_search_screen(x, y, PCB_OBJ_PSTK | PCB_OBJ_SUBC_PART, &p1, &p2, &p3);
				if (type == PCB_OBJ_VOID) /* fall back to plain padstacks */
					type = pcb_search_screen(x, y, PCB_OBJ_PSTK, &p1, &p2, &p3);
				if (type == PCB_OBJ_VOID) /* fall back to subc */
					type = pcb_search_screen(x, y, PCB_OBJ_SUBC, &p1, &p2, &p3);
				if (type == PCB_OBJ_VOID) /* fall back to trace lines and arcs */
					type = pcb_search_screen(x, y, PCB_OBJ_ARC | PCB_OBJ_LINE, &p1, &p2, &p3);
				if (type == PCB_OBJ_VOID) /* fall back to subc lines and arcs */
					type = pcb_search_screen(x, y, PCB_OBJ_ARC | PCB_OBJ_LINE | PCB_OBJ_SUBC_PART, &p1, &p2, &p3);
				if (type == PCB_OBJ_VOID) /* fall back to polygons */
					type = pcb_search_screen(x, y, PCB_OBJ_POLY, &p1, &p2, &p3);

				if (type == PCB_OBJ_VOID) {
					rnd_message(RND_MSG_ERROR, "XProbeSend(): no suitable object found for cross-probe\n");
					break;
				}

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

		case F_Selected: case F_Selection:
			{
				pcb_any_obj_t *o, *t;
				pcb_data_it_t it, it2;
				int found = 0;

				for(o = pcb_data_first(&it, pcb->Data, PCB_OBJ_ARC | PCB_OBJ_POLY | PCB_OBJ_LINE | PCB_OBJ_PSTK | PCB_OBJ_SUBC | PCB_OBJ_SUBC_PART); o != NULL; o = pcb_data_next(&it)) {
					if (!PCB_FLAG_TEST(PCB_FLAG_SELECTED, (o))) {
						if (o->type == PCB_OBJ_SUBC) { /* check for terminals and nets of unselected subcircuits*/
							pcb_subc_t *subc = (pcb_subc_t *)o;
							TODO("subc-in-subc: this assumes subc is a component");
							for(t = pcb_data_first(&it2, subc->data, PCB_OBJ_ARC | PCB_OBJ_POLY | PCB_OBJ_LINE | PCB_OBJ_PSTK | PCB_OBJ_SUBC_PART); t != NULL; t = pcb_data_next(&it2)) {
								if (PCB_FLAG_TEST(PCB_FLAG_SELECTED, (t)))
									if (xprobe_send_obj(dsg, t, a3) == 0)
										found = 1;
							}
						}
						continue;
					}
					if (xprobe_send_obj(dsg, o, a2) == 0)
						found = 1;
				}

				if (!found)
					rnd_message(RND_MSG_ERROR, "XProbeSend(): nothing suitable 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;
}

static int list_obj(vtp0_t *hits, pcb_board_t *pcb, void *obj_, const char *name, int how)
{
	pcb_any_obj_t *obj = obj_;

	switch(how) {
		case F_Show:     vtp0_append(hits, obj); vtp0_append(hits, (void*)name); break;
		case F_Select:   pcb_select_object_(pcb, obj, PCB_CHGFLG_SET); break;
		case F_Unselect: pcb_select_object_(pcb, obj, PCB_CHGFLG_CLEAR); break;
	}

	return 1;
}

typedef struct list_net_ctx_s {
	vtp0_t *hits;
	pcb_board_t *pcb;
	int how;
	const char *name;
} list_net_ctx_t;

static int list_net_cb(pcb_find_t *fctx, pcb_any_obj_t *new_obj, pcb_any_obj_t *arrived_from, pcb_found_conn_type_t ctype)
{
	list_net_ctx_t *lctx = fctx->user_data;
	list_obj(lctx->hits, lctx->pcb, new_obj, lctx->name, lctx->how);
	return 0; /* continue the search, reaching all objects */
}

static long list_by_name(vtp0_t *hits, pcb_board_t *pcb, int op, const char *name, int how)
{
	long found = 0;
	pcb_any_obj_t *obj;
	char *sep;

	switch(op) {
		case F_Comp: case F_Component:
			PCB_SUBC_LOOP(pcb->Data) {
				if ((subc->refdes != NULL) && (strcmp(subc->refdes, name) == 0))
					found += list_obj(hits, pcb, subc, name, how);
			}
			PCB_END_LOOP;
			break;
		case F_Net: case F_Network:
			{
				pcb_find_t fctx = {0};
				pcb_net_term_t *t;
				rnd_cardinal_t res = 0, n;
				list_net_ctx_t lctx;
				int first = 0;
				pcb_net_t *net;

				net = pcb_net_get(PCB, &PCB->netlist[PCB_NETLIST_EDITED], name, 0);
				if (net == NULL) {
					rnd_message(RND_MSG_ERROR, "XProbeRecv(): net '%s': not found\n", name);
					break;
				}

				lctx.hits = hits;
				lctx.pcb = pcb;
				lctx.name = name;
				lctx.how = how;
				fctx.user_data = &lctx;
				fctx.found_cb = list_net_cb;

				for(t = pcb_termlist_first(&net->conns), n = 0; t != NULL; t = pcb_termlist_next(t), n++) {
					res += pcb_net_term_crawl(pcb, t, &fctx, &first, NULL);
				}

				pcb_find_free(&fctx);
				return res;
			}
			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 0;
			}
			if (sep != NULL) {
				*sep = '\0';
				obj = pcb_term_find_name(pcb, pcb->Data, PCB_LYT_COPPER, name, sep+1, NULL, NULL);
				*sep = '-';
				if (obj != NULL)
					found += list_obj(hits, pcb, obj, name, how);
			}
			break;
	}
	return found;
}


static const char pcb_acts_XProbeRecv[] =
	"XProbeRecv(comp|net|term, name, [Show|Select|Unselect)\n"
	"XProbeRecv(All, All, Unselect)"
	;
static const char pcb_acth_XProbeRecv[] = "Receive a cross-probe request from other software in the suite";
static fgw_error_t pcb_act_XProbeRecv(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	pcb_board_t *pcb = PCB_ACT_BOARD;
	char *name;
	int op, how = F_Select;
	long found;

	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)) {
		rnd_actionva(&pcb->hidlib, "unselect", "all", NULL);
		return 0;
	}


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

	/* Some ops will need to do more than just listing */
	switch(how) {
		case F_Show:
			{
				fgw_arg_t args[4], ares;
				long n;
				pcb_view_list_t *vl = calloc(sizeof(pcb_view_list_t), 1);

				for(n = 0; n < hits.used; n+=2) {
					pcb_any_obj_t *obj = hits.array[n];
					const char *name = hits.array[n+1];
					char id[256];
					pcb_view_t *v;
					
					rnd_snprintf(id, sizeof(id), "%s:#%ld", pcb_obj_type_name(obj->type), (long)obj->ID);
					v = pcb_view_new(&pcb->hidlib, "cross-probe", id, name);
					pcb_view_append_obj(v, 0, obj);
					pcb_view_set_bbox_by_objs(pcb->Data, v);
					pcb_view_list_append(vl, v);
				}

				args[1].type = FGW_STR; args[1].val.str = "Cross-probe 'show'";
				args[2].type = FGW_STR; args[2].val.str = "XProbeRecvShow";
				fgw_ptr_reg(&rnd_fgw, &args[3], PCB_PTR_DOMAIN_VIEWLIST, FGW_PTR | FGW_STRUCT, vl);
				rnd_actionv_bin(RND_ACT_DESIGN, "viewlist", &ares, 4, args);
				fgw_ptr_unreg(&rnd_fgw, &argv[3], PCB_PTR_DOMAIN_VIEWLIST);
			}
			break;
		default: break; /* the others are handled during listing */
	}


	if ((rnd_gui != NULL) && (rnd_gui->invalidate_all != NULL))
		rnd_gui->invalidate_all(rnd_gui);

	RND_ACT_IRES(0);
	return 0;
}


static rnd_action_t suite_action_list[] = {
	{"XProbeSend", pcb_act_XProbeSend, pcb_acth_XProbeSend, pcb_acts_XProbeSend},
	{"XProbeRecv", pcb_act_XProbeRecv, pcb_acth_XProbeRecv, pcb_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(&tmp);
	vtp0_uninit(&hits);
	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_hid_menu_load(rnd_gui, NULL, suite_cookie, 153, NULL, 0, suite_menu, "plugin: suite");
	return 0;
}
