/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - libcschem (core library)
 *  Copyright (C) 2022 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 PET Fund in 2022)
 *
 *  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
 */

/* Data integrity check */

#include "config.h"

#include <librnd/core/error.h>

#include "project.h"
#include "cnc_any_obj.h"
#include "cnc_conn.h"
#include "intersect.h"

#include "integrity.h"

int csch_integrity_auto = 1;

static void igy_brk() {}

#define ASSERT1(expr, reason, arg1) \
	do { \
		if (!(expr)) { \
			const char *sheetname = sheet->hidlib.loadname; \
			rnd_message(RND_MSG_ERROR,  "Integrity violation: " reason, arg1); \
			rnd_message(RND_MSG_ERROR,  " (" #expr ") at " CSCH_IGY_LOC  ", sheet: %s; called from: %s\n", sheetname, called_from); \
			igy_brk(); \
			res |= 1; \
		} \
	} while(0)

#define ASSERT(expr, reason) ASSERT1(expr, reason, NULL)

RND_INLINE int igy_cnc_grp(void *parent, csch_cgrp_t *grp, const char *called_from);

RND_INLINE int igy_cnc_conn(void *parent, csch_conn_t *c, const char *called_from)
{
	int res = 0, had_two_grps = 0;
	csch_sheet_t *sheet = c->hdr.sheet;
	csch_cgrp_t *first = NULL;
	long n;

	if (c->conn_path.used != 0)
		csch_conn_text2ptr(sheet, c);

	ASSERT(c->conn.used != 0, "Can't resolve connections");
	ASSERT(c->conn.used != 1, "Connection object with a single object ref");

	/* require each object to be in a wirenet or symbol */
	for(n = 0; n < c->conn.used; n++) {
		csch_chdr_t *obj = c->conn.array[n];

		ASSERT(obj != NULL, "Connection object with invalid ref");
		if (obj != NULL) {

			/* count if we have two differnet parent groups */
			if (first == NULL)
				first = obj->parent;
			else if (first != obj->parent)
				had_two_grps = 1;

			ASSERT((obj->parent != NULL), "conn with no parent");
			if (obj->parent != NULL)
				ASSERT((obj->parent->role == CSCH_ROLE_TERMINAL) || (obj->parent->role == CSCH_ROLE_WIRE_NET), "unusual parent group for connection referee");
		}
	}

	ASSERT(had_two_grps, "Connection's all ref'd objects are of the same parent group");

	return res;
}

RND_INLINE int igy_cnc_obj(csch_cgrp_t *parent, csch_chdr_t *obj, const char *called_from)
{
	int res = 0;
	csch_sheet_t *sheet = obj->sheet;

	if (parent != NULL)
		ASSERT(parent == obj->parent, "broken object parent");

	switch(obj->type) {
		case CSCH_CTYPE_LINE:
		case CSCH_CTYPE_TEXT:
		case CSCH_CTYPE_ARC:
		case CSCH_CTYPE_POLY:
		case CSCH_CTYPE_BITMAP:
		case CSCH_CTYPE_PEN:
			break;

		case CSCH_CTYPE_CONN:
			res |= igy_cnc_conn(NULL, (csch_conn_t *)obj, called_from);
			break;

		case CSCH_CTYPE_GRP:
		case CSCH_CTYPE_GRP_REF:
			res |= igy_cnc_grp(NULL, (csch_cgrp_t *)obj, called_from);
			break;

		case CSCH_CTYPE_invalid:
		case CSCH_CTYPE_max:
			ASSERT(0, "invalid object type");
	}

	return res;
}

/* Recursive intersection search: set ->mark on any object that is in the
   same group and on the CSCH_DSPLY_WIRE layer that are reachable through
   intersections from obj */
RND_INLINE void igy_wirenet_find_segs_from(csch_chdr_t *obj)
{
	csch_chdr_t *pair;
	csch_rtree_it_t it;

	/* collect all relevant intersections and mark them and schedule them for visit */
	for(pair = csch_rtree_first(&it, &obj->sheet->dsply[CSCH_DSPLY_WIRE], &obj->bbox); pair != NULL; pair = csch_rtree_next(&it)) {
		csch_chdr_t *po = (csch_chdr_t *)pair;
		if (!po->mark && (po->parent == obj->parent)) {
			if (csch_obj_intersect_obj(obj->sheet, obj, po, NULL, 0)) {
				po->mark = 1;
				po->visit = 1;
			}
		}
	}

	/* execute scheduled visits (recursion) */
	for(pair = csch_rtree_first(&it, &obj->sheet->dsply[CSCH_DSPLY_WIRE], &obj->bbox); pair != NULL; pair = csch_rtree_next(&it)) {
		csch_chdr_t *po = (csch_chdr_t *)pair;
		if (po->visit) {
			po->visit = 0;
			igy_wirenet_find_segs_from(po);
		}
	}
}

RND_INLINE int igy_wirenet(void *parent, csch_cgrp_t *wn, const char *called_from)
{
	int res = 0;
	long wnlen = htip_length(&wn->id2obj);
	csch_sheet_t *sheet = wn->hdr.sheet;

	if (wnlen > 1) {
		htip_entry_t *e;
		csch_chdr_t *obj;

		for(e = htip_first(&wn->id2obj); e != NULL; e = htip_next(&wn->id2obj, e)) {
			csch_chdr_t *obj = e->value;
			obj->mark = obj->visit = 0;
		}

		/* find the first non-floater to start from */
		obj = NULL;
		for(e = htip_first(&wn->id2obj); e != NULL; e = htip_next(&wn->id2obj, e)) {
			csch_chdr_t *o = e->value;
			if (!o->floater) {
				obj = o;
				break;
			}
		}

		if (obj != NULL) {
			obj->mark = 1;

			igy_wirenet_find_segs_from(obj);

			for(e = htip_first(&wn->id2obj); e != NULL; e = htip_next(&wn->id2obj, e)) {
				csch_chdr_t *obj = e->value;
				if (!obj->floater)
					ASSERT(obj->mark, "Disjoint wirenet");
				obj->mark = obj->visit = 0;
			}
		}
		else {
			ASSERT1(0, "wirenet #%ld conists of floaters only", wn->hdr.oid);
		}
	}
	else if (wnlen == 1) {
		htip_entry_t *e = htip_first(&wn->id2obj);
		csch_chdr_t *o = e->value;
		ASSERT1(!o->floater, "wirenet #%ld conists of floaters only", wn->hdr.oid);
	}
	else if (wnlen < 1)
		ASSERT(parent == wn->hdr.parent, "empty wirenet (invisible ghost group)");

	return res;
}

RND_INLINE int igy_cnc_grp(void *parent, csch_cgrp_t *grp, const char *called_from)
{
	int res = 0, sym_mode_direct;
	htip_entry_t *e;
	csch_sheet_t *sheet = grp->hdr.sheet;

	if (parent != NULL)
		ASSERT(parent == grp->hdr.parent, "broken group parent");

	if (grp->role == CSCH_ROLE_WIRE_NET)
		res |= igy_wirenet(parent, grp, called_from);

	if (grp->loclib_name != NULL)
		ASSERT(csch_cobj_is_indirect(&grp->hdr), "group with loclib_name set shall be under the indirect subtree");

	/* check each child object's integrity */
	for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) {
		csch_chdr_t *obj = e->value;
		ASSERT(obj->oid == e->key, "wrong ID in hash");
		res |= igy_cnc_obj(grp, obj, called_from);
	}

	if (grp->hdr.type == CSCH_CTYPE_GRP_REF) {
		ASSERT((grp->data.ref.grp != NULL) || (grp->data.ref.ref_str != NULL), "grp_ref with no referee");
		if (grp->data.ref.grp != NULL)
			ASSERT(grp->data.ref.grp->hdr.sheet == grp->hdr.sheet, "grp_ref referee is on another sheet");
	}

	/* in symbol edit mode the sheet is a dummy sheet so does not have an uuid */
	sym_mode_direct = sheet->is_symbol && (grp == &grp->hdr.sheet->direct);

	if (grp != &grp->hdr.sheet->indirect && !sym_mode_direct)
		ASSERT(!minuid_is_nil(grp->uuid), "grp without (instance) UUID");

	return res;
}

RND_INLINE int igy_cnc_direct(csch_cgrp_t *grp, const char *called_from)
{
	int res = 0;
	csch_sheet_t *sheet = grp->hdr.sheet;

	ASSERT(grp->spec_rot == 0, "sheet's direct group must not be rotated");
	ASSERT(grp->x == 0, "sheet's direct group must not be translated (x)");
	ASSERT(grp->y == 0, "sheet's direct group must not be translated (y)");
	ASSERT(grp->mirx == 0, "sheet's direct group must not be mirrored (x)");
	ASSERT(grp->miry == 0, "sheet's direct group must not be mirrored (y)");

	return res;
}

RND_INLINE int igy_cnc_rtree(csch_sheet_t *sheet, csch_rtree_t *rtree, const char *called_from)
{
	csch_rtree_it_t it;
	csch_chdr_t *o;
	int res = 0;

	for(o = csch_rtree_all_first(&it, rtree); o != NULL; o = csch_rtree_all_next(&it)) {
		ASSERT(!csch_obj_is_deleted(o), "Ghost object: presents in rtree but has been deleted");
	}

	return res;
}

RND_INLINE int igy_cnc_rtrees(csch_sheet_t *sheet, const char *called_from)
{
	int n, res = 0;
	for(n = 0; n < CSCH_DSPLY_max; n++) {
		res |= igy_cnc_rtree(sheet, &sheet->dsply[n], called_from);
		res |= igy_cnc_rtree(sheet, &sheet->dsply_fill[n], called_from);
	}
	return res;
}

RND_INLINE int igy_sheet(csch_project_t *proj, csch_sheet_t *sheet, const char *called_from)
{
	int res = 0;

	if (sheet->non_graphical)
		return 0; /* don't check them for now */

	if (proj != NULL)
		ASSERT(proj == (csch_project_t *)sheet->hidlib.project, "broken sheet parent");

	ASSERT(sheet->direct.hdr.parent == NULL, "broken direct grp parent");
	ASSERT(sheet->indirect.hdr.parent == NULL, "broken indirect grp parent");

	res |= igy_cnc_direct(&sheet->direct, called_from);

	res |= igy_cnc_grp(NULL, &sheet->direct, called_from);
	res |= igy_cnc_grp(NULL, &sheet->indirect, called_from);
	res |= igy_cnc_rtrees(sheet, called_from);

	return res;
}

int csch_integrity_sheet(csch_sheet_t *sheet, const char *called_from)
{
	return igy_sheet(NULL, sheet, called_from);
}

int csch_integrity_project(csch_project_t *proj, const char *called_from)
{
	long n;
	int res = 0;

	for(n = 0; n < proj->hdr.designs.used; n++)
		res |= igy_sheet(proj, proj->hdr.designs.array[n], called_from);


	return res;
}

static const char integrity_cookie[] = "libcschem/integrity.c";

static const char csch_acts_Integrity[] = "Integrity()";
static const char csch_acth_Integrity[] = "Run data integrity checks on current sheet.";
fgw_error_t csch_act_Integrity(fgw_arg_t *res, int argc, fgw_arg_t *argv)
{
	csch_sheet_t *sheet = CSCH_ACT_SHEET;
	RND_ACT_IRES(csch_integrity_sheet(sheet, "Integrity() action"));
	return 0;
}

static rnd_action_t csch_integrity_act_list[] = {
	{"Integrity", csch_act_Integrity, csch_acth_Integrity, csch_acts_Integrity}
};

void csch_integrity_act_init(void)
{
	RND_REGISTER_ACTIONS(csch_integrity_act_list, integrity_cookie);
}

void csch_integrity_act_uninit(void)
{
	rnd_remove_actions_by_cookie(integrity_cookie);
}

