/*
 *                            COPYRIGHT
 *
 *  sch-rnd - modular/flexible schematics editor - lihata format support
 *  Copyright (C) 2020,2022,2023 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 <stdio.h>
#include <string.h>
#include <genvector/vtp0.h>
#include <libcschem/config.h>
#include <libcschem/cnc_pen.h>
#include <libcschem/cnc_grp.h>
#include <libcschem/cnc_grp_child.h>
#include <libcschem/cnc_line.h>
#include <libcschem/cnc_arc.h>
#include <libcschem/cnc_poly.h>
#include <libcschem/cnc_text.h>
#include <libcschem/cnc_conn.h>
#include <libcschem/cnc_bitmap.h>
#include <libcschem/project.h>
#include <liblihata/dom.h>
#include <liblihata/tree.h>
#include <librnd/core/safe_fs.h>
#include <librnd/core/compat_misc.h>
#include "write.h"

typedef struct {
	FILE *f;
	const csch_sheet_t *sheet;
} write_ctx_t;

static char *ind_gen(char *dst, int max, int lev)
{
	if (lev > max) lev = max;
	memset(dst, '\t', lev);
	dst[lev] = '\0';
	return dst;
}

#define IND(indlev) \
	char ind_[128], *ind = ind_gen(ind_, sizeof(ind_)-1, indlev);

static int lht_save_group(write_ctx_t *ctx, int indlev, const csch_cgrp_t *grp, const char *name, long oid, int inhibit_uuid);


static int attr_cmp(const void *v1, const void *v2)
{
	csch_attrib_t * const *a1 = v1, * const *a2 = v2;
	return strcmp((*a1)->key, (*a2)->key);
}

void lht_print_str(FILE *f, const char *s)
{
	if (s == NULL) {
		fprintf(f, "{}");
		return;
	}
	if (!lht_need_brace(LHT_TEXT, s, 0)) {
		fprintf(f, "%s", s);
		return;
	}
	fputc('{', f);
	for(; *s != '\0'; s++) {
		if ((*s == '\\') || (*s == '}'))
			fputc('\\', f);
		fputc(*s, f);
	}
	fputc('}', f);
}

static int lht_save_attribs(write_ctx_t *ctx, int indlev, char *parind, const htsp_t *att)
{
	htsp_entry_t *e;
	vtp0_t ord;
	long n;

	memset(&ord, 0, sizeof(ord));

	for(e = htsp_first(att); e != NULL; e = htsp_next(att, e))
		vtp0_append(&ord, e->value);
	if (ord.used > 0) {
		qsort(ord.array, ord.used, sizeof(void *), attr_cmp);
		fprintf(ctx->f, "%s	ha:attrib {\n", parind);
		for(n = 0; n < ord.used; n++) {
			const csch_attrib_t *a = ord.array[n];
			if (a->val != NULL) {
				if (a->prio != CSCH_ATP_USER_DEFAULT) {
					fprintf(ctx->f, "%s		ha:%s = { value=", parind, a->key);
					lht_print_str(ctx->f, a->val);
					fprintf(ctx->f, "; prio=%d; }\n", a->prio);
				}
				else {
					fprintf(ctx->f, "%s		", parind);
					lht_print_str(ctx->f, a->key);
					fprintf(ctx->f, "=");
					lht_print_str(ctx->f, a->val);
					fprintf(ctx->f, "\n");
				}
			}
			else {
				long n;

				if (a->prio != CSCH_ATP_USER_DEFAULT)
					fprintf(ctx->f, "%s		ha:%s = { li:value = {\n", parind, a->key);
				else
					fprintf(ctx->f, "%s		li:%s {\n", parind, a->key);

				for(n = 0; n < a->arr.used; n++) {
					fprintf(ctx->f, "%s			", parind);
					lht_print_str(ctx->f, a->arr.array[n]);
					fprintf(ctx->f, "\n");
				}
				if (a->prio != CSCH_ATP_USER_DEFAULT) {
					fprintf(ctx->f, "%s		}\n", parind);
					fprintf(ctx->f, "%s		prio=%d; }\n", parind, a->prio);
				}
				else
					fprintf(ctx->f, "%s		}\n", parind);
			}
		}
		fprintf(ctx->f, "%s	}\n", parind);
	}
	vtp0_uninit(&ord);
	return 0;
}

/* Returns if anything has been written */
static int lht_print_common_properties(write_ctx_t *ctx, const csch_chdr_t *obj, const char *ind, const char *ind2)
{
	int res = 0;

	if (ind == NULL) ind = " ";
	if (ind2 == NULL) ind2 = "";

	if (obj->lock) {
		fprintf(ctx->f, "%s%slock=1;", ind, ind2);
		ind = " ";
		ind2 = "";
		res = 1;
	}
	if (obj->floater) {
		fprintf(ctx->f, "%s%sfloater=1;", ind, ind2);
		ind = " ";
		ind2 = "";
		res = 1;
	}

	return res;
}

static int lht_print_line_(write_ctx_t *ctx, const csch_line_t *line, int in_poly)
{
	fprintf(ctx->f, " x1=%ld; y1=%ld; x2=%ld; y2=%ld;",
		line->spec.p1.x, line->spec.p1.y,
		line->spec.p2.x, line->spec.p2.y);

	if (!in_poly) {
		fprintf(ctx->f, " stroke=%s;", line->hdr.stroke_name.str);
		lht_print_common_properties(ctx, &line->hdr, NULL, NULL);
	}

	fprintf(ctx->f, " }\n");
	return 0;
}

static int lht_print_line(write_ctx_t *ctx, int indlev, char *parind, const csch_line_t *line)
{
	return lht_print_line_(ctx, line, 0);
}


static int lht_print_arc_(write_ctx_t *ctx, const csch_arc_t *arc, int in_poly)
{
	fprintf(ctx->f, " cx=%ld; cy=%ld; r=%ld; sang=%f; dang=%f;",
		arc->spec.c.x, arc->spec.c.y, arc->spec.r,
		arc->spec.start * RND_RAD_TO_DEG, arc->spec.delta * RND_RAD_TO_DEG);

	if (arc->svalid)
		fprintf(ctx->f, " sx=%ld; sy=%ld;", arc->spec_sx, arc->spec_sy);
	if (arc->evalid)
		fprintf(ctx->f, " ex=%ld; ey=%ld;", arc->spec_ex, arc->spec_ey);

	if (!in_poly) {
		fprintf(ctx->f, " stroke=%s;", arc->hdr.stroke_name.str);
		lht_print_common_properties(ctx, &arc->hdr, NULL, NULL);
	}

	fprintf(ctx->f, " }\n");
	return 0;
}

static int lht_print_arc(write_ctx_t *ctx, int indlev, char *parind, const csch_arc_t *arc)
{
	return lht_print_arc_(ctx, arc, 0);
}

static int lht_print_cpoly(write_ctx_t *ctx, int indlev, char *parind, const csch_cpoly_t *poly)
{
	long n;
	csch_coutline_t *o;

	/* write outline list */
	fprintf(ctx->f, "\n%s		li:outline {\n", parind);
	for(n = 0, o = poly->outline.array; n < poly->outline.used; n++, o++) {
		switch(o->hdr.type) {
			case CSCH_CTYPE_LINE:
				fprintf(ctx->f, "%s			ha:line {", parind);
				lht_print_line_(ctx, &o->line, 1);
				break;
			case CSCH_CTYPE_ARC:
				fprintf(ctx->f, "%s			ha:arc {", parind);
				lht_print_arc_(ctx, &o->arc, 1); break;
				break;
			default:
				rnd_message(RND_MSG_ERROR, "Invalid object in polygon outline\n");
				break;
		}
	}
	fprintf(ctx->f, "%s		}\n", parind);

	/* write pens */
	if (poly->has_stroke)
		fprintf(ctx->f, "%s		stroke=%s;\n", parind, poly->hdr.stroke_name.str);
	if (poly->has_fill)
		fprintf(ctx->f, "%s		fill=%s;\n", parind, poly->hdr.fill_name.str);

	if (lht_print_common_properties(ctx, &poly->hdr, parind, "\t\t"))
		fprintf(ctx->f, "\n");

	fprintf(ctx->f, "%s	}\n", parind);
	return 0;
}

static int lht_print_cbitmap(write_ctx_t *ctx, int indlev, char *parind, const csch_cbitmap_t *bitmap)
{
	return -1;
}

static int lht_print_conn(write_ctx_t *ctx, int indlev, char *parind, const csch_conn_t *conn)
{
	long n;

	fprintf(ctx->f, "\n%s		li:conn {\n", parind);
	if (conn->conn.used != 0) {
		for(n = 0; n < conn->conn.used; n++) {
			char *path = csch_chdr_to_oidpath_str(conn->conn.array[n]);
			fprintf(ctx->f, "%s			%s\n", parind, path);
			free(path);
		}
	}
	else {
		for(n = 0; n < conn->conn_path.used; n++) {
			char *path = csch_oidpath_to_str(&conn->conn_path.array[n]);
			fprintf(ctx->f, "%s			%s\n", parind, path);
			free(path);
		}
	}
	fprintf(ctx->f, "%s		}\n", parind);
	fprintf(ctx->f, "%s	}\n", parind);
	return 0;
}

static int lht_print_text(write_ctx_t *ctx, int indlev, char *parind, const csch_text_t *text)
{
	fprintf(ctx->f, " x1=%ld; y1=%ld;", text->spec1.x, text->spec1.y);

	if (text->has_bbox)
		fprintf(ctx->f, " x2=%ld; y2=%ld;", text->spec2.x, text->spec2.y);

	if (text->spec_rot != 0)
		fprintf(ctx->f, " rot=%f;", text->spec_rot);
	if (text->spec_mirx != 0)
		fprintf(ctx->f, " mirx=1;");
	if (text->spec_miry != 0)
		fprintf(ctx->f, " miry=1;");

	if (text->halign != CSCH_HALIGN_START)
	fprintf(ctx->f, " halign=%s;", csch_halign2str(text->halign));

	fprintf(ctx->f, " dyntext=%d; stroke=%s;",
		text->dyntext, text->hdr.stroke_name.str);

	fprintf(ctx->f, " text=");
	lht_print_str(ctx->f, text->text);
	fprintf(ctx->f, ";");


	lht_print_common_properties(ctx, &text->hdr, NULL, NULL);

	fprintf(ctx->f, " }\n");
	return 0;
}

static int lht_print_cgrp(write_ctx_t *ctx, int indlev, char *parind, const csch_cgrp_t *grp)
{
	return lht_save_group(ctx, indlev, grp, "group", grp->hdr.oid, 0);
}

static int lht_print_cgrp_ref(write_ctx_t *ctx, int indlev, char *parind, const csch_cgrp_t *grp)
{
	return lht_save_group(ctx, indlev, grp, "group_ref", grp->hdr.oid, 0);
}

static int lht_print_pen(write_ctx_t *ctx, int indlev, char *parind, const csch_cpen_t *pen)
{
	char clr[8];

	/* truncate alpha */
	strncpy(clr, pen->color.str, 7);
	clr[7] = '\0';
	fprintf(ctx->f, " shape=%s; size=%ld; color=%s; font_height=%ld;",
		csch_pen_shape_name(pen->shape), pen->size, clr, pen->font_height);

	if (pen->dash != 0)
		fprintf(ctx->f, " dash=%04x; dash_period=%ld;", pen->dash, pen->dash_period);

	if (pen->font_family != NULL) {
		fprintf(ctx->f, " font_family=");
		lht_print_str(ctx->f, pen->font_family);
		fprintf(ctx->f, ";");
	}

	if (pen->font_style != NULL) {
		fprintf(ctx->f, " font_style=");
		lht_print_str(ctx->f, pen->font_style);
		fprintf(ctx->f, ";");
	}

	lht_print_common_properties(ctx, &pen->hdr, NULL, NULL);

	fprintf(ctx->f, " }\n");

	return 0;
}



static int lht_print_obj(write_ctx_t *ctx, int indlev, char *parind, const csch_chdr_t *obj)
{
	int r = -1, want_hdr = (obj->type != CSCH_CTYPE_GRP) && (obj->type != CSCH_CTYPE_GRP_REF);

	/* pen is special case: instead of oid, its name is the ID */
	if (obj->type == CSCH_CTYPE_PEN) {
TODO("may need to quote ID")
		csch_cpen_t *pen = (csch_cpen_t *)obj;
		fprintf(ctx->f, "%s	ha:pen.%s {", parind, pen->name.str);
		r = lht_print_pen(ctx, indlev+1, parind, pen);
		return r;
	}

	if (want_hdr)
		fprintf(ctx->f, "%s	ha:%s.%d {", parind, csch_ctype_name(obj->type), obj->oid);
	switch(obj->type) {
		case CSCH_CTYPE_max:
		case CSCH_CTYPE_invalid:  r = -1; break;
		case CSCH_CTYPE_LINE:     r = lht_print_line(ctx, indlev+1, parind, (const csch_line_t *)obj); break;
		case CSCH_CTYPE_ARC:      r = lht_print_arc(ctx, indlev+1, parind, (const csch_arc_t *)obj); break;
		case CSCH_CTYPE_POLY:     r = lht_print_cpoly(ctx, indlev+1, parind, (const csch_cpoly_t *)obj); break;
		case CSCH_CTYPE_TEXT:     r = lht_print_text(ctx, indlev+1, parind, (const csch_text_t *)obj); break;
		case CSCH_CTYPE_BITMAP:   r = lht_print_cbitmap(ctx, indlev+1, parind, (const csch_cbitmap_t *)obj); break;
		case CSCH_CTYPE_CONN:     r = lht_print_conn(ctx, indlev+1, parind, (const csch_conn_t *)obj); break;
		case CSCH_CTYPE_GRP:      r = lht_print_cgrp(ctx, indlev, parind, (const csch_cgrp_t *)obj); break;
		case CSCH_CTYPE_GRP_REF:  r = lht_print_cgrp_ref(ctx, indlev, parind, (const csch_cgrp_t *)obj); break;
		case CSCH_CTYPE_PEN:      break; /* can't get here: already handled above */
	}
	return r;
}

static int ord_cmp(const void *v1, const void *v2)
{
	csch_chdr_t * const *o1 = v1, * const *o2 = v2;
	if (((*o1)->oid < 0) && ((*o2)->oid < 0)) {
		/* special case: order negative numbers the other way, that's how
		   save-load-save round trips won't invert their order because of
		   new negative IDs are assigned decreasing order */
		if ((*o1)->oid >= (*o2)->oid)
			return -1;
		return +1;
	}
	if ((*o1)->oid >= (*o2)->oid)
		return +1;
	return -1;
}

static void ord_objs(write_ctx_t *ctx, const htip_t *id2obj, vtp0_t *ord)
{
	htip_entry_t *e;
	ord->used = 0;
	for(e = htip_first(id2obj); e != NULL; e = htip_next(id2obj, e))
		vtp0_append(ord, e->value);
	qsort(ord->array, ord->used, sizeof(void *), ord_cmp);
}

int lht_save_grp_ref_child_xforms(write_ctx_t *ctx, const char *ind, const csch_cgrp_t *grp)
{
	long n, i;
	fprintf(ctx->f, "%s\tli:child_xform {\n", ind);
	for(n = 0; n < grp->data.ref.child_xform.used; n++) {
		csch_child_xform_t *cx = grp->data.ref.child_xform.array[n];
		if ((cx == NULL) || cx->cache || cx->removed || csch_grp_ref_child_xform_is_empty(cx)) continue;
		/* print path */
		fprintf(ctx->f, "%s\t\t{ha:", ind);
		for(i = 0; i < cx->path.vt.used; i++) {
			if (i > 0) fprintf(ctx->f, "/");
			fprintf(ctx->f, "%d", cx->path.vt.array[i]);
		}
		fprintf(ctx->f, "} {");

		if (cx->movex != 0) fprintf(ctx->f, " movex=%ld;", cx->movex);
		if (cx->movey != 0) fprintf(ctx->f, " movey=%ld;", cx->movey);
		if (cx->rot != 0) rnd_fprintf(ctx->f, " rot=%.04f;", cx->rot);
		if (cx->mirx != 0) fprintf(ctx->f, " mirx=1;");
		if (cx->miry != 0) fprintf(ctx->f, " miry=1;");
		if (cx->remove != 0) fprintf(ctx->f, " remove=1;");

		fprintf(ctx->f, " }\n");

	}
	fprintf(ctx->f, "%s\t}\n", ind);
	return 0;
}

int lht_save_grp_objects(write_ctx_t *ctx, int indlev, const csch_cgrp_t *grp, vtp0_t *ord)
{
	long n;
	IND(indlev);

	ord_objs(ctx, &grp->id2obj, ord);
	fprintf(ctx->f, "%sli:objects {\n", ind);
	for(n = 0; n < ord->used; n++) {
		csch_chdr_t *obj = ord->array[n];

		if (obj->type == CSCH_CTYPE_PEN) {
			csch_cpen_t *pen = (csch_cpen_t *)obj;
			if (pen->copied_from_default)
				continue; /* happens when saving sheet-as-symbol - skip saving unchanged pens inherited from the default sheet */
		}
		if (lht_print_obj(ctx, indlev+1, ind, obj) != 0)
			return -1;
	}
	fprintf(ctx->f, "%s}\n", ind);
	return 0;
}

static int lht_save_group(write_ctx_t *ctx, int indlev, const csch_cgrp_t *grp, const char *name, long oid, int inhibit_uuid)
{
	static minuid_bin_t uuid0 = {0};
	minuid_str_t utmp;
	int r = 0;
	int optind = 0;
	vtp0_t ord = {0};
	IND(indlev);

#define OPTIND \
	if (optind == 0) { \
		fprintf(ctx->f, "%s	", ind); \
		optind = 1; \
	} \
	else \
		fprintf(ctx->f, " ");

	/* print placement */
	fprintf(ctx->f, "%sha:%s.%ld {\n", ind, name, oid);
	if (!inhibit_uuid && (minuid_cmp(grp->uuid, uuid0) != 0)) {
		OPTIND;
		minuid_bin2str(utmp, grp->uuid);
		fprintf(ctx->f, "uuid=");
		lht_print_str(ctx->f, utmp);
		fprintf(ctx->f, ";");
	}
	if ((grp->hdr.type == CSCH_CTYPE_GRP) && (minuid_cmp(grp->data.grp.src_uuid, uuid0) != 0)) {
		OPTIND;
		minuid_bin2str(utmp, grp->data.grp.src_uuid);
		fprintf(ctx->f, "src_uuid=");
		lht_print_str(ctx->f, utmp);
		fprintf(ctx->f, ";");
	}
	if (grp->loclib_name != NULL) {
		OPTIND;
		fprintf(ctx->f, "loclib_name=");
		lht_print_str(ctx->f, grp->loclib_name);
		fprintf(ctx->f, ";");
	}

	if (optind != 0) {
		fprintf(ctx->f, "\n");
		optind = 0;
	}


	if ((grp->x != 0) || (grp->y != 0)) {
		OPTIND;
		fprintf(ctx->f, "x=%ld; y=%ld;", grp->x, grp->y);
	}

	if (grp->spec_rot != 0) {
		OPTIND;
		fprintf(ctx->f, "rot=%f;", grp->spec_rot);
	}
	if (grp->mirx) {
		OPTIND;
		fprintf(ctx->f, "mirx=1;");
	}
	if (grp->miry) {
		OPTIND;
		fprintf(ctx->f, "miry=1;");
	}
	if (optind != 0)
		fprintf(ctx->f, "\n");

	/* print ref */
	if (grp->hdr.type == CSCH_CTYPE_GRP_REF) {
		char *path, *fp = NULL;
		if (grp->data.ref.ref_str == NULL)
			fp = path = csch_chdr_to_oidpath_str(&grp->data.ref.grp->hdr);
		else
			path = grp->data.ref.ref_str;
		fprintf(ctx->f, "%s	ref=%s\n", ind, path);
		free(fp);
		if (lht_save_grp_ref_child_xforms(ctx, ind, grp) != 0)
			return -1;
	}

	/* print objects */
	if (grp->hdr.type != CSCH_CTYPE_GRP_REF)
		r |= lht_save_grp_objects(ctx, indlev+1, grp, &ord);
	r |= lht_save_attribs(ctx, indlev+1, ind, &grp->attr);

	if (lht_print_common_properties(ctx, &grp->hdr, ind, NULL))
		fprintf(ctx->f, "\n");

	fprintf(ctx->f, "%s}\n", ind);
	vtp0_uninit(&ord);
	return r;
}

static int lht_save_conf(write_ctx_t *ctx)
{
	lht_err_t r;
	const char **s, *del_paths[] = { "editor/mode", NULL };
	lht_node_t *n, *tmp, *root = rnd_conf_lht_get_first(RND_CFR_DESIGN, 0);

	if (root == NULL)
		return 0;

	tmp = lht_dom_duptree(root->parent);

	for(n = tmp->data.list.first; n != NULL; n = n->next) {
		for(s = del_paths; *s != NULL; s++) {
			lht_node_t *sub = lht_tree_path_(n->doc, n, *s, 0, 0, NULL);
			if (sub != NULL) {
				lht_tree_del(sub);
			}
		}
	}

	assert(tmp->type == LHT_LIST);
	assert(strncmp(tmp->name, "sch-rnd-conf-v", 14) == 0);

	r = lht_dom_export(tmp, ctx->f, "  ");

	lht_tree_del(tmp);

	return r;
}

static int io_lihata_save_sheet_or_buff(const char *fn, const char *fmt, const csch_sheet_t *dst, const char *root_prefix, int with_conf)
{
	int r = 0;
	write_ctx_t ctx;

	ctx.sheet = dst;
	ctx.f = rnd_fopen((rnd_design_t *)&dst->hidlib, fn, "w");
	if (ctx.f == NULL)
		return -1;

	fprintf(ctx.f, "ha:%s1 {\n", root_prefix);
	r |= lht_save_group(&ctx, 1, &ctx.sheet->indirect, "obj_indirect", 1, 0);
	r |= lht_save_group(&ctx, 1, &ctx.sheet->direct, "obj_direct", 2, 0);
	if (with_conf)
		r |= lht_save_conf(&ctx);
	fprintf(ctx.f, "}\n");

	fclose(ctx.f);
	return r;
}

int io_lihata_save_sheet(const char *fn, const char *fmt, const csch_sheet_t *dst)
{
	return io_lihata_save_sheet_or_buff(fn, fmt, dst, "cschem-sheet-v", 1);
}

int io_lihata_save_buffer(const char *fn, const char *fmt, const csch_sheet_t *dst)
{
	return io_lihata_save_sheet_or_buff(fn, fmt, dst, "cschem-buffer-v", 0);
}


int io_lihata_save_grp(const char *fn, const char *fmt, const csch_cgrp_t *src, int inhibit_uuid)
{
	int r = 0, do_close = 0;
	write_ctx_t ctx;

	ctx.sheet = src->hdr.sheet;
	if (strcmp(fn, "/dev/stdout") != 0) {
		ctx.f = rnd_fopen((rnd_design_t *)&ctx.sheet->hidlib, fn, "w");
		if (ctx.f == NULL)
			return -1;
		do_close = 1;
	}
	else
		ctx.f = stdout;

	fprintf(ctx.f, "ha:cschem-group-v1 {\n");
	r |= lht_save_group(&ctx, 1, src, "group", 1, inhibit_uuid);
	fprintf(ctx.f, "}\n");

	if (do_close)
		fclose(ctx.f);
	return r;
}

