/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - sheet-local symbol library
 *  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 <assert.h>

#include <genvector/gds_char.h>

#include <libcschem/concrete.h>
#include <libcschem/cnc_any_obj.h>
#include <libcschem/cnc_grp.h>
#include <libcschem/cnc_grp_child.h>
#include <libcschem/cnc_text.h>
#include <libcschem/plug_library.h>
#include <libcschem/event.h>
#include <libcschem/util_loclib.h>
#include <libcschem/actions_csch.h>
#include <libcschem/operation.h>
#include <libcschem/op_common.h>

#include <librnd/core/compat_misc.h>
#include <librnd/core/error.h>
#include <librnd/core/plugins.h>
#include <librnd/core/actions.h>
#include <librnd/hid/hid.h>

#include <sch-rnd/conf_core.h>

static csch_lib_backend_t be_symlib_local;
static csch_lib_master_t *loclib_master;

static const char symlib_local_cookie[] = "symlib_local";

static unsigned ll_hash(const void *obj)                   { return csch_cobj_hash((void *)obj); }
static int ll_keyeq(const void *obj1, const void *obj2)    { return csch_cobj_keyeq((void *)obj1, (void *)obj2); }
static unsigned ll_hashna(const void *obj)                 { return csch_cobj_hash_((void *)obj, CSCH_HIGN_FLOATER_GEO | CSCH_HIGN_GRP_ATTRIB | CSCH_HIGN_GRP_PLACEMENT); }
static int ll_keyeqna(const void *obj1, const void *obj2)  { return csch_cobj_keyeq_((void *)obj1, (void *)obj2, CSCH_HIGN_FLOATER_GEO | CSCH_HIGN_GRP_ATTRIB | CSCH_HIGN_GRP_PLACEMENT); }

/* hash: a htpi_t, keyed with indirect symlib groups are stored in the
   indirect/purpose=symbol group's backend_data->ptr[0]; [1] is the version
   without attribute matching. Also remember our local root group in ptr[2].
*/
static void symlib_local_sheet_init(rnd_design_t *hl, csch_lib_t *root_dir, const csch_cgrp_t *symlib)
{
	root_dir->backend_data.ptr[0] = htpi_alloc(ll_hash, ll_keyeq);
	root_dir->backend_data.ptr[1] = htpi_alloc(ll_hashna, ll_keyeqna);
	root_dir->backend_data.ptr[2] = (void *)symlib;
}

static void symlib_local_sheet_uninit(csch_lib_t *root_dir)
{
	csch_cgrp_t *symlib = root_dir->backend_data.ptr[2];

	if (symlib == NULL) return;

	htpi_free(root_dir->backend_data.ptr[0]);
	htpi_free(root_dir->backend_data.ptr[1]);
	root_dir->backend_data.ptr[0] = NULL;
	root_dir->backend_data.ptr[1] = NULL;
	root_dir->backend_data.ptr[2] = NULL;
}

/* Insert grp into the local lib cache (assuming it's already in the indirect
   group's symlib group). Returns 0 on success, 1 on non-fatal error */
static int symlib_local_insert_sym(csch_lib_t *root_dir, htpi_t *sh, htpi_t *shna, csch_cgrp_t *grp)
{
	int res = 0;
	const char *gname = NULL;
	csch_lib_t *newent;
	char tmp[64];
	gds_t nm = {0};

	if (grp->loclib_name != NULL) {
		const char *end = strrchr(grp->loclib_name, '/');
		gname = end == NULL ? grp->loclib_name : end+1;
	}
	if (gname == NULL)
		gname = csch_attrib_get_str(&grp->attr, "name");
	if (gname != NULL)
		gds_append_str(&nm, gname);
	sprintf(tmp, ".%d",  grp->hdr.oid);
	gds_append_str(&nm, tmp);

	if (htpi_has(sh, grp)) {
		rnd_message(RND_MSG_WARNING, "Redundant local lib symbol '%s'\n", nm.array);
		res = 1;
	}
	else {
		htpi_set(sh, grp, 0);
		htpi_set(shna, grp, 0);
	}

	newent = csch_lib_alloc_append(&be_symlib_local, root_dir, nm.array, CSCH_SLIB_STATIC);
	newent->backend_data.lng[0] = grp->hdr.oid;
	/* do not free nm: name ownership passed on to newent */

	return res;
}

static int symlib_local_map_local_(rnd_design_t *hl, csch_lib_t *root_dir, csch_cgrp_t *symlib)
{
	htip_entry_t *e;
	htpi_t *sh = root_dir->backend_data.ptr[0];
	htpi_t *shna = root_dir->backend_data.ptr[1];

	for(e = htip_first(&symlib->id2obj); e != NULL; e = htip_next(&symlib->id2obj, e)) {
		csch_cgrp_t *grp = e->value;
		if ((grp->hdr.type == CSCH_CTYPE_GRP) && (grp->role == CSCH_ROLE_SYMBOL))
			symlib_local_insert_sym(root_dir, sh, shna, grp);
	}
	return 0;
}

static int symlib_local_map_local(rnd_design_t *hl, csch_lib_t *root_dir, const csch_cgrp_t *indirect)
{
	csch_cgrp_t *grp = csch_loclib_get_root((csch_sheet_t *)hl, loclib_master, NULL, 0, NULL);
	if (grp != NULL) {
		symlib_local_sheet_init(hl, root_dir, grp);
		symlib_local_map_local_(hl, root_dir, grp);
	}
	return 0;
}

static int symlib_local_load(csch_sheet_t *sheet, void *dst, csch_lib_t *src_lib, const char *params)
{
	long oid;
	csch_sheet_t *dst_sheet = dst;
	csch_cgrp_t *sym, *src_grp, *symlib = src_lib->parent->backend_data.ptr[2];


	if ((params != NULL) || (symlib == NULL))
		return -1;

	oid = src_lib->backend_data.lng[0];
	src_grp = htip_get(&symlib->id2obj, oid);
	if (src_grp == NULL)
		return -1;

	sym = csch_cgrp_dup(dst_sheet, &dst_sheet->direct, src_grp, 0);
	sym->sym_prefer_loclib = 1;
	csch_cgrp_render_all(dst_sheet, &dst_sheet->direct);
	csch_cobj_update(dst_sheet, &dst_sheet->direct.hdr, 1);
	csch_text_invalidate_all_grp(sym, 0); /* let the draw code recalculate text objects, with proper fallback to sheet's pens */

	return 0;
}

static void symlib_local_free(csch_lib_t *src)
{
	csch_lib_t *root_dir = src->parent;

	if (root_dir != NULL) {
		htpi_t *sh, *shna;
		csch_cgrp_t *symlib, *grp;
		long oid;

		sh = root_dir->backend_data.ptr[0];
		shna = root_dir->backend_data.ptr[1];
		symlib = root_dir->backend_data.ptr[2];

		oid = src->backend_data.lng[0];
		grp = htip_get(&symlib->id2obj, oid);
		if (grp == NULL)
			return;

		if (sh != NULL)
			htpi_pop(sh, grp);
		if (shna != NULL)
			htpi_pop(shna, grp);
	}
	else
		symlib_local_sheet_uninit(src); /* src is a root dir */
}

/* We have *grp already placed on the sheet; move it to indirect; this happens
   if some other plugin already handled the event and decided to place the
   group */
static void local_paste_already_placed(csch_sheet_t *sheet, csch_chdr_t **grp)
{
	/* no other plugin handles this event at the moment */
	rnd_message(RND_MSG_ERROR, "symlib_local local_paste_already_placed() not yet implemented\nPlease report this bug!\n");
}

/* Copy src as a new group in the local lib and return the new group's pointer */
static csch_cgrp_t *loclib_create_entry(csch_sheet_t *sheet, csch_lib_t *root_dir, htpi_t *sh, htpi_t *shna, csch_cgrp_t *symlib, csch_cgrp_t *src)
{
	csch_cgrp_t *loc_lib_sym;
	
	loc_lib_sym = csch_cgrp_dup(sheet, symlib, src, 1);
	if (loc_lib_sym == NULL)
		loc_lib_sym = csch_cgrp_dup(sheet, symlib, src, 0); /* don't insist on keeping the original ID too hard */

	if (loc_lib_sym != NULL) {
		loc_lib_sym->x = 0;
		loc_lib_sym->y = 0;
		loc_lib_sym->spec_rot = 0;
		loc_lib_sym->mirx = 0;
		loc_lib_sym->miry = 0;
		loc_lib_sym->sym_prefer_loclib = 0;
		if (src->file_name != NULL)
			loc_lib_sym->loclib_name = rnd_strdup(src->file_name);
		symlib_local_insert_sym(root_dir, sh, shna, loc_lib_sym);
		rnd_event(&sheet->hidlib, CSCH_EVENT_LIBRARY_CHANGED, NULL);
	}
	else
		rnd_message(RND_MSG_ERROR, "Failed to create a copy of symbol in the local library\n(falling back to embedded copy)\n");

	return loc_lib_sym;
}

/* Take loc_lib_sym and create a group ref within dst (normally sheet->direct),
   copying src's transformations */
static csch_cgrp_t *loclib_create_ref(csch_sheet_t *sheet, csch_cgrp_t *dst, csch_cgrp_t *loc_lib_sym, csch_cgrp_t *src)
{
	csch_cgrp_t *new_grp = csch_cgrp_ref_alloc(sheet, dst, csch_oid_new(sheet, dst));

	if (new_grp == NULL) {
		rnd_message(RND_MSG_ERROR, "Failed to alocate group reference to place local library symbol\n(falling back to embedded copy)\n");
		return NULL;
	}

	new_grp->x = src->x;
	new_grp->y = src->y;
	new_grp->spec_rot = src->spec_rot;
	new_grp->mirx = src->mirx;
	new_grp->miry = src->miry;
	new_grp->data.ref.grp = loc_lib_sym;

	csch_sheet_set_changed(sheet, 1);

	return new_grp;
}

/* insert grp in indirect */
static csch_cgrp_t *local_insert(csch_sheet_t *sheet, csch_cgrp_t *grp, int strict)
{
	csch_source_arg_t *src;
	csch_cgrp_t *symlib, *loc;
	csch_lib_t *root_dir;
	htpi_t *sh, *shna;
	htpi_entry_t *e;
	int alloced = 0;

	src = csch_attrib_src_p("symlib_local", NULL);
	if (csch_loclib_get_roots(&root_dir, &symlib, loclib_master, sheet, src, 1, &alloced) != 0)
		return NULL;

	if (alloced) {
		symlib_local_sheet_init(&sheet->hidlib, root_dir, symlib);
		symlib_local_map_local_(&sheet->hidlib, root_dir, symlib);
	}

	sh = root_dir->backend_data.ptr[0];
	shna = root_dir->backend_data.ptr[1];

	if (sh == NULL) { /* local lib is not yet created */
		symlib_local_sheet_init(&sheet->hidlib, root_dir, symlib);
		sh = root_dir->backend_data.ptr[0];
		assert(sh != NULL);
	}

	/* check for direct match: the very same symbol already in lib */
	e = htpi_getentry(strict ? sh : shna, grp);
/*	rnd_trace("*** loclib: root=%p sh=%p e=%p\n", root_dir, sh, e);*/

	if (0) { /* hash debug: take a sheet with exactly 1 local lib entry and place it */
		htpi_entry_t *e = htpi_first(sh);
		csch_chdr_t *l = e->key, *obj = &grp->hdr;
		long lh, gh;
		int eq;

		rnd_trace("---- h l ----\n");
		lh = csch_cobj_hash_(l, CSCH_HIGN_FLOATER_GEO | CSCH_HIGN_GRP_ATTRIB | CSCH_HIGN_GRP_PLACEMENT);
		rnd_trace("---- h g ----\n");
		gh = csch_cobj_hash_(obj, CSCH_HIGN_FLOATER_GEO | CSCH_HIGN_GRP_ATTRIB | CSCH_HIGN_GRP_PLACEMENT);
		rnd_trace("---- eq ----\n");
		eq = csch_cobj_keyeq_(l, obj, CSCH_HIGN_FLOATER_GEO | CSCH_HIGN_GRP_ATTRIB | CSCH_HIGN_GRP_PLACEMENT);
		rnd_trace("l=%p lh=%ld gh=%ld -> %d\n", l, lh, gh, eq);
	}

	if (e == NULL)
		loc = loclib_create_entry(sheet, root_dir, sh, shna, symlib, grp);
	else
		loc = e->key;

	return loc;
}

static void copy_std_sym_attribs(csch_cgrp_t *dst, csch_cgrp_t *src, csch_cgrp_t *loc)
{
	const char *tmp;
	csch_source_arg_t *src_none;
	htsp_entry_t *e;

	/* always copy name, it should be unique */
	tmp = csch_attrib_get_str(&src->attr, "name");
	src_none = csch_attrib_src_c(NULL, 0, 0, NULL);
	csch_attrib_set(&dst->attr, CSCH_ATP_USER_DEFAULT, "name", tmp, src_none, NULL);

	/* always copy role, we shouldn't depend on the lib */
	tmp = csch_attrib_get_str(&src->attr, "role");
	src_none = csch_attrib_src_c(NULL, 0, 0, NULL);
	csch_attrib_set(&dst->attr, CSCH_ATP_USER_DEFAULT, "role", tmp, src_none, NULL);

	/* copy any attribute of src to dst that's not coming from or differs from loc */
	for(e = htsp_first(&src->attr); e; e = htsp_next(&src->attr, e)) {
		csch_attrib_t *asrc = e->value;
		csch_attrib_t *aloc = csch_attrib_get(&loc->attr, e->key);
		csch_attrib_t *adst = csch_attrib_get(&dst->attr, e->key);

		if (adst != NULL) continue; /* already present in dst, don't overwrite */

		if ((aloc == NULL) || !csch_attrib_eq_(aloc, asrc)) {
			csch_attrib_t *anew = csch_attrib_dup(asrc);
			htsp_set(&dst->attr, anew->key, anew);
		}
	}
	TODO("extobj");
}


/* insert *grp in indirect, place ref on the sheet */
static void local_paste_place(csch_sheet_t *sheet, csch_cgrp_t **grp)
{
	csch_cgrp_t *loc = local_insert(sheet, *grp, 0);

	if (loc != NULL) {
		csch_cgrp_t *new_ref = loclib_create_ref(sheet, &sheet->direct, loc, *grp);
		if (new_ref != NULL) {
			copy_std_sym_attribs(new_ref, *grp, loc);
			csch_cgrp_ref_render(sheet, new_ref);
			*grp = new_ref;
		}
	}
}

/* Perform a "place a ref from local lib" when pasting a symbol from the buffer */
static void symlib_local_paste(rnd_design_t *hidlib, void *user_data, int argc, rnd_event_arg_t argv[])
{
	csch_sheet_t *sheet = (csch_sheet_t *)hidlib;
	csch_chdr_t **obj = argv[1].d.p;


	if (argv[1].type != RND_EVARG_PTR) {
		rnd_message(RND_MSG_ERROR, "symlib_local_paste() wrong first arg\n");
		return;
	}

	/* when placing a symbol group, take over normal placement and create a
	   group ref instead */
	if ((*obj)->type == CSCH_CTYPE_GRP) {
		csch_cgrp_t *grp = (csch_cgrp_t *)*obj;

		if (!conf_core.editor.paste_to_local_lib)
			return;

		if ((grp->role == CSCH_ROLE_SYMBOL) && (grp->sym_prefer_loclib)) {
			if (grp->hdr.sheet == sheet)
				local_paste_already_placed(sheet, obj);
			else
				local_paste_place(sheet, (csch_cgrp_t **)obj);
		}
	}
	else if ((*obj)->type == CSCH_CTYPE_GRP_REF) {
		csch_cgrp_t *src = (csch_cgrp_t *)*obj, *bsymlib, *loc, *newgr;
		const char *purp;

		/* Doc: trunk/doc/developer/symbol_loclib_paste.txt */

/*		rnd_trace("-- symlib_local_paste\n");*/
		if ((src->data.ref.grp == NULL) && (csch_cgrp_ref_text2ptr(src->hdr.sheet, src) != 0)) {
/*			rnd_trace(" Failed to resolve group ref\n");*/
			return;
		}
		bsymlib = src->data.ref.grp->hdr.parent;
		purp = csch_attrib_get_str(&bsymlib->attr, "purpose");
		if ((purp == NULL) || (strcmp(purp, "symbol") != 0)) {
/*			rnd_trace(" not a symlib grp_ref\n");*/
			return;
		}

		loc = local_insert(sheet, src->data.ref.grp, 1);
		if (loc == NULL) {
			rnd_message(RND_MSG_ERROR, "Failed to paste symbol prototype in loclib of the sheet\n");
			return;
		}

/*		rnd_trace(" place as loclib grp_ref %p\n", loc);*/
		newgr = (csch_cgrp_t *)csch_cobj_dup(sheet, &sheet->direct, &src->hdr, 0, 0);
		if (newgr != NULL) {
			free(newgr->data.ref.ref_str);
			newgr->data.ref.ref_str = NULL;
			newgr->data.ref.grp = loc;
			*obj = &newgr->hdr;
		}
		else
			rnd_message(RND_MSG_ERROR, "Failed to paste loclib symbol grp_ref on the sheet\n");
	}
}


/* Perform a "copy to buffer" on local lib refs: need to include the referenced
   group from local lib into buffer's indirect so the buffer is self-contained */
static void symlib_local_copy(rnd_design_t *hidlib, void *user_data, int argc, rnd_event_arg_t argv[])
{
	csch_chdr_t **obj = argv[1].d.p;
	csch_cgrp_t *src, *symlib, *newg, *newr, *bsymlib;
	csch_sheet_t *buffer = argv[2].d.p, *sheet = (csch_sheet_t *)hidlib;
	const char *purp;
	int alloced;

/* Doc: trunk/doc/developer/symbol_loclib_paste.txt */

	if ((argv[1].type != RND_EVARG_PTR) || (argv[2].type != RND_EVARG_PTR)) {
		rnd_message(RND_MSG_ERROR, "symlib_local_copy() wrong argument types\n");
		return;
	}

/*	rnd_trace("--- symlib_local_copy()\n");*/

	/* veryify source and ignore anything that's not a loclib ref */
	src = (csch_cgrp_t *)(*obj);

	if (src->hdr.type != CSCH_CTYPE_GRP_REF) {
/*		rnd_trace(" not a group ref\n");*/
		return;
	}

	if (src->data.ref.grp == NULL) {
		if (csch_cgrp_ref_text2ptr(sheet, src) != 0) {
/*			rnd_trace(" can't resolve referee so can't decide if it's a loclib ref\n");*/
			return;
		}
	}

	symlib = src->data.ref.grp->hdr.parent;
	purp = csch_attrib_get_str(&symlib->attr, "purpose");
	if ((purp == NULL) || (strcmp(purp, "symbol") != 0)) {
/*		rnd_trace(" not a loclib reference\n");*/
		return;
	}

/*	rnd_trace(" took over copy\n");*/
	{
		csch_source_arg_t *src = csch_attrib_src_p("symlib_local", NULL);
		bsymlib = csch_loclib_get_root(buffer, loclib_master, src, 1, &alloced);
	}

	/* check if we already have it in buffer's loclib */
	newr = (csch_cgrp_t *)htip_get(&bsymlib->id2obj, src->data.ref.grp->hdr.oid);
	if (newr == NULL)
		newr = (csch_cgrp_t *)csch_cobj_dup(buffer, bsymlib, &src->data.ref.grp->hdr, 1, 0);
	newg = (csch_cgrp_t *)csch_cobj_dup(buffer, &buffer->direct, &src->hdr, 0, 0);
	*obj = &newg->hdr;

	/* break original pointer ref pointing into sheet by converting ref back to
	   text. Whenever needed it will be resolved within the buffer */
	newg->data.ref.grp = newr;
	csch_cgrp_ref_ptr2text(buffer, newg);
}

#include "loc_ops.c"
#include "loc_acts.c"

static void symlib_local_sheet_postload_ev(rnd_design_t *hidlib, void *user_data, int argc, rnd_event_arg_t argv[])
{
	csch_sheet_t *sheet = (csch_sheet_t *)hidlib;
	csch_lib_add_local(sheet, loclib_master);
}


int pplg_check_ver_symlib_local(int ver_needed) { return 0; }

void pplg_uninit_symlib_local(void)
{
	rnd_event_unbind_allcookie(symlib_local_cookie);
	rnd_remove_actions_by_cookie(symlib_local_cookie);
}

int pplg_init_symlib_local(void)
{
	RND_API_CHK_VER;

	RND_REGISTER_ACTIONS(symlib_local_action_list, symlib_local_cookie);

	be_symlib_local.name = "symlib_local";
	be_symlib_local.map_local = symlib_local_map_local;
	be_symlib_local.load = symlib_local_load;
	be_symlib_local.free = symlib_local_free;
	be_symlib_local.loc_list = symlib_local_loc_list;
	be_symlib_local.loc_del = symlib_local_loc_del;
	be_symlib_local.loc_refresh_from_ext = symlib_local_loc_refresh_from_ext;
	be_symlib_local.loc_edit = symlib_local_loc_edit;

	loclib_master = csch_lib_get_master("symbol", 1);
	csch_lib_backend_reg(loclib_master, &be_symlib_local);

	rnd_event_bind(CSCH_EVENT_BUFFER_PASTE_CUSTOM, symlib_local_paste, NULL, symlib_local_cookie);
	rnd_event_bind(CSCH_EVENT_BUFFER_COPY_CUSTOM, symlib_local_copy, NULL, symlib_local_cookie);
	rnd_event_bind(CSCH_EVENT_SHEET_POSTLOAD, symlib_local_sheet_postload_ev, NULL, symlib_local_cookie);

	return 0;
}

