/*
 *                            COPYRIGHT
 *
 *  pcb-rnd, interactive printed circuit board design - map junftions and 2nets
 *  Copyright (C) 2025 Tibor 'Igor2' Palinkas
 *  (Supported by NLnet NGI0 Entrust in 2025)
 *
 *  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., 31 Milk Street, # 960789 Boston, MA 02196 USA.
 *
 *  Contact:
 *    Project page: http://repo.hu/projects/pcb-rnd
 *    lead developer: http://repo.hu/projects/pcb-rnd/contact.html
 *    mailing list: pcb-rnd (at) list.repo.hu (send "subscribe")
 */

#include "config.h"

#include <librnd/core/error.h>
#include <genht/htpp.h>

#include "data.h"
#include "flag.h"
#include "find.h"
#include "obj_pstk_inlines.h"

#include "map_j2nets.h"

/* Overcome rounding errors; see htjunc_get_smart() */
#define KEY_ROUND_MASK 7

static unsigned htjunc_keyhash(pcb_j2n_jkey_t key)
{
	rnd_coord_t x = key.x | KEY_ROUND_MASK, y = key.y | KEY_ROUND_MASK;
	return jenhash(&x, sizeof(x)) ^ jenhash(&y, sizeof(y)) ^ jenhash(&key.gid, sizeof(key.gid));
}

static int htjunc_keyeq(pcb_j2n_jkey_t keya, pcb_j2n_jkey_t keyb)
{
	if ((keya.x | KEY_ROUND_MASK) != (keyb.x | KEY_ROUND_MASK)) return 0;
	if ((keya.y | KEY_ROUND_MASK) != (keyb.y | KEY_ROUND_MASK)) return 0;
	if (keya.gid != keyb.gid) return 0;
	return 1;
}


RND_INLINE pcb_j2n_junc_t *junc_alloc(pcb_j2netmap_t *map)
{
	pcb_j2n_junc_t *j = calloc(sizeof(pcb_j2n_junc_t), 1);
	gdl_append(&map->all_juncs, j, link_alljuncs);
	j->split_next = j;
	return j;
}

RND_INLINE void junc_free(pcb_j2netmap_t *map, pcb_j2n_junc_t *j)
{
	free(j);
}

RND_INLINE pcb_j2n_seg_t *seg_alloc(pcb_j2netmap_t *map)
{
	pcb_j2n_seg_t *s = calloc(sizeof(pcb_j2n_seg_t), 1);
	gdl_append(&map->all_segs, s, link_allsegs);
	return s;
}

RND_INLINE void seg_free(pcb_j2netmap_t *map, pcb_j2n_seg_t *s)
{
	free(s);
}



RND_INLINE pcb_j2n_seg_t *seg_new(pcb_j2netmap_t *map, pcb_j2n_junc_t *start, pcb_j2n_junc_t *end, pcb_any_obj_t *obj)
{
	pcb_j2n_seg_t *s = seg_alloc(map);

	gdl_append(&start->ssegs, s, jslink);
	gdl_append(&end->esegs, s, jelink);
	s->obj = obj;
	htpp_insert(&map->o2s, obj, s);
	return s;
}

RND_INLINE pcb_j2n_junc_t *j2net_junc_bump_size(pcb_j2n_junc_t *j, rnd_coord_t copdia, rnd_coord_t clrgap)
{
	rnd_coord_t clrdia = copdia + 2*clrgap;

	if (copdia > j->copdia)
		j->copdia = copdia;
	if (clrdia > j->clrdia)
		j->clrdia = clrdia;

	return j;
}

/* Junctions are stored in a hash table of KEY_ROUND_MASK*KEY_ROUND_MASK
   nanometer cells; to overcome rounding errors check all neighboring cells
   as well and snap to an existing object in a neighbor. This assumes
   no two distinct junctions will be closer than 2*KEY_ROUND_MASK.
   This should check at most 4 cells. */
pcb_j2n_junc_t *htjunc_get_smart(htjunc_t *juncs, rnd_coord_t x, rnd_coord_t y, rnd_layergrp_id_t gid)
{
	pcb_j2n_junc_t *j;
	pcb_j2n_jkey_t key, lastk = {0,0}; /* 0;0 won't match if KEY_ROUND_MASK > 0 */
	int dx, dy;

	key.gid = gid;

	for(dy = -1; dy <= +1; dy++) {
		key.y = (y+dy) | KEY_ROUND_MASK;
		if (key.y == lastk.y)
			continue; /* don't check the same y that we last checked (+dy didn't step into the next cell) */
		lastk.y = key.y;
		lastk.x = 0; /* so dx==-1 won't think that was already checked */

		for(dx = -1; dx <= +1; dx++) {
			key.x = (x+dx) | KEY_ROUND_MASK;
			if (key.x == lastk.x)
				continue; /* don't check the same x that we last checked (+dx didn't step into the next cell) */
			lastk.x = key.x;
			j = htjunc_get(juncs, key);
			if (j != NULL)
				return j;
		}
	}
	return NULL;
}

RND_INLINE pcb_j2n_junc_t *j2net_junc_add_obj(pcb_j2netmap_t *map, pcb_any_obj_t *obj, rnd_coord_t x, rnd_coord_t y, rnd_layergrp_id_t gid, int set_junc_obj, rnd_coord_t copdia, rnd_coord_t clrgap)
{
	pcb_j2n_junc_t *j, *other;


	j = htjunc_get_smart(&map->juncs, x, y, gid);
	if (j != NULL) {
		if (set_junc_obj) {
			if (j->obj != NULL) {
				if (j->obj != obj) {
					rnd_message(RND_MSG_ERROR, "j2net map error: multiple junction objects at %$mm %$mm: #%ld and #%ld\n", x, y, j->obj->ID, obj->ID);
					map->error = 1;
				}
				/* else multiple levels of a padstack: */
				return j2net_junc_bump_size(j, copdia, clrgap);
			}
			j->obj = obj;
		}
	}
	else {
		htjunc_entry_t *e;
		pcb_j2n_jkey_t key;

		key.x = x; key.y = y; key.gid = gid;
		j = junc_alloc(map);
		htjunc_insert(&map->juncs, key, j);

		e = htjunc_getentry(&map->juncs, key);
		memcpy(&j->key, &e->key, sizeof(e->key));

		if (set_junc_obj) {
			j->obj = obj;
			other = htpp_get(&map->o2j, obj);
			if (other != NULL) {
				/* link in an existing junction group */
				j->split_next = other->split_next;
				other->split_next = j;
			}
			else
				htpp_insert(&map->o2j, obj, j);
		}
		else
			j->obj = NULL;
	}

	rnd_trace("junc: %mm %mm %ld (gid=%p) obj=#%ld\n", x, y, gid, j, obj->ID);
	return j2net_junc_bump_size(j, copdia, clrgap);
}

RND_INLINE void j2net_shape_geo(pcb_pstk_tshape_t *ts, rnd_coord_t *copdia, rnd_coord_t *clrgap)
{
	TODO("figure values from ts");
	*copdia = *clrgap = 0;
}

RND_INLINE void j2net_found_pstk(pcb_j2netmap_t *map, pcb_pstk_t *ps)
{
	pcb_layer_stack_t *stack = &map->pcb->LayerGroups;
	rnd_layergrp_id_t gid, top, bot;
	pcb_bb_type_t bbty;
	int n, found, has_copper;
	pcb_pstk_tshape_t *ts;
	rnd_coord_t copdia, clrgap;

	bbty = pcb_pstk_bbspan(map->pcb, ps, &top, &bot, NULL);
	switch(bbty) {
		case PCB_BB_NONE: /* no drill; could be smd */

			/* get the cache generated for cheap repeated search for top/bottom copper group */
			if (!stack->cache.copper_valid)
				pcb_layergrp_copper_cache_update(stack);

			if (stack->cache.copper_len <= 0) {
				rnd_message(RND_MSG_ERROR, "j2net map error: board with no copper layers?!\n");
				map->error = 1;
				break;
			}

			ts = pcb_pstk_get_tshape(ps);
			for(n = found = has_copper = 0; n < ts->len; n++) {
				if (ts->shape[n].layer_mask & PCB_LYT_COPPER) {
					has_copper = 1;
					if (ts->shape[n].layer_mask & PCB_LYT_TOP) {
						j2net_shape_geo(ts, &copdia, &clrgap);
						j2net_junc_add_obj(map, (pcb_any_obj_t *)ps, ps->x, ps->y, stack->cache.copper[0], 1, copdia, clrgap);
						found = 1;
						break;
					}
					if (ts->shape[n].layer_mask & PCB_LYT_BOTTOM) {
						j2net_shape_geo(ts, &copdia, &clrgap);
						j2net_junc_add_obj(map, (pcb_any_obj_t *)ps, ps->x, ps->y, stack->cache.copper[stack->cache.copper_len-1], 1, copdia, clrgap);
						found = 1;
						break;
					}
				}
			}
			if (has_copper && !found) {
				rnd_message(RND_MSG_ERROR, "j2net map error: padstack #%ld has only intern copper, sounds like a bad idea\n", ps->ID);
				map->error = 1;
			}
			break;

		case PCB_BB_THRU: /* all way thru */
			top = 0;
			bot = stack->len;
			/* intentional fall-thru for mapping all copper groups */

		case PCB_BB_BB:
			for(gid = top; (gid < bot) && (gid < stack->len); gid++)
				if (stack->grp[gid].ltype & PCB_LYT_COPPER) {
					j2net_shape_geo(ts, &copdia, &clrgap);
					j2net_junc_add_obj(map, (pcb_any_obj_t *)ps, ps->x, ps->y, gid, 1, copdia, clrgap);
				}
			break;

		case PCB_BB_INVALID:
			rnd_message(RND_MSG_ERROR, "j2net map error: padstack #%ld has invalid hole span\n", ps->ID);
			map->error = 1;
			break;
	}
}

RND_INLINE void j2net_found_line(pcb_j2netmap_t *map, pcb_line_t *line)
{
	rnd_layergrp_id_t gid;
	pcb_j2n_junc_t *js, *je;

	gid = pcb_layer_get_group_(line->parent.layer);
	js = j2net_junc_add_obj(map, (pcb_any_obj_t *)line, line->Point1.X, line->Point1.Y, gid, 0, line->Thickness, line->Clearance);
	je = j2net_junc_add_obj(map, (pcb_any_obj_t *)line, line->Point2.X, line->Point2.Y, gid, 0, line->Thickness, line->Clearance);
	seg_new(map, js, je, (pcb_any_obj_t *)line);
}

RND_INLINE void j2net_found_arc(pcb_j2netmap_t *map, pcb_arc_t *arc)
{
	rnd_layergrp_id_t gid;
	pcb_j2n_junc_t *js, *je;
	rnd_coord_t x, y;

	gid = pcb_layer_get_group_(arc->parent.layer);

	pcb_arc_get_end(arc, 0, &x, &y);
	js = j2net_junc_add_obj(map, (pcb_any_obj_t *)arc, x, y, gid, 0, arc->Thickness, arc->Clearance);

	pcb_arc_get_end(arc, 1, &x, &y);
	je = j2net_junc_add_obj(map, (pcb_any_obj_t *)arc, x, y, gid, 0, arc->Thickness, arc->Clearance);

	seg_new(map, js, je, (pcb_any_obj_t *)arc);
}

RND_INLINE void j2net_found_rat(pcb_j2netmap_t *map, pcb_rat_t *rat)
{
	pcb_line_t *line = (pcb_line_t *)rat;
	rnd_layergrp_id_t gid = -1;
	pcb_j2n_junc_t *js, *je;

	js = j2net_junc_add_obj(map, (pcb_any_obj_t *)line, line->Point1.X, line->Point1.Y, gid, 0, 0, 0);
	je = j2net_junc_add_obj(map, (pcb_any_obj_t *)line, line->Point2.X, line->Point2.Y, gid, 0, 0, 0);
	seg_new(map, js, je, (pcb_any_obj_t *)line);
}

static int j2net_found_cb(pcb_find_t *ctx, pcb_any_obj_t *new_obj, pcb_any_obj_t *arrived_from, pcb_found_conn_type_t ctype)
{
	pcb_j2netmap_t *map = ctx->user_data;

	/* make sure each object is mapped only once */
	if (PCB_DFLAG_TEST(&new_obj->Flags, map->mapped))
		return 0;
	PCB_DFLAG_SET(&new_obj->Flags, map->mapped);

	rnd_trace("j2net found: #%ld -> #%ld\n", arrived_from == NULL ? 0 : arrived_from->ID, new_obj->ID);

	switch(new_obj->type) {
		case PCB_OBJ_PSTK: j2net_found_pstk(map, (pcb_pstk_t *)new_obj); break;
		case PCB_OBJ_LINE: j2net_found_line(map, (pcb_line_t *)new_obj); break;
		case PCB_OBJ_ARC:  j2net_found_arc(map, (pcb_arc_t *)new_obj); break;
		case PCB_OBJ_POLY:
			TODO("handle this\n");
			break;

		case PCB_OBJ_RAT:
			if (map->find_rats)
				j2net_found_rat(map, (pcb_rat_t *)new_obj); break;
			break;

		case PCB_OBJ_TEXT:
			rnd_message(RND_MSG_ERROR, "j2net map error: text #%ld is making connection, can't handle that\n", new_obj->ID);
			map->error = 1;
			break;

		case PCB_OBJ_SUBC:
		case PCB_OBJ_GFX:
		case PCB_OBJ_NET:
		case PCB_OBJ_NET_TERM:
		case PCB_OBJ_LAYER:
		case PCB_OBJ_LAYERGRP:
			break; /* not handled as they can't make connection */
	}

	TODO("if there's an overlap but no enpoint match: return error or fix it")

	return 0; /* continue the search */
}

RND_INLINE void j2nets_find_from_obj(pcb_find_t *fctx, pcb_data_t *data, pcb_any_obj_t *from)
{
	if (PCB_DFLAG_TEST(&from->Flags, fctx->mark) != 0)
		return; /* already found */

	if (!fctx->in_use)
		pcb_find_from_obj(fctx, data, from);
	else
		pcb_find_from_obj_next(fctx, data, from);
}

static void j2nets_find_from_all(pcb_find_t *fctx, pcb_data_t *data)
{
	pcb_pstk_t *ps;
	pcb_subc_t *subc;
	rnd_layer_id_t lid;

	pcb_data_dynflag_clear(data, fctx->mark);

	for(ps = padstacklist_first(&data->padstack); ps != NULL; ps = ps->link.next)
		j2nets_find_from_obj(fctx, data, (pcb_any_obj_t *)ps);

	for(lid = 0; lid < data->LayerN; lid++) {
		pcb_layer_t *ly = &(data->Layer[lid]);
		unsigned int lyt = pcb_layer_flags_(ly);
		pcb_line_t *line;
		pcb_arc_t *arc;

		if (lyt & PCB_LYT_COPPER) {
			for(line = linelist_first(&ly->Line); line != NULL; line = line->link.next)
				j2nets_find_from_obj(fctx, data, (pcb_any_obj_t *)line);
			for(arc = arclist_first(&ly->Arc); arc != NULL; arc = arc->link.next)
				j2nets_find_from_obj(fctx, data, (pcb_any_obj_t *)arc);
			TODO("search from polygons and text")
		}
	}

	/* no need to search from rats: rats are from terminal to terminal and
	   terminals are searched anyway */

	/* recurse in subcircuits for copper objects */
	for(subc = pcb_subclist_first(&data->subc); subc != NULL; subc = subc->link.next)
		j2nets_find_from_all(fctx, subc->data);

	pcb_data_dynflag_clear(data, fctx->mark);
}

int pcb_map_j2nets_init(pcb_j2netmap_t *map, pcb_board_t *pcb)
{
	pcb_find_t fctx = {0};

	map->pcb = pcb;
	htjunc_init(&map->juncs, htjunc_keyhash, htjunc_keyeq);
	memset(&map->all_juncs, 0, sizeof(map->all_juncs));
	memset(&map->all_segs, 0, sizeof(map->all_segs));
	map->error = 0;
	htpp_init(&map->o2j, ptrhash, ptrkeyeq);
	htpp_init(&map->o2s, ptrhash, ptrkeyeq);

	fctx.found_cb = j2net_found_cb;
	fctx.user_data = map;

	map->mapped = pcb_dynflag_alloc("mapped");
	pcb_data_dynflag_clear(pcb->Data, map->mapped);

	j2nets_find_from_all(&fctx, pcb->Data);

	pcb_find_free(&fctx);
	pcb_data_dynflag_clear(pcb->Data, map->mapped);
	pcb_dynflag_free(map->mapped);

	return map->error;
}


int pcb_map_j2nets_uninit(pcb_j2netmap_t *map)
{
	pcb_j2n_seg_t *s, *sn;
	pcb_j2n_junc_t *j, *jn;

	htjunc_uninit(&map->juncs); /* no need to free any data here, the hash table is not owning any */
	htpp_uninit(&map->o2j);
	htpp_uninit(&map->o2s);

	for(s = gdl_first(&map->all_segs); s != NULL; s = sn) {
		sn = s->link_allsegs.next;
		seg_free(map, s);
	}
	for(j = gdl_first(&map->all_juncs); j != NULL; j = jn) {
		jn = j->link_alljuncs.next;
		junc_free(map, j);
	}
	return 0;
}

/*** crawl ***/

RND_INLINE pcb_j2n_junc_t *seg2junc(pcb_j2n_seg_t *seg, int end)
{
	if (end == 0)
		return (pcb_j2n_junc_t *)((char *)seg->jslink.parent - (char *)offsetof(pcb_j2n_junc_t, ssegs));

	return (pcb_j2n_junc_t *)((char *)seg->jelink.parent - (char *)offsetof(pcb_j2n_junc_t, esegs));
}

/* A virtual junction has exactly two segs connected and has no junction object */
RND_INLINE int junc_is_virtual(pcb_j2n_junc_t *j)
{
	if (j->obj != NULL)
		return 0;

	return (j->ssegs.length + j->esegs.length) == 2;
}

/* pick the segment connected to j that differs from not_s; when found,
   sets n to the end of the object that connects to j */
RND_INLINE pcb_j2n_seg_t *junc_other_seg(pcb_j2n_junc_t *j, pcb_j2n_seg_t *not_s, int *end)
{
	pcb_j2n_seg_t *s;

	for(s = gdl_first(&j->ssegs); s != NULL; s = s->jslink.next) {
		if (s != not_s) {
			*end = 0;
			return s;
		}
	}

	for(s = gdl_first(&j->esegs); s != NULL; s = s->jelink.next) {
		if (s != not_s) {
			*end = 1;
			return s;
		}
	}

	return NULL;
}

/* Starting from one of the endpoints of the "start" segment (endpoint selected
   by "end"), crawl the twonet, jumping over virtual junctions until reaching
   a terminal or unterminated end. If force_virtual is 1, start even from
   virtual (non-end) junctions - useful for mapping loops that have no end */
static void crawl_from_seg(pcb_j2netmap_t *map, const pcb_j2nets_crawl_t *cr, pcb_j2n_seg_t *start, int end, int force_virtual)
{
	pcb_j2n_junc_t *j, *startj;
	pcb_j2n_seg_t *s;
	pcb_any_obj_t *prev_obj = NULL;

	if (start->crawled)
		return;

	/* get start junction; refuse to start from a virtual junction (that'd be
	   starting from the middle of a twonet) */
	startj = j = seg2junc(start, end);
	if (!force_virtual && junc_is_virtual(j))
		return;

	if (cr->twonet_begin != NULL)
		cr->twonet_begin(map, j);

	if (j->obj != NULL)
		cr->twonet_obj(map, j->obj, -1, j);
	else if (cr->twonet_vjunct != NULL)
		cr->twonet_vjunct(map, start->obj, j);


	for(s = start;;) {
		if (map->grbs_law && (prev_obj != NULL)) {
			if (prev_obj->type == s->obj->type) {
				if (s->obj->type == PCB_OBJ_LINE) {
					/* line-line: break the two-net at the junction */

					if (cr->twonet_vjunct != NULL)
						cr->twonet_vjunct(map, prev_obj, j);
					if (cr->twonet_end != NULL)
						cr->twonet_end(map, j);
					if (cr->twonet_begin != NULL)
						cr->twonet_begin(map, j);
					if (cr->twonet_vjunct != NULL)
						cr->twonet_vjunct(map, s->obj, j);
				}
				if (s->obj->type == PCB_OBJ_ARC) {
					rnd_message(RND_MSG_ERROR, "j2net map error: arc-arc intersection\n");
					map->error = 1;
					return;
				}
			}
		}
		prev_obj = s->obj;

		cr->twonet_obj(map, s->obj, (end != 0), NULL);

		/* jump to other end of the segment */
		end = !end;

		s->crawled = 1;
		j = seg2junc(s, end);
/*		rnd_trace("_ seg hop #%ld to junc obj #%ld\n", s->obj->ID, j->obj == NULL ? -1 : j->obj->ID);*/

		if (!junc_is_virtual(j) || (j == startj)) {
			/* a real junction - terminates two-net; report junction */
			if (j->obj != NULL)
				cr->twonet_obj(map, j->obj, -1, j);
			else if (cr->twonet_vjunct != NULL)
				cr->twonet_vjunct(map, s->obj, j);
			break;
		}


		s = junc_other_seg(j, s, &end);
		if (s == NULL) {
			/* junction with a single object: end of twonet */
			if (cr->twonet_vjunct != NULL)
				cr->twonet_vjunct(map, s->obj, j);
			break;
		}
	}

	if (cr->twonet_end != NULL)
		cr->twonet_end(map, j);
}


int pcb_map_j2nets_crawl(pcb_j2netmap_t *map, const pcb_j2nets_crawl_t *cr)
{
	pcb_j2n_seg_t *s;
	pcb_j2n_junc_t *j;

	for(s = gdl_first(&map->all_segs); s != NULL; s = s->link_allsegs.next)
		s->crawled = 0;

	/* pick up end-to-end two-nets */
	for(j = gdl_first(&map->all_juncs); j != NULL; j = j->link_alljuncs.next) {
		for(s = gdl_first(&j->ssegs); s != NULL; s = s->jslink.next)
			crawl_from_seg(map, cr, s, 0, 0);
		for(s = gdl_first(&j->esegs); s != NULL; s = s->jelink.next)
			crawl_from_seg(map, cr, s, 1, 0);
	}

	/* remaining two-nets are loops, pick them up starting from any virtual junction */
	for(j = gdl_first(&map->all_juncs); j != NULL; j = j->link_alljuncs.next) {
		for(s = gdl_first(&j->ssegs); s != NULL; s = s->jslink.next)
			crawl_from_seg(map, cr, s, 0, 1);
		for(s = gdl_first(&j->esegs); s != NULL; s = s->jelink.next)
			crawl_from_seg(map, cr, s, 1, 1);
	}

	return 0;
}


#define HT(x) htjunc_ ## x
#include <genht/ht.c>
#undef HT


