/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - back annotation
 *  Copyright (C) 2022 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 PET Fund in 2022)
 *
 *  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 <libcschem/abstract.h>
#include <libcschem/concrete.h>
#include <libcschem/project.h>
#include <libcschem/cnc_conn.h>
#include <libcschem/intersect.h>
#include <libcschem/util_abst.h>
#include <libcschem/util_wirenet.h>

#include <gengeo2d/cline.h>

#include "backann.h"
#include "auto_ba.h"

/*** generic/helper ***/

/* Figure if an attribute modification entry can be autofixed */
static sch_rnd_backann_auto_t *auto_attr(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba, csch_attribs_t *attrs, const char *key, const char *want, csch_ahdr_t *fallback_aobj)
{
	csch_attrib_t *attr = csch_attrib_get(attrs, key);
	sch_rnd_backann_auto_t *res;
	long n;
	int prio, found = 0, is_str;
	csch_chdr_t *cobj;
	csch_ahdr_t *aobj;
	const char *desc;

	if ((attr != NULL) && (attr->source.used >= 1)) {
		/* pick the first actual write going back from the end, ignore failed writes */
		for(n = attr->source.used-1; n >= 0; n--) {
			int pr;
			csch_attrib_src_type_t type;
			char *source = attr->source.array[n];

			pr = csch_attrib_src_parse(ctx->sheet, source, &prio, &type, &cobj, &aobj, NULL, &desc);
			if ((pr != 0) || (type & CSCH_ASRCT_FAIL))
				continue;

			found = 1;
			break;
		}

		if (!found)
			return NULL;

		is_str = (attr->val != NULL);
	}
	else { /* setting an attribute that doesn't exist -> set it on the first source object */
		aobj = fallback_aobj;
		if (aobj->srcs.used < 1)
			return NULL; /* no source object available */
		cobj = aobj->srcs.array[0];
		is_str = 1;
	}

	assert(csch_obj_is_grp(cobj));

	res = malloc(sizeof(sch_rnd_backann_auto_t));
	res->attr.type = SCH_RND_BAA_ATTR;
	res->attr.grp = (csch_cgrp_t *)cobj;
	res->attr.key = rnd_strdup(key);
	res->attr.want_val = (want == NULL) ? NULL : rnd_strdup(want);
	res->attr.is_str = is_str;

	return res;
}

void csch_get_term_conns(vtp0_t *dst, csch_cgrp_t *term)
{
	htip_entry_t *e;
	for(e = htip_first(&term->id2obj); e != NULL; e = htip_next(&term->id2obj, e)) {
		csch_chdr_t *obj = e->value;
		long n;

		for(n = 0; n < obj->conn.used; n++)
			vtp0_append(dst, obj->conn.array[n]);

		if (csch_obj_is_grp(obj))
			csch_get_term_conns(dst, (csch_cgrp_t *)obj);
	}
}

/* Figure whuch end of tl is furhter away from the center of term's bbox and
   load that into x;y */
static void term_line_outer_coord(csch_cgrp_t *term, csch_line_t *tl, csch_coord_t *x, csch_coord_t *y)
{
	csch_coord_t cx, cy;

	cx = (term->hdr.bbox.x1 + term->hdr.bbox.x2)/2;
	cy = (term->hdr.bbox.x1 + term->hdr.bbox.x2)/2;
	if (rnd_distance2(cx, cy, tl->inst.c.p1.x, tl->inst.c.p1.y) > rnd_distance2(cx, cy, tl->inst.c.p2.x, tl->inst.c.p2.y)) {
		*x = tl->inst.c.p1.x;
		*y = tl->inst.c.p1.y;
	}
	else {
		*x = tl->inst.c.p2.x;
		*y = tl->inst.c.p2.y;
	}
}

/* term is a terminal group, wn is a wirenet group, they are on the same
   sheet. Find the endpoint of the terminal to draw a wire from to
   the closest point of the wirenet. Return length^2 and fill in
   termo with the terminal object and wno with the wirenet object affected.
   Lin is csch_coord_t[4], filled in with the x1;y1;x2;y2 coords of the
   new wire to be drawn. */
double find_straight_line_between(csch_cgrp_t *term, csch_cgrp_t *wn, csch_chdr_t **termo, csch_chdr_t **wno, csch_coord_t *lin)
{
	htip_entry_t *te, *we;
	double best_len2 = 0, len2;


	assert(term->hdr.sheet == wn->hdr.sheet);
	assert(csch_obj_is_grp(&term->hdr));
	assert(csch_obj_is_grp(&wn->hdr));
	assert(term->role == CSCH_ROLE_TERMINAL);
	assert(wn->role == CSCH_ROLE_WIRE_NET);

	*termo = NULL;
	*wno = NULL;

	for(te = htip_first(&term->id2obj); te != NULL; te = htip_next(&term->id2obj, te)) {
		csch_line_t *tl = te->value;
		csch_coord_t tx, ty;
		g2d_vect_t tc, pr;

		if (tl->hdr.type != CSCH_CTYPE_LINE) continue;

		term_line_outer_coord(term, tl, &tx, &ty);
		tc.x = tx; tc.y = ty;

		for(we = htip_first(&wn->id2obj); we != NULL; we = htip_next(&wn->id2obj, we)) {
			double offs;
			csch_line_t *wl = we->value;

			if (wl->hdr.type != CSCH_CTYPE_LINE) continue;

			/* project preferred terminal end onto the wirenet line to get the other end */
			offs = g2d_project_pt_cline(tc, &wl->inst.c);
			if (offs < 0.0) offs = 0.0;
			else if (offs > 1.0) offs = 1.0;
			pr = g2d_cline_offs(&wl->inst.c, offs);
			len2 = rnd_distance2(pr.x, pr.y, tc.x, tc.y);
			if ((best_len2 == 0) || (len2 < best_len2)) {
				best_len2 = len2;
				lin[0] = tc.x; lin[1] = tc.y;
				lin[2] = pr.x; lin[3] = pr.y;
				*termo = &tl->hdr;
				*wno = &wl->hdr;
			}
		}
	}

	return best_len2;
}

/* Figure graphical connection and fill in related objects; if the connection
   exists, conn_out is filled with the conn object making the connection. 
   termo and wno are filled in with the non-group drawing objects participating
   in the connection from the terminal's or the wirenet's side, respectively.
   If newline is not NULL and there's no connection, find a terminal and a
   wirenet on the same sheet and figure a straight line that'd connect them */
static int get_term_net_gr_conn(csch_project_t *prj, const char *netname, const char *compname, const char *termname, csch_chdr_t **termo_out, csch_chdr_t **wno_out, csch_conn_t **conn_out, csch_coord_t *newline)
{
	csch_anet_t *net;
	csch_acomp_t *comp;
	csch_aport_t *port;
	long tn, cn;
	vtp0_t conns = {0};
	int resi = -1;
	csch_chdr_t *found_wire = NULL, *found_term = NULL;

	if (termo_out != NULL) *termo_out = NULL;
	if (wno_out != NULL) *wno_out = NULL;
	if (conn_out != NULL) *conn_out = NULL;

	if (prj->abst == NULL)
		return -1;

	net = csch_anet_get(prj->abst, netname);
	if (net == NULL)
		return -1;

	comp = csch_acomp_get(prj->abst, compname);
	if (comp == NULL)
		return -1;

	port = csch_aport_get(prj->abst, comp, termname, 0);
	if (port == NULL)
		return -1;

	/* collect all connection objects of all our concrete terminals, recursively */
	for(tn = 0; tn < port->hdr.srcs.used; tn++) {
		csch_chdr_t *termo = port->hdr.srcs.array[tn];
		csch_cgrp_t *termg = (csch_cgrp_t *)termo;

		if (!csch_obj_is_grp(termo) || (termg->role != CSCH_ROLE_TERMINAL))
			continue;

		rnd_trace("Found term: %p\n", termo);

		csch_get_term_conns(&conns, termg);
	}

	/* find a conn object that connects our terminal to our wirenet */
	for(cn = 0; cn < conns.used; cn++) {
		csch_chdr_t *termo = NULL;
		long on;
		csch_conn_t *conn = conns.array[cn];
		rnd_trace(" [%ld] conn %p\n", cn, conn);

		for(on = 0; on < conn->conn.used; on++) {
			csch_chdr_t *obj = conn->conn.array[on];
			if ((obj->parent->role == CSCH_ROLE_TERMINAL) && (csch_cgrp_is_in_asrc(obj->parent, &port->hdr)))
				found_term = termo = obj;
			else if ((obj->parent->role == CSCH_ROLE_WIRE_NET) && (csch_cgrp_is_in_asrc(obj->parent, &net->hdr))) {
				rnd_trace("  found wn obj %p\n", obj);
				resi = 0;
				found_wire = obj;
				if (conn_out != NULL) *conn_out = conn;
			}

			/* we are done if we have both a wire and a terminal object */
			if ((found_wire != NULL) && (found_term != NULL)) {
				if (termo_out != NULL) *termo_out = found_term;
				if (wno_out != NULL) *wno_out = found_wire;
				goto done;
			}
		}
	}

	/* no suitable existing connection found */
	if (newline != NULL) {
		double best_len2 = 0;

		/* find a pair of terminal-wirenet on the same sheet */
		for(tn = 0; tn < port->hdr.srcs.used; tn++) {
			csch_cgrp_t *term = (csch_cgrp_t *)port->hdr.srcs.array[tn];
			long w;

			for(w = 0; w < net->hdr.srcs.used; w++) {
				csch_cgrp_t *wn = (csch_cgrp_t *)net->hdr.srcs.array[w];
				if (wn->hdr.sheet == term->hdr.sheet) {
					csch_coord_t lin[4];
					csch_chdr_t *termo, *wno;
					double len2 = find_straight_line_between(term, wn, &termo, &wno, lin);
					if ((len2 > 0) && ((best_len2 == 0) || (len2 < best_len2))) {
						best_len2 = len2;
						memcpy(newline, lin, sizeof(lin));
						if (termo_out != NULL) *termo_out = termo;
						if (wno_out != NULL) *wno_out = wno;
					}
				}
			}
		}
	}

	done:;
	vtp0_uninit(&conns);
	return resi;
}


void calc_break_grconn_fixable(sch_rnd_backann_auto_t *res)
{
	csch_chdr_t *wireo = res->break_grconn.wire_obj;
	csch_chdr_t *termo = res->break_grconn.term_obj;
	csch_line_t *wirel = (csch_line_t *)wireo;
	csch_sheet_t *sheet = wireo->sheet;
	g2d_vect_t iscp[2];
	long len;
	double wlen, dx, dy;

	/* assume we can't fix this by default */
	res->break_grconn.endp = 0;

	/* can tune endpoint of line wires only */
	if (wireo->type != CSCH_CTYPE_LINE)
		return;

	/* if there is not exactly one intersections it's a complicated case */
	len = csch_obj_intersect_obj(sheet, wireo, termo, iscp, 2);
	if (len != 1)
		return;

	dx = wirel->inst.c.p1.x - wirel->inst.c.p2.x;
	dy = wirel->inst.c.p1.y - wirel->inst.c.p2.y;
	wlen = sqrt(dx*dx + dy*dy);

	/* wire too short for safe endpoint modification */
	if (wlen < 2000)
		return;

	/* iscp[0] has the wire point that should be moved away - if it's an
	   endpoint of that wire; else it's a T junction and we are not going to
	   fix that */
	if ((wirel->inst.c.p1.x == iscp[0].x) && (wirel->inst.c.p1.y == iscp[0].y)) {
		res->break_grconn.endp = 1;
		res->break_grconn.newx = wirel->inst.c.p1.x - dx / wlen * 1000;
		res->break_grconn.newy = wirel->inst.c.p1.y - dy / wlen * 1000;
/*		rnd_trace("endp1 to %ld %ld\n", res->break_grconn.newx, res->break_grconn.newy);*/
	}
	else if ((wirel->inst.c.p2.x == iscp[0].x) && (wirel->inst.c.p2.y == iscp[0].y)) {
		res->break_grconn.endp = 2;
		res->break_grconn.newx = wirel->inst.c.p2.x + dx / wlen * 1000;
		res->break_grconn.newy = wirel->inst.c.p2.y + dy / wlen * 1000;
/*		rnd_trace("endp2 to %ld %ld\n", res->break_grconn.newx, res->break_grconn.newy);*/
	}
}

/*** instructions ***/
static sch_rnd_backann_auto_t *auto_conn_del(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;
	csch_chdr_t *termo, *wno;
	csch_anet_t *net;
	csch_conn_t *conn;
	sch_rnd_backann_auto_t *res;
	int jnc_mid;
	int tricky = 0;



	if (get_term_net_gr_conn(prj, ba->value.conn_del.net, ba->value.conn_del.comp, ba->value.conn_del.term, &termo, &wno, &conn, NULL) != 0)
		return NULL;

	net = csch_anet_get(prj->abst, ba->value.conn_del.net);
	if (net == NULL)
		return NULL;

	if (net->conns.used == 1) {
		/* by now we are sure our terminal is connected to the net so this one
		   connection must be the same as net's conn list's. In this case
		   remove the whole wirenet (removal of the last connection means
		   removal of the wirenet). */
		res = calloc(sizeof(sch_rnd_backann_auto_t), 1);
		res->del.type = SCH_RND_BAA_DEL;
		res->del.obj = &wno->parent->hdr;
		return res;
	}


	csch_wire_count_junctions(wno, &jnc_mid);

	/* t1---+---t2; if removed for t1, the section between the mid junction and t2 is also broken */
	/* t1-------t2; if removed for t1, the connection to t2 is also broken */
	tricky |= (wno->conn.used > 1);

	/* t1---+---+; if removed for t1, the section between the mid junction and endpoint is also broken */
	tricky |= (jnc_mid > 0);

	if (tricky) {
		res = malloc(sizeof(sch_rnd_backann_auto_t));
		res->break_grconn.type = SCH_RND_BAA_BREAK_GRCONN;
		res->break_grconn.wire_obj = wno;
		res->break_grconn.term_obj = termo;
		calc_break_grconn_fixable(res);
	}
	else {
		res = calloc(sizeof(sch_rnd_backann_auto_t), 1);
		res->del.type = SCH_RND_BAA_DEL;
		res->del.obj = wno;
	}

	return res;
}

static sch_rnd_backann_auto_t *auto_conn_add(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;
	csch_chdr_t *termo, *wno;
	csch_conn_t *conn;
	sch_rnd_backann_auto_t *res;
	csch_coord_t newline[4];

	if (get_term_net_gr_conn(prj, ba->value.conn_del.net, ba->value.conn_del.comp, ba->value.conn_del.term, &termo, &wno, &conn, newline) == 0)
		return NULL;

	res = malloc(sizeof(sch_rnd_backann_auto_t));
	res->make_grconn.type = SCH_RND_BAA_MAKE_GRCONN;
	res->make_grconn.wire_obj = wno;
	res->make_grconn.term_obj = termo;
	res->make_grconn.x1 = newline[0];
	res->make_grconn.y1 = newline[1];
	res->make_grconn.x2 = newline[2];
	res->make_grconn.y2 = newline[3];

	return res;
}

static sch_rnd_backann_auto_t *auto_net_attr(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;
	csch_anet_t *net;

	net = csch_anet_get(prj->abst, ba->value.net_attr.net);
	if (net == NULL)
		return NULL;

	return auto_attr(ctx, ba, &net->hdr.attr, ba->value.net_attr.key, ba->value.net_attr.val, &net->hdr);
}

static sch_rnd_backann_auto_t *auto_comp_attr(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;
	csch_acomp_t *comp;

	comp = csch_acomp_get(prj->abst, ba->value.comp_attr.comp);
	if (comp == NULL)
		return NULL;

	return auto_attr(ctx, ba, &comp->hdr.attr, ba->value.comp_attr.key, ba->value.comp_attr.val, &comp->hdr);
}

static sch_rnd_backann_auto_t *auto_term_attr(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;
	csch_acomp_t *comp;
	csch_aport_t *port;

	comp = csch_acomp_get(prj->abst, ba->value.term_attr.comp);
	if (comp == NULL)
		return NULL;

	port = csch_aport_get(prj->abst, comp, ba->value.term_attr.term, 0);
	if (port == NULL)
		return NULL;

	return auto_attr(ctx, ba, &port->hdr.attr, ba->value.term_attr.key, ba->value.term_attr.val, &port->hdr);
}

static sch_rnd_backann_auto_t *auto_comp_add(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
/*	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;*/
	sch_rnd_backann_auto_t *res;

	res = malloc(sizeof(sch_rnd_backann_auto_t));
	res->log.type = SCH_RND_BAA_LOG;
	res->log.str = rnd_strdup_printf("Use the {w l} library window to place a symbol\nthen {a a} to change its name to %s\n", ba->value.comp_add.comp);

	return res;
}

static sch_rnd_backann_auto_t *auto_comp_del(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;
	csch_acomp_t *comp;
	sch_rnd_backann_auto_t *res;

	comp = csch_acomp_get(prj->abst, ba->value.comp_del.comp);
	if ((comp == NULL) || (comp->hdr.srcs.used < 1))
		return NULL;

	res = calloc(sizeof(sch_rnd_backann_auto_t), 1);
	res->del.type = SCH_RND_BAA_DEL;
	if (comp->hdr.srcs.used > 1) {
		long n;
		vtp0_enlarge(&res->del.objlist, comp->hdr.srcs.used-1);
		for(n = 0; n < comp->hdr.srcs.used; n++)
			res->del.objlist.array[n] = comp->hdr.srcs.array[n];
	}
	else
		res->del.obj = comp->hdr.srcs.array[0]; /* save an extra allocation for a common case when there's only one object */

	return res;
}

/*** API ***/

sch_rnd_backann_auto_t *sch_rnd_backann_auto_entry(sch_rnd_backann_t *ctx, sch_rnd_ba_t *ba)
{
	switch(ba->type) {
		case SCH_RND_BAT_NETINFO:     return NULL;
		case SCH_RND_BAT_CONN_DEL:    return auto_conn_del(ctx, ba);
		case SCH_RND_BAT_CONN_ADD:    return auto_conn_add(ctx, ba);
		case SCH_RND_BAT_NET_ATTR:    return auto_net_attr(ctx, ba);
		case SCH_RND_BAT_COMP_ATTR:   return auto_comp_attr(ctx, ba);
		case SCH_RND_BAT_TERM_ATTR:   return auto_term_attr(ctx, ba);
		case SCH_RND_BAT_COMP_DEL:    return auto_comp_del(ctx, ba);
		case SCH_RND_BAT_COMP_ADD:    return auto_comp_add(ctx, ba);
		case SCH_RND_BAT_invalid:     break;
	}
	return NULL;
}

void sch_rnd_backann_auto_free(sch_rnd_backann_auto_t *au)
{
	if (au == NULL) return;
	switch(au->any.type) {
		case SCH_RND_BAA_ATTR:
			free(au->attr.key);
			free(au->attr.want_val);
			break;

		case SCH_RND_BAA_LOG:
			free(au->log.str);
			break;

		case SCH_RND_BAA_DEL:
			vtp0_uninit(&au->del.objlist);
			break;

		/* no dynamic allocated fields */
		case SCH_RND_BAA_BREAK_GRCONN:
		case SCH_RND_BAA_MAKE_GRCONN:
			break;
	}
	free(au);
}

