/*
 *                            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., 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 "config.h"

#include <stdlib.h>
#include <stdio.h>
#include <genht/htsp.h>
#include <genht/hash.h>
#include <genht/ht_utils.h>
#include <librnd/core/error.h>
#include <librnd/core/compat_misc.h>

#include "concrete.h"
#include "cnc_obj.h"

#include "plug_library.h"

static htsp_t lib_masters;
static int master_uids = 0;


TODO("librnd: Use libualloc")
csch_lib_t *csch_lib_alloc_append(const csch_lib_backend_t *be, csch_lib_t *parent, char *name, csch_lib_type_t type)
{
	csch_lib_t *sl = calloc(sizeof(csch_lib_t), 1);
	sl->backend = be;
	sl->parent = parent;
	sl->name = name;
	sl->type = type;
	if (parent != NULL)
		vtp0_append(&parent->children, sl);
	return sl;
}

RND_INLINE void csch_lib_uninit(csch_lib_t *sl)
{
	long n;

	for(n = 0; n < sl->children.used; n++) {
		csch_lib_uninit(sl->children.array[n]);
		free(sl->children.array[n]);
	}

	if ((sl->backend != NULL) && (sl->backend->free != NULL))
		sl->backend->free(sl);
	if (sl->realpath != sl->name)
		free(sl->realpath);
	free(sl->name);

	vtp0_uninit(&sl->children);
}

void csch_lib_free(csch_lib_t *slib)
{
	csch_lib_uninit(slib);
	free(slib);
}

void csch_lib_remove(csch_lib_t *l)
{
	long n, end = l->parent->children.used;
	int removing = 0;

	for(n = 0; n < end; n++) {
		if (l->parent->children.array[n] == l) {
			removing++;
			end--;
		}
		if (removing && (n < end))
			l->parent->children.array[n] = l->parent->children.array[n+removing];
	}
	l->parent->children.used -= removing;

	csch_lib_free(l);
}



RND_INLINE char *slib_find_realpath(rnd_design_t *hl, csch_lib_master_t *master, const char *path, const csch_lib_backend_t **be_out)
{
	long n;

	for(n = 0; n < master->backends.used; n++) {
		const csch_lib_backend_t *be = master->backends.array[n];
		char *rp;

		if (be->realpath == NULL)
			continue;

		rp = be->realpath(hl, path);
		if (rp != NULL) {
			*be_out = be;
			return rp;
		}
	}
	return NULL;
}

int slib_sort_cmp(const void *l1_, const void *l2_)
{
	csch_lib_t * const *l1 = l1_;
	csch_lib_t * const *l2 = l2_;
	return strcmp((*l1)->name, (*l2)->name);
}

/* recursively sort a lib dir */
RND_INLINE void csch_lib_sort(rnd_design_t *hl, csch_lib_t *l)
{
	long n;

	qsort(l->children.array, l->children.used, sizeof(csch_lib_t *), slib_sort_cmp);
	for(n = 0; n < l->children.used; n++) {
		csch_lib_t *c = l->children.array[n];
		if (c->type == CSCH_SLIB_DIR)
			csch_lib_sort(hl, c);
	}
}

RND_INLINE csch_lib_t *csch_lib_map(rnd_design_t *hl, csch_lib_master_t *master, const csch_lib_backend_t *be, char *path, char *real_path)
{
	csch_lib_t *res;

	res = csch_lib_alloc_append(be, NULL, path, CSCH_SLIB_DIR);
	res->realpath = real_path;
	htsp_set(&master->roots, real_path, res);

	/* keep it in the hash so it doesn't get re-mapped */
	if (be->map(hl, res) != 0) {
		res->error = "plugin mapping error";
		if (*path == '?')
			res->hide = 1;
	}
	else
		csch_lib_sort(hl, res);

	return res;
}

int csch_lib_add(csch_sheet_t *sheet, csch_lib_master_t *master, char *path, int append, int do_rehash, int skip_just_mapped)
{
	rnd_design_t *hl = &sheet->hidlib;
	csch_lib_root_t *libroot;
	csch_lib_t *root;
	const csch_lib_backend_t *be_new;
	char *real_path = slib_find_realpath(hl, master, path, &be_new);
	int optional = (*path == '?');

	if (real_path == NULL) {
		if (optional) {
			free(path); path = NULL;
			return 0;
		}
		rnd_message(RND_MSG_ERROR, "%s library mapping error: no plugin to handle '%s' (or plugin failed to map it, e.g. path does not exist)\n", master->name, path);
		free(path); path = NULL;
		return -1;
	}

	root = htsp_get(&master->roots, real_path);
	if ((root != NULL) && skip_just_mapped && root->just_mapped)
		do_rehash = 0;

	if (root == NULL) {
		/* need to map be_new */
		root = csch_lib_map(hl, master, be_new, path, real_path);
		if (root == NULL) {
			fprintf(stderr, "Internal error: symlib map returns NULL\n");
			abort(); /* at the moment this shouldn't happen */
#if 0
			if (real_path != path) free(real_path);
			rnd_message(RND_MSG_ERROR, "%s library mapping error: %s failed to map '%s'\n", master->name, be_new->name, path);
			free(path); path = NULL;
			return -1;
#endif
		}
		root->just_mapped = 1;
	}
	else { /* matching real_path: reuse */
		TODO("TODO#41: sharing root pointers may not be a good idea: they will not have unique names; see the other TODO#41");
		if (real_path != path) free(real_path);
		free(path); path = NULL;/* we are going to use the existing one in root */
		if (root->backend != be_new) {
			if (optional)
				return 0;
			rnd_message(RND_MSG_ERROR, "%s library mapping error: different plugins claim to handle '%s'\n", master->name, root->name);
			return -1;
		}
		if (do_rehash) {
			if (csch_lib_rehash(sheet, master, root) != 0) {
				rnd_message(RND_MSG_ERROR, "%s library mapping error: failed to re-scan library '%s'\n", master->name, root->name);
				root->just_mapped = 1;
			}
			else
				append = 0; /* do not add orig_root, rehash has replaced it */
		}
	}

	if (sheet->libs.used <= master->uid)
		vtp0_enlarge(&sheet->libs, master->uid);

	if (sheet->libs.array[master->uid] == NULL)
		sheet->libs.array[master->uid] = calloc(sizeof(csch_lib_root_t), 1);

	if (append) {
		long n, found = 0;

		libroot = sheet->libs.array[master->uid];
		for(n = 0; n < libroot->roots.used; n++) {
			if (libroot->roots.array[n] == root) {
				found = 1;
				break;
			}
		}
		if (!found)
			vtp0_append(&libroot->roots, root);
	}
	return 0;
}


static void csch_lib_root_uninit(csch_lib_root_t *root)
{
	long n;
	for(n = 0; n < root->roots.used; n++)
		if (root->roots.array[n] != NULL)
			csch_lib_free(root->roots.array[n]);
	vtp0_uninit(&root->roots);
}

static void csch_lib_root_free(csch_lib_root_t *root)
{
	csch_lib_root_uninit(root);
	free(root);
}

void csch_lib_free_sheet_local_libs(csch_sheet_t *sheet)
{
	long n;
	for(n = 0; n < sheet->local_libs.used; n++)
		if (sheet->local_libs.array[n] != NULL)
			csch_lib_root_free(sheet->local_libs.array[n]);
	vtp0_uninit(&sheet->local_libs);
}

void csch_lib_clear_sheet_lib(csch_sheet_t *sheet, int master_uid)
{
	if (master_uid < sheet->libs.used) {
		csch_lib_root_t *libroot = sheet->libs.array[master_uid];
		vtp0_uninit(&libroot->roots);
	}
}

void csch_lib_free_sheet_libs(csch_sheet_t *sheet)
{
	long n;
	/* do not uninit vector entries because they are shared among sheets */
	for(n = 0; n < sheet->libs.used; n++) {
		csch_lib_root_t *libroot = sheet->libs.array[n];
		if (libroot != NULL) {
			vtp0_uninit(&libroot->roots);
			free(libroot);
		}
	}
	vtp0_uninit(&sheet->libs);
}

int csch_lib_add_local(csch_sheet_t *sheet, csch_lib_master_t *master)
{
	rnd_design_t *hl = &sheet->hidlib;
	csch_lib_root_t *libroot;
	csch_lib_t *root;
	long n;

	if (sheet->local_libs.used <= master->uid)
		vtp0_enlarge(&sheet->local_libs, master->uid);

	/* need to have a local libroot for master (library type) uid */
	if (sheet->local_libs.array[master->uid] == NULL)
		sheet->local_libs.array[master->uid] = calloc(sizeof(csch_lib_root_t), 1);
	libroot = sheet->local_libs.array[master->uid];

	/* ensure exactly one root, <local> */
	if (libroot->roots.used < 1)
		vtp0_enlarge(&libroot->roots, 1);
	if (libroot->roots.array[0] == NULL)
		libroot->roots.array[0] = csch_lib_alloc_append(NULL, NULL, rnd_strdup("<local>"), CSCH_SLIB_DIR);
	root = libroot->roots.array[0];


	TODO("flush old local libs under libroot->roots.array[0]");

	for(n = 0; n < master->backends.used; n++) {
		const csch_lib_backend_t *be = master->backends.array[n];
		if (be->map_local == NULL) continue;
		be->map_local(hl, root, &sheet->indirect);
	}

	return 0;
}

/* Replace orig_root with new_root in a sheet's lib */
static void lib_rehash_update_root_in_all_sheets(csch_sheet_t *sheet, csch_lib_master_t *master, csch_lib_t *orig_root, csch_lib_t *new_root)
{
	long n;
	csch_lib_root_t *libroot = sheet->libs.array[master->uid];

	assert(libroot != NULL);  /* csch_lib_add() created it */
	for(n = 0; n < libroot->roots.used; n++)
		if (libroot->roots.array[n] == orig_root)
			libroot->roots.array[n] = new_root;
}

int csch_lib_rehash(csch_sheet_t *sheet, csch_lib_master_t *master, csch_lib_t *node)
{
	csch_lib_t *root = node, *orig_root, *new_root;
	rnd_design_t *dsg;
	long n;

	while(root->parent != NULL) root = root->parent;

	if (root->realpath == NULL) {
		rnd_message(RND_MSG_ERROR, "csch_lib_rehash(): library tree does not have a realpath, can not be refreshed\n");
		return -1;
	}
	orig_root = htsp_pop(&master->roots, root->realpath);
	if (orig_root == NULL) {
		rnd_message(RND_MSG_ERROR, "csch_lib_rehash(): failed to find original root\n");
		return -1;
	}
	if (csch_lib_add(sheet, master, rnd_strdup(root->name), 0, 1, 0) != 0) {
		rnd_message(RND_MSG_ERROR, "csch_lib_rehash(): failed to rehash %s\nDid it disappear meanwhile?\n", root->realpath);
		htsp_set(&master->roots, root->realpath, orig_root);
		return -1;
	}

	/* got a new one */
	new_root = htsp_get(&master->roots, root->realpath);
	assert(new_root != NULL); /* csch_lib_add() created it */

	TODO("TODO#41: sharing root pointers may not be a good idea: they will not have unique names; see the other TODO#41");

	/* have to fix up aliases - in all open sheets because of shared roots! */
	for(dsg = gdl_first(&rnd_designs); dsg != NULL; dsg = dsg->link.next)
		lib_rehash_update_root_in_all_sheets((csch_sheet_t *)dsg, master, orig_root, new_root);

	csch_lib_free(orig_root);
	return 0;
}

void csch_lib_add_all(csch_sheet_t *sheet, csch_lib_master_t *master, const rnd_conflist_t *searchpath, int do_rehash)
{
	rnd_conf_listitem_t *ci;

	/* clear the just_mapped bit so that csch_lib_add() can set it */
	for(ci = rnd_conflist_first((rnd_conflist_t *)searchpath); ci != NULL; ci = rnd_conflist_next(ci)) {
		const char *p = ci->val.string[0];
		const csch_lib_backend_t *be_new;
		char *real_path = slib_find_realpath(&sheet->hidlib, master, p, &be_new);
		if (real_path != NULL) {
			csch_lib_t *root = htsp_get(&master->roots, real_path);
			if (root != NULL)
				root->just_mapped = 0;
			free(real_path);
		}
	}

	/* (re)load each, but make sure everything is scanned only once */
	for(ci = rnd_conflist_first((rnd_conflist_t *)searchpath); ci != NULL; ci = rnd_conflist_next(ci)) {
		const char *p = ci->val.string[0];
		csch_lib_add(sheet, master, rnd_strdup(p), 1, do_rehash, 1);
	}
}


void csch_lib_backend_reg(csch_lib_master_t *master, const csch_lib_backend_t *be)
{
	vtp0_append(&master->backends, (void *)be);
}

static void csch_lib_uninit_master(csch_lib_master_t *master)
{
	free(master->name);
	vtp0_uninit(&master->backends);
	genht_uninit_deep(htsp, &master->roots, {
		csch_lib_uninit(htent->value);
		free(htent->value);
	});
}

csch_lib_master_t *csch_lib_get_master(const char *name, int alloc)
{
	csch_lib_master_t *res = htsp_get(&lib_masters, name);

	if (res != NULL)
		return res;

	if (!alloc)
		return NULL;

	res = calloc(sizeof(csch_lib_master_t), 1);
	res->name = rnd_strdup(name);
	res->uid = master_uids++;
	htsp_init(&res->roots, strhash, strkeyeq);
	htsp_set(&lib_masters, res->name, res);
	return res;
}

int csch_lib_load(csch_sheet_t *sheet, void *dst, csch_lib_t *src, const char *params)
{
	int res;
	if ((src == NULL) || (src->backend == NULL) || (src->backend->load == NULL))
		return -1;

	csch_cobj_redraw_freeze(sheet);
	res = src->backend->load(sheet, dst, src, params);
	csch_cobj_redraw_unfreeze(sheet);

	return res;
}

csch_lib_t *csch_lib_search_(csch_lib_t *node, const char *name, csch_lib_type_t mask)
{
	if (node == NULL)
		return NULL;

	/* check for direct match */
	if ((node->type & mask) && (strcmp(node->name, name) == 0))
		return node;

	/* recurse */
	if (node->type == CSCH_SLIB_DIR) {
		long n;
		for(n = 0; n < node->children.used; n++) {
			csch_lib_t *res = csch_lib_search_(node->children.array[n], name, mask);
			if (res != NULL)
				return res;
		}
	}

	return NULL;
}

csch_lib_t *csch_lib_search(csch_lib_root_t *lib, const char *name, csch_lib_type_t mask)
{
	long n;
	for(n = 0; n < lib->roots.used; n++) {
		csch_lib_t *res = csch_lib_search_(lib->roots.array[n], name, mask);
		if (res != NULL)
			return res;
	}
	return NULL;
}

csch_lib_t *csch_lib_search_master(csch_lib_master_t *master, const char *name, csch_lib_type_t mask)
{
	htsp_entry_t *e;
	for(e = htsp_first(&master->roots); e != NULL; e = htsp_next(&master->roots, e)) {
		csch_lib_t *res = csch_lib_search_(e->value, name, mask);
		if (res != NULL)
			return res;
	}
	return NULL;
}


void csch_plug_library_init(void)
{
	htsp_init(&lib_masters, strhash, strkeyeq);
	csch_lib_get_master("symbol", 1);
	csch_lib_get_master("hlibrary", 1);

	/* backends is all-zero already */
}

void csch_plug_library_uninit(void)
{
	genht_uninit_deep(htsp, &lib_masters, {
		csch_lib_uninit_master(htent->value);
		free(htent->value);
	});
}
