/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - high level sim (non-GUI)
 *  Copyright (C) 2023 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 Entrust 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., 31 Milk Street, # 960789 Boston, MA 02196 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
 */

#include <libcschem/config.h>
#include <assert.h>
#include <ctype.h>
#include <librnd/core/error.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/misc_util.h>

#include <libcschem/libcschem.h>
#include <sch-rnd/conf_core.h>

#include "sim_conf.h"

#include "util.h"

static int skip_composite(lht_node_t *nmod, lht_node_t *n)
{
	if (strcmp(n->name, "tdf_params") == 0) {
		n = lht_dom_hash_get(nmod, "tdf");
		if ((n == NULL) || (n->type != LHT_TEXT))
			return 0;
		return (strcmp(n->data.text.value, "none") == 0);
	}
	return 0;
}

static void render_text(gds_t *tmp, lht_node_t *n)
{
	gds_append_str(tmp, n->name);
	gds_append(tmp, '=');
	gds_append_str(tmp, n->data.text.value);
}

static void render_composite(gds_t *tmp, lht_node_t *nd, const char *fld_sep)
{
	gds_append_str(tmp, nd->name);
	gds_append(tmp, '=');

	if ((nd->type == LHT_HASH) || (nd->type == LHT_LIST)) {
		lht_dom_iterator_t it;
		lht_node_t *n;
		int first = 1;

		gds_append(tmp, '{');
		for(n = lht_dom_first(&it, nd); n != NULL; n = lht_dom_next(&it)) {
			if (!first)
				gds_append_str(tmp, fld_sep);
			if (n->type == LHT_TEXT)
				render_text(tmp, n);
			else
				render_composite(tmp, n, fld_sep);
			first = 0;
		}
		gds_append(tmp, '}');
	}
	else
		gds_append_str(tmp, "<composite>");
}

void sch_sim_append_print_mod(gds_t *tmp, lht_node_t *nmod, const char *fld_sep)
{
	int first = 1;
	lht_node_t *n, *skip = NULL, *lead = NULL;
	lht_dom_iterator_t it;

	assert(nmod->type == LHT_HASH);

	if (strcmp(nmod->name, "add") == 0) {
		lht_node_t *ntype = lht_dom_hash_get(nmod, "type"), *ndev = lht_dom_hash_get(nmod, "device");

		if (ntype != NULL) {
			skip = ntype;
			gds_append_str(tmp, ntype->data.text.value);
			first = 0;
		}

		if (ndev != NULL) {
			lead = ndev;
			gds_append_str(tmp, ndev->data.text.value);
			first = 0;
		}
	}

	for(n = lht_dom_first(&it, nmod); n != NULL; n = lht_dom_next(&it)) {
		if ((n == skip) || (n == lead))
			continue;

		if (!first)
			gds_append_str(tmp, fld_sep);
		if (n->type == LHT_TEXT)
			render_text(tmp, n);
		else if (!skip_composite(nmod, n))
			render_composite(tmp, n, fld_sep);

		first = 0;
	}
}

#define MAN SCH_SIMREQ_MANDATORY
#define OPT SCH_SIMREQ_OPTIONAL
#define NO  SCH_SIMREQ_NO

/* indexed by sch_sim_analysis_type_t */
static const sch_sim_analysis_field_tab_t afld[] = {
	/*                             start stop incr incr_max numpt port1 port2 src */
	/* SCH_SIMAN_OP */            {NO,   NO,  NO,  NO,      NO,   NO,   NO,   NO},
	/* SCH_SIMAN_TRAN_LIN */      {NO,   MAN, MAN, OPT,     NO,   NO,   NO,   NO},
	/* SCH_SIMAN_AC_DEC */        {MAN,  MAN, NO,  NO,      OPT,  NO,   NO,   NO},
	/* SCH_SIMAN_AC_OCT */        {MAN,  MAN, NO,  NO,      OPT,  NO,   NO,   NO},
	/* SCH_SIMAN_AC_LIN */        {MAN,  MAN, NO,  NO,      NO,   NO,   NO,   NO},
	/* SCH_SIMAN_DC_LIN */        {MAN,  MAN, MAN, NO,      NO,   NO,   NO,   MAN},
	/* SCH_SIMAN_DC_DISTO_DEC */  {MAN,  MAN, NO,  NO,      OPT,  NO,   NO,   NO},
	/* SCH_SIMAN_DC_DISTO_OCT */  {MAN,  MAN, NO,  NO,      OPT,  NO,   NO,   NO},
	/* SCH_SIMAN_DC_DISTO_LIN */  {MAN,  MAN, NO,  NO,      NO,   NO,   NO,   NO},
	/* SCH_SIMAN_DC_NOISE_DEC */  {MAN,  MAN, NO,  NO,      MAN,  MAN,  MAN,  NO},
	/* SCH_SIMAN_PREVIOUS     */  {NO,   NO,  NO,  NO,      NO,   NO,   NO,   NO}
};

#undef MAN
#undef OPT
#undef NO

const sch_sim_analysis_field_tab_t *sch_sim_get_analysis_fieldreq(sch_sim_analysis_type_t type)
{
	if ((type < 0) || (type >= sizeof(afld)/sizeof(afld[0])))
		return NULL;
	return &afld[type];
}

void sch_sim_analysis_free(sch_sim_analysis_t *sa)
{
	free(sa->start); sa->start = NULL;
	free(sa->stop); sa->stop = NULL;
	free(sa->incr); sa->incr = NULL;
	free(sa->incr_max); sa->incr_max = NULL;
	free(sa->src);
	free(sa->port1[0]); sa->port1[0] = NULL;
	free(sa->port1[1]); sa->port1[1] = NULL;
	free(sa->port2[0]); sa->port2[0] = NULL;
	free(sa->port2[1]); sa->port2[1] = NULL;
	sa->type = 0;
	sa->numpt = 0;
}


RND_INLINE int load_cstr(const char **dst, lht_node_t *nanalysis, const char *path, sch_sim_field_req_t req, int quiet)
{
	lht_node_t *nd = lht_dom_hash_get(nanalysis, path);
	const char *tval = NULL;

	*dst = NULL;

	/* determine text value (tval) */
	if (nd != NULL) {
		if (nd->type == LHT_TEXT) {
			char *s = nd->data.text.value;
			if (s == NULL) s = "";
			else while(isspace(*s)) s++;
			if (*s == '\0')
				tval = NULL;
			else
				tval = s;
		}
	}

	/* validate existence of the field */
	switch(req) {
		case SCH_SIMREQ_NO:
			if (tval != NULL) {
				if (!quiet) rnd_message(RND_MSG_WARNING, "Ignoring configured %s for sim analysis %s\n(the specific analysis doesn't have such parameter)\n", path, nanalysis->name);
			}
			return 0;

		case SCH_SIMREQ_MANDATORY:
			if (tval == NULL) {
				if (!quiet) rnd_message(RND_MSG_ERROR, "analysis %s requires a %s field\n", nanalysis->name, path);
				return -1;
			}
			break;
		case SCH_SIMREQ_OPTIONAL:
			/* okay either-way */
			break;
	}

	*dst = tval;
	return 0;
}

static int load_str(char **dst, lht_node_t *nanalysis, const char *path, sch_sim_field_req_t req, int quiet)
{
	const char *tval;

	if (load_cstr(&tval, nanalysis, path, req, quiet) != 0)
		return -1;

	*dst = (tval != NULL) ? rnd_strdup(tval) : NULL;
	return 0;
}


static int load_int(int *dst, lht_node_t *nanalysis, const char *path, sch_sim_field_req_t req, int quiet)
{
	const char *tval;
	char *end;
	long l;

	*dst = 0;

	if (load_cstr(&tval, nanalysis, path, req, quiet) != 0)
		return -1;

	if (tval == NULL) {
		TODO("default numpts?");
		*dst = 4;
		return 0;
	}

	l = strtol(tval, &end, 10);
	if (*end != '\0') {
		if (!quiet) rnd_message(RND_MSG_ERROR, "analysis %s requires %s field to be an integer, but it is '%s' instead\n", nanalysis->name, path, tval);
		return -1;
	}

	*dst = l;
	return 0;
}

static int load_port(char *dst[2], lht_node_t *nanalysis, const char *path, const char *path_neg, sch_sim_field_req_t req, int quiet)
{
	int res1, res2;

	res1 = load_str(dst+0, nanalysis, path, req, quiet);

	/* the negative node is either not accepted or optional */
	if (req == SCH_SIMREQ_MANDATORY)
		req = SCH_SIMREQ_OPTIONAL;

	res2 = load_str(dst+1, nanalysis, path_neg, req, quiet);

	if ((res1 != 0) || (res2 != 0))
		return -1;

	return 0;
}

int sch_sim_analysis_build(sch_sim_analysis_t *dst, csch_abstract_t *abst, lht_node_t *noutput, int quiet)
{
	lht_node_t *nanalysis, *natype;
	sch_sim_analysis_type_t atype;
	const sch_sim_analysis_field_tab_t *reqs;

	memset(dst, 0, (char *)&dst->user_data - (char *)dst);

	if (noutput->type != LHT_HASH)
		return -1;

	nanalysis = lht_dom_hash_get(noutput, "analysis");
	if ((nanalysis == NULL) || (nanalysis->type != LHT_HASH)) {
		if (!quiet) rnd_message(RND_MSG_ERROR, "Invalid node analysis: must be exist and must be a hash\n");
		return -1;
	}

	/* convert analysis type */
	natype = lht_dom_hash_get(nanalysis, "type");
	if ((natype == NULL) || (natype->type != LHT_TEXT)) {
		if (!quiet) rnd_message(RND_MSG_ERROR, "Invalid node analysis/type: must exist and must be a text\n");
		return -1;
	}
	atype = sch_sim_str2analysis_type(natype->data.text.value);
	if (atype == SCH_SIMAN_invalid) {
		if (!quiet) rnd_message(RND_MSG_ERROR, "Invalid value of analysis/type (#1)\n");
		return -1;
	}

	reqs = sch_sim_get_analysis_fieldreq(atype);
	if (reqs == NULL) {
		if (!quiet) rnd_message(RND_MSG_ERROR, "Invalid value of analysis/type (#2)\n");
		return -1;
	}

	if (load_str(&dst->start, nanalysis, "start", reqs->start, quiet) != 0) goto error;
	if (load_str(&dst->stop, nanalysis, "stop", reqs->stop, quiet) != 0) goto error;
	if (load_str(&dst->incr, nanalysis, "incr", reqs->incr, quiet) != 0) goto error;
	if (load_str(&dst->incr_max, nanalysis, "incr_max", reqs->incr_max, quiet) != 0) goto error;
	if (load_int(&dst->numpt, nanalysis, "numpt", reqs->numpt, quiet) != 0) goto error;
	if (load_str(&dst->src, nanalysis, "src", reqs->src, quiet) != 0) goto error;
	if (load_port(dst->port1, nanalysis, "port1", "port1_neg", reqs->port1, quiet) != 0) goto error;
	if (load_port(dst->port2, nanalysis, "port2", "port2_neg", reqs->port2, quiet) != 0) goto error;

	dst->type = atype;

	return 0;
	error:;
	sch_sim_analysis_free(dst);
	return -1;
}

static void free_strlist_items(vts0_t *dst)
{
	long n;
	for(n = 0; n < dst->used; n++)
		free(dst->array[n]);
	dst->used = 0;
}

static int load_strlist(vts0_t *dst, lht_node_t *npresentation, const char *path, sch_sim_field_req_t req, int quiet)
{
	lht_node_t *n, *nd = lht_dom_hash_get(npresentation, path);

	free_strlist_items(dst);

	if ((nd != NULL) && (nd->type != LHT_LIST))
		nd = NULL;

	/* validate existence of the field */
	switch(req) {
		case SCH_SIMREQ_NO:
			if (nd != NULL) {
				if (!quiet) rnd_message(RND_MSG_WARNING, "Ignoring configured %s for sim presentation %s\n(the specific presentation doesn't have such parameter)\n", path, npresentation->name);
			}
			return 0;

		case SCH_SIMREQ_MANDATORY:
			if (nd == NULL) {
				if (!quiet) rnd_message(RND_MSG_ERROR, "presentations %s requires a %s field (list of text nodes)\n", npresentation->name, path);
				return -1;
			}
			break;
		case SCH_SIMREQ_OPTIONAL:
			/* okay either-way */
			break;
	}

	for(n = nd->data.list.first; n != NULL; n = n->next) {
		if (n->type != LHT_TEXT) {
			if (!quiet) rnd_message(RND_MSG_ERROR, "presentations %s's field %s needs to be a list containing text nodes\n", npresentation->name, path);
			continue;
		}
		vts0_append(dst, rnd_strdup(n->data.text.value));
	}

	return 0;
}

void sch_sim_presentation_free(sch_sim_presentation_t *sp)
{
	if (sp->outfn != NULL)
		free(sp->outfn);
	free_strlist_items(&sp->props);
	vts0_uninit(&sp->props);
}

int sch_sim_presentation_build(sch_sim_presentation_t *dst, csch_abstract_t *abst, lht_node_t *noutput, int quiet)
{
	lht_node_t *npresentation, *nptype;
	sch_sim_presentation_type_t ptype;

	memset(dst, 0, (char *)&dst->user_data - (char *)dst);

	if (noutput->type != LHT_HASH)
		return -1;

	npresentation = lht_dom_hash_get(noutput, "presentation");

	if ((npresentation == NULL) || (npresentation->type != LHT_HASH)) {
		if (!quiet) rnd_message(RND_MSG_ERROR, "Invalid node presentation: must exist and must be a hash\n");
		return -1;
	}

	/* convert presentation type */
	nptype = lht_dom_hash_get(npresentation, "type");
	if ((nptype == NULL) || (nptype->type != LHT_TEXT)) {
		if (!quiet) rnd_message(RND_MSG_ERROR, "Invalid node presentation/type: must exist and must be a text\n");
		return -1;
	}
	ptype = sch_sim_str2presentation_type(nptype->data.text.value);
	if (ptype == SCH_SIMPRES_invalid) {
		if (!quiet) rnd_message(RND_MSG_ERROR, "Invalid value of presentation/type\n");
		return -1;
	}

	if (load_strlist(&dst->props, npresentation, "props", SCH_SIMREQ_MANDATORY, quiet) != 0) goto error;


	switch(ptype) {
		case SCH_SIMPRES_PRINT:
		case SCH_SIMPRES_PLOT:
			/* no extra fields */
			break;
		case SCH_SIMPRES_invalid:
			goto error;
	}

	dst->type = ptype;

	return 0;
	error:;
	sch_sim_presentation_free(dst);
	return -1;
}


void sch_sim_hook_eng_call(fgw_obj_t *obj, const char *funcname, fgw_error_t (**origf)(fgw_arg_t *, int , fgw_arg_t *), fgw_error_t (*newf)(fgw_arg_t *, int , fgw_arg_t *))
{
	fgw_func_t *fun = fgw_func_lookup_in(obj, funcname);
	if (fun != NULL) {
		*origf = fun->func;
		fgw_func_unreg(obj, funcname);
		htsp_pop(&obj->func_tbl, funcname);
		free(fun->name);
		free(fun);
	}
	else
		*origf = NULL;

	fgw_func_reg(obj, funcname, newf);
}

void sch_sim_omit_no_test_bench_comp(csch_acomp_t *comp, int eng_prio)
{
	if (csch_attrib_get(&comp->hdr.attr, "forge-if/test_bench") == NULL) {
		csch_source_arg_t *src;
		src = csch_attrib_src_p("sim", "omit_no_test_bench");
		csch_attrib_set(&comp->hdr.attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, "omit", "yes", src, NULL);
/*		csch_compile_update_obj_cache(&comp->hdr); -- no need to run this here, the compiler does it automatically*/
	}
}

static void sch_sim_omit_no_test_bench_(csch_abstract_t *abst, int eng_prio)
{
	htsp_entry_t *e;

	for(e = htsp_first(&abst->comps); e != NULL; e = htsp_next(&abst->comps, e))
		sch_sim_omit_no_test_bench_comp(e->value, eng_prio);
}

int sch_sim_omit_no_test_bench_is_on(csch_project_t *prj)
{
	int omit = 0;
		lht_node_t *nsetup = sch_sim_get_setup(prj, sch_sim_conf.plugins.sim.active_setup, 0);
		if ((nsetup != NULL) && (nsetup->type == LHT_HASH)) {
			lht_node_t *nomit = lht_dom_hash_get(nsetup, "omit_no_test_bench");
			lht_node_t *ntb = lht_dom_hash_get(nsetup, "test_bench");
			int ntb_valid, nomit_valid;

			ntb_valid = ((ntb != NULL) && (ntb->type == LHT_TEXT));
			nomit_valid = ((nomit != NULL) && (nomit->type == LHT_TEXT));

			omit = nomit_valid ? rnd_istrue(nomit->data.text.value) : 0;

			/* if whole circuit is selected, ignore the value of omit and never omit anything */
			if (!ntb_valid || *ntb->data.text.value == '\0')
				omit = 0;
	}

	return omit;
}

void sch_sim_omit_no_test_bench(csch_project_t *prj, csch_abstract_t *abst, int engprio)
{
	/* apply the omit_no_test_bench directive */
	if (sch_sim_conf.plugins.sim.active_setup != NULL) {
			if (sch_sim_omit_no_test_bench_is_on(prj))
				sch_sim_omit_no_test_bench_(abst, engprio);
	}
}

void sch_sim_set_test_bench(csch_project_t *prj, csch_abstract_t *abst, const char *cookie, int engprio)
{
	const char *new_tb = NULL;
	int new_tb_valid = 0;
	rnd_conf_native_t *nat;

	if (sch_sim_conf.plugins.sim.active_setup != NULL) {
		lht_node_t *nsetup = sch_sim_get_setup(prj, sch_sim_conf.plugins.sim.active_setup, 0);
		if ((nsetup != NULL) && (nsetup->type == LHT_HASH)) {
			lht_node_t *ntb = lht_dom_hash_get(nsetup, "test_bench");
			
			if ((ntb == NULL) || (ntb->type == LHT_TEXT))
				new_tb_valid = 1;

			if ((ntb != NULL) && (ntb->type == LHT_TEXT))
				new_tb = ntb->data.text.value;
		}
	}

	if (!new_tb_valid)
		rnd_message(RND_MSG_INFO, "simulation setup has invalid test bench or simulation is not activated\n");

	if ((new_tb != NULL) || (conf_core.stance.test_bench != NULL)) {
		if (RND_NSTRCMP(conf_core.stance.test_bench, new_tb) != 0)
			rnd_message(RND_MSG_INFO, "simulation target overrides test bench from '%s' to '%s'\n", RND_EMPTY(conf_core.stance.test_bench), RND_EMPTY(new_tb));
	}

	htpp_set(&abst->eng_transient, (void *)cookie, (void *)conf_core.stance.test_bench);

	nat = rnd_conf_get_field("stance/test_bench");
	nat->val.string[0] = new_tb;
	rnd_conf_force_set_str(conf_core.stance.test_bench, new_tb);
}


void sch_sim_restore_test_bench(csch_project_t *prj, csch_abstract_t *abst, const char *cookie, int engprio)
{
	const char *old_tb;
	rnd_conf_native_t *nat;

	old_tb = htpp_pop(&abst->eng_transient, (void *)cookie);
	rnd_conf_force_set_str(conf_core.stance.test_bench, old_tb);
	nat = rnd_conf_get_field("stance/test_bench");
	nat->val.string[0] = old_tb;
}

