/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - ngspice to high level sim glue
 *  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 <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <libfungw/fungw.h>
#include <genvector/gds_char.h>
#include <libcschem/abstract.h>
#include <libcschem/engine.h>
#include <libcschem/project.h>
#include <libcschem/actions_csch.h>
#include <libcschem/attrib.h>
#include <libcschem/libcschem.h>
#include <libcschem/util_compile.h>
#include <librnd/core/actions.h>
#include <librnd/core/hidlib.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/rnd_printf.h>
#include <librnd/core/plugins.h>
#include <librnd/core/misc_util.h>
#include <librnd/core/safe_fs.h>
#include <librnd/core/compat_fs_dir.h>

#include <plugins/sim/sim.h>
#include <plugins/sim/sim_conf.h>
#include <plugins/sim/mods.h>
#include <plugins/sim/util.h>

static const fgw_eng_t *target_spice; /* wrap target_spice */

static const char sim_ngspice_cookie[] = "sim_ngspice";

struct sch_sim_setup_s {  /* per simulation data */
	gds_t tmpdir;
	int tmpdir_restore;
	vtp0_t output; /* pairs of analysis + presentation */
	gds_t read_buff;
};

static void tempdir_restore(sch_sim_setup_t *setup)
{
	if (setup->tmpdir.array != NULL)
		setup->tmpdir.used = setup->tmpdir_restore;
}


static sch_sim_setup_t *se_ngspice_alloc(void)
{
	sch_sim_setup_t *setup = calloc(sizeof(sch_sim_setup_t), 1);
	rnd_design_t *dsg = rnd_multi_get_current();

	if (setup == NULL)
		return NULL;


	if (rnd_mktempdir(dsg, &setup->tmpdir, "sch-rnd-sim") != 0) {
		free(setup);
		return NULL;
	}

	gds_append(&setup->tmpdir, RND_DIR_SEPARATOR_C);
	setup->tmpdir_restore = setup->tmpdir.used;

	return setup;
}

static void se_ngspice_free(sch_sim_setup_t *setup)
{
	rnd_design_t *dsg = rnd_multi_get_current();
	long n;

	for(n = 0; n < setup->output.used; n+=2) {
		sch_sim_analysis_free(setup->output.array[n+0]);
		sch_sim_presentation_free(setup->output.array[n+1]);
	}

	tempdir_restore(setup);
	setup->tmpdir.array[setup->tmpdir.used] = '\0';
	if (sch_sim_conf.plugins.sim.preserve_tmp)
		rnd_message(RND_MSG_INFO, "Not removing sim tmp dir %s\n(as requested by the config node plugins/sim/preserve_tmp)\n", setup->tmpdir);
	else
		rnd_rmtempdir(dsg, &setup->tmpdir);

	gds_uninit(&setup->read_buff);

	free(setup);
}

#include <librnd/hid/hid.h>
#include <librnd/hid/hid_init.h>
#include <librnd/hid/hid_export.h>
#include <librnd/core/event.h>

static int sim_export(rnd_design_t *dsg, char *exporter_name, int argc, char *args[])
{
	if (rnd_hid_export_using(dsg, exporter_name, argc, args) < 0) {
		rnd_message(RND_MSG_ERROR, "sim_ngspice circuit export failed: spice exporter not found (see above).\n");
		return -1;
	}

	return 0;
}

static const char *cirname(sch_sim_setup_t *setup)
{
	gds_append_str(&setup->tmpdir, "prj.cir");
	tempdir_restore(setup);
	return setup->tmpdir.array;
}

static int se_ngspice_set_global(csch_abstract_t *abst, int eng_prio, const char *name, fgw_arg_t *val)
{
	gds_t tmp = {0};

	if (strcmp(name, "temp") == 0) {
		csch_acomp_t *comp;
		csch_source_arg_t *src;

		fgw_arg_conv(&rnd_fgw, val, FGW_STR);
		gds_append_str(&tmp, ".temp ");
		gds_append_str(&tmp, val->val.str);

		comp = csch_acomp_get(abst, "sim_ngspice_env_temp");
		if (comp == NULL)
			csch_acomp_new(abst, abst->hroot, CSCH_ASCOPE_GLOBAL, "sim_ngspice_env_temp", "sim_ngspice_env_temp");
		

		src = csch_attrib_src_p("sim_ngspice", "'temp' modifier");
		csch_attrib_set(&comp->hdr.attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, "spice/command", tmp.array, src, NULL);
	}

	gds_uninit(&tmp);
	return 0;
}


static int se_ngspice_add_circuit(sch_sim_setup_t *setup)
{
	rnd_design_t *dsg = rnd_multi_get_current();
	char *args[128];
	int argc, res;


	argc = 0;
	args[argc++] = "--outfile";
	args[argc++] = (char *)cirname(setup); /* as of 4.0.x the export API doesn't yet take const char *[] but it won't modify the strings anyway */
	args[argc] = NULL;

	res = sim_export(dsg, "spice", argc, args);

	return res;
}

static int se_ngspice_add_output(sch_sim_setup_t *setup, sch_sim_analysis_t *an, sch_sim_presentation_t *pres)
{
	vtp0_append(&setup->output, an);
	vtp0_append(&setup->output, pres);
	return 0;
}

static void se_ngspice_mod_add_params(csch_project_t *prj, csch_acomp_t *comp, sch_sim_mod_device_t device, const char *value, const char *ac_value, sch_sim_mod_tdf_t tdf, lht_node_t *tdf_params, int eng_prio, lht_node_t *mod, long mod_idx)
{
	csch_source_arg_t *src;
	int val_appended = 0, supports_tdf = 0;
	gds_t tmp = {0};

	if ((device >= 0) && (device < SCH_SIMDEV_max))
		supports_tdf = sch_sim_device_has_tdf[device];

	if (tdf == SCH_SIMTDF_invalid)
			rnd_message(RND_MSG_ERROR, "Ignoring invalid tdf in sim mod 'add' #%ld\n", mod_idx);

	switch(device) {
		case SCH_SIMDEV_V:
		case SCH_SIMDEV_I:
			if (value != NULL) {
				gds_append_str(&tmp, "dc ");
				gds_append_str(&tmp, value);
				val_appended = 1;
			}
			if (ac_value != NULL) {
				if (val_appended)
					gds_append(&tmp, ' ');
				gds_append_str(&tmp, "ac ");
				gds_append_str(&tmp, ac_value);
				val_appended = 1;
			}
			break;
		default:
			val_appended = 0;
	}


	if ((tdf > SCH_SIMTDF_NONE) && (tdf < SCH_SIMTDF_max) && supports_tdf) {
		const sch_sim_mod_tdf_param_t *p, *partab = sch_sim_mod_tdf_params[tdf];

		if (!val_appended && (value != NULL))
			rnd_message(RND_MSG_ERROR, "Ignoring value (in sim mod 'add' #%ld) because tdf is also specified\n", mod_idx);

		if (val_appended)
			gds_append(&tmp, ' ');

		gds_append_str(&tmp, sch_simmod_tdf_names[tdf]);
		gds_append(&tmp, '(');

		for(p = partab; p->name != NULL; p++) {
			lht_node_t *nd = lht_dom_hash_get(tdf_params, p->name);
			const char *nds = NULL;

			if ((nd != NULL) && (nd->type == LHT_TEXT)) {
				nds = nd->data.text.value;
				while(isspace(*nds)) nds++;
				if (*nds == '\0') nds = NULL;
			}

			if (nds == NULL) {
				if (p->optional)
					break; /* first missing optional; stop emitting parameters */
				rnd_message(RND_MSG_ERROR, "Missing mandatory tdf parameter '%s' (in sim mod 'add' #%ld)\n", p->name, mod_idx);
				continue; /* do not append */
			}

			if (p != partab)
				gds_append_str(&tmp, ", ");
			gds_append_str(&tmp, nds);
		}
		gds_append(&tmp, ')');
		src = csch_attrib_src_p("sim_ngspice", "'add' modifier, tdf value");
		csch_attrib_set(&comp->hdr.attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, "value", tmp.array, src, NULL);
	}
	else if (value != NULL) {
		src = csch_attrib_src_p("sim_ngspice", "'add' modifier, scalar value");
		if (val_appended)
			csch_attrib_set(&comp->hdr.attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, "value", tmp.array, src, NULL);
		else
			csch_attrib_set(&comp->hdr.attr, eng_prio + CSCH_PRI_PLUGIN_NORMAL, "value", value, src, NULL);
	}
	else {
		rnd_message(RND_MSG_ERROR, "No value for %c (in sim mod 'add' #%ld)\n", comp->name[0], mod_idx);
	}

	gds_uninit(&tmp);
}

static void fprint_prop_nets(FILE *f, csch_project_t *prj, vts0_t *props)
{
	long m;

	for(m = 0; m < props->used; m++) {
		const char *addr = props->array[m];
		char *sep;
		csch_anet_t *anet;

		/* remove function wrapping, like vdb() */
		sep = strchr(addr, '(');
		if (sep != NULL) {
			char *end, *freeme = rnd_strdup(sep+1);
			end = strrchr(freeme, ')');
			if (end != NULL)
				*end = '\0';
			anet = sch_sim_lookup_net(prj->abst, freeme, 0);
			if (anet != NULL) {
				gds_t tmp = {0};
				gds_append_len(&tmp, addr, sep-addr+1);
				gds_append_str(&tmp, anet->name);
				gds_append(&tmp, ')');
				free(freeme);
				fprintf(f, " %s", tmp.array);
				gds_uninit(&tmp);
			}
			else
				rnd_message(RND_MSG_ERROR, "sim: can't print or plot %s: not found\n", addr);
		}
		else {
			anet = sch_sim_lookup_net(prj->abst, addr, 0);
			if (anet != NULL)
				fprintf(f, " %s", anet->name);
			else
				rnd_message(RND_MSG_ERROR, "sim: can't print or plot %s: not found\n", addr);
		}
	}
}

static int se_ngspice_exec(csch_project_t *prj, sch_sim_setup_t *setup)
{
	FILE *fcmd, *f;
	rnd_design_t *dsg = prj->hdr.designs.array[0];
	const char *cmdname = "cmd";
	char *line, line_[1024], *cmd;
	long n;

	gds_append_str(&setup->tmpdir, "cmd");
	tempdir_restore(setup);
	cmdname = setup->tmpdir.array;
	cmd = rnd_concat("ngspice -b ", cmdname, NULL);

	fcmd = rnd_fopen(dsg, cmdname, "w");

	fprintf(fcmd, ".include %s\n\n", cirname(setup));
	fprintf(fcmd, ".control\n");

	for(n = 0; n < setup->output.used; n+=2) {
		sch_sim_analysis_t *an = setup->output.array[n+0];
		sch_sim_presentation_t *pres = setup->output.array[n+1];

		rnd_append_printf(&setup->tmpdir, "out.%ld", n);
		tempdir_restore(setup);
		pres->outfn = rnd_strdup(setup->tmpdir.array);

		fprintf(fcmd, "echo @@@output %ld of %ld to %s\n", n/2, setup->output.used/2, pres->outfn);
		switch(an->type) {
			case SCH_SIMAN_OP: fprintf(fcmd, "op\n"); break;
			case SCH_SIMAN_TRAN_LIN: fprintf(fcmd, "tran %s %s\n", an->incr, an->stop); break;
			case SCH_SIMAN_AC_DEC: fprintf(fcmd, "ac dec %d %s %s\n", an->numpt, an->start, an->stop); break;
			case SCH_SIMAN_AC_OCT: fprintf(fcmd, "ac oct %d %s %s\n", an->numpt, an->start, an->stop); break;
			case SCH_SIMAN_AC_LIN: fprintf(fcmd, "ac lin %d %s %s\n", an->numpt, an->start, an->stop); break;
			case SCH_SIMAN_DC_LIN: fprintf(fcmd, "dc %s %s %s %s\n", an->src, an->start, an->stop, an->incr); break;
			case SCH_SIMAN_DC_DISTO_DEC: fprintf(fcmd, "disto dec %d %s %s\n", an->numpt, an->start, an->stop); break;
			case SCH_SIMAN_DC_DISTO_OCT: fprintf(fcmd, "disto oct %d %s %s\n", an->numpt, an->start, an->stop); break;
			case SCH_SIMAN_DC_DISTO_LIN: fprintf(fcmd, "disto lin %d %s %s\n", an->numpt, an->start, an->stop); break;
			case SCH_SIMAN_DC_NOISE_DEC: fprintf(fcmd, "noise v(%s) %s dec %d %s %s\n", an->port2, an->port1, an->numpt, an->start, an->stop); break;

			case SCH_SIMAN_PREVIOUS: break; /* do not print anything */

			case SCH_SIMAN_invalid:
				rnd_message(RND_MSG_ERROR, "se_ngspice_exec(): invalid analysis type - using previous\n");
				break;
		}

		switch(pres->type) {
			case SCH_SIMPRES_PRINT:
/* Rather use wrdata so the same result reader works */
/*				fprintf(fcmd, "print");
				fprint_prop_nets(fcmd, prj, &pres->props);
				fprintf(fcmd, " > %s\n", pres->outfn);
				break;*/

			case SCH_SIMPRES_PLOT:
				fprintf(fcmd, "wrdata %s", pres->outfn);
				fprint_prop_nets(fcmd, prj, &pres->props);
				fprintf(fcmd, "\n");
				break;

			case SCH_SIMPRES_invalid:
				rnd_message(RND_MSG_ERROR, "se_ngspice_exec(): invalid presentation type\n");
				break;
		}
	}

	fprintf(fcmd, ".endc\n");
	fclose(fcmd);

	f = rnd_popen(dsg, cmd, "r");
	while((line = fgets(line_, sizeof(line_), f)) != NULL)
		printf(" line=%s", line);

	rnd_pclose(f);

	free(cmd);

	return 0;
}

static void *se_ngspice_result_open(csch_project_t *prj, sch_sim_setup_t *setup, int output_idx)
{
	sch_sim_presentation_t *pres;
	long n = output_idx*2+1;
	rnd_design_t *dsg = prj->hdr.designs.array[0];

	if ((n < 0) || (n >= setup->output.used))
		return NULL;

	pres = setup->output.array[n];
	if (pres == NULL)
		return NULL;

	return rnd_fopen(dsg, pres->outfn, "r");
}

static void se_ngspice_result_close(sch_sim_setup_t *setup, void *stream)
{
	if (stream != NULL)
		fclose(stream);
}

static int se_ngspice_result_read(sch_sim_setup_t *setup, void *stream, vts0_t *dst)
{
	char *s, *start;

	if (dst != NULL) {
		setup->read_buff.used = 0;
		dst->used = 0;
	}

	for(;;) {
		int c = fgetc(stream);
		if (c == EOF)
			return -1;
		if (((c == '\n') || (c == '\r')) && ((setup->read_buff.used > 0) || (dst == NULL)))
			break;
		if (dst != NULL)
			gds_append(&setup->read_buff, c);
	}

	/* append every 2nd field */
	if (dst != NULL) {
		int idx = 0;

		/* ltrim */
		for(s = setup->read_buff.array; isspace(*s); s++) ;

		for(start = s; *s != '\0'; s++) {
			if (isspace(*s)) {
				*s = '\0';
				if ((idx % 2) == 1)
					vts0_append(dst, start);
				idx++;
				s++;
				while(isspace(*s)) s++;
				start = s;
				s--;
			}
		}

		/* append time */
		vts0_append(dst, setup->read_buff.array);
	}
	return 0;
}

void se_ngspice_result_rewind(sch_sim_setup_t *setup, void *stream)
{
	if (stream != NULL)
		rewind(stream);
}



static sch_sim_exec_t sim_ngspice_sim_exec = {
	"ngspice",
	se_ngspice_alloc, se_ngspice_free,
	se_ngspice_set_global,
	se_ngspice_add_circuit,
	se_ngspice_add_output,
	se_ngspice_mod_add_params,
	se_ngspice_exec,
	se_ngspice_result_open,
	se_ngspice_result_close,
	se_ngspice_result_read,
	se_ngspice_result_rewind
};


/*** hooks ***/

static fgw_error_t (*spice_compile_project_after)(fgw_arg_t *res, int argc, fgw_arg_t *argv);
static fgw_error_t (*spice_compile_project_before)(fgw_arg_t *res, int argc, fgw_arg_t *argv);
static fgw_error_t (*spice_compile_component0)(fgw_arg_t *res, int argc, fgw_arg_t *argv);

typedef struct sim_ngspice_trans_s {
	int ontb;
} sim_ngspice_trans_t;

static fgw_error_t sim_ngspice_compile_project_before(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_hook_call_ctx_t *cctx = argv[0].val.argv0.user_call_ctx;
/*	fgw_obj_t *obj = argv[0].val.argv0.func->obj;*/
/*	spicelib_ctx_t *ctx = obj->script_data; - because the object is used there */
	csch_abstract_t *abst;
	csch_project_t *prj;
	sim_ngspice_trans_t *trans;

	CSCH_HOOK_CONVARG(1, FGW_STRUCT|FGW_PTR, std_cschem_comp_update, abst = argv[1].val.ptr_void);
	CSCH_HOOK_CONVARG(2, FGW_STRUCT|FGW_PTR, std_cschem_comp_update, prj = argv[2].val.ptr_void);

	sch_sim_set_test_bench(prj, abst, sim_ngspice_cookie, cctx->view_eng->eprio);

	trans = malloc(sizeof(sim_ngspice_trans_t));
	trans->ontb = sch_sim_omit_no_test_bench_is_on(prj);
	htpp_set(&abst->eng_transient, (void *)sim_ngspice_cookie, trans);


	/* call through to the spice backend */
	if (spice_compile_project_before != NULL)
		return spice_compile_project_before(res, argc, argv);

	return 0;
}

static fgw_error_t sim_ngspice_compile_project_after(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	int r = 0;
	csch_hook_call_ctx_t *cctx = argv[0].val.argv0.user_call_ctx;
/*	fgw_obj_t *obj = argv[0].val.argv0.func->obj;*/
/*	spicelib_ctx_t *ctx = obj->script_data; - because the object is used there */
	csch_abstract_t *abst;
	csch_project_t *prj;
	sim_ngspice_trans_t *trans;

	CSCH_HOOK_CONVARG(1, FGW_STRUCT|FGW_PTR, std_cschem_comp_update, abst = argv[1].val.ptr_void);
	CSCH_HOOK_CONVARG(2, FGW_STRUCT|FGW_PTR, std_cschem_comp_update, prj = argv[2].val.ptr_void);

	/* call through to the spice backend */
	if (spice_compile_project_after != NULL)
		r = spice_compile_project_after(res, argc, argv);

	sch_sim_restore_test_bench(prj, abst, sim_ngspice_cookie, cctx->view_eng->eprio);

	trans = htpp_pop(&abst->eng_transient, (void *)sim_ngspice_cookie);
	free(trans);

	if (sch_sim_mods_perform(prj, NULL, abst, &sim_ngspice_sim_exec, cctx->view_eng->eprio) != 0)
		return -1;

	return r;
}

static fgw_error_t sim_ngspice_compile_component0(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	int r = 0;
	csch_hook_call_ctx_t *cctx = argv[0].val.argv0.user_call_ctx;
	csch_acomp_t *comp;
	sim_ngspice_trans_t *trans;
	csch_abstract_t *abst;


	CSCH_HOOK_CONVARG(1, FGW_AOBJ, std_forge_comp_update, comp = fgw_aobj(&argv[1]));
	assert(comp->hdr.type == CSCH_ATYPE_COMP);

	abst = comp->hdr.abst;
	trans = htpp_get(&abst->eng_transient, (void *)sim_ngspice_cookie);

	if ((trans != NULL) && trans->ontb)
		sch_sim_omit_no_test_bench_comp(comp, cctx->view_eng->eprio);

	/* call through to the spice backend */
	if (spice_compile_component0 != NULL)
		r = spice_compile_component0(res, argc, argv);

	return r;
}

static fgw_error_t sim_ngspice_sim_exec_get(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	res->type = FGW_PTR | FGW_STRUCT;
	res->val.ptr_void = &sim_ngspice_sim_exec;
	return 0;
}

static int on_load(fgw_obj_t *obj, const char *filename, const char *opts)
{
	int res;

	fgw_func_reg(obj, "sim_exec_get", sim_ngspice_sim_exec_get);

	/* on the target level we are mostly a transparent wrapper around target_spice */
	res = target_spice->load(obj, filename, opts);
	if (res != 0)
		return res;


	/* hook some of the calls */
	sch_sim_hook_eng_call(obj, "compile_project_before", &spice_compile_project_before, sim_ngspice_compile_project_before);
	sch_sim_hook_eng_call(obj, "compile_project_after", &spice_compile_project_after, sim_ngspice_compile_project_after);
	sch_sim_hook_eng_call(obj, "compile_component0", &spice_compile_component0, sim_ngspice_compile_component0);

	return 0;
}

static int on_unload(fgw_obj_t *obj)
{
	target_spice->unload(obj);
	return 0;
}


static const fgw_eng_t fgw_sim_ngspice_eng = {
	"target_sim_ngspice",
	csch_c_call_script,
	NULL,
	on_load,
	on_unload
};

int pplg_check_ver_sim_ngspice(int ver_needed) { return 0; }

void pplg_uninit_sim_ngspice(void)
{
}

int pplg_init_sim_ngspice(void)
{
	RND_API_CHK_VER;

	target_spice = fgw_eng_lookup("target_spice");
	if (target_spice == NULL) {
		rnd_message(RND_MSG_ERROR, "target_sim_spice: can't find target_spice\n");
		return -1;
	}

	fgw_eng_reg(&fgw_sim_ngspice_eng);

	return 0;
}

