/*
 *                            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
 */

/* Helper functions to parse funcmap list attributes */

#include <genht/hash.h>

/* Make a list of functions for a port and/or return the index of a function
   on the list. If dst is NULL, no list is created; if find_func is NULL no
   search for the given fuinction is done. If find_func is not NULL,
   out_idx is set to the index of the function on the list or -1 if not found */
static int funcmap_get_port_funcs(vts0_t *dst, csch_attribs_t *fmattrs, const char *port_name, const char *find_func, long *out_idx)
{
	long n, pnlen, find_func_len, idx = 0;
	csch_attrib_t *lst = htsp_get(fmattrs, "funcmap/ports");

	if (lst == NULL)
		return -1;

	if (find_func != NULL) {
		find_func_len = strlen(find_func);
		*out_idx = -1;
	}

	pnlen = strlen(port_name);
	for(n = 0; n < lst->arr.used; n++) {
		const char *line = lst->arr.array[n];
		char *sep, *func;

		while(isspace(*line)) line++;

		sep = strchr(line, '/'); /* find port name (left of /) */
		if (sep != NULL) {
			int len = sep - line;

			if ((pnlen != len) || (memcmp(line, port_name, pnlen) != 0))
				continue; /* ignore lines not for this port */

			/* fetch functionality supported by this port (right side of /) */
			func = sep+1;
			sep = strpbrk(func, " \t\r\n>");
			if (sep != NULL) {
				char *func_dup;
				if ((*sep == '>') && (sep > line) && (sep[-1] == '-')) /* found -> */
					sep--;
				len = sep - func;
				if (dst != NULL) {
					func_dup = rnd_strndup(func, len);
					vts0_append(dst, func_dup);
				}
				if ((find_func != NULL) && (find_func_len == len) && (strncmp(find_func, func, len) == 0)) {
					*out_idx = idx;
					find_func = NULL;
				}
				idx++;
			}
		}
	}

	return 0;
}

static int funcmap_has_func(csch_acomp_t *comp, const char *port_name, csch_attribs_t *fmattrs, const char *func)
{
	long idx;

	funcmap_get_port_funcs(NULL, fmattrs, port_name, func, &idx);

	return (idx >= 0);
}

static void funcmap_comp2dlg_hashgrp(htsp_t *func2col, csch_attrib_t *lst, int *grpcol)
{
	long n;


	if (lst == NULL)
		return;

	for(n = 0; n < lst->arr.used; n++) {
		const char *line = lst->arr.array[n];
		char *sep, *func, *next, *fname;

		while(isspace(*line)) line++;

		sep = strchr(line, '>');
		if (sep == NULL)
			continue;
		if ((*sep != '>') || (sep <= line) || (sep[-1] != '-'))
			continue; /* not -> */

		for(func = sep+1; func != NULL; func = next) {
			vtl0_t *cols;

			while((*func == ',') || isspace(*func)) func++;
			if (*func == '\0') break;
			next = strpbrk(func, " \t,");

			if (next != NULL)
				fname = rnd_strndup(func, next-func);
			else
				fname = rnd_strdup(func);
			cols = htsp_get(func2col, fname);
			if (cols == NULL) {
				cols = calloc(sizeof(vtl0_t), 1);
				htsp_set(func2col, fname, cols);
			}
			else
				free(fname);

			vtl0_append(cols, *grpcol);
		}

		(*grpcol)++;
	}
}

static void funcmap_comp2dlg_hashgrps(htsp_t *func2col, csch_attribs_t *fmattrs, int *grpcol)
{
	htsp_init(func2col, strhash, strkeyeq);

	*grpcol = 1;
	funcmap_comp2dlg_hashgrp(func2col, htsp_get(fmattrs, "funcmap/weak_groups"), grpcol);
	funcmap_comp2dlg_hashgrp(func2col, htsp_get(fmattrs, "funcmap/strong_groups"), grpcol);
}

static void funcmap_hashgrps_free(htsp_t *func2col)
{
	htsp_entry_t *e;

	for(e = htsp_first(func2col); e != NULL; e = htsp_next(func2col, e)) {
		vtl0_t *cols = e->value;
		vtl0_uninit(cols);
		free(cols);
		free(e->key);
	}
	htsp_uninit(func2col);
}


static void funcmap_comp2dlg_hashcells(htsp_t *func2col, htsp_t *port2cell, csch_attrib_t *lst, int nogrp)
{
	long n;
	gds_t tmp = {0};

	htsp_init(port2cell, strhash, strkeyeq);

	if (lst == NULL)
		return;

	for(n = 0; n < lst->arr.used; n++) {
		const char *line = lst->arr.array[n];
		char *sep, *func;
		vts0_t *cells;

		while(isspace(*line)) line++;


		sep = strchr(line, '/'); /* find port name (left of /) */
		if (sep != NULL) {
			int len = sep - line;
			vtl0_t *cols;

			/* ensure cells for this port */
			tmp.used = 0;
			gds_append_len(&tmp, line, len);
			cells = htsp_get(port2cell, tmp.array);
			if (cells == NULL) {
				cells = calloc(sizeof(vts0_t), 1);
				htsp_set(port2cell, tmp.array, cells);
				tmp.array = NULL;
				tmp.used = tmp.alloced = 0;
			}

			/* fetch functionality supported by this port (right side of /) */
			func = sep+1;
			sep = strpbrk(func, " \t\r\n>");
			if (sep != NULL) {
				if ((*sep == '>') && (sep > line) && (sep[-1] == '-')) /* found -> */
					sep--;
				len = sep - func;
				tmp.used = 0;
				gds_append_len(&tmp, func, len);
				func = tmp.array;

				cols = htsp_get(func2col, func);

				if ((cols == NULL) || (cols->used == 0)) {
					if ((nogrp >= cells->used) || (cells->array[nogrp] == NULL)) {
						vts0_set(cells, nogrp, func);
						tmp.array = NULL;
						tmp.alloced = 0;
					}
				}
				else if (cols->used == 1) { /* the function is assigned to a single col - avoid extra strdup */
					if ((cols->array[0] >= cells->used) || (cells->array[cols->array[0]] == NULL)) {
						vts0_set(cells, cols->array[0], func);
						tmp.array = NULL;
						tmp.alloced = 0;
					}
				}
				else { /* the function is assigned to more than one cols */
					int m;
					for(m = 0; m < cols->used; m++) {
						if ((cols->array[m] >= cells->used) || (cells->array[cols->array[m]] == NULL))
							vts0_set(cells, cols->array[m], rnd_strdup(func));
					}
				}
				tmp.used = 0;
			}
		}
	}

	gds_uninit(&tmp);
}

static void funcmap_hashcells_free(htsp_t *port2cell)
{
	htsp_entry_t *e;

	for(e = htsp_first(port2cell); e != NULL; e = htsp_next(port2cell, e)) {
		long n;
		vts0_t *cells = e->value;

		free(e->key);
		for(n = 0; n < cells->used; n++)
			free(cells->array[n]);
		vts0_uninit(cells);
		free(cells);
	}
	htsp_uninit(port2cell);
}

static int funcmap_row_cmp(const void *r1, const void *r2)
{
	const vts0_t **in_cells1 = (const vts0_t **)r1, **in_cells2 = (const vts0_t **)r2;
	const char *k1 = (*in_cells1)->array[0], *k2 = (*in_cells2)->array[0];

	if ((k1 == NULL) && (k2 != NULL)) return +1;
	if ((k2 == NULL) && (k1 != NULL)) return -1;
	if ((k1 == NULL) && (k2 == NULL)) return +1;
	return strcmp(k1, k2);
}

/* Append table rows (each a vts0_t *in_cells) in dst and sort dst by the
   table column sort_by_col (if it's >= 0). The caller needs to call
   vtp0_free() on dst (vector content doesn't need to be free'd) */
static void funcmap_sort_rows(vtp0_t *dst, htsp_t *port2cell, int sort_by_col)
{
	long r;
	htsp_entry_t *e;

	for(e = htsp_first(port2cell); e != NULL; e = htsp_next(port2cell, e)) {
		vts0_t *in_cells = e->value;
		char *key = rnd_strdup(e->key);

		in_cells->array[0] = key; /* remember key as first col (room allocated already) */

		if (sort_by_col > 0) /* get sort-by column to be the first temporarily */
			rnd_swap(char *, in_cells->array[0], in_cells->array[sort_by_col]);

		vtp0_append(dst, in_cells);
	}

	if (sort_by_col >= 0)
		qsort(dst->array, dst->used, sizeof(vts0_t *), funcmap_row_cmp);

 /* undo the swap we did for the sort */
	for(r = 0; r < dst->used; r++) {
		vts0_t *in_cells = dst->array[r];

		if (sort_by_col > 0)
			rnd_swap(char *, in_cells->array[0], in_cells->array[sort_by_col]);
	}
}

static void funcmap_comp_gen_hdr_(vts0_t *dst, csch_attrib_t *lst)
{
	long n;

	if (lst == NULL)
		return;

	for(n = 0; n < lst->arr.used; n++) {
		const char *line = lst->arr.array[n];
		char *sep, *tmp;

		while(isspace(*line)) line++;
		sep = strpbrk(line, " \t\r\n>");
		if (sep != NULL) {
			int len;

			if ((*sep == '>') && (sep > line) && (sep[-1] == '-')) /* found -> */
				sep--;

			len = sep - line;
			tmp = rnd_strndup(line, len);
			vts0_append(dst, tmp);
		}
	}
}


/*** set group ***/

/* split up a list of functions and add them in the hash */
static int funcmap_set_func_grp_want_found(htsp_t *dst, const char *grp_name, int grp_name_len, const char *funcs, int for_search)
{
	const char *curr, *next;
	vts0_t *lst;

	if (!for_search) {
		char *key = rnd_strndup(grp_name, grp_name_len);
		lst = calloc(sizeof(vts0_t), 1);
		htsp_set(dst, key, lst);
	}

	/* skip whitespace and -> */
	while(isspace(*funcs)) funcs++;
	if ((funcs[0] == '-') && (funcs[1] == '>'))
		funcs+=2;


	for(curr = funcs; curr != NULL; curr = next) {
		char *fname;

		while((*curr == ',') || isspace(*curr)) curr++;
		if (*curr == '\0') break;
		next = strpbrk(curr, " \t,");

		if (next != NULL)
			fname = rnd_strndup(curr, next-curr);
		else
			fname = rnd_strdup(curr);

		if (for_search) {
			if (htsp_has(dst, fname))
				free(fname);
			else
				htsp_set(dst, fname, dst);
		}
		else
			vts0_append(lst, fname);
	}

	return 1;
}

static int funcmap_func_grp_seek_or_list_(htsp_t *dst, csch_attribs_t *fmattrs, const char *grp_name, int grp_name_len, csch_attrib_t *grp_lst)
{
	long n;

	for(n = 0; n < grp_lst->arr.used; n++) {
		const char *sep, *line = grp_lst->arr.array[n];

		while(isspace(*line)) line++;

		sep = strpbrk(line, " \t\r\n>");
		if (sep != NULL) {
			int len;

			if ((*sep == '>') && (sep > line) && (sep[-1] == '-')) /* found -> */
				sep--;

			len = sep - line;
			if (grp_name != NULL) { /* search for a specific group */
				if ((grp_name_len == len) && (strncmp(line, grp_name, len) == 0))
					return funcmap_set_func_grp_want_found(dst, NULL, 0, sep+1, 1);
			}
			else {
				/* list groups */
				funcmap_set_func_grp_want_found(dst, line, len, sep+1, 0);
			}
		}
	}

	return 0;
}

/* Returns 1 if grp_name is found and hash table is filled in */
static int funcmap_set_func_grp_want(htsp_t *dst, csch_attribs_t *fmattrs, const char *grp_name, int grp_name_len, csch_attrib_t *grp_lst)
{
	return funcmap_func_grp_seek_or_list_(dst, fmattrs, grp_name, grp_name_len, grp_lst);
}


/* Create a list of functions per group in dst. Key is the name of the group,
   value is a vts0_t with all strings allocated. Use funcmap_map_grps_uninit()
   to free all fields. */
static int funcmap_map_grps(htsp_t *dst, csch_attribs_t *fmattrs, csch_attrib_t *grp_lst)
{
	return funcmap_func_grp_seek_or_list_(dst, fmattrs, NULL, 0, grp_lst);
}

/* Free all fields of dst (but not dst itself) */
static void funcmap_map_grps_uninit(htsp_t *dst)
{
	htsp_entry_t *e;
	for(e = htsp_first(dst); e != NULL; e = htsp_next(dst, e)) {
		vts0_t *funcs = e->value;
		long n;

		free(e->key);
		for(n = 0; n < funcs->used; n++)
			free(funcs->array[n]);
		vts0_uninit(funcs);
		free(funcs);
	}
	htsp_uninit(dst);
}


static int funcmap_set_on_port(csch_acomp_t *comp, const char *port_name, const char *new_val);

/* change the functionality of all ports that belong to a group to activate
   the group */
static int funcmap_set_func_grp(csch_acomp_t *comp, csch_attribs_t *fmattrs, const char *grp_name)
{
	int res = 0;
	long n;
	gds_t func_name = {0};
	csch_attrib_t *lst = htsp_get(fmattrs, "funcmap/ports");
	htsp_t wanted;
	int grp_name_len;
	htsp_entry_t *e;

	if (lst == NULL)
		return -1;

	for(n = 0; n < comp->hdr.srcs.used; n++) {
		csch_cgrp_t *sym = comp->hdr.srcs.array[n];
		uundo_freeze_serial(&sym->hdr.sheet->undo);
	}


	/* create a hash table of functions for this group */
	grp_name_len = strlen(grp_name);
	htsp_init(&wanted, strhash, strkeyeq);
	(void)( /* get the first group with matching name */
		funcmap_set_func_grp_want(&wanted, fmattrs, grp_name, grp_name_len, htsp_get(fmattrs, "funcmap/weak_groups"))
		||
		funcmap_set_func_grp_want(&wanted, fmattrs, grp_name, grp_name_len, htsp_get(fmattrs, "funcmap/strong_groups"))
	);

	for(n = 0; n < lst->arr.used; n++) {
		const char *line = lst->arr.array[n];
		char *sep, *func;

		while(isspace(*line)) line++;

		sep = strchr(line, '/'); /* find port name (left of /) */
		if (sep != NULL) {
			int r, func_len, port_len = sep - line;

			/* fetch functionality supported by this port (right side of /) */
			func = sep+1;
			sep = strpbrk(func, " \t\r\n>");
			if (sep != NULL) {
				if ((*sep == '>') && (sep > line) && (sep[-1] == '-')) /* found -> */
					sep--;
				func_len = sep - func;
				func_name.used = 0;
				gds_append_len(&func_name, func, func_len);

				/* check if function is in the hash of functions we want to have */
				e = htsp_popentry(&wanted, func_name.array);
				if (e != NULL) {
					const char *port_name;

					/* copy port name to temp storage so it can be truncated */
					func_name.used = 0;
					gds_append_len(&func_name, line, port_len);
					port_name = func_name.array;

					r = funcmap_set_on_port(comp, port_name, e->key);
					if (r != 0) {
						rnd_message(RND_MSG_ERROR, "Failed to set port '%s' functionality to '%s' for group '%s'\n", port_name, e->key, grp_name);
						res = 1;
					}

					/* set port functionality */
					free(e->key);
				}
			}
		}
	}

	for(e = htsp_first(&wanted); e != NULL; e = htsp_next(&wanted, e)) {
		rnd_message(RND_MSG_ERROR, "Failed to find a port for functionality '%s' in group '%s'\n", e->key, grp_name);
		res = 1;
		free(e->key);
	}
	htsp_uninit(&wanted);
	gds_uninit(&func_name);


	for(n = 0; n < comp->hdr.srcs.used; n++) {
		csch_cgrp_t *sym = comp->hdr.srcs.array[n];
		uundo_unfreeze_serial(&sym->hdr.sheet->undo);
		uundo_inc_serial(&sym->hdr.sheet->undo);
	}

	return res;
}
