/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - easyeda file format support
 *  Copyright (C) 2024 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 Entrust Fund in 2024)
 *
 *  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
 */

/* high level parser: formad-independent generic utility and entry points */

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <genht/htsp.h>
#include <genht/htsi.h>
#include <genht/hash.h>
#include <genht/ht_utils.h>
#include <genvector/vtd0.h>
#include <libcschem/config.h>
#include <libcschem/cnc_pen.h>
#include <libcschem/cnc_grp.h>
#include <libcschem/cnc_line.h>
#include <libcschem/cnc_poly.h>
#include <libcschem/cnc_text.h>
#include <libcschem/cnc_conn.h>
#include <libcschem/cnc_any_obj.h>
#include <libcschem/operation.h>
#include <libcschem/project.h>
#include <libcschem/util_parse.h>
#include <libcschem/util_wirenet.h>
#include <libcschem/util_loclib.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/rnd_printf.h>
#include <librnd/core/safe_fs.h>
#include <librnd/core/safe_fs_dir.h>
#include <librnd/core/compat_fs.h>
#include <librnd/core/compat_fs_dir.h>
#include <librnd/core/compat_lrealpath.h>
#include <librnd/core/paths.h>
#include <librnd/core/math_helper.h>
#include <librnd/core/misc_util.h>
#include <sch-rnd/buffer.h>
#include <sch-rnd/util_sheet.h>
#include <rnd_inclib/lib_svgpath/svgpath.h>
#include <libnanojson/semantic.h>

#include <plugins/lib_alien/read_helper.h>
#include <plugins/lib_alien/read_postproc.h>

#include "io_easyeda_conf.h"
extern conf_io_easyeda_t io_easyeda_conf;

#include "read_low_std.h"
#include "read_low_pro.h"

#include "read.h"

/* Return the first node that has valid location info (or fall back to root),
   starting from node, traversing up in the tree */
static gdom_node_t *node_parent_with_loc(gdom_node_t *node)
{
	for(;node->parent != NULL; node = node->parent)
		if (node->lineno > 0)
			return node;

	return node;
}

#define error(ctx, args) \
	do { \
		rnd_message(RND_MSG_ERROR, "easyeda parse error in %s:\n", ctx->fn); \
		rnd_msg_error args; \
	} while(0)

#define error_at(ctx, node, args) \
	do { \
		gdom_node_t *__loc__ = node_parent_with_loc(node); \
		rnd_message(RND_MSG_ERROR, "easyeda parse error at %s:%ld.%ld\n", ctx->fn, __loc__->lineno, __loc__->col); \
		rnd_msg_error args; \
	} while(0)

#define warn_at(ctx, node, args) \
	do { \
		gdom_node_t *__loc__ = node_parent_with_loc(node); \
		rnd_message(RND_MSG_WARNING, "easyeda parse warning at %s:%ld.%ld\n", ctx->fn, __loc__->lineno, __loc__->col); \
		rnd_msg_error args; \
	} while(0)


typedef struct read_ctx_s {
	FILE *f;
	const char *fn;
	long ver;            /* file version */
	gdom_node_t *root;

	csch_sheet_t *sheet;
	csch_project_t *proj;

	csch_alien_read_ctx_t alien;

	/* these are used by the pro format only */
	double version;
	csch_cgrp_t *pro_last; /* last group object (e.g. part, pin) created; ATTRs are added to this */
	htsi_t pro_pen_fill;
	int pro_want_slot;     /* which slot to load; -1 means all */
	int pro_slot;          /* current slot ID, numbered from 1 */
	int pro_stop;          /* stop reading the root array */
	htsp_t *pro_symtab;    /* if not NULL, add each sym PART as a separate group */
	htsp_t *pro_tabtab;    /* if not NULL, each table is remembered here by its ID; value is easypro_table_t * */
	htss_t *pro_devmap;    /* inherited from bnd, read from the project file */
	const char *symtab_key;/* also add first PART of a symbol into the symtab with this key (ver 1.0 files tend to reference symbols via a Symbol attr using the hash-like name) */
	unsigned warned_bus:1; /* warning about graphical buses is already printed */


	int pass; /* do multiple passes because some objects can be created only when others are already created */
} read_ctx_t;

#define DECOR_PEN_NAME(parent)  \
	(((parent)->role == CSCH_ROLE_SYMBOL) ? "sym-decor" : \
	(((parent)->role == CSCH_ROLE_TERMINAL) ? "term-decor" : "sheet-decor"))

csch_source_arg_t *easyeda_attrib_src_c(read_ctx_t *ctx, gdom_node_t *loc_nd, const char *desc)
{
	loc_nd = node_parent_with_loc(loc_nd);
	return csch_attrib_src_c(ctx->fn, loc_nd->lineno, loc_nd->col, desc);
}

#if 0
static long easyeda_get_long(read_ctx_t *ctx, gdom_node_t *nd)
{
	if (nd == NULL) {
		error(ctx, ("Missing data (long)\n"));
		return 0;
	}
	if (nd->type != GDOM_LONG) {
		error_at(ctx, nd, ("Expected data type: long\n"));
		return 0;
	}
	return nd->value.lng;
}
#endif

static double easyeda_get_double(read_ctx_t *ctx, gdom_node_t *nd)
{
	if (nd == NULL) {
		error(ctx, ("Missing data (double)\n"));
		return 0;
	}
	if (nd->type != GDOM_DOUBLE) {
		error_at(ctx, nd, ("Expected data type: double\n"));
		return 0;
	}
	return nd->value.dbl;
}

/* Look up (long) lname within (gdom_nod_t *)nd and load the result in dst.
   Require dst->type to be typ. Invoke err_stmt when anything fails and
   print an error message */
#define HASH_GET_SUBTREE(dst, nd, lname, typ, err_stmt) \
do { \
	dst = gdom_hash_get(nd, lname); \
	if (dst == NULL) { \
		error_at(ctx, nd, ("internal: fieled to find " #lname " within %s\n", easy_keyname(nd->name))); \
		err_stmt; \
	} \
	if (dst->type != typ) { \
		error_at(ctx, dst, ("internal: " #lname " in %s must be of type " #typ "\n", easy_keyname(nd->name))); \
		err_stmt; \
	} \
} while(0)

/* Look up (long) lname within (gdom_nod_t *)nd, expect a double or long there
   and load its value into dst.
   Invoke err_stmt when anything fails and print an error message */
#define HASH_GET_DOUBLE(dst, nd, lname, err_stmt) \
do { \
	gdom_node_t *tmp; \
	HASH_GET_SUBTREE(tmp, nd, lname, GDOM_DOUBLE, err_stmt); \
	dst = tmp->value.dbl; \
} while(0)

#define HASH_GET_LONG(dst, nd, lname, err_stmt) \
do { \
	gdom_node_t *tmp; \
	HASH_GET_SUBTREE(tmp, nd, lname, GDOM_LONG, err_stmt); \
	dst = tmp->value.lng; \
} while(0)

#define HASH_GET_STRING(dst, nd, lname, err_stmt) \
do { \
	gdom_node_t *tmp; \
	HASH_GET_SUBTREE(tmp, nd, lname, GDOM_STRING, err_stmt); \
	dst = tmp->value.str; \
} while(0)

/*** svgpath rendering ***/

static svgpath_cfg_t pathcfg;
typedef struct {
	read_ctx_t *ctx;
	csch_cgrp_t *parent;
	csch_chdr_t *in_poly;
	gdom_node_t *nd;
	const char *penname;
} path_ctx_t;

static void easyeda_mkpath_line(void *uctx, double x1, double y1, double x2, double y2)
{
	path_ctx_t *pctx = uctx;
	if (pctx->in_poly)
		csch_alien_append_poly_line(&pctx->ctx->alien, pctx->in_poly, x1, y1, x2, y2);
	else
		csch_alien_mkline(&pctx->ctx->alien, pctx->parent, x1, y1, x2, y2, pctx->penname);
}

static void easyeda_mkpath_error(void *uctx, const char *errmsg, long offs)
{
	path_ctx_t *pctx = uctx;
	error(pctx->ctx, ("easyeda svg-path: '%s' at offset %ld\n", errmsg, offs));
}

static void easyeda_svgpath_setup(void)
{
	if (pathcfg.line == NULL) {
		pathcfg.line = easyeda_mkpath_line;
		pathcfg.error = easyeda_mkpath_error;
		pathcfg.curve_approx_seglen = io_easyeda_conf.plugins.io_easyeda.line_approx_seg_len;
	}
}

/* Create an (svg)path as a line approximation within parent */
static int easyeda_mkpath(read_ctx_t *ctx, csch_cgrp_t *parent, const char *pathstr, gdom_node_t *nd, const char *penname, int filled)
{
	path_ctx_t pctx;

	easyeda_svgpath_setup();

	pctx.ctx = ctx;
	pctx.parent = parent;
	pctx.nd = nd;
	pctx.penname = penname;
	if (filled)
		pctx.in_poly = csch_alien_mkpoly(&ctx->alien, parent, penname, penname);
	else
		pctx.in_poly = NULL;

	return svgpath_render(&pathcfg, &pctx, pathstr);
}

/* Set cobj locked if the locked subtree says so */
static int easyeda_apply_lock(read_ctx_t *ctx, gdom_node_t *nd, csch_chdr_t *cobj)
{
	long locked;

	HASH_GET_LONG(locked,   nd, easy_locked,   return -1);

	if (locked)
		cobj->lock = 1;

	return 0;
}

/* create an attribute out of a single string */
static int easyeda_parse_attr_str(read_ctx_t *ctx, csch_attribs_t *attribs, gdom_node_t *nd, const char *attr_name)
{
	csch_source_arg_t *src;

	if (nd == NULL)
		return 0;

	if (nd->type != GDOM_STRING) {
		error_at(ctx, nd, ("Expected string for attribute\n"));
		return -1;
	}

	src = easyeda_attrib_src_c(ctx, nd, NULL);
	csch_attrib_set(attribs, CSCH_ATP_USER_DEFAULT, attr_name, nd->value.str, src, NULL);

	return 0;
}

/* reconfigure an existing text object to the required alignemnt ("anchor") */
static void easyeda_text_anchor(read_ctx_t *ctx, csch_text_t *txt, const char *anchor, gdom_node_t *error_nd)
{
	switch(*anchor) {
		case '\0':
		case 'S': case 's': txt->halign = CSCH_HALIGN_START;  txt->spec_mirx = 0; break; /* "start" */
		case 'M': case 'm': txt->halign = CSCH_HALIGN_CENTER; txt->spec_mirx = 0; break; /* "middle" */
		case 'E': case 'e': txt->halign = CSCH_HALIGN_END;    txt->spec_mirx = 1; break; /* "end" */
		default:
			error_at(ctx, error_nd, ("Invalid text anchor '%s' ignored\n", anchor));
	}
}

/* Create a rail symbol, e.g. Vcc */
static void easyeda_mkrail(read_ctx_t *ctx, csch_cgrp_t *sym, const char *netname, gdom_node_t *src_nd)
{
	csch_source_arg_t *src;
	static const char *forge[] = {
		"delete,forge/tmp",
		"scalar,forge/tmp",
		"sub,^,1:,forge/tmp",
		"suba,$,rail,forge/tmp",
		"array,connect",
		"append,connect,forge/tmp",
		NULL
	};

	src = easyeda_attrib_src_c(ctx, src_nd, NULL);
	csch_attrib_set(&sym->attr, CSCH_ATP_USER_DEFAULT, "rail", netname, src, NULL);
	src = easyeda_attrib_src_c(ctx, src_nd, NULL);
	csch_attrib_set_arr_c(&sym->attr, CSCH_ATP_USER_DEFAULT, "forge", forge, src, NULL);
}

typedef enum {
	EASYEDA_FILL_AUTO,    /* look at the nd's fill_color field; if it's empty or none, don't fill, else fill with normal pen */
	EASYEDA_FILL_ALWAYS,
	EASYEDA_FILL_NEVER
} easyede_fill_t;

/* parse and draw a subtree that has a path in it, with or without fill */
static int easyeda_parse_path_(read_ctx_t *ctx, csch_cgrp_t *parent, gdom_node_t *nd, int group, easyede_fill_t fill)
{
	const char *pathstr, *penname;

	HASH_GET_STRING(pathstr, nd, easy_path, return -1);
	penname = DECOR_PEN_NAME(parent);

	if (fill == EASYEDA_FILL_AUTO) {
		const char *fillstr;
		HASH_GET_STRING(fillstr, nd, easy_fill_color, return -1);

		if ((fillstr == NULL) || (*fillstr == '\0') || (rnd_strcasecmp(fillstr, "none") == 0))
			fill = EASYEDA_FILL_NEVER;
		else
			fill = EASYEDA_FILL_ALWAYS;
	}

	easyeda_mkpath(ctx, parent, pathstr, nd, penname, (fill == EASYEDA_FILL_ALWAYS));

	return 0;
}

/* Create a symbol that represents a dummy image: rectangle with X in diagonals
   and the text "Image" */
static csch_cgrp_t *easyeda_mkimage_sym(read_ctx_t *ctx, csch_cgrp_t *parent, gdom_node_t *nd, double x, double y, double w, double h)
{
	csch_source_arg_t *src;
	csch_cgrp_t *sym;
	csch_text_t *txt;

	/* create symbol */
	sym = csch_cgrp_alloc(ctx->sheet, parent, csch_oid_new(ctx->sheet, parent));
	src = easyeda_attrib_src_c(ctx, nd, NULL);
	csch_cobj_attrib_set(ctx->sheet, sym, CSCH_ATP_HARDWIRED, "role", "symbol", src);

	csch_alien_mkline(&ctx->alien, sym, x, y, x+w, y, "sym-decor");
	csch_alien_mkline(&ctx->alien, sym, x+w, y, x+w, y+h, "sym-decor");
	csch_alien_mkline(&ctx->alien, sym, x+w, y+h, x, y+h, "sym-decor");
	csch_alien_mkline(&ctx->alien, sym, x, y+h, x, y, "sym-decor");
	csch_alien_mkline(&ctx->alien, sym, x, y, x+w, y+h, "sym-decor");
	csch_alien_mkline(&ctx->alien, sym, x+w, y, x, y+h, "sym-decor");

	txt = (csch_text_t *)csch_alien_mktext(&ctx->alien, sym, x+w/2, y+h/2, "sym-decor");
	txt->text = rnd_strdup("Image");

	return sym;
}

static void alien_setup(read_ctx_t *ctx)
{
	ctx->alien.sheet = ctx->sheet;
	ctx->alien.coord_factor = io_easyeda_conf.plugins.io_easyeda.coord_mult;
	ctx->alien.fmt_prefix = "io_easyeda";
	ctx->alien.flip_y = 1;
}

/* Heuristics to find the frame symbol on the sheet and lock it */
static void postproc_auto_lock_frame(read_ctx_t *ctx)
{
	csch_chdr_t *h;

	for(h = gdl_first(&ctx->sheet->active); h != NULL; h = gdl_next(&ctx->sheet->active, h)) {
		csch_cgrp_t *grp = (csch_cgrp_t *)h;
		const char *device, *name, *package;

		if (h->indirect) continue;
		if ((h->type != CSCH_CTYPE_GRP) && (h->type != CSCH_CTYPE_GRP_REF)) continue;
		if (grp->role != CSCH_ROLE_SYMBOL) continue;

		device = csch_attrib_get_str(&grp->attr, "device");
		if ((device == NULL) || (*device != 'A'))
			continue;

		name = csch_attrib_get_str(&grp->attr, "name");
		if ((name == NULL) || (*name != 'A'))
			continue;

		package = csch_attrib_get_str(&grp->attr, "package");
		if ((package == NULL) || (rnd_strcasecmp(package, "none") != 0))
			continue;

		/* this is the frame */
		grp->hdr.lock = 1;
	}
}

static int io_easyeda_postproc(read_ctx_t *ctx, int is_sheet)
{
	/* get sheet bbox computed */
	csch_cgrp_update(ctx->sheet, &ctx->sheet->direct, 1);

	/* Do not call csch_alien_postproc_text_autorot(): 14autorot.json shows
	   text objects are stored in rotated state */

	if (is_sheet && io_easyeda_conf.plugins.io_easyeda.auto_lock_frame)
		postproc_auto_lock_frame(ctx);

	/* use this for attribute conversion instead of hardwired C code: read
	   and store attributes as they are in the native format and let this
	   configurable postproc deal with things like symbols's "rename
	   refdes attribute to name attribute" - this is more flexible and
	   user configurable this way */
	return csch_alien_postproc_sheet(&ctx->alien);
}

#include "read_hi_std.c"
#include "read_hi_pro_draw.c"
#include "read_hi_pro_glue.c"
#include "read_hi_pro_io.c"
