/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - alternate function mapper
 *  Copyright (C) 2023 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 Entrust Fund in 2023)
 *
 *  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
 */

/* Locally implemented dialog boxes */

#include <librnd/hid/hid_dad.h>
#include <librnd/hid/hid_dad_tree.h>
#include <sch-rnd/funchash_core.h>
#include <sch-rnd/crosshair.h>
#include <sch-rnd/search.h>
#include <sch-rnd/draw.h>

/*** Common ***/
/* Resolve component and fetch the funcmap attrib subtree (typically from
   local lib). Returns 0 on success. */
RND_INLINE int funcmap_fetch(csch_project_t *prj, const char *comp_name, csch_acomp_t **comp_out, csch_attribs_t **fmattrs_out)
{
	csch_abstract_t *abst = prj->abst;
	csch_acomp_t *comp;
	csch_attrib_t *afuncmap;

	if (comp_out != NULL)
		*comp_out = NULL;

	if (fmattrs_out != NULL)
		*fmattrs_out = NULL;

	if (abst == NULL)
		return -1;

	comp = csch_acomp_get(abst, comp_name, 0);
	if (comp == NULL)
		return -1;

	afuncmap = csch_attrib_get(&comp->hdr.attr, "funcmap");
	if (afuncmap == NULL)
		return -1;

	if (comp_out != NULL)
		*comp_out = comp;

	if (fmattrs_out != NULL) {
		anymap_ctx_t *actx;
		csch_hook_call_ctx_t cctx = {0};

		cctx.project = prj;
		actx = csch_p4_get_by_project(&p4_funcmap, prj);
		*fmattrs_out = anymap_get_from_any_lib(actx, comp, afuncmap, &cctx);
		if (*fmattrs_out == NULL)
			return -1;
	}

	return 0;
}

static const char *funcmap_get_selected(csch_acomp_t *comp, const char *port_name)
{
	csch_aport_t *port = htsp_get(&comp->ports, port_name);

	if (port == NULL)
		return NULL;

	return csch_attrib_get_str(&port->hdr.attr, "funcmap/name");
}

/* Undoably modify the "funcmap/name" attribute of comp-port_name in
   source terminals. Values in existing terminal attributes are modified
   or if there is no attribute and new_val is not NULL, the attribute is
   created on one of the terminals. If new_val is NULL, the attribute is
   removed */
static int funcmap_set_on_port(csch_acomp_t *comp, const char *port_name, const char *new_val)
{
	csch_aport_t *port = htsp_get(&comp->ports, port_name);
	csch_cgrp_t *fallback = NULL;
	int done = 0;
	long n;

	if (port == NULL)
		return -1;

	/* find terminal object(s) to set the attribute in; change the attribute
	   in all terminals where it already exist */
	for(n = 0; n < port->hdr.srcs.used; n++) {
		csch_cgrp_t *term = port->hdr.srcs.array[n];
		csch_attrib_t *a;
		if (!csch_obj_is_grp(&term->hdr)) continue;
		if (term->role != CSCH_ROLE_TERMINAL) continue;
		if (fallback == NULL) fallback = term;

		/* if this terminal has a funcmap/name attribute, change it */
		a = csch_attrib_get(&term->attr, "funcmap/name");
		if (a != NULL) {
			if (new_val != NULL) {
				if ((a->val == NULL) || (strcmp(a->val, new_val) != 0)) {
					csch_source_arg_t *source = csch_attrib_src_c(NULL, 0, 0, "funcmap action (modify existing attr)");
					csch_attr_modify_str(term->hdr.sheet, term, CSCH_ATP_USER_DEFAULT, "funcmap/name", new_val, source, 1);
				}
			}
			else
				csch_attr_modify_del(term->hdr.sheet, term, "funcmap/name", 1);
			done = 1;
		}
	}


	if (!done && (new_val != NULL)) {
		/* no terminal group had the attribute; set it on the fallback terminal */
		if (fallback == NULL) {
			rnd_message(RND_MSG_ERROR, "funcmap_set_on_port(): can't set attribute on port %s-%s: terminal not found on the drawing\n", comp->name, port_name);
			return -1;
		}
		else {
			csch_source_arg_t *source = csch_attrib_src_c(NULL, 0, 0, "funcmap action (create new attr)");
			csch_attr_modify_str(fallback->hdr.sheet, fallback, CSCH_ATP_USER_DEFAULT, "funcmap/name", new_val, source, 1);
		}
	}

	return 0;
}

static void funcmap_step(csch_acomp_t *comp, const char *port_name, csch_attribs_t *fmattrs, const char *curr, int dir)
{
	vts0_t funcs = {0};
	long n, curridx = -1, setidx = -1;

	if (funcmap_get_port_funcs(&funcs, fmattrs, port_name, curr, &curridx) < 0)
		goto quit;

	if (curridx < 0) {
		if (dir > 0)
			setidx = 0;
		else
			setidx = funcs.used-1;
	}
	else {
		if (dir > 0) {
			setidx = curridx + 1;
			if (setidx >= funcs.used)
				setidx = 0;
		}
		else {
			setidx = curridx - 1;
			if (setidx < 0)
				setidx = funcs.used-1;
		}
	}

	if (setidx >= 0)
		funcmap_set_on_port(comp, port_name, funcs.array[setidx]);

	quit:;
	for(n = 0; n < funcs.used; n++)
		free(funcs.array[n]);
	vts0_uninit(&funcs);
}

static const char csch_acts_FuncmapChange[] =
	"FuncmapChange(previous|next|remove, component, port)\n"
	"FuncmapChange(set, component, port, [newfunc])\n"
	"FuncmapChange(setgrp, component, grpname)\n";
static const char csch_acth_FuncmapChange[] = "Change the alternate function of a component-port. The setgrp command activates a group by setting all affected ports to that group's funcionality. All arguments are case-sensitive.";
/* DOC: funcmapchange.html */
static fgw_error_t csch_act_FuncmapChange(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = CSCH_ACT_SHEET;
	csch_project_t *prj = (csch_project_t *)sheet->hidlib.project;
	const char *cmd, *comp_name, *port_name, *grp_name, *new_func = NULL;
	const char *curr;
	csch_acomp_t *comp;
	csch_attribs_t *fmattrs;

	RND_ACT_CONVARG(1, FGW_STR, FuncmapChange, cmd = argv[1].val.str);
	RND_ACT_CONVARG(2, FGW_STR, FuncmapChange, comp_name = argv[2].val.str);
	RND_ACT_CONVARG(3, FGW_STR, FuncmapChange, grp_name = port_name = argv[3].val.str);
	RND_ACT_MAY_CONVARG(4, FGW_STR, FuncmapChange, new_func = argv[4].val.str);

	RND_ACT_IRES(-1);

	if (funcmap_fetch(prj, comp_name, &comp, &fmattrs) != 0) {
		rnd_message(RND_MSG_ERROR, "FuncmapChange: failed to fetch component funcmap attr from the abstract model: '%s'\n", comp_name);
		return 0;
	}


	if (rnd_strcasecmp(cmd, "previous") == 0) {
		curr = funcmap_get_selected(comp, port_name);
		funcmap_step(comp, port_name, fmattrs, curr, -1);
	}
	else if (rnd_strcasecmp(cmd, "next") == 0) {
		curr = funcmap_get_selected(comp, port_name);
		funcmap_step(comp, port_name, fmattrs, curr, +1);
	}
	else if (rnd_strcasecmp(cmd, "remove") == 0) {
		funcmap_set_on_port(comp, port_name, NULL);
	}
	else if (rnd_strcasecmp(cmd, "set") == 0) {
		if (new_func == NULL) {
			/* invoke the GUI */
			rnd_actionva(&sheet->hidlib, "FuncmapPortDialog", "object", comp->name, port_name, NULL);
		}
		else {
			if (funcmap_has_func(comp, port_name, fmattrs, new_func))
				funcmap_set_on_port(comp, port_name, new_func);
			else
				rnd_message(RND_MSG_ERROR, "Port %s-%s doesn't have alternate functio '%s'\n", comp->name, port_name, new_func);
		}
	}
	else if (rnd_strcasecmp(cmd, "setgrp") == 0) {
		funcmap_set_func_grp(comp, fmattrs, grp_name);
	}
	else {
		rnd_message(RND_MSG_ERROR, "FuncmapChange: invalid first attribute\n");
		return 0;
	}


	RND_ACT_IRES(0);
	return 0;
}

static int funcmap_print_table(FILE *f, csch_project_t *prj, const char *comp_name, const char *sort_by)
{
	csch_acomp_t *comp;
	csch_attribs_t *fmattrs;
	htsp_t func2col, port2cell;
	int c, grpcol, sort_by_col = -1;
	long r;
	vtp0_t rows = {0}; /* contains: vts0_t *in_cells */
	vts0_t hdr = {0}, *hdrs = &hdr;
	vtl0_t colw = {0};

	/* build the table */
	if (funcmap_fetch(prj, comp_name, &comp, &fmattrs) != 0)
		return -1;


	/* map header */
	vts0_append(&hdr, rnd_strdup("<port>"));
	funcmap_comp_gen_hdr_(&hdr, htsp_get(fmattrs, "funcmap/weak_groups"));
	funcmap_comp_gen_hdr_(&hdr, htsp_get(fmattrs, "funcmap/strong_groups"));
	vts0_append(&hdr, rnd_strdup("<no-group>"));

	/* set up sort-by */
	if (sort_by != NULL) {
		for(c = 0; c < hdr.used; c++) {
			if (strcmp(hdr.array[c], sort_by) == 0) {
				sort_by_col = c;
				break;
			}
		}
		if (sort_by_col < 0)
			rnd_message(RND_MSG_ERROR, "funcmap_print_table(): can't sort by '%s': no such column\n(Printing the table unsorted)\n", sort_by);
	}

	/* map table */
	funcmap_comp2dlg_hashgrps(&func2col, fmattrs, &grpcol);
	funcmap_comp2dlg_hashcells(&func2col, &port2cell, htsp_get(fmattrs, "funcmap/ports"), grpcol);
	vtl0_enlarge(&colw, grpcol+1);

	funcmap_sort_rows(&rows, &port2cell, sort_by_col);

	vtp0_insert_len(&rows, 0, (void **)&hdrs, 1);

	/* calculate col widths */
	for(r = 0; r < rows.used; r++) {
		long n;
		vts0_t *in_cells = rows.array[r];
		const char *selected, *key;

		key = in_cells->array[0];
		selected = funcmap_get_selected(comp, key);
		for(n = 0; n <= grpcol; n++) {
			int len;

			if ((in_cells->used > n) & (in_cells->array[n] != NULL)) {
				if ((n > 0) && (selected != NULL) && (strcmp(in_cells->array[n], selected) == 0))
					len = strlen(in_cells->array[n]) + 4;
				else
					len = strlen(in_cells->array[n]);
			}
			else
				len = 1;
			if (len > colw.array[n])
				colw.array[n] = len;
		}
	}

	/* print table */
	for(r = 0; r < rows.used; r++) {
		long n;
		vts0_t *in_cells = rows.array[r];
		const char *selected, *key;

		key = in_cells->array[0];
		selected = funcmap_get_selected(comp, key);
		for(n = 0; n <= grpcol; n++) {
			int len;

			if (n != 0)
				fprintf(f, " ");

			if ((in_cells->used > n) & (in_cells->array[n] != NULL)) {
				if ((n > 0) && (selected != NULL) && (strcmp(in_cells->array[n], selected) == 0))
					len = fprintf(f, "[[%s]]", in_cells->array[n]);
				else
					len = fprintf(f, "%s", in_cells->array[n]);
			}
			else
				len = fprintf(f, "-");

			if (n != grpcol) {
				while(len < colw.array[n]) {
					fprintf(f, " ");
					len++;
				}
			}

		}
		printf("\n");
	}

	vtl0_uninit(&colw);
	vtp0_uninit(&rows);
	funcmap_hashgrps_free(&func2col);
	funcmap_hashcells_free(&port2cell);

	for(c = 0; c < hdr.used; c++)
		free(hdr.array[c]);
	vts0_uninit(&hdr);

	return 0;
}


static const char csch_acts_FuncmapPrintTable[] = "FuncmapPrintTable(component, [column])";
static const char csch_acth_FuncmapPrintTable[] = "Print a table of all alternative port functions for a component If column is specified, sort the table by the named column.";
/* DOC: funcmapprinttable.html */
static fgw_error_t csch_act_FuncmapPrintTable(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = CSCH_ACT_SHEET;
	csch_project_t *prj = (csch_project_t *)sheet->hidlib.project;
	const char *comp_name, *sort_by = NULL;

	RND_ACT_CONVARG(1, FGW_STR, FuncmapPrintTable, comp_name = argv[1].val.str);
	RND_ACT_MAY_CONVARG(2, FGW_STR, FuncmapPrintTable, sort_by = argv[2].val.str);

	RND_ACT_IRES(funcmap_print_table(stdout, prj, comp_name, sort_by));
	return 0;
}

/*** Funcmap for a port ***/
typedef struct funcmap_port_ctx_s funcmap_port_ctx_t;

struct funcmap_port_ctx_s {
	RND_DAD_DECL_NOINIT(dlg)
	csch_sheet_t *sheet;
	const char *comp_name, *port_name;
	int wenum;
	vts0_t funcs;
};


static void pcb_dlg_funcmap_port(csch_sheet_t *sheet, const char *comp_name, const char *port_name)
{
	funcmap_port_ctx_t *ctx, ctx_ = {0};
	rnd_hid_dad_buttons_t clbtn[] = {{"Cancel", 0}, {"Set and Close", 1}, {NULL, 0}};
	const char *curr;
	char *title;
	int okay;
	long curridx = -1, n;
	csch_acomp_t *comp;
	csch_attribs_t *fmattrs;
	csch_project_t *prj = (csch_project_t *)sheet->hidlib.project;
	rnd_hid_attr_val_t hv;

	/* set up a full feature context, just in case we need to go non-modal later */
	ctx = &ctx_;
	ctx->sheet = sheet;
	ctx->comp_name = comp_name;
	ctx->port_name = port_name;

	/* fetch component and funcmap */
	if (funcmap_fetch(prj, comp_name, &comp, &fmattrs) != 0) {
		rnd_message(RND_MSG_ERROR, "FuncmapPortDialog: failed to fetch component %s funcmap\n", comp_name);
		return;
	}

	curr = funcmap_get_selected(comp, port_name);
	if (funcmap_get_port_funcs(&ctx->funcs, fmattrs, port_name, curr, &curridx) != 0) {
		rnd_message(RND_MSG_ERROR, "FuncmapPortDialog: failed to list port functions\n");
		return;
	}


	RND_DAD_BEGIN_VBOX(ctx->dlg);
		RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL);
		RND_DAD_ENUM(ctx->dlg, ctx->funcs.array);
			ctx->wenum = RND_DAD_CURRENT(ctx->dlg);

		RND_DAD_BEGIN_VBOX(ctx->dlg); /* spring */
			RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL);
		RND_DAD_END(ctx->dlg);

		RND_DAD_BUTTON_CLOSES(ctx->dlg, clbtn);
	RND_DAD_END(ctx->dlg);

	title = rnd_concat("Alternate function mapping of port ", ctx->comp_name, "-", ctx->port_name, NULL);
	RND_DAD_DEFSIZE(ctx->dlg, 200, 100);
	RND_DAD_NEW("funcmap_port_dlg", ctx->dlg, title, ctx, rnd_true, NULL); /* type=local/modal */
	free(title);

	hv.lng = curridx;
	rnd_gui->attr_dlg_set_value(ctx->dlg_hid_ctx, ctx->wenum, &hv);

	okay = RND_DAD_RUN(ctx->dlg);
	if (okay) {
		int idx = ctx->dlg[ctx->wenum].val.lng;
		if ((idx >= 0) && (idx < ctx->funcs.used)) {
			const char *new_val = ctx->funcs.array[idx];
			rnd_actionva(&ctx->sheet->hidlib, "FuncmapChange", "set", ctx->comp_name, ctx->port_name, new_val, NULL);
		}
	}
	RND_DAD_FREE(ctx->dlg);

	for(n = 0; n < ctx->funcs.used; n++)
		free(ctx->funcs.array[n]);
	vts0_uninit(&ctx->funcs);
}

static csch_cgrp_t *get_term_from_obj(csch_chdr_t *obj)
{
	for(;obj != NULL;obj = &obj->parent->hdr) {
		if (csch_obj_is_grp(obj)) {
			csch_cgrp_t *grp = (csch_cgrp_t *)obj;
			if (grp->role == CSCH_ROLE_TERMINAL)
				return grp;
		}
	}

	return NULL;
}


static const char csch_acts_FuncmapPortDialog[] = "FuncmapPortDialog(object, component, port)";
static const char csch_acth_FuncmapPortDialog[] = "Open the modal dialog for changing the alternate function of a port. ";
static fgw_error_t csch_act_FuncmapPortDialog(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = CSCH_ACT_SHEET;
	csch_project_t *prj = (csch_project_t *)sheet->hidlib.project;
	const char *comp_name = NULL, *port_name = NULL;
	int scope = F_Object;

	RND_ACT_CONVARG(1, FGW_KEYWORD, FuncmapPortDialog, scope = fgw_keyword(&argv[1]));
	RND_ACT_MAY_CONVARG(2, FGW_STR, FuncmapPortDialog, comp_name = argv[2].val.str);
	RND_ACT_MAY_CONVARG(3, FGW_STR, FuncmapPortDialog, port_name = argv[3].val.str);

	RND_ACT_IRES(-1);

	if (scope != F_Object) {
		rnd_message(RND_MSG_ERROR, "FuncmapPortDialog: invalid first argument\n");
		return 0;
	}

	if ((comp_name != NULL) || (port_name != NULL)) {
		if ((comp_name == NULL) || (port_name == NULL)) {
			rnd_message(RND_MSG_ERROR, "FuncmapPortDialog: if either comp_name or port_name is specified, both need to be specified\n");
			return 0;
		}
	}

	if (comp_name == NULL) {
		/* need to pick one on the GUI */
		csch_coord_t x, y;
		csch_chdr_t *obj;
		csch_cgrp_t *term;

		if (sch_rnd_get_coords("Click on a terminal for funcmaps", &x, &y, 0) != 0)
			return 0;

		obj = sch_rnd_search_first_gui_inspect(sheet, C2P(x), C2P(y));
		if (obj == NULL) {
			rnd_message(RND_MSG_ERROR, "FuncmapPortDialog(): no object under cursor\n");
			return 0;
		}

		term = get_term_from_obj(obj);
		if (term == NULL) {
			rnd_message(RND_MSG_ERROR, "FuncmapPortDialog(): not a terminal\n");
			return 0;
		}

		if (term->aid.used == 1) {
			csch_aport_t *port = htip_get(&prj->abst->aid2obj, term->aid.array[0]);
			if (port == NULL) {
				rnd_message(RND_MSG_ERROR, "FuncmapPortDialog(): no port found for this terminal; is the project compiled?\n");
				return 0;
			}
			port_name = port->name;
			comp_name = port->parent->name;
		}
		else {
			rnd_message(RND_MSG_ERROR, "FuncmapPortDialog(): there is no direct mapping from this terminal to a single abstract port\n");
			return 0;
		}
	}


	pcb_dlg_funcmap_port(sheet, comp_name, port_name);

	RND_ACT_IRES(0);
	return 0;
}


/*** Funcmap for components ***/
typedef struct funcmap_comp_ctx_s funcmap_comp_ctx_t;

#define MAXGRP 128

struct funcmap_comp_ctx_s {
	RND_DAD_DECL_NOINIT(dlg)
	csch_sheet_t *sheet;
	char *comp_name;
	vts0_t hdr;
	int sort_by; /* column ID */
	int wtable, wsort, wact_grp;
	gdl_elem_t link;
};

static gdl_list_t funcmap_comps;

/* Modal dialog to choose a column */
static int funcmap_comp_choose_column(funcmap_comp_ctx_t *ctx)
{
	rnd_hid_dad_buttons_t clbtn[] = {{"Cancel", 0}, {"Select and Close", 1}, {NULL, 0}};
	int okay, wenum, res = -1;
	rnd_hid_attr_val_t hv;
	RND_DAD_DECL(dlg);

	RND_DAD_BEGIN_VBOX(dlg);
		RND_DAD_COMPFLAG(dlg, RND_HATF_EXPFILL);
		RND_DAD_ENUM(dlg, ctx->hdr.array);
			wenum = RND_DAD_CURRENT(dlg);

		RND_DAD_BEGIN_VBOX(dlg); /* spring */
			RND_DAD_COMPFLAG(dlg, RND_HATF_EXPFILL);
		RND_DAD_END(dlg);

		RND_DAD_BUTTON_CLOSES(dlg, clbtn);
	RND_DAD_END(dlg);

	RND_DAD_DEFSIZE(dlg, 200, 100);
	RND_DAD_NEW("funcmap_column_dlg", dlg, "Alternate function mapping: choose column", ctx, rnd_true, NULL); /* type=local/modal */

	hv.lng = ctx->sort_by;
	rnd_gui->attr_dlg_set_value(dlg_hid_ctx, wenum, &hv);

	okay = RND_DAD_RUN(dlg);
	if (okay)
		res = dlg[wenum].val.lng;
	RND_DAD_FREE(dlg);
	return res;
}


static void funcmap_comp_clear_hdr(funcmap_comp_ctx_t *ctx)
{
	int n;
	for(n = 0; n < ctx->hdr.used; n++)
		free(ctx->hdr.array[n]);
	ctx->hdr.used = 0;
}

static void funcmap_comp_close_cb(void *caller_data, rnd_hid_attr_ev_t ev)
{
	funcmap_comp_ctx_t *ctx = caller_data;

	RND_DAD_FREE(ctx->dlg);
	free(ctx->comp_name);
	gdl_remove(&funcmap_comps, ctx, link);
	funcmap_comp_clear_hdr(ctx);
	vts0_uninit(&ctx->hdr);
	free(ctx);
}

static int get_selected_col(rnd_hid_row_t *r)
{
	int n;

	for(n = 1; n < r->cols; n++) {
		const char *c = r->cell[n];
		if ((c != NULL) && (c[0] == '[') && (c[1] == '['))
			return n;
	}

	return -1;
}

static void funcmap_comp2dlg_act_grp(funcmap_comp_ctx_t *ctx)
{
	rnd_hid_attr_val_t hv;
	rnd_hid_attribute_t *attr = &ctx->dlg[ctx->wtable];
	int btn_set = 0;
	rnd_hid_row_t *r;

	r = rnd_dad_tree_get_selected(attr);
	if (r != NULL) {
		int col = get_selected_col(r);
		if (col >= 0) {
			char *lab = rnd_concat("act. ", ctx->hdr.array[col], NULL);
			hv.str = lab;
			rnd_gui->attr_dlg_set_value(ctx->dlg_hid_ctx, ctx->wact_grp, &hv);
			free(lab);
			btn_set = 1;
		}
	}

	if (!btn_set) {
		hv.str = "set. grp.";
		rnd_gui->attr_dlg_set_value(ctx->dlg_hid_ctx, ctx->wact_grp, &hv);
	}
}

static void funcmap_comp2dlg(funcmap_comp_ctx_t *ctx)
{
	rnd_hid_attr_val_t hv;
	rnd_hid_attribute_t *attr;
	rnd_hid_tree_t *tree;
	rnd_hid_row_t *r;
	char *cursor_path = NULL;
	csch_acomp_t *comp;
	csch_attribs_t *fmattrs;
	htsp_t func2col, port2cell;
	int grpcol;

	attr = &ctx->dlg[ctx->wtable];
	tree = attr->wdata;

	/* remember cursor */
	r = rnd_dad_tree_get_selected(attr);
	if (r != NULL)
		cursor_path = rnd_strdup(r->cell[0]);

	/* remove existing items */
	rnd_dad_tree_clear(tree);

	/* build the table */
	if (funcmap_fetch((csch_project_t *)ctx->sheet->hidlib.project, ctx->comp_name, &comp, &fmattrs) != 0)
		return;
	funcmap_comp2dlg_hashgrps(&func2col, fmattrs, &grpcol);
	funcmap_comp2dlg_hashcells(&func2col, &port2cell, htsp_get(fmattrs, "funcmap/ports"), grpcol);

	/*** fill in the GUI ***/
	{
		long r;
		vtp0_t rows = {0}; /* contains: vts0_t *in_cells */

		funcmap_sort_rows(&rows, &port2cell, ctx->sort_by);

		for(r = 0; r < rows.used; r++) {
			long n;
			vts0_t *in_cells = rows.array[r];
			char *cells[MAXGRP+3];
			const char *selected, *key;

			key = in_cells->array[0];
			selected = funcmap_get_selected(comp, key);
			for(n = 0; n <= grpcol; n++) {
				if ((in_cells->used > n) & (in_cells->array[n] != NULL)) {
					if ((n > 0) && (selected != NULL) && (strcmp(in_cells->array[n], selected) == 0)) {
						char *tmp = rnd_concat("[[", in_cells->array[n], "]]", NULL);
						free(in_cells->array[n]);
						in_cells->array[n] = tmp;
					}
					cells[n] = in_cells->array[n];
					in_cells->array[n] = NULL;
				}
				else
					cells[n] = rnd_strdup("-");
			}
			cells[n] = NULL;
			rnd_dad_tree_append(attr, NULL, cells);
		}
	
		vtp0_uninit(&rows);
	}

	/* restore cursor */
	if (cursor_path != NULL) {
		hv.str = cursor_path;
		rnd_gui->attr_dlg_set_value(ctx->dlg_hid_ctx, ctx->wtable, &hv);
		free(cursor_path);
	}

	/*** set buttons ***/
	if (ctx->sort_by >= 0)
		hv.str = ctx->hdr.array[ctx->sort_by];
	else
		hv.str = "<unsorted>";
	rnd_gui->attr_dlg_set_value(ctx->dlg_hid_ctx, ctx->wsort, &hv);

	funcmap_comp2dlg_act_grp(ctx);

	/*** clean up temp ***/
	funcmap_hashgrps_free(&func2col);
	funcmap_hashcells_free(&port2cell);
}

static void funcmap_comp_gen_hdr(funcmap_comp_ctx_t *ctx)
{
	csch_acomp_t *comp;
	csch_attribs_t *fmattrs;

	funcmap_comp_clear_hdr(ctx);

	if (funcmap_fetch((csch_project_t *)ctx->sheet->hidlib.project, ctx->comp_name, &comp, &fmattrs) != 0)
		return;

	vts0_append(&ctx->hdr, rnd_strdup("<port>"));

	funcmap_comp_gen_hdr_(&ctx->hdr, htsp_get(fmattrs, "funcmap/weak_groups"));
	funcmap_comp_gen_hdr_(&ctx->hdr, htsp_get(fmattrs, "funcmap/strong_groups"));

	vts0_append(&ctx->hdr, rnd_strdup("<no-group>"));
}

static void funcmap_comp_any_change(funcmap_comp_ctx_t *ctx, const char *cmd)
{
	rnd_hid_attribute_t *attr = &ctx->dlg[ctx->wtable];
	rnd_hid_row_t *r = rnd_dad_tree_get_selected(attr);

	if (r == NULL) {
		rnd_message(RND_MSG_ERROR, "Select a port from the above list first\n");
		return;
	}

	rnd_actionva(&ctx->sheet->hidlib, "FuncmapChange", cmd, ctx->comp_name, r->cell[0], NULL);
	rnd_actionva(&ctx->sheet->hidlib, "CompileProject", NULL);
	funcmap_comp2dlg(ctx);
}

/* Step function left or right, by the ordering of the GUI columns (which
   typically differs from the ordering of functionality for a specific
   port in the file, so FuncmapChange() would step differently) */
static void funcmap_comp_gui_step(funcmap_comp_ctx_t *ctx, int dir)
{
	rnd_hid_attribute_t *attr = &ctx->dlg[ctx->wtable];
	rnd_hid_row_t *r = rnd_dad_tree_get_selected(attr);
	const char *new_val = NULL;
	int curridx, newidx, collen = ctx->hdr.used;

	if (r == NULL) {
		rnd_message(RND_MSG_ERROR, "Select a port from the above list first\n");
		return;
	}

	curridx = get_selected_col(r);

	if (curridx < 0) {
		if (dir > 0)
			newidx = collen-1;
		else
			newidx = 1;
	}
	else
		newidx = curridx + dir;

	for(;;) {
		if (newidx >= collen)
			newidx = 1;
		else if (newidx < 1)
			newidx = collen-1;

		if (newidx == curridx)
			break;

		new_val = r->cell[newidx];
		if ((new_val[0] != '-') || (new_val[1] != '\0'))
			break;

		newidx += dir;
	}

	if (newidx != curridx) {
		rnd_actionva(&ctx->sheet->hidlib, "FuncmapChange", "set", ctx->comp_name, r->cell[0], new_val, NULL);
		rnd_actionva(&ctx->sheet->hidlib, "CompileProject", NULL);
		funcmap_comp2dlg(ctx);
	}
}


static void funcmap_comp_left_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr)
{
	funcmap_comp_ctx_t *ctx = caller_data;
	funcmap_comp_gui_step(ctx, -1);
}

static void funcmap_comp_right_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr)
{
	funcmap_comp_ctx_t *ctx = caller_data;
	funcmap_comp_gui_step(ctx, +1);
}

static void funcmap_comp_del_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr)
{
	funcmap_comp_ctx_t *ctx = caller_data;
	funcmap_comp_any_change(ctx, "remove");
}

static void funcmap_comp_refresh_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr)
{
	funcmap_comp_ctx_t *ctx = caller_data;
	funcmap_comp2dlg(ctx);
}

static void funcmap_comp_change_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr)
{
	funcmap_comp_ctx_t *ctx = caller_data;
	funcmap_comp_any_change(ctx, "set");
}

static void funcmap_comp_act_grp_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr_btn)
{
	funcmap_comp_ctx_t *ctx = caller_data;
	rnd_hid_attribute_t *attr = &ctx->dlg[ctx->wtable];
	rnd_hid_row_t *r = rnd_dad_tree_get_selected(attr);
	int curridx;

	if (r == NULL) {
		rnd_message(RND_MSG_ERROR, "Select a port from the above list first\n");
		return;
	}

	curridx = get_selected_col(r);
	if (curridx < 0) {
		rnd_message(RND_MSG_ERROR, "Internal error: can't find selected column\n");
		return;
	}

	rnd_actionva(&ctx->sheet->hidlib, "FuncmapChange", "setgrp", ctx->comp_name, ctx->hdr.array[curridx], NULL);
	rnd_actionva(&ctx->sheet->hidlib, "CompileProject", NULL);
	funcmap_comp2dlg(ctx);
}

static void funcmap_comp_sort_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr)
{
	funcmap_comp_ctx_t *ctx = caller_data;
	int col = funcmap_comp_choose_column(ctx);

	if (col >= 0) {
		ctx->sort_by = col;
		funcmap_comp2dlg(ctx);
	}
}

static void funcmap_comp_select_cb(rnd_hid_attribute_t *attrib, void *hid_ctx, rnd_hid_row_t *row)
{
	rnd_hid_tree_t *tree = attrib->wdata;
	funcmap_comp_ctx_t *ctx = tree->user_ctx;
	funcmap_comp2dlg_act_grp(ctx);
}


static void pcb_dlg_funcmap_comp(csch_sheet_t *sheet, csch_acomp_t *comp)
{
	funcmap_comp_ctx_t *ctx;
	rnd_hid_dad_buttons_t clbtn[] = {{"Close", 0}, {NULL, 0}};
	char *title;

	ctx = calloc(sizeof(funcmap_comp_ctx_t), 1);
	ctx->sheet = sheet;
	ctx->comp_name = rnd_strdup(comp->name);
	ctx->sort_by = -1;

	funcmap_comp_gen_hdr(ctx);

	gdl_append(&funcmap_comps, ctx, link);

	RND_DAD_BEGIN_VBOX(ctx->dlg);
		RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL);
		RND_DAD_TREE(ctx->dlg, MAXGRP, 0, (const char **)ctx->hdr.array);
			RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL | RND_HATF_SCROLL);
			RND_DAD_TREE_SET_CB(ctx->dlg, selected_cb, funcmap_comp_select_cb);
			RND_DAD_TREE_SET_CB(ctx->dlg, ctx, ctx);
			ctx->wtable = RND_DAD_CURRENT(ctx->dlg);

		/* bottom row of buttons */
		RND_DAD_BEGIN_HBOX(ctx->dlg);
			RND_DAD_LABEL(ctx->dlg, "sort:");
			RND_DAD_BUTTON(ctx->dlg, "<unsorted>");
				ctx->wsort = RND_DAD_CURRENT(ctx->dlg);
				RND_DAD_CHANGE_CB(ctx->dlg, funcmap_comp_sort_cb);
				RND_DAD_HELP(ctx->dlg, "Choose a column to sort entries by.");

			RND_DAD_BEGIN_VBOX(ctx->dlg); /* sep */
				RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_FRAME);
			RND_DAD_END(ctx->dlg);

			RND_DAD_BUTTON(ctx->dlg, "<--");
				RND_DAD_CHANGE_CB(ctx->dlg, funcmap_comp_left_cb);
				RND_DAD_HELP(ctx->dlg, "Choose the previous function of the port selected.");
			RND_DAD_BUTTON(ctx->dlg, "-->");
				RND_DAD_CHANGE_CB(ctx->dlg, funcmap_comp_right_cb);
				RND_DAD_HELP(ctx->dlg, "Choose the next function of the port selected.");
			RND_DAD_BUTTON(ctx->dlg, "del");
				RND_DAD_CHANGE_CB(ctx->dlg, funcmap_comp_del_cb);
				RND_DAD_HELP(ctx->dlg, "Remove the function chocice of the port selected.\nThe funcmap/name attribute is removed from all terminals contributing to this port.\nIn turn the port functionality falls back to its default (primary function).");
			RND_DAD_BUTTON(ctx->dlg, "change");
				RND_DAD_CHANGE_CB(ctx->dlg, funcmap_comp_change_cb);
				RND_DAD_HELP(ctx->dlg, "Select port functionality from a list of available functions.");
			RND_DAD_BUTTON(ctx->dlg, "act. grp.");
				RND_DAD_CHANGE_CB(ctx->dlg, funcmap_comp_act_grp_cb);
				ctx->wact_grp = RND_DAD_CURRENT(ctx->dlg);
				RND_DAD_HELP(ctx->dlg, "Activate group.\nTake the current row's selected function and select the same function for all other ports in the same functionality group.\nFor example select MISO from the SPI group for a port then click this button to get all other SPI ports of the same SPI group also activated.");
			RND_DAD_BUTTON(ctx->dlg, "refresh");
				RND_DAD_CHANGE_CB(ctx->dlg, funcmap_comp_refresh_cb);
				RND_DAD_HELP(ctx->dlg, "The dialog is not refreshed automatically. Click this button to refresh the dialog after a compilation.");


			RND_DAD_BEGIN_VBOX(ctx->dlg); /* spring */
				RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL);
			RND_DAD_END(ctx->dlg);

			RND_DAD_BUTTON_CLOSES(ctx->dlg, clbtn);
		RND_DAD_END(ctx->dlg);
	RND_DAD_END(ctx->dlg);

	title = rnd_concat("Alternate function mapping of component ", ctx->comp_name, NULL);
	RND_DAD_DEFSIZE(ctx->dlg, 500, 400);
	RND_DAD_NEW("funcmap_comp_dlg", ctx->dlg, title, ctx, rnd_false, funcmap_comp_close_cb); /* type=local */
	free(title);

	funcmap_comp2dlg(ctx);
}

static csch_cgrp_t *get_sym_from_obj(csch_chdr_t *obj)
{

	for(;obj != NULL;obj = &obj->parent->hdr) {
		if (csch_obj_is_grp(obj)) {
			csch_cgrp_t *grp = (csch_cgrp_t *)obj;
			if (grp->role == CSCH_ROLE_SYMBOL)
				return grp;
		}
	}

	return NULL;
}

static const char csch_acts_FuncmapComponentDialog[] = "FuncmapComponentDialog(object)\n";
static const char csch_acth_FuncmapComponentDialog[] = "Open the alternate function mapping dialog for a component";
static fgw_error_t csch_act_FuncmapComponentDialog(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = CSCH_ACT_SHEET;
	csch_project_t *prj = (csch_project_t *)sheet->hidlib.project;
	csch_acomp_t *comp = NULL;
	int scope = F_Object;

	RND_ACT_MAY_CONVARG(1, FGW_KEYWORD, FuncmapComponentDialog, scope = fgw_keyword(&argv[1]));

	RND_ACT_IRES(-1);
	if (prj->abst == NULL) {
		rnd_message(RND_MSG_ERROR, "FuncmapComponentDialog(): abstract model is not available - compile first\n");
		return 0;
	}

	switch(scope) {
		case F_Object:
			{
				csch_coord_t x, y;
				csch_chdr_t *obj;
				csch_cgrp_t *sym;

				if (sch_rnd_get_coords("Click on a symbol for funcmaps", &x, &y, 0) != 0)
					break;

				obj = sch_rnd_search_obj_at(sheet, x, y, sch_rnd_slop);
				if (obj == NULL) {
					rnd_message(RND_MSG_ERROR, "FuncmapComponentDialog(): no object under cursor\n");
					break;
				}

				sym = get_sym_from_obj(obj);
				if (sym == NULL) {
					rnd_message(RND_MSG_ERROR, "FuncmapComponentDialog(): not a symbol\n");
					return 0;
				}

				if (sym->aid.used == 1)
					comp = htip_get(&prj->abst->aid2obj, sym->aid.array[0]);
				else
					rnd_message(RND_MSG_ERROR, "FuncmapComponentDialog(): there is no direct mapping from this symbol to a single abstract component\n");
			}
			break;
	}

	if (comp == NULL) {
		rnd_message(RND_MSG_ERROR, "FuncmapComponentDialog(): can not find the corresponding component\n");
		return 0;
	}

	pcb_dlg_funcmap_comp(sheet, comp);
	RND_ACT_IRES(0);
	return 0;
}
