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

#include <libcschem/config.h>

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

#include <libcschem/libcschem.h>
#include <libcschem/compile.h>

#include "sim.h"
#include "sim_conf.h"

#include "mods.h"

static const char *get_node_str(lht_node_t *hash, const char *name)
{
	lht_node_t *nd;
	assert(hash->type == LHT_HASH);

	nd = lht_dom_hash_get(hash, name);
	if (nd == NULL)
		return NULL;

	if (nd->type != LHT_TEXT) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: expected %s/%s to be a text node\n", hash->name, name);
		return NULL;
	}

	return nd->data.text.value;
}

/* refdes-portname; sep is pointing to the '-' within addr; if sep is NULL
   it is searched for */
static csch_aport_t *lookup_port(csch_abstract_t *abst, const char *addr, const char *sep)
{
	csch_acomp_t *comp;
	const char *pname;
	char *cname;

	if (sep == NULL)
		sep = strchr(addr, '-');

	if (sep == NULL)
		return NULL;

	cname = rnd_strndup(addr, sep-addr);
	comp = csch_acomp_get(abst, cname);
	free(cname);

	if (comp == NULL)
		return NULL;

	pname = sep+1;
	return csch_aport_get(abst, comp, pname, 0);
}

csch_anet_t *sch_sim_lookup_net(csch_abstract_t *abst, const char *addr, int create)
{
	const char *sep = strchr(addr, '-');
	csch_anet_t *net;
	csch_aport_t *port = NULL;

	/* if it has a dash, try comp-port first */
	if ((sep != NULL) && (sep > addr)) {
		port = lookup_port(abst, addr, sep);
		if ((port != NULL) && (port->conn.net != NULL))
			return port->conn.net;

		/* port is not connected anywhere, create a dummy net */
		new_dummy_net:;
		{
			char tmpname[128];

			if (!create) {
				rnd_message(RND_MSG_ERROR, "sim lookup_net(): can't find net '%s'\n", addr);
				return NULL;
			}

			abst->ucnt.wirenet++;
			sprintf(tmpname, "__sim_net_%ld", abst->ucnt.wirenet);
			net = csch_anet_new(abst, NULL, CSCH_ASCOPE_GLOBAL, tmpname, tmpname, 1);
			if (net == NULL)
				rnd_message(RND_MSG_ERROR, "sim lookup_net(): internal error: can't allocate new dummy net\n");
			if (port != NULL) {
				if (csch_compile_connect_net_to(&net, &port->hdr, 0) != 0)
					rnd_message(RND_MSG_ERROR, "sim lookup_net(): internal error: failed to connect port to new dummy net\n");
			}
			return net;
		}
	}

	/* fall back to net */
	net = csch_anet_get(abst, addr);
	if (net == NULL)
		goto new_dummy_net;

	return net;
}

static csch_ahdr_t *lookup_type_and_name(csch_abstract_t *abst, const char *type, const char *name)
{
	sch_sim_mod_target_type_t tt = sch_sim_str2mod_target_type(type);

	switch(tt) {
		case SCH_SIMTT_COMP: return (csch_ahdr_t *)csch_acomp_get(abst, name); break;
		case SCH_SIMTT_PORT: return (csch_ahdr_t *)lookup_port(abst, name, NULL); break;
		case SCH_SIMTT_NET:  return (csch_ahdr_t *)csch_anet_get(abst, name); break;
		default: return NULL;
	}
	return NULL;
}


static int mod_do_add(csch_project_t *prj, csch_abstract_t *abst, sch_sim_exec_t *sim_exec, int eng_prio, lht_node_t *mod, long mod_idx)
{
	const char *device = get_node_str(mod, "device"), *addr;
	const char *value = get_node_str(mod, "value");
	const char *name = get_node_str(mod, "name");
	const char *ac_value = get_node_str(mod, "ac_value");
	const char *stdf = get_node_str(mod, "tdf");
	sch_sim_mod_device_t dt = sch_sim_str2mod_device(device);
	sch_sim_mod_tdf_t tdf = sch_sim_str2mod_tdf(stdf);
	csch_source_arg_t *src;
	csch_acomp_t *comp;
	csch_aport_t *port;
	csch_anet_t *net_pos, *net_neg;
	char name_tmp[128];
	lht_node_t *tdf_params;

	if (dt == SCH_SIMDEV_invalid) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: expected %s/mods/add (mod #%ld): unknown device '%s'\n", mod->parent->parent->name, mod_idx, device);
		return -1;
	}

	if ((name == NULL) || (*name == '\0')) {
		sprintf(name_tmp, "%c__sim_mod_%ld", device[0], mod_idx);
		name = name_tmp;
	}
	comp = csch_acomp_get(abst, name);
	if (comp == NULL)
		comp = csch_acomp_new(abst, abst->hroot, CSCH_ASCOPE_GLOBAL, name, name);

	/* connect positive */
	addr = get_node_str(mod, "pos");
	if (addr == NULL) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/add (mod #%ld): positive pin is not connected anywhere\n", mod->parent->parent->name, mod_idx);
		return -1;
	}

	net_pos = sch_sim_lookup_net(abst, addr, 1);
	if (net_pos == NULL)
		return -1;

	port = csch_aport_get(abst, comp, "1", 1);
	if (csch_compile_connect_net_to(&net_pos, &port->hdr, 0) != 0) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/add (mod #%ld): failed to connect positive pin '%s' to net '%s'\n", mod->parent->parent->name, mod_idx, addr, net_pos->name);
		return -1;
	}


	/* connect negative */
	addr = get_node_str(mod, "neg");
	if (addr == NULL) {
		net_neg = sch_sim_lookup_net(abst, "GND", 0);
		if (net_neg == NULL)
			net_neg = sch_sim_lookup_net(abst, "gnd", 0);
		if (net_neg == NULL) {
			rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/add (mod #%ld): failed to find gnd net for implicit negative connection on pin '%s'\n", mod->parent->parent->name, mod_idx, addr);
			return -1;
		}
	}
	else
		net_neg = sch_sim_lookup_net(abst, addr, 1);

	if (net_neg == NULL) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/add (mod #%ld): failed to find gnd net for negative connection on pin '%s' failed\n", mod->parent->parent->name, mod_idx, addr);
		return -1;
	}

	port = csch_aport_get(abst, comp, "2", 1);
	if (csch_compile_connect_net_to(&net_neg, &port->hdr, 0) != 0) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/add (mod #%ld): failed to connect negative pin '%s' to net '%s'\n", mod->parent->parent->name, mod_idx, addr, net_pos->name);
		return -1;
	}

	src = csch_attrib_src_p("sim_mods", "'add' modifier");
	csch_attrib_set(&comp->hdr.attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, "name", name, src, NULL);

	tdf_params = lht_dom_hash_get(mod, "tdf_params");

	if ((sim_exec != NULL) && (sim_exec->mod_add_params != NULL)) {
		sim_exec->mod_add_params(prj, comp, dt, value, ac_value, tdf, tdf_params, eng_prio, mod, mod_idx);
	}
	else {
		rnd_message(RND_MSG_ERROR, "mod_do_add(): sim_exec doesn't have mod_add_params(), %s is left without a value\n", name);
		return -1;
	}

	return 0;
}


static int mod_do_omit(csch_project_t *prj, csch_abstract_t *abst, sch_sim_exec_t *sim_exec, int eng_prio, lht_node_t *mod, long mod_idx)
{
	const char *type = get_node_str(mod, "type");
	const char *name = get_node_str(mod, "name");
	csch_ahdr_t *obj = lookup_type_and_name(abst, type, name);
	csch_source_arg_t *src;

	if (obj != NULL) {
		src = csch_attrib_src_p("sim_mods", "'omit' modifier");
		csch_attrib_set(&obj->attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, "omit", "yes", src, NULL);
		csch_compile_update_obj_cache(obj);
	}
	else {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/omit (mod #%ld): invalid or missing type or name\n", mod->parent->parent->name, mod_idx);
		return -1;
	}

	return 0;
}

static int mod_do_edit_attr(csch_project_t *prj, csch_abstract_t *abst, sch_sim_exec_t *sim_exec, int eng_prio, lht_node_t *mod, long mod_idx)
{
	const char *type = get_node_str(mod, "type");
	const char *name = get_node_str(mod, "name");
	const char *key = get_node_str(mod, "key");
	const char *value = get_node_str(mod, "value");
	csch_ahdr_t *obj = lookup_type_and_name(abst, type, name);
	csch_source_arg_t *src;

	if (obj == NULL) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/edit_attr (mod #%ld): invalid or missing type or name\n", mod->parent->parent->name, mod_idx);
		return -1;
	}

	if (key != NULL)
		while(isspace(*key)) key++;

	if ((key == NULL) || (*key == '\0')) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/edit_attr (mod #%ld): missing or empty key\n", mod->parent->parent->name, mod_idx);
		return -1;
	}

	src = csch_attrib_src_p("sim_mods", "'edit_attr' modifier");
	csch_attrib_set(&obj->attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, key, value, src, NULL);

	return 0;
}

static int mod_do_disconn(csch_project_t *prj, csch_abstract_t *abst, sch_sim_exec_t *sim_exec, int eng_prio, lht_node_t *mod, long mod_idx)
{
	const char *scomp = get_node_str(mod, "comp");
	const char *sport = get_node_str(mod, "port");
	csch_acomp_t *comp;
	csch_aport_t *port;

	if (scomp != NULL)
		while(isspace(*scomp)) scomp++;

	if (sport != NULL)
		while(isspace(*sport)) sport++;


	if ((scomp == NULL) || (*scomp == '\0')) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/disconn (mod #%ld): missing or comp\n", mod->parent->parent->name, mod_idx);
		return -1;
	}

	if ((sport == NULL) || (*sport == '\0')) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/disconn (mod #%ld): missing or port\n", mod->parent->parent->name, mod_idx);
		return -1;
	}

	comp = csch_acomp_get(abst, scomp);
	if (comp == NULL) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/disconn (mod #%ld): component '%s' not found\n", mod->parent->parent->name, mod_idx, scomp);
		return -1;
	}

	port = csch_aport_get(abst, comp, sport, 0);
	if (port == NULL) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/disconn (mod #%ld): port '%s' of component '%s' not found\n", mod->parent->parent->name, mod_idx, sport, scomp);
		return -1;
	}

	csch_compile_disconnect(&port->hdr);

	return 0;
}

static int mod_do_temp(csch_project_t *prj, csch_abstract_t *abst, sch_sim_exec_t *sim_exec, int eng_prio, lht_node_t *mod, long mod_idx)
{
	fgw_arg_t val;
	const char *scomp = get_node_str(mod, "temp");


	val.type = FGW_STR;
	val.val.cstr = scomp;
	if (sim_exec->set_global(abst, eng_prio, "temp", &val) != 0) {
		rnd_message(RND_MSG_ERROR, "sim mod execution: %s/mods/temp (mod #%ld): sim backend failed to set temperature\n", mod->parent->parent->name, mod_idx);
		return -1;
	}
	fgw_arg_free(&rnd_fgw, &val);

	return 0;
}


static int mod_perform(csch_project_t *prj, csch_abstract_t *abst, sch_sim_exec_t *sim_exec, int eng_prio, lht_node_t *mod, long mod_idx)
{
	sch_sim_mod_type_t t = sch_sim_str2mod_type(mod->name);

	switch(t) {
		case SCH_SIMOD_ADD:          return mod_do_add(prj, abst, sim_exec, eng_prio, mod, mod_idx);
		case SCH_SIMOD_OMIT:         return mod_do_omit(prj, abst, sim_exec, eng_prio, mod, mod_idx);
		case SCH_SIMOD_EDIT_ATTR:    return mod_do_edit_attr(prj, abst, sim_exec, eng_prio, mod, mod_idx);
		case SCH_SIMOD_DISCON:       return mod_do_disconn(prj, abst, sim_exec, eng_prio, mod, mod_idx);
		case SCH_SIMOD_TEMP:         return mod_do_temp(prj, abst, sim_exec, eng_prio, mod, mod_idx);
		default:
			rnd_message(RND_MSG_ERROR, "Can not perform modification for sim:\n'%s' is not a valid modifcation type\n", mod->name);
	}
	return -1;
}

int sch_sim_mods_perform(csch_project_t *prj, const char *setup_name, csch_abstract_t *abst, sch_sim_exec_t *sim_exec, int eng_prio)
{
	lht_node_t *setup, *mods, *mod;
	int res;
	long idx;

	if (setup_name == NULL)
		setup_name = sch_sim_conf.plugins.sim.active_setup;

	if ((setup_name == NULL) || (*setup_name == '\0')) {
		rnd_message(RND_MSG_ERROR, "Failed to perform modifications for simulation setup:\nactive simulation setup name is empty\n");
		return -1;
	}

	setup = sch_sim_get_setup(prj, setup_name, 0);
	if (setup == NULL) {
		rnd_message(RND_MSG_ERROR, "No such simulation setup: '%s'\n", setup_name);
		return -1;
	}

	mods = lht_dom_hash_get(setup, "mods");
	if (mods == NULL)
		return 0; /* no mods */

	if (mods->type != LHT_LIST) {
		rnd_message(RND_MSG_ERROR, "The mods conf node in setup '%s' is not a list as it needs to be\n", setup_name);
		return -1;
	}

	res = 0;
	for(mod = mods->data.list.first, idx = 0; mod != NULL; mod = mod->next, idx++) {
		if (mod->type != LHT_HASH) {
			rnd_message(RND_MSG_ERROR, "The mods conf node '%s' in setup '%s' must be a hash\n", mod->name, setup_name);
			res = -1;
		}
		else
			res |= mod_perform(prj, abst, sim_exec, eng_prio, mod, idx);
	}

	return res;
}

