/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - SPICE netlist export
 *  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 <stdio.h>
#include <string.h>
#include <ctype.h>

#include <genvector/vtp0.h>
#include <genht/htsp.h>
#include <genht/hash.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/safe_fs.h>
#include <librnd/core/plugins.h>
#include <librnd/core/error.h>
#include <librnd/core/math_helper.h>
#include <libcschem/config.h>
#include <libcschem/plug_io.h>

#include <plugins/lib_netlist_exp/lib_netlist_exp.h>

static csch_plug_io_t espice_net;
static vtp0_t pin2port = {0}; /* cache */
static vtp0_t pin2vect = {0}; /* cache; each item is a dynamically allocated vtp0_t of ports */

extern int export_spice_is_dbg(void);


static int spice_export_prio(const char *fn, const char *fmt, csch_plug_io_type_t type)
{
	if (type != CSCH_IOTYP_NETLIST)
		return 0;
	if (rnd_strcasecmp(fmt, "spice") == 0)
		return 100;
	if (rnd_strcasecmp(fmt, "cir") == 0)
		return 98;
	if (rnd_strcasecmp(fmt, "net") == 0)
		return 95;
	return 0;
}

typedef struct {
	csch_anet_t *net0;
} spice_ctx_t;

static const char *spice_get_netname(spice_ctx_t *ctx, const csch_anet_t *net)
{
	if (net == ctx->net0)
		return "0";
	return sch_nle_get_netname(net);
}


static void print_port_net(spice_ctx_t *ctx, FILE *f, const csch_aport_t *port, const char *refdes, long pinnum, long *noconn, int spc)
{
	const csch_anet_t *net;
	const char *netname;

	if (spc)
		fputc(' ', f);

	net = port->conn.net;
	if ((net == NULL) || (net->hdr.omit)) {
		rnd_message(RND_MSG_ERROR, "spice: pin '%d' in component %s is not connected anywhere\n", pinnum, refdes);
		fprintf(f, "__noconn_%ld__", (*noconn)++);
		return;
	}

	netname = spice_get_netname(ctx, net); /* can't be NULL, auto-named to anon then */
	fprintf(f, "%s", netname);
}

static void print_vect_nets(spice_ctx_t *ctx, FILE *f, vtp0_t *vect, const char *refdes, long pinnum, long *noconn)
{
	long n;
	fprintf(f, " [");
	for(n = 0; n < vect->used; n++)
		print_port_net(ctx, f, vect->array[n], refdes, pinnum, noconn, (n > 0));
	fprintf(f, "]");
}

static void print_port_name(spice_ctx_t *ctx, FILE *f, const csch_aport_t *port)
{
	fprintf(f, " %s", port->name);
}

static void print_vect_names(spice_ctx_t *ctx, FILE *f, vtp0_t *vect)
{
	long n;
	fprintf(f, " [");
	for(n = 0; n < vect->used; n++)
		print_port_name(ctx, f, vect->array[n]);
	fprintf(f, "]");
}



static void spice_export_comps(FILE *f, csch_abstract_t *abs)
{
	long noconn = 0;
	spice_ctx_t ctx = {0};
	htsp_entry_t *e, *p;
	int debug = export_spice_is_dbg();

	/* find net 0 and store it in ctx */
	for(e = htsp_first(&abs->nets); e != NULL; e = htsp_next(&abs->nets, e)) {
		csch_anet_t *net = e->value;
		if (!net->hdr.omit) {
			const char *attr = csch_attrib_get_str(&net->hdr.attr, "spice/gnd");
			if (attr != NULL) {
				ctx.net0 = net;
				break;
			}
		}
	}

	/* write instances */
	for(e = htsp_first(&abs->comps); e != NULL; e = htsp_next(&abs->comps, e)) {
		csch_acomp_t *comp = e->value;
		const char *refdes = sch_nle_get_refdes(comp), *val, *parms;
		long n;

		if ((refdes == NULL) || (comp->hdr.omit))
			continue;

		if (debug) {
			const char *dev = sch_nle_get_alt_attr(&comp->hdr.attr, "device", NULL);

			fputc('\n', f);
			fprintf(f, "* comp.name = %s\n", comp->name);
			if (dev != NULL)
				fprintf(f, "* comp.a.device = %s\n", dev);
		}


		/* map (order) ports into pin2port */
		pin2port.used = 0;
		pin2vect.used = 0;
		for(p = htsp_first(&comp->ports); p != NULL; p = htsp_next(&comp->ports, p)) {
			const csch_aport_t *port = p->value;
			void **already, **alreadyv;
			const char *spinnum = sch_nle_get_pinnum(port), *spn;
			char *end;
			long pinnum;
			int is_vect = 0;

			if (spinnum == NULL)
				continue; /* pin without pin number should be ignored for simulation */

			spn = spinnum;
			if (*spn == '[') {
				spn++;
				is_vect = 1;
			}
			pinnum = strtol(spn, &end, 10);
			if (is_vect ? (*end != ']') : (*end != '\0')) {
				rnd_message(RND_MSG_ERROR, "spice: pin number '%s' is not an integer in component %s (ignoring pin)\n", spinnum, refdes);
				continue;
			}

			already = vtp0_get(&pin2port, pinnum, 0);
			if ((already != NULL) && (*already != NULL)) {
				rnd_message(RND_MSG_ERROR, "spice: duplicate pin '%s' in component %s (ignoring pin)\n", spinnum, refdes);
				continue;
			}

			alreadyv = vtp0_get(&pin2vect, pinnum, 0);

			if (is_vect) {
				vtp0_t *vect;
				if ((alreadyv == NULL) || (*alreadyv == NULL)) {
					vect = calloc(sizeof(vtp0_t), 1);
					vtp0_set(&pin2vect, pinnum, (void *)vect);
				}
				else
					vect = *alreadyv;
				vtp0_append(vect, (void *)port);
			}
			else
				vtp0_set(&pin2port, pinnum, (void *)port);
		}

		val = sch_nle_get_alt_attr(&comp->hdr.attr, "display/value", "value", NULL);
		parms = sch_nle_get_alt_attr(&comp->hdr.attr, "display/spice/params", "spice/params", NULL);

		/* debug print port names and values */
		if (debug) {
			fprintf(f, "* port_names =");
			for(n = 1; n < RND_MAX(pin2port.used, pin2vect.used); n++) {
				const csch_aport_t *port = NULL;
				vtp0_t *vect = NULL;

				if (n < pin2port.used) port = pin2port.array[n];
				if (n < pin2vect.used) vect = pin2vect.array[n];

				if (vect != NULL)
					print_vect_names(&ctx, f, vect);
				else if (port != NULL)
					print_port_name(&ctx, f, port);
			}
			fputc('\n', f);


			if (val != NULL)
				fprintf(f, "* comp.a.value = %s\n", val);
			if (parms != NULL)
				fprintf(f, "* comp.a.spice/params = %s\n", parms);
		}

		/* print the instance */
		fprintf(f, "%s", refdes);
		for(n = 1; n < RND_MAX(pin2port.used, pin2vect.used); n++) {
			const csch_aport_t *port = NULL;
			vtp0_t *vect = NULL;

			if (n < pin2port.used) port = pin2port.array[n];
			if (n < pin2vect.used) vect = pin2vect.array[n];

			if ((port != NULL) && (vect != NULL)) {
				const char *spinnum = sch_nle_get_pinnum(port);
				rnd_message(RND_MSG_ERROR, "spice: pin '%s' in component %s can not be both scalar and vector; going for the vector\n", spinnum, refdes);
				port = NULL;
			}

			if ((port == NULL) && (vect == NULL)) {
				rnd_message(RND_MSG_ERROR, "spice: missing pin '%d' in component %s (broken export)\n", n, refdes);
				fprintf(f, " _missing");
				continue;
			}

			if (vect != NULL)
				print_vect_nets(&ctx, f, vect, refdes, n, &noconn);
			else if (port != NULL)
				print_port_net(&ctx, f, port, refdes, n, &noconn, 1);
			else
				abort(); /* can't get here, we've already checked that we have either port or vect */
		}

		/* free all vectors */
		for(n = 1; n < pin2vect.used; n++) {
			vtp0_t *vect = pin2vect.array[n];
			if (vect != NULL) {
				vtp0_uninit(vect);
				free(vect);
			}
			pin2vect.array[n] = NULL;
		}

		/* print val and params */
		if (val != NULL)
			fprintf(f, " %s", val);

		val = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/model_card", NULL);
		if (val != NULL) {
			val = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/model_card_uname", NULL);
			if (val != NULL)
				fprintf(f, " %s", val);
			else
				fprintf(f, " sch_rnd_error_mc_uname");
		}
		else {
			val = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/model_card_uname", NULL);
			if (val == NULL)
				val = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/model", NULL);
			if (val != NULL)
				fprintf(f, " %s", val);
		}

		if (parms != NULL)
			fprintf(f, " %s", parms);

		fprintf(f, "\n");
	}
}

/* Copy model_card's headers until the first non-comment; return the first
   non-white-space character of the first non-comment line */
static const char *find_model_card_head(FILE *f, const char *model_card)
{
	const char *s;
	int nl = 1;

	for(s = model_card; *s != '\0'; s++) {
		if (nl && !isspace(*s)) {
			/* first non-space after a newline */
			if (*s != '*')
				return s;
		}

		if (*s == '\n')
			nl = 1;
		else if (isspace(*s) && nl)
			nl = 1; /* ingore leading whitespace */
		else
			nl = 0;

		fputc(*s, f);
	}
	return NULL;
}

static void spice_export_model_cards(FILE *f, csch_abstract_t *abs)
{
	htsp_entry_t *e;

	for(e = htsp_first(&abs->comps); e != NULL; e = htsp_next(&abs->comps, e)) {
		csch_acomp_t *comp = e->value;
		const char *model_card, *head;

		if (comp->hdr.omit) continue;

		model_card = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/model_card", NULL);
		if (model_card != NULL) {
			const char *uname = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/model_card_uname", NULL);

			if (uname == NULL)
				uname = "schrnd_error_no_uname";

			fprintf(f, "\n*** sch-rnd/export_spice: model card: %s ***\n", comp->name);

			head = find_model_card_head(f, model_card);
			if (head == NULL) {
				rnd_message(RND_MSG_ERROR, "spice model card export: model card should start with .model or .subckt (component %s)\n", comp->name);
			}
			else if ((rnd_strncasecmp(head, ".model", 6) == 0) && isspace(head[6])) {
				head += 6;
				while(isspace(*head)) head++;
				while(!isspace(*head)) head++; /* skip over original name */
				while(isspace(*head)) head++;
				fprintf(f, ".MODEL %s %s\n", uname, head);
			}
			else if ((rnd_strncasecmp(head, ".subckt", 7) == 0) && isspace(head[7])) {
				head += 7;
				while(isspace(*head)) head++;
				while(!isspace(*head)) head++; /* skip over original name */
				while(isspace(*head)) head++;
				fprintf(f, ".SUBCKT %s %s\n", uname, head);
			}
		}
	}
}

static void spice_export_file_headers(FILE *f, csch_abstract_t *abs)
{
	int announced = 0;
	htsp_entry_t *e;

	for(e = htsp_first(&abs->comps); e != NULL; e = htsp_next(&abs->comps, e)) {
		csch_acomp_t *comp = e->value;
		const char *hdr;

		if (comp->hdr.omit) continue;

		hdr = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/file_header", NULL);
		if (hdr != NULL) {
			if (!announced) {
				fprintf(f, "\n*** sch-rnd/export_spice: file headers ***\n");
				announced = 1;
			}
			fprintf(f, "\n%s\n", hdr);
		}
	}
}

static void spice_export_includes(FILE *f, csch_abstract_t *abs)
{
	htsp_entry_t *e;
	int hash_inited = 0;
	htsp_t fns = {0};

	for(e = htsp_first(&abs->comps); e != NULL; e = htsp_next(&abs->comps, e)) {
		csch_acomp_t *comp = e->value;
		const char *inc;

		if (comp->hdr.omit) continue;

		inc = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/model_include", NULL);
		if (inc != NULL) {
			if (!hash_inited) {
				htsp_init(&fns, strhash, strkeyeq);
				hash_inited = 1;
			}
			htsp_set(&fns, (char *)inc, NULL);
		}
	}

	if (hash_inited) {
		htsp_entry_t *e;

		fprintf(f, "\n*** sch-rnd/export_spice: model includes ***\n");
		for(e = htsp_first(&fns); e != NULL; e = htsp_next(&fns, e))
			fprintf(f, ".INCLUDE %s\n", e->key);

		htsp_uninit(&fns);
	}
}

static void spice_export_cmds(FILE *f, csch_abstract_t *abs)
{
	int announced = 0;
	htsp_entry_t *e;

	for(e = htsp_first(&abs->comps); e != NULL; e = htsp_next(&abs->comps, e)) {
		csch_acomp_t *comp = e->value;
		const char *hdr;

		if (comp->hdr.omit) continue;

		hdr = sch_nle_get_alt_attr(&comp->hdr.attr, "spice/command", NULL);
		if (hdr != NULL) {
			if (!announced) {
				fprintf(f, "\n*** sch-rnd/export_spice: commands ***\n.control\n");
				announced = 1;
			}
			fprintf(f, "\n%s\n", hdr);
		}
	}
	if (announced)
		fprintf(f, "\n.endc\n");
}

extern int export_spice_omit(const char *what);

static int spice_export_project_abst(const char *fn, const char *fmt, csch_abstract_t *abs, rnd_hid_attr_val_t *options)
{
	TODO("get hidlib as an arg")
	rnd_design_t *hidlib = NULL;
	FILE *f = rnd_fopen(hidlib, fn, "w");
	if (f == NULL)
		return -1;

	fprintf(f, ".title sch-rnd export using export_spice\n");

	if (!export_spice_omit("headers"))
		spice_export_file_headers(f, abs);
	if (!export_spice_omit("includes"))
		spice_export_includes(f, abs);
	if (!export_spice_omit("model_cards"))
		spice_export_model_cards(f, abs);

	fprintf(f, "\n*** circuit ***\n");
	spice_export_comps(f, abs);

	if (!export_spice_omit("commands"))
		spice_export_cmds(f, abs);
	fprintf(f, ".end\n");

	fclose(f);
	return 0;
}

#include "hid_impl.c"

int pplg_check_ver_export_spice(int ver_needed) { return 0; }

void pplg_uninit_export_spice(void)
{
	csch_plug_io_unregister(&espice_net);
	rnd_export_remove_opts_by_cookie(spice_cookie);
	rnd_hid_remove_hid(&spice_hid);

	vtp0_uninit(&pin2port);
	vtp0_uninit(&pin2vect);
}

int pplg_init_export_spice(void)
{
	RND_API_CHK_VER;

	espice_net.name = "export to SPICE netlist";
	espice_net.export_prio = spice_export_prio;
	espice_net.export_project_abst = spice_export_project_abst;
	espice_net.ext_export_project = ".cir";
	csch_plug_io_register(&espice_net);


	rnd_hid_nogui_init(&spice_hid);

	spice_hid.struct_size = sizeof(rnd_hid_t);
	spice_hid.name = "spice";
	spice_hid.description = "Exports project's SPICE netlist";
	spice_hid.exporter = 1;

	spice_hid.get_export_options = spice_get_export_options;
	spice_hid.do_export = spice_do_export;
	spice_hid.parse_arguments = spice_parse_arguments;
	spice_hid.argument_array = spice_values;

	spice_hid.usage = spice_usage;

	rnd_hid_register_hid(&spice_hid);
	rnd_hid_load_defaults(&spice_hid, spice_options, NUM_OPTIONS);


	return 0;
}

