/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - libcschem (core library)
 *  Copyright (C) 2018,2019,2022,2023 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 PET Fund in 2022, Entrust in 2023)
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.*
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; 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 "config.h"

#include <stdio.h>

#include <librnd/core/compat_misc.h>

#include "abstract.h"
#include "actions_csch.h"
#include "concrete.h"
#include "event.h"
#include "cnc_line.h"
#include "cnc_arc.h"
#include "cnc_poly.h"
#include "cnc_text.h"
#include "cnc_bitmap.h"
#include "cnc_conn.h"
#include "cnc_grp.h"
#include "cnc_pen.h"
#include "cnc_obj.h"
#include "engine.h"
#include "project.h"
#include "util_compile.h"
#include "util_project.h"
#include "libcschem.h"
#include "non_graphical.h"

#include "compile.h"

int csch_compile_attribute(csch_ahdr_t *dst, const char *dstkey, const csch_attrib_t *srca, csch_source_arg_t *src, const char *err1, const char *err2, int referee, int append, csch_attrib_t **dsta_out)
{
	csch_attrib_t *dsta = NULL;
	const char *key = (dstkey == NULL ? srca->key : dstkey);

	if (key[0] == '-') {
		if (dsta_out != NULL) *dsta_out = NULL;
		return 0;
	}

	if (key[0] == '+') {
		key++;
		append = 1;
	}

	dsta = csch_attrib_get(&dst->attr, key);
	if (dsta_out != NULL) *dsta_out = dsta;

	/* attribute merging logic */
	if (srca->val != NULL) {
		if (append)
			return csch_attrib_append_val(&dst->attr, srca->prio, key, srca, src);
		if (dsta != NULL) {
			if (dsta->val != NULL) {
				if (strcmp(srca->val, dsta->val) == 0) {
					/* both set, same value, remember lowest prio */
					if (srca->prio >= dsta->prio) {
						csch_attrib_append_src(dsta, srca->prio, src, 1);
						return 0; /* ignore new */
					}
				}
				else {
					/* different values, look at prios */
					if (srca->prio > dsta->prio) {
						csch_attrib_append_src(dsta, srca->prio, src, 1);
						return 0; /* lower prio, ignore */
					}
					if (srca->prio == dsta->prio) {
						if (!referee) /* it's okay if a group ref's referee had it - we are overwriting it from the group ref */
							rnd_message(RND_MSG_ERROR, "compile_attributes(): %s %s: attribute collision for '%s'\n", err1, err2, srca->key);
						csch_attr_src_free(src);
						return -1;
					}
					/* else set */
				}
			}
		}
		csch_attrib_set(&dst->attr, srca->prio, key, srca->val, src, dsta_out);
	}
	else {
		if (append) {
			if ((dsta != NULL) && (dsta->val != NULL)) {
				csch_attr_src_free(src);
				rnd_message(RND_MSG_ERROR, "compile_attributes(): %s %s: can not append to destination attribute '%s', it is a string not an array\n", err1, err2, key);
				return -1;
			}
			return csch_attrib_append_val(&dst->attr, srca->prio, key, srca, src);
		}
		else
			csch_attrib_set_arr(&dst->attr, srca->prio, key, &srca->arr, src, dsta_out);
	}

	return 0;
}

/* Copy all sources from srca in front of the sources in dst */
static void compile_attribute_copy_srcs_front(csch_attrib_t *dst, const csch_attrib_t *srca)
{
	long n;

	if (srca->source.used <= 0)
		return;

	vts0_alloc_insert(&dst->source, 0, srca->source.used);
	for(n = 0; n < srca->source.used; n++)
		dst->source.array[n] = rnd_strdup(srca->source.array[n]);
}

static int compile_attributes(csch_ahdr_t *dst, const csch_attribs_t *src, csch_cgrp_t *cgrp, const char *err1, const char *err2, int referee)
{
	htsp_entry_t *e;
	int res = 0, r1;

	for(e = htsp_first(src); e; e = htsp_next(src, e)) {
		const csch_attrib_t *srca = e->value;
		csch_source_arg_t *src = csch_attrib_src_ac(&cgrp->hdr, srca->key, "compile_attributes()");
		csch_attrib_t *dsta;

		r1 = csch_compile_attribute(dst, NULL, srca, src, err1, err2, referee, 0, &dsta);

		if ((r1 == 0) && (dsta != NULL))
			compile_attribute_copy_srcs_front(dsta, srca);

		res |= r1;
	}

	TODO("this should return res");
	return 0;
}

static int compile_wire_net(csch_abstract_t *dst, const csch_sheet_t *sheet, csch_cgrp_t *src)
{
	char tmpname[128];
	const char *name;
	const csch_attrib_t *aname;
	csch_anet_t *net;
	int no_uname = 0;

	aname = csch_attrib_get(&src->attr, "name");
	dst->ucnt.wirenet++;
	if ((aname == NULL) || (aname->key == NULL) || (*aname->key == '\0')) {
		sprintf(tmpname, "anon_net_%ld", dst->ucnt.wirenet);
		name = tmpname;
		no_uname = 1;
	}
	else
		name = aname->val;

	net = csch_anet_get(dst, name, 1, no_uname);
	if (net == NULL)
		return -1;

	csch_compile_add_source(src, &net->hdr);
	return compile_attributes(&net->hdr, &src->attr, src, "net", name, 0);
}

static csch_aport_t *compile_port(csch_abstract_t *dst, int viewid, const csch_sheet_t *sheet, csch_acomp_t *comp, csch_cgrp_t *t)
{
	char tmpname[128];
	const csch_attrib_t *trole, *tname;
	const char *pname, *pname_tmp = NULL;
	csch_aport_t *port = NULL;

	if ((t->hdr.type != CSCH_CTYPE_GRP) && (t->hdr.type != CSCH_CTYPE_GRP_REF))
		return NULL;

	if (t->hdr.type == CSCH_CTYPE_GRP_REF)
		trole = csch_cgrp_ref_get_attr(t, "role");
	else
		trole = csch_attrib_get(&t->attr, "role");
	if ((trole == NULL) || (trole->val == NULL) || (strcmp(trole->val, "terminal") != 0))
		return NULL;

	if (t->hdr.type == CSCH_CTYPE_GRP_REF)
		tname = csch_cgrp_ref_get_attr(t, "name");
	else
		tname = csch_attrib_get(&t->attr, "name");

	dst->ucnt.port++;

	if ((tname == NULL) || (tname->key == NULL) || (*tname->key == '\0')) {
		sprintf(tmpname, "anon_%ld", dst->ucnt.port);
		pname = tmpname;
	}
	else
		pname = tname->val;

	csch_eng_call_strmod((csch_project_t *)sheet->hidlib.project, viewid, CSCH_ENGHK_TERMINAL_NAME_TO_PORT_NAME,
		&pname_tmp, pname, FGW_AOBJ, comp, FGW_COBJ, t, FGW_INVALID);

	if (pname_tmp == NULL)
		port = csch_aport_get(dst, comp, pname, 1);
	else
		port = csch_aport_get(dst, comp, pname_tmp, 1);
	port->parent = comp;
	csch_compile_add_source(t, &port->hdr);

	compile_attributes(&port->hdr, &t->attr, t, "port", pname, 0);

	csch_eng_free_strmod(&pname_tmp, pname);

	return port;
}

static int compile_symbol(csch_abstract_t *dst, int viewid, const csch_sheet_t *sheet, csch_cgrp_t *src)
{
	char tmpname[128];
	htip_entry_t *e;
	const char *name, *name_tmp = NULL;
	const csch_attrib_t *aname = NULL;
	csch_acomp_t *comp;
	int res = 0;

	if (src->hdr.type == CSCH_CTYPE_GRP_REF)
		aname = csch_cgrp_ref_get_attr(src, "name");
	else
		aname = csch_attrib_get(&src->attr, "name");

	dst->ucnt.comp++;
	if ((aname == NULL) || (aname->key == NULL) || (*aname->key == '\0')) {
		sprintf(tmpname, "anon_comp_%ld", dst->ucnt.comp);
		name = tmpname;
	}
	else
		name = aname->val;

	csch_eng_call_strmod((csch_project_t *)sheet->hidlib.project, viewid, CSCH_ENGHK_SYMBOL_NAME_TO_COMPONENT_NAME,
		&name_tmp, name, FGW_COBJ, src, FGW_INVALID);

	if (name_tmp != NULL)
		comp = csch_acomp_get(dst, name_tmp, 1);
	else if (name != NULL)
		comp = csch_acomp_get(dst, name, 1);
	else
		return 0; /* comp has no name -> do not compile */

	if (comp == NULL) {
		csch_eng_free_strmod(&name_tmp, name);
		return -1;
	}

	csch_compile_add_source(src, &comp->hdr);

	res |= compile_attributes(&comp->hdr, &src->attr, src, "component", name, 0);
	if ((src->hdr.type == CSCH_CTYPE_GRP_REF) && (src->data.ref.grp != NULL))
		res |= compile_attributes(&comp->hdr, &src->data.ref.grp->attr, src->data.ref.grp, "component", name, 1);

	csch_eng_call((csch_project_t *)sheet->hidlib.project, viewid, CSCH_ENGHK_SYMBOL_JOINED_COMPONENT,
		FGW_COBJ, src, FGW_AOBJ, comp, FGW_INVALID);

	/* compile ports */
	for(e = htip_first(&src->id2obj); e != NULL; e = htip_next(&src->id2obj, e))
		compile_port(dst, viewid, sheet, comp, e->value);

	csch_eng_free_strmod(&name_tmp, name);
	return res;
}

int csch_compile_connect_net_to(csch_anet_t **net, csch_ahdr_t *a, int allow_reconn)
{
	if (a->type == CSCH_ATYPE_PORT) {
		csch_aport_t *ap = (csch_aport_t *)a;
		csch_anet_t *new_net;

		if (ap->conn.net != NULL) {
			int allowed = 0;

			/* special case: resistor term overlap with vcc term and a wire is
			   also connected; this means we have a term-term net and a wirenet
			   and this shouldn't be considered as a reconn because the
			   term-term connection is really invisible */
			if (ap->conn.net->term_term || (*net)->term_term)
				allowed = 1;

			if (!allow_reconn && !allowed) {
				rnd_message(RND_MSG_ERROR, "csch_compile_connect_net_to(): port %s:%s already connected\n(This error is generated because multiport_net_merge is disabled in the config)\n", ap->parent->name, ap->name);
				return -1;
			}
			new_net = csch_cmp_merge_nets(*net, ap->conn.net);
			if (new_net == NULL) {
				rnd_message(RND_MSG_ERROR, "csch_compile_connect_net_to(): failed to merge nets %s and %s at port %s:%s\n", (*net)->name, ap->conn.net->name, ap->parent->name, ap->name);
				return -1;
			}
			*net = new_net;
		}
		ap->conn.net = *net;

		TODO("bus: do the same for buses and bus ports? Or do we put nets in buses?");
		vtp0_append(&(*net)->conns, a);
		return 0;
	}

	rnd_message(RND_MSG_ERROR, "csch_compile_connect_net_to(): unsupported object type\n");
	return -1;
}

int csch_compile_disconnect(csch_ahdr_t *a)
{
	if (a->type == CSCH_ATYPE_PORT) {
		csch_aport_t *ap = (csch_aport_t *)a;
		csch_anet_t *net = ap->conn.net;
		long n;

		if (net == NULL)
			return 0; /* already disconnected */

		ap->conn.net = NULL;
		for(n = 0; n < net->conns.used; n++) {
			if (net->conns.array[n] == ap) {
				vtp0_remove(&net->conns, n, 1);
				break;
			}
		}

		return 0;
	}
	rnd_message(RND_MSG_ERROR, "csch_compile_disconnect(): unsupported object type\n");
	return -1;
}


static int compile_conn(csch_abstract_t *dst, const csch_sheet_t *sheet, const csch_conn_t *src)
{
	long n;
	csch_anet_t *net = NULL;

	if (src->conn.used < 2) {
		rnd_msg_error("Ignoring conn object with %d connection(s) - needs at least 2\n", src->conn.used);
		return -1;
	}


	/* first pass: find the net */
	for(n = 0; n < src->conn.used; n++) {
		const csch_chdr_t *obj = src->conn.array[n];
		const csch_cgrp_t *grp = obj->parent;
		csch_ahdr_t *a;

		TODO("remove this:");
		if (grp == NULL) continue;

		if ((grp->hdr.type != CSCH_CTYPE_GRP) && (grp->hdr.type != CSCH_CTYPE_GRP_REF))
			continue;

		if (grp->aid.used < 1) {
			rnd_msg_error("Invalid object on the conn list (empty); conn id: #%ld\n", src->hdr.oid);
			return -1;
		}

		a = htip_get(&dst->aid2obj, grp->aid.array[0]);
		if (a == NULL) {
			rnd_msg_error("Invalid object on the conn list (NULL); conn id: #%ld\n", src->hdr.oid);
			return -1;
		}
		switch(a->type) {
			case CSCH_ATYPE_NET:
				if ((net != NULL) && (&net->hdr != a)) {
					TODO("figure how to handle network collisions");
					rnd_msg_error("Network collision\n");
					return -1;
				}
				net = (csch_anet_t *)a;
				break;
			case CSCH_ATYPE_PORT:
				break;
			default:
				rnd_msg_error("Invalid object on the conn list (%s)\n", csch_atype_name(a->type));
				return -1;
		}
	}

 /* create an anon net if there was no wirenet in the conn list; this happens
    in a term-term connection */
	if (net == NULL) {
		char tmpname[128];
		sprintf(tmpname, "anon_net_%ld", ++dst->ucnt.wirenet);
		net = csch_anet_get(dst, tmpname, 1, 1);
		if (net == NULL) {
			rnd_msg_error("failed to create anon net for terminal-terminal connection\n");
			return -1;
		}
		net->term_term = 1;
	}

	/* second pass: make the connections */
	for(n = 0; n < src->conn.used; n++) {
		const csch_chdr_t *obj = src->conn.array[n];
		const csch_cgrp_t *grp = obj->parent;
		csch_ahdr_t *a;

		TODO("remove this:");
		if (grp == NULL) continue;

		if ((grp->hdr.type != CSCH_CTYPE_GRP) && (grp->hdr.type != CSCH_CTYPE_GRP_REF))
			continue;

		if (grp->aid.used < 1)
			continue;

		a = htip_get(&dst->aid2obj, grp->aid.array[0]);
		if (a == &net->hdr)
			continue;

		if (csch_compile_connect_net_to(&net, a, CSCH_CFG(multiport_net_merge, 0)) != 0)
			return -1;
	}
	return 0;
}

static void compile_reset_abstract(csch_abstract_t *dst)
{
	/* reset uniq name counters */
	dst->ucnt.wirenet = 0;
	dst->ucnt.comp = 0;
	dst->ucnt.port = 0;
	dst->ucnt.target_tmp = 0;
}

static int csch_compile_sheet_(csch_abstract_t *dst, int viewid, const csch_sheet_t *src)
{
	int res = 0;
	const csch_chdr_t *h;

	if (src->non_graphical) {
		if ((src->non_graphical_impl == NULL) || (src->non_graphical_impl->compile_sheet == NULL)) {
			rnd_message(RND_MSG_ERROR, "Don't know how to compile non-graphical sheet %s\n", src->hidlib.fullpath);
			return -1;
		}
		return src->non_graphical_impl->compile_sheet(dst, viewid, src);
	}

	/* reset aid */
	for(h = gdl_first(&src->active); h != NULL; h = gdl_next(&src->active, h)) {
		if (csch_obj_is_grp(h)) {
			csch_cgrp_t *grp = (csch_cgrp_t *)h;
			grp->aid.used = 0;
		}
	}

	/* compile nets and components */
	for(h = gdl_first(&src->active); h != NULL; h = gdl_next(&src->active, h)) {
		csch_cgrp_t *grp = (csch_cgrp_t *)h;

		if (h->indirect)
			continue;

		if ((h->type != CSCH_CTYPE_GRP) && (h->type != CSCH_CTYPE_GRP_REF))
			continue;

		/* can use enum role here because role attribute overwrite is handled in csch_cgrp_ref_render() */
		switch(grp->role) {
			case CSCH_ROLE_WIRE_NET:   res |= compile_wire_net(dst, src, grp); break;
			case CSCH_ROLE_SYMBOL:     res |= compile_symbol(dst, viewid, src, grp); break;

			case CSCH_ROLE_TERMINAL:
				if (grp->hdr.parent == &src->direct) {
					csch_aport_t *aport = compile_port(dst, viewid, src, NULL, grp);
					TODO("hierarchic: this probably won't connect sheet port to nets; maybe we should remember sheet ports in a hash separately?");
					csch_eng_call((csch_project_t *)src->hidlib.project, viewid, CSCH_ENGHK_COMPILE_PORT, FGW_AOBJ, aport, FGW_INVALID);
				}
				break;

			case CSCH_ROLE_invalid:
			case CSCH_ROLE_empty:
			case CSCH_ROLE_BUS_NET:
			case CSCH_ROLE_BUS_TERMINAL:
			case CSCH_ROLE_HUB_POINT:
			case CSCH_ROLE_JUNCTION:
				break;
		}
	}


	csch_eng_call((csch_project_t *)src->hidlib.project, viewid, CSCH_ENGHK_CONNS_BEFORE,
		FGW_PTR, dst, FGW_INVALID);

	/* compile connections between nets and components (the "netlist" part) */
	for(h = gdl_first(&src->active); h != NULL; h = gdl_next(&src->active, h)) {
		const csch_conn_t *conn = (const csch_conn_t *)h;

		if (h->indirect)
			continue;

		if (h->type != CSCH_CTYPE_CONN)
			continue;
		res |= compile_conn(dst, src, conn);
	}

	csch_eng_call((csch_project_t *)src->hidlib.project, viewid, CSCH_ENGHK_CONNS_AFTER,
		FGW_PTR, dst, FGW_INVALID);

	return res;
}

int csch_compile_sheet(csch_abstract_t *dst, int viewid, const csch_sheet_t *src)
{
	compile_reset_abstract(dst);
	return csch_compile_sheet_(dst, viewid, src);
}

/* Returns whether the loop should be restarted because of new components added */
RND_INLINE int csch_compile_post_port(csch_project_t *proj, int viewid, csch_abstract_t *dst, csch_aport_t *port)
{
	if (port->hdr.compiled) return 0;
	port->hdr.compiled = 1;

	csch_eng_call(proj, viewid, CSCH_ENGHK_COMPILE_PORT, FGW_AOBJ, port, FGW_INVALID);

	return dst->new_ports;
}


/* Returns whether the loop should be restarted because of new components added */
RND_INLINE int csch_compile_post_comp(csch_project_t *proj, int viewid, csch_abstract_t *dst, csch_acomp_t *comp)
{
	htsp_entry_t *p;

	if (comp->hdr.compiled) return 0;
	comp->hdr.compiled = 1;

	csch_eng_call(proj, viewid, CSCH_ENGHK_COMPILE_COMPONENT1, FGW_AOBJ, comp, FGW_INVALID);

	restart_port:;
	dst->new_ports = 0;
	for(p = htsp_first(&comp->ports); p != NULL; p = htsp_next(&comp->ports, p))
		if (csch_compile_post_port(proj, viewid, dst, p->value))
			goto restart_port; /* if new components are created restart the loop because genht gets confused */

	csch_eng_call(proj, viewid, CSCH_ENGHK_COMPILE_COMPONENT2, FGW_AOBJ, comp, FGW_INVALID);
	return dst->new_comps;
}

/* Returns whether the loop should be restarted because of new components added */
RND_INLINE int csch_compile_post_net(csch_project_t *proj, int viewid, csch_abstract_t *dst, csch_anet_t *net)
{
	if (net->hdr.compiled) return 0;
	net->hdr.compiled = 1;

	csch_eng_call(proj, viewid, CSCH_ENGHK_COMPILE_NET, FGW_AOBJ, net, FGW_INVALID);

	return dst->new_nets;
}

int csch_compile_post(csch_project_t *proj, int viewid, csch_abstract_t *dst)
{
	htsp_entry_t *e;
	csch_sheet_t *sheet;

	restart_comp:;
	dst->new_comps = 0;
	for(e = htsp_first(&dst->comps); e != NULL; e = htsp_next(&dst->comps, e))
		if (csch_compile_post_comp(proj, viewid, dst, e->value))
			goto restart_comp; /* if new components are created restart the loop because genht gets confused */

	restart_net:;
	dst->new_nets = 0;
	for(e = htsp_first(&dst->nets); e != NULL; e = htsp_next(&dst->nets, e))
		if (csch_compile_post_net(proj, viewid, dst, e->value))
			goto restart_net; /* if new components are created restart the loop because genht gets confused */

	csch_text_invalidate_all_project(proj, 1);

	csch_eng_call(proj, viewid, CSCH_ENGHK_PROJECT_AFTER,
		FGW_PTR, dst, FGW_PTR, proj, FGW_INVALID);

	sheet = *vtp0_get(&proj->hdr.designs, 0, 0);
	rnd_event(&sheet->hidlib, CSCH_EVENT_PRJ_COMPILED, NULL);
	return 0;
}

int csch_compile_project(csch_project_t *prj, int viewid, csch_abstract_t *dst, int quiet)
{
	int res = 0, r;
	long n;

	compile_reset_abstract(dst);

	if (csch_project_is_partial(prj))
		rnd_message(RND_MSG_ERROR, "*** PARTIAL PROJECT ***\nNot all root sheets are loaded, the resulting abstract model\nwill be partial too.\n");

	csch_eng_call(prj, viewid, CSCH_ENGHK_PROJECT_BEFORE,
		FGW_PTR, dst, FGW_PTR, prj, FGW_INVALID);

	for(n = 0; n < vtp0_len(&prj->hdr.designs); n++) {
		csch_sheet_t *sheet = *vtp0_get(&prj->hdr.designs, n, 0);

		/* hierarchy: start from root sheets only, skip aux sheets; unlisted/unknown are rather compiled so no-project-file works */
		if (sheet->stype == CSCH_SHTY_AUX) continue;
		if ((sheet->stype == CSCH_SHTY_UNLISTED) || (sheet->stype == CSCH_SHTY_unknown)) {
			if (prj->num_root_sheets != 0) {
				rnd_message(RND_MSG_WARNING, "Compile: not compiling sheet %s: unlisted file in a project with explicit sheet list in project file\n", sheet->hidlib.loadname);
				continue;
			}
			else {
				/* implicit project file, accept and compile unlisted sheets */
			}
		}

		r = csch_compile_sheet_(dst, viewid, sheet);
		if (r != 0)
			rnd_message(RND_MSG_ERROR, "sheet #%d (%s) compilation failed: %d\n", n, sheet->hidlib.fullpath, r);
		res |= r;
	}
	csch_compile_post(prj, viewid, dst);
	return res;
}
