/*
 *                            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., 31 Milk Street, # 960789 Boston, MA 02196 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 <librnd/hid/hid_dad_tree.h>

#include <libcschem/actions_csch.h>
#include <libcschem/attrib.h>
#include <libcschem/project.h>
#include <libcschem/operation.h>
#include <libcschem/util_wirenet.h>

static void backann_dlg_close_cb(void *caller_data, rnd_hid_attr_ev_t ev)
{
	sch_rnd_backann_t *ctx = caller_data;

	RND_DAD_FREE(ctx->dlg);
	sch_rnd_backann_uninit(ctx);
}

static char *backann_entry_status(sch_rnd_ba_t *ba)
{
	const char *stname = "<inv>";
	switch(ba->status) {
		case SCH_RND_BAS_UNKNOWN: stname = "n/a"; break;
		case SCH_RND_BAS_PENDING: stname = "PEND"; break;
		case SCH_RND_BAS_DONE:    stname = "done"; break;
	}
	return rnd_strdup(stname);
}

static char *backann_sprint_entry(sch_rnd_ba_t *ba)
{
	switch(ba->type) {
		case SCH_RND_BAT_NETINFO:
			return rnd_strdup_printf("net info: net %s connects to comp %s term %s%s", ba->value.netinfo.net, ba->value.netinfo.comp, ba->value.netinfo.term, (ba->optional ? " OPTIONAL" : ""));
		case SCH_RND_BAT_CONN_DEL:
			return rnd_strdup_printf("delete conn: net %s and comp %s term %s", ba->value.conn_del.net, ba->value.conn_del.comp, ba->value.conn_del.term);
		case SCH_RND_BAT_CONN_ADD:
			return rnd_strdup_printf("create conn: net %s and comp %s term %s", ba->value.conn_add.net, ba->value.conn_add.comp, ba->value.conn_add.term);
		case SCH_RND_BAT_NET_ATTR:
			return rnd_strdup_printf("set attr: net %s and %s=%s", ba->value.net_attr.net, ba->value.net_attr.key, ba->value.net_attr.val);
		case SCH_RND_BAT_COMP_ATTR:
			return rnd_strdup_printf("set attr: component %s and %s=%s", ba->value.comp_attr.comp, ba->value.comp_attr.key, ba->value.comp_attr.val);
		case SCH_RND_BAT_TERM_ATTR:
			return rnd_strdup_printf("set attr: comp %s term %s and %s=%s", ba->value.term_attr.comp, ba->value.term_attr.term, ba->value.term_attr.key, ba->value.term_attr.val);
		case SCH_RND_BAT_COMP_DEL:
			return rnd_strdup_printf("delete comp %s", ba->value.comp_del.comp);
		case SCH_RND_BAT_COMP_ADD:
			return rnd_strdup_printf("create comp %s", ba->value.comp_add.comp);
		case SCH_RND_BAT_invalid: break;
	}
	return rnd_strdup_printf("Invalid entry (%ld)", ba->type);
}

static void backann_data2dlg(sch_rnd_backann_t *ctx)
{
	rnd_hid_attribute_t *attr;
	rnd_hid_tree_t *tree;
	rnd_hid_row_t *r;
	char *cursor_path = NULL;
	char *cell[4];
	long n;

	attr = &ctx->dlg[ctx->wlist];
	tree = attr->wdata;

	/* remember cursor path */
	r = rnd_dad_tree_get_selected(attr);
	if (r != NULL)
		cursor_path = rnd_strdup(r->path);

	rnd_dad_tree_clear(tree);
	for(n = 0; n < ctx->list.used; n++) {
		sch_rnd_ba_t *ba = &ctx->list.array[n];

		if (ba->type == SCH_RND_BAT_NETINFO)
			continue;

		cell[0] = rnd_strdup_printf("%ld", n);
		cell[1] = backann_entry_status(ba);
		cell[2] = backann_sprint_entry(ba);
		cell[3] = NULL;
		r = rnd_dad_tree_append_under(attr, NULL, cell);
		r->user_data2.lng = n;
	}


	/* restore cursor */
	if (cursor_path != NULL) {
		rnd_hid_attr_val_t hv;
		hv.str = cursor_path;
		rnd_gui->attr_dlg_set_value(ctx->dlg_hid_ctx, ctx->wlist, &hv);
		free(cursor_path);
	}
}

static void ba_select_cb(rnd_hid_attribute_t *attrib, void *hid_ctx, rnd_hid_row_t *row)
{
/*	rnd_hid_tree_t *tree = attrib->wdata;
	sch_rnd_backann_t *ctx = tree->user_ctx;*/
}

static int sch_rnd_backann_ensure_abst(sch_rnd_backann_t *ctx)
{
	csch_project_t *prj = (csch_project_t *)ctx->sheet->hidlib.project;
	if (prj->abst == NULL)
		rnd_actionva(&ctx->sheet->hidlib, "CompileProject", NULL);
	if (prj->abst != NULL)
		return 0;
	return -1;
}

#define BTN_GET_BA_AU \
	rnd_hid_attribute_t *attr = &ctx->dlg[ctx->wlist]; \
	rnd_hid_row_t *r = rnd_dad_tree_get_selected(attr); \
	sch_rnd_ba_t *ba = NULL; \
	sch_rnd_backann_auto_t *au; \
	long baid; \
	if (r == NULL) { \
		rnd_message(RND_MSG_ERROR, "Select a back annotation item first!\n"); \
		return; \
	} \
	baid = r->user_data2.lng; \
	if ((baid >= 0) && (baid < ctx->list.used)) \
		ba = &ctx->list.array[baid]; \
	if (ba == NULL) { \
		rnd_message(RND_MSG_ERROR, "Internal error: invalid ba index\n"); \
		return; \
	} \
	if (sch_rnd_backann_ensure_abst(ctx) != 0) { \
		rnd_message(RND_MSG_ERROR, "Failed to compile the project; can't do much without an abstract model!\n"); \
		return; \
	} \
	au = sch_rnd_backann_auto_entry(ctx, ba); \
	if (au == NULL) { \
		rnd_message(RND_MSG_ERROR, "Can not figure this entry (backann_auto_entry failed)\n"); \
		return; \
	}

static void append_print_obj(gds_t *dst, csch_chdr_t *obj)
{
	csch_oidpath_t oidp = {0};

	gds_append_str(dst, "object:");
	csch_oidpath_from_obj(&oidp, obj);
	csch_oidpath_to_str_append(dst, &oidp);
	csch_oidpath_free(&oidp);
}

static void details_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr_btn)
{
	sch_rnd_backann_t *ctx = caller_data;
	BTN_GET_BA_AU;

	switch(au->any.type) {
		case SCH_RND_BAA_ATTR:
			if (au->attr.grp != NULL) {
				gds_t tmp = {0};
				csch_oidpath_t oidp = {0};
				fgw_arg_t args[4], ares;
				csch_sheet_t *sheet = au->attr.grp->hdr.sheet;

				gds_append_str(&tmp, "object:");
				csch_oidpath_from_obj(&oidp, &au->attr.grp->hdr);
				csch_oidpath_to_str_append(&tmp, &oidp);
				csch_oidpath_free(&oidp);

				args[1].type = FGW_STR | FGW_DYN; args[1].val.str = tmp.array;
				args[2].type = FGW_STR; args[2].val.cstr = au->attr.key;
				rnd_actionv_bin(&sheet->hidlib, "AttributeDialog", &ares, 3, args);
				fgw_arg_free(&rnd_fgw, &ares);
				
				/* tmp is free'd because it's been passed with FGW_DYN */
			}
			else
				rnd_message(RND_MSG_ERROR, "Can not find the object whose attributes are to be edited\n");
			break;

		case SCH_RND_BAA_DEL:
			if ((au->del.obj != NULL) || (au->del.objlist.used > 0)) {
				if (au->del.obj != NULL) {
					gds_t tmp = {0};
					fgw_arg_t args[4], ares;
					csch_sheet_t *sheet = au->del.obj->sheet;

					append_print_obj(&tmp, au->del.obj);

					args[1].type = FGW_STR | FGW_DYN; args[1].val.str = tmp.array;
					rnd_actionv_bin(&sheet->hidlib, "TreeDialog", &ares, 2, args);
					fgw_arg_free(&rnd_fgw, &ares);
					/* tmp is free'd because it's been passed with FGW_DYN */
				}
				if (au->del.objlist.used > 0) {
					fgw_arg_t args[4], ares;
					csch_chdr_t *obj = au->del.objlist.array[0];
					csch_sheet_t *sheet = obj->sheet;

					args[1].type = FGW_STR; args[1].val.str = "objarr";
					fgw_ptr_reg(&rnd_fgw, &args[2], CSCH_PTR_DOMAIN_COBJ_ARR, FGW_PTR | FGW_STRUCT, &au->del.objlist);
					rnd_actionv_bin(&sheet->hidlib, "TreeDialog", &ares, 3, args);
					fgw_ptr_unreg(&rnd_fgw, &args[2], CSCH_PTR_DOMAIN_COBJ_ARR);
				}
			}
			else
				rnd_message(RND_MSG_ERROR, "Can not find the object to delete\n");
			break;

		case SCH_RND_BAA_BREAK_GRCONN:
			if (au->break_grconn.wire_obj != NULL) {
				gds_t tmp = {0};
				csch_oidpath_t oidp = {0};
				fgw_arg_t args[4], ares;
				csch_sheet_t *sheet = au->break_grconn.wire_obj->sheet;

				gds_append_str(&tmp, "object:");
				csch_oidpath_from_obj(&oidp, au->break_grconn.wire_obj);
				csch_oidpath_to_str_append(&tmp, &oidp);
				csch_oidpath_free(&oidp);

				args[1].type = FGW_STR | FGW_DYN; args[1].val.str = tmp.array;
				rnd_actionv_bin(&sheet->hidlib, "TreeDialog", &ares, 2, args);
				fgw_arg_free(&rnd_fgw, &ares);
				/* tmp is free'd because it's been passed with FGW_DYN */
			}
			else
				rnd_message(RND_MSG_ERROR, "Can not find the wire to disconnect\n");
			break;

		case SCH_RND_BAA_MAKE_GRCONN:
			if (au->make_grconn.term_obj != NULL) {
				gds_t tmp = {0};
				csch_oidpath_t oidp = {0};
				fgw_arg_t args[4], ares;
				csch_sheet_t *sheet = au->make_grconn.term_obj->sheet;

				gds_append_str(&tmp, "object:");
				csch_oidpath_from_obj(&oidp, au->break_grconn.term_obj);
				csch_oidpath_to_str_append(&tmp, &oidp);
				csch_oidpath_free(&oidp);

				args[1].type = FGW_STR | FGW_DYN; args[1].val.str = tmp.array;
				rnd_actionv_bin(&sheet->hidlib, "TreeDialog", &ares, 2, args);
				fgw_arg_free(&rnd_fgw, &ares);
				/* tmp is free'd because it's been passed with FGW_DYN */
			}
			else
				rnd_message(RND_MSG_ERROR, "Can not find the terminal to connect\n");
			break;

		case SCH_RND_BAA_LOG:
			rnd_message(RND_MSG_ERROR, "Tips for this entry:\n%s\n", au->log.str);
			break;


		default:
			rnd_message(RND_MSG_ERROR, "Can not figure this entry (operation not handled in details_cb switch)\n");
	}

	sch_rnd_backann_auto_free(au);
}

typedef enum { HOR, VER, PT, OTHER } dir_t;

/* Returns if a line is horizontal, vertical, a point or other (non-line or diag) */
static dir_t line_dir(csch_line_t *lin)
{
	int h, v;

	if (lin->hdr.type != CSCH_CTYPE_LINE)
		return OTHER;


	h = (lin->inst.c.p1.y == lin->inst.c.p2.y);
	v = (lin->inst.c.p1.x == lin->inst.c.p2.x);

	if (h && v) return PT;
	if (h) return HOR;
	if (v) return VER;
	return OTHER;
}

/* Draw two wires, x1;y1 -> x2;y2 -> x3;y3 if they are safe */
static int draw_L_wires_(csch_sheet_t *sheet, csch_coord_t x1, csch_coord_t y1, csch_coord_t x2, csch_coord_t y2, csch_coord_t x3, csch_coord_t y3)
{
	csch_comm_str_t pen;

	/* colinear -> return error so straight line is drawn */
	if ((x1 == x2) && (x1 == x3))
		return -1;
	if ((y1 == y2) && (y1 == y3))
		return -1;

	if (!csch_is_wireline_safe(sheet, x1, y1, x2, y2))
		return -1;
	if (!csch_is_wireline_safe(sheet, x2, y2, x3, y3))
		return -1;

	pen = csch_comm_str(sheet, "wire", 1);
	csch_wirenet_draw(sheet, pen, x1, y1, x2, y2);
	csch_wirenet_draw(sheet, pen, x2, y2, x3, y3);
	return 0;
}

/* Attempt to draw an L shaped wire pair from mk's term (choose most fitting
   direction). Returns -1 if not found/drawn */
static int draw_L_wires(sch_rnd_backann_auto_make_grconn_t *mk)
{
	csch_sheet_t *sheet = mk->term_obj->sheet;
	dir_t dir1 = line_dir((csch_line_t *)mk->term_obj);
	dir_t dir2 = line_dir((csch_line_t *)mk->wire_obj);

	if ((dir1 == HOR) && (dir2 == HOR)) return -1; /* go for a single straight line */
	if ((dir1 == VER) && (dir2 == VER)) return -1; /* go for a single straight line */

	/* try to leave terminal in the same direction as the terminal is */
	if (dir1 == HOR)
		return draw_L_wires_(sheet, mk->x1, mk->y1, mk->x2, mk->y1, mk->x2, mk->y2);

	if (dir1 == VER)
		return draw_L_wires_(sheet, mk->x1, mk->y1, mk->x1, mk->y2, mk->x2, mk->y2);

	return -1; /* terminal is not a line */
}

static void autofix_cb(void *hid_ctx, void *caller_data, rnd_hid_attribute_t *attr_btn)
{
	sch_rnd_backann_t *ctx = caller_data;
	BTN_GET_BA_AU;

	switch(au->any.type) {
		case SCH_RND_BAA_ATTR:
			if (au->attr.grp == NULL) {
				rnd_message(RND_MSG_ERROR, "Can not modify derived attribute\n");
				break;
			}
			if (au->attr.is_str) {
				csch_sheet_t *sheet = au->attr.grp->hdr.sheet;
				csch_source_arg_t *source = csch_attrib_src_c(NULL, 0, 0, "back annotation autofix");

				if (au->attr.want_val != NULL)
					csch_attr_modify_str(sheet, au->attr.grp, -1, au->attr.key, au->attr.want_val, source, 1);
				else
					csch_attr_modify_del(sheet, au->attr.grp, au->attr.key, 1);
			}
			else
				rnd_message(RND_MSG_ERROR, "Can not modify array type attribute\n");
			break;

		case SCH_RND_BAA_DEL:
			if (au->del.obj != NULL) {
				if (!csch_obj_is_deleted(au->del.obj))
					csch_op_remove(au->del.obj->sheet, au->del.obj);
			}
			if (au->del.objlist.used > 0) {
				long n;
				for(n = 0; n < au->del.objlist.used; n++) {
					csch_chdr_t *obj = au->del.objlist.array[n];
					if (!csch_obj_is_deleted(obj))
						csch_op_remove(obj->sheet, obj);
				}
			}
			break;

		case SCH_RND_BAA_BREAK_GRCONN:
			if (au->break_grconn.endp == 1) {
				csch_line_t *wire = (csch_line_t *)au->break_grconn.wire_obj;
				uundo_freeze_serial(&wire->hdr.sheet->undo);
				csch_line_modify(wire->hdr.sheet, wire, &au->break_grconn.newx, &au->break_grconn.newy, NULL, NULL, 1, 0, 0);
				uundo_unfreeze_serial(&wire->hdr.sheet->undo);
				uundo_inc_serial(&wire->hdr.sheet->undo);
			}
			else if (au->break_grconn.endp == 2) {
				csch_line_t *wire = (csch_line_t *)au->break_grconn.wire_obj;
				uundo_freeze_serial(&wire->hdr.sheet->undo);
				csch_line_modify(wire->hdr.sheet, wire, NULL, NULL, &au->break_grconn.newx, &au->break_grconn.newy, 1, 0, 0);
				uundo_unfreeze_serial(&wire->hdr.sheet->undo);
				uundo_inc_serial(&wire->hdr.sheet->undo);
			}
			else
				rnd_message(RND_MSG_ERROR, "Graphical connection can not be safely broken by removing a wire\nPlease edit the wirenet manually!\n");
			break;

		case SCH_RND_BAA_MAKE_GRCONN:
			if ((au->make_grconn.term_obj != NULL) && (au->make_grconn.wire_obj != NULL)) {
				csch_sheet_t *sheet = au->make_grconn.term_obj->sheet;
				if (draw_L_wires(&au->make_grconn) == 0)
					break;
				if (csch_is_wireline_safe(sheet, au->make_grconn.x1, au->make_grconn.y1, au->make_grconn.x2, au->make_grconn.y2)) {
					csch_wirenet_draw(sheet, csch_comm_str(sheet, "wire", 1), au->make_grconn.x1, au->make_grconn.y1, au->make_grconn.x2, au->make_grconn.y2);
					rnd_gui->invalidate_all(rnd_gui);
				}
				else
					rnd_message(RND_MSG_ERROR, "Can't make graphical connection: wire would hit other wire ends.\nPlease make the connection manually!\n");
			}
			else
				rnd_message(RND_MSG_ERROR, "Can't make graphical connection: wirenet and terminal doesn't share the same sheet.\nPlease make the connection manually!\n");

			break;

		case SCH_RND_BAA_LOG:
			rnd_message(RND_MSG_ERROR, "This one needs to be done manually:\n%s\n", au->log.str);
			break;

		default:
			rnd_message(RND_MSG_ERROR, "Can not figure this entry (operation not handled in autofix_cb switch)\n");
	}

	sch_rnd_backann_auto_free(au);
}

static void backann_dlg(sch_rnd_backann_t *ctx)
{
	const char *hdr[] = {"#", "status", "instruction", NULL};
	rnd_hid_dad_buttons_t clbtn[] = {{"Close", 0}, {NULL, 0}};

	RND_DAD_BEGIN_VBOX(ctx->dlg);
		RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL);
		RND_DAD_TREE(ctx->dlg, 3, 0, hdr); /* top: list of instructions */
			RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL);
			RND_DAD_TREE_SET_CB(ctx->dlg, selected_cb, ba_select_cb);
			RND_DAD_TREE_SET_CB(ctx->dlg, ctx, ctx);
			ctx->wlist = RND_DAD_CURRENT(ctx->dlg);

		RND_DAD_BEGIN_HBOX(ctx->dlg);
			RND_DAD_BUTTON(ctx->dlg, "Details");
				RND_DAD_HELP(ctx->dlg, "Show details of the issue and tips on resolving it");
				RND_DAD_CHANGE_CB(ctx->dlg, details_cb);

			RND_DAD_BUTTON(ctx->dlg, "Auto-fix");
				RND_DAD_HELP(ctx->dlg, "If possible, perform the change to resolve the issue automatically");
				RND_DAD_CHANGE_CB(ctx->dlg, autofix_cb);

			RND_DAD_BEGIN_VBOX(ctx->dlg); /* spring */
				RND_DAD_COMPFLAG(ctx->dlg, RND_HATF_EXPFILL);
			RND_DAD_END(ctx->dlg);

			RND_DAD_BUTTON_CLOSES(ctx->dlg, clbtn);
		RND_DAD_END(ctx->dlg);
	RND_DAD_END(ctx->dlg);

	RND_DAD_DEFSIZE(ctx->dlg, 200, 400);
	RND_DAD_NEW("Backann", ctx->dlg, "Back annotation", ctx, 0, backann_dlg_close_cb); /* type=local */

	backann_data2dlg(ctx);
}
