/*  libgrbs - geometric rubber band sketch model
    Copyright (C) 2021  Tibor 'Igor2' Palinkas
    (Supported by NLnet NGI0 PET Fund in 2021)

    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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

    Contact:
      Project page: http://repo.hu/projects/libgrbs
      lead developer: http://repo.hu/projects/pcb-rnd/contact.html
*/

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <genht/htsp.h>
#include <genht/ht_utils.h>
#include <genht/hash.h>

#include <libgrbs/grbs.h>
#include <libgrbs/debug.h>
#include <libgrbs/geo.h>
#include <libgrbs/route.h>

grbs_t ctx;
htsp_t name2obj;
int CGI = 0;

#define shift_name(args, req_uniq) \
do { \
	args = strpbrk(args, " \t"); \
	if (args != NULL) { \
		*args = '\0'; \
		args++; \
	} \
	else \
		args = ""; \
	if (req_uniq) { \
		if (htsp_get(&name2obj, name) != NULL) { \
			fprintf(stderr, "error: name %s is already in use\n", name); \
			return -1; \
		} \
	} \
} while(0)

#define obj_by_name(dst, name, errmsg) \
do { \
	dst = htsp_get(&name2obj, name); \
	if (dst == NULL) { \
		fprintf(stderr, "%s by the name '%s'\n", errmsg, name); \
		return -1; \
	} \
} while(0)

#define remember(name, obj) \
	htsp_set(&name2obj, strdup(name), obj)

#define forget(name) \
do { \
	htsp_entry_t *e = htsp_popentry(&name2obj, name); \
	if (e != NULL) \
		free(e->key); \
} while(0)

#define ERR_SYNTAX -42
#define ERR_EXIT   -43

static int parse_cmd(char *cmd, char *args);

static int cmd_point_new(char *args)
{
	char *name = args;
	double x, y, co, cl;
	grbs_point_t *p;

	shift_name(args, 1);
	if (sscanf(args, "%lf %lf %lf %lf", &x, &y, &co, &cl) != 4) {
		fprintf(stderr, "syntax error in point_new args; should be: name x y copper clearance\n");
		return -1;
	}

	p = grbs_point_new(&ctx, x, y, co, cl);
	remember(name, p);

	return 0;
}

static int cmd_point_chg(char *args)
{
	char *name = args;
	double co, cl;
	grbs_point_t *pt;

	shift_name(args, 0);
	obj_by_name(pt, name, "point_chg: no point");

	if (sscanf(args, "%lf %lf", &co, &cl) != 2) {
		fprintf(stderr, "syntax error in point_chg args; should be: name copper clearance\n");
		return -1;
	}

	grbs_point_unreg(&ctx, pt);
	pt->copper = co;
	pt->clearance = cl;
	grbs_point_reg(&ctx, pt);

	return 0;
}

static int cmd_svg_begin(char *args)
{
	char *name = args;
	FILE *f;

	shift_name(args, 1);
	f = fopen(args, "w");
	if (f == NULL) {
		fprintf(stderr, "svg_begin: can't open file '%s' for write\n", args);
		return -1;
	}

	grbs_draw_begin(&ctx, f);
	remember(name, f);

	return 0;
}

static int cmd_svg_end(char *args)
{
	char *name = args;
	FILE *f;

	obj_by_name(f, name, "svg_end: no open svg");

	grbs_draw_end(&ctx, f);
	fclose(f);
	forget(name);

	return 0;
}

static int cmd_fill_circle(char *args)
{
	char *name = args;
	double x, y, sr;
	int pos;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "fill_circle: no open svg");

	if (sscanf(args, "%lf %lf %lf %n", &x, &y, &sr, &pos) != 3) {
		fprintf(stderr, "syntax error in fill_circle; should be: svgname x y sr color\n");
		return -1;
	}

	grbs_svg_fill_circle(f, x, y, sr, args+pos);
	return 0;
}

static int cmd_wf_circle(char *args)
{
	char *name = args;
	double x, y, sr;
	int pos;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "wf_circle: no open svg");

	if (sscanf(args, "%lf %lf %lf %n", &x, &y, &sr, &pos) != 3) {
		fprintf(stderr, "syntax error in wf_circle; should be: svgname x y sr color\n");
		return -1;
	}

	grbs_svg_wf_circle(f, x, y, sr, args+pos);
	return 0;
}

static int cmd_fill_line(char *args)
{
	char *name = args;
	double x1, y1, x2, y2, sr;
	int pos;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "fill_line: no open svg");

	if (sscanf(args, "%lf %lf %lf %lf %lf %n", &x1, &y1, &x2, &y2, &sr, &pos) != 5) {
		fprintf(stderr, "syntax error in fill_line; should be: svgname x1 y1 x2 y2 sr color\n");
		return -1;
	}

	grbs_svg_fill_line(f, x1, y1, x2, y2, sr, args+pos);
	return 0;
}

static int cmd_wf_line(char *args)
{
	char *name = args;
	double x1, y1, x2, y2, sr;
	int pos;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "wf_line: no open svg");

	if (sscanf(args, "%lf %lf %lf %lf %lf %n", &x1, &y1, &x2, &y2, &sr, &pos) != 5) {
		fprintf(stderr, "syntax error in wf_line; should be: svgname x1 y1 x2 y2 sr color\n");
		return -1;
	}

	grbs_svg_wf_line(f, x1, y1, x2, y2, sr, args+pos);
	return 0;
}

static int cmd_fill_arc(char *args)
{
	char *name = args;
	double cx, cy, r, sa, da, sr;
	int pos;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "fill_arc: no open svg");

	if (sscanf(args, "%lf %lf %lf %lf %lf %lf %n", &cx, &cy, &r, &sa, &da, &sr, &pos) != 6) {
		fprintf(stderr, "syntax error in fill_arc; should be: svgname cx, cy, r, start_abgle, end_angle, stroke_radius, color\n");
		return -1;
	}

	grbs_svg_fill_arc(f, cx, cy, r, sa, da, sr, args+pos);
	return 0;
}

static int cmd_wf_arc(char *args)
{
	char *name = args;
	double cx, cy, r, sa, da, sr;
	int pos;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "wf_arc: no open svg");

	if (sscanf(args, "%lf %lf %lf %lf %lf %lf %n", &cx, &cy, &r, &sa, &da, &sr, &pos) != 6) {
		fprintf(stderr, "syntax error in wf_arc; should be: svgname cx, cy, r, start_abgle, end_angle, stroke_radius, color\n");
		return -1;
	}

	grbs_svg_wf_arc(f, cx, cy, r, sa, da, sr, args+pos);
	return 0;
}

static void d1() {}

#define shift_word(s, next) \
do { \
	if (s != NULL) { \
		next = strpbrk(s, " \t"); \
		if (next != NULL) { \
			*next = '\0'; \
			next++; \
		} \
		while(isspace(*s)) s++; \
		if ((strcmp(s, "break") == 0) || (strcmp(s, "bp") == 0)) { \
			d1(); \
			s = next; \
			continue; \
		} \
	} \
	else \
		next = NULL; \
	break; \
} while(1)

static int cmd_draw(char *args)
{
	char *name = args, *s, *next;
	int points = 0, wires = 0;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "draw: no open svg");

	for(s = args; s != NULL; s = next) {
		shift_word(s, next);
		if (*s == '\0')
			break;
		if (strcmp(s, "all") == 0)
			points = wires = 1;
		else if (strcmp(s, "points") == 0)
			points = 1;
		else if (strcmp(s, "wires") == 0)
			wires = 1;
		else {
			fprintf(stderr, "error: draw: no such thing to draw: '%s'\n", s);
		}
	}

	if (points)
		grbs_draw_points(&ctx, f);

	if (wires)
		grbs_draw_wires(&ctx, f);

	return 0;
}

static int cmd_dump(char *args)
{
	char *s, *next;
	int points = 0, wires = 0;
	FILE *f;

	f = stdout;

	for(s = args; s != NULL; s = next) {
		shift_word(s, next);
		if (*s == '\0')
			break;
		if (strcmp(s, "all") == 0)
			points = wires = 1;
		else if (strcmp(s, "points") == 0)
			points = 1;
		else if (strcmp(s, "wires") == 0)
			wires = 1;
		else {
			fprintf(stderr, "error: draw: no such thing to draw: '%s'\n", s);
		}
	}

	if (points)
		grbs_dump_points(&ctx, f);

	if (wires)
		grbs_dump_wires(&ctx, f);

	return 0;
}

#define DETACHED -42
grbs_addr_t *last_addr = NULL, *head = NULL;
static int path_parse_append(grbs_2net_t *tn, char *args)
{

	grbs_addr_t *addr;
	char *s, *cmd, *pt, *next;
	int curr_dir, res = -1;
	grbs_point_t *curr = NULL;


	for(s = args; s != NULL; s = next) {
		shift_word(s, next); cmd = s; s = next;

		if (strcmp(cmd, "reattach") == 0) {
			grbs_detached_addr_t *det;
			char *name;

			shift_word(s, next); name = s; s = next;
			obj_by_name(det, name, "no such detached address");
			addr = grbs_reattach_addr(&ctx, det);
			free(det);
			if (addr == NULL) {
				fprintf(stderr, "error: failed to reattach at %s\n", s);
				return -1;
			}

			addr->user_data = head;
			head = last_addr = addr;
			continue;
		}


		curr_dir = -1;

		shift_word(s, next); pt = s;

		if (*cmd == '\0')
			break;

		/* decode cmd */
		if (strcmp(cmd, "pause") == 0)
			return 0;
		else if (strcmp(cmd, "detach") == 0) {
			grbs_detached_addr_t *det = malloc(sizeof(grbs_detached_addr_t)*3);
			grbs_detach_addr(&ctx, det, last_addr);
			shift_word(s, next);
			remember(s, det);
			res = DETACHED;
			goto err;
		}
		else if ((strcmp(cmd, "from") == 0) || (strcmp(cmd, "to") == 0))
			curr_dir = GRBS_ADIR_INC;
		else if (strcmp(cmd, "cw") == 0)
			curr_dir = GRBS_ADIR_CONVEX_CW;
		else if (strcmp(cmd, "ccw") == 0)
			curr_dir = GRBS_ADIR_CONVEX_CCW;
		else if (strcmp(cmd, "vconcave") == 0) {
			curr_dir = GRBS_ADIR_VCONCAVE;
		}
		else {
			fprintf(stderr, "Invalid path at '%s': expected from, to or around\n", cmd);
			goto err;
		}


		/* find the point */
		obj_by_name(curr, pt, "Path: no point");

		if (last_addr != NULL) {
			addr = grbs_path_next(&ctx, tn, last_addr, curr, curr_dir);
/*			printf("draw to   %p:%d a=%p\n", curr, curr_dir, addr);*/
			last_addr = addr;
		}
		else {
			addr = last_addr = grbs_addr_new(&ctx, ADDR_POINT, curr);
/*			printf("draw from %p:%d a=%p\n", curr, curr_dir, last_addr);*/
		}

		if (addr != NULL) {
			/* use ->user_data to build a singly linked list of addresses for this route */
			addr->user_data = head;
			head = addr;
		}
		else {
			fprintf(stderr, "Invalid path at '%s': failed to create arc\n", s);
			goto err;
		}
	}

	/* check if the final path doesn't contain intersecting lines */
	last_addr = NULL;
	for(addr = head; addr != NULL; addr = addr->user_data) {
		if (grbs_path_validate(&ctx, tn, last_addr, addr, addr->user_data) != 0) {
			fprintf(stderr, "Invalid path at '%s': failed to validate\n", s);
			goto err;
		}
		last_addr = addr;
	}

	/* reconstruct the path */
	for(addr = head; addr != NULL; addr = addr->user_data)
		grbs_path_realize(&ctx, tn, addr, 0);

	res = 0;

	err:;
	/* free all temporary addrs used for the routing */
	grbs_path_cleanup_all(&ctx);

	last_addr = NULL;
	head = NULL;
	return res;
}

static int cmd_2net_new(char *args)
{
	char *name = args;
	double co, cl;
	grbs_2net_t *tn;
	int pos, res;

	shift_name(args, 1);

	if (sscanf(args, "%lf %lf %n", &co, &cl, &pos) != 2) {
		fprintf(stderr, "syntax error in 2net_new args; should be: name copper clearance [path]\n");
		return -1;
	}

	args += pos;
	tn = grbs_2net_new(&ctx, co, cl);

	res = path_parse_append(tn, args);
	if (res == DETACHED)
		res = 0;
	else
		remember(name, tn);
	return res;
}

static int cmd_2net_del(char *args)
{
	char *name = args;
	grbs_2net_t *tn;

	shift_name(args, 0);
	obj_by_name(tn, name, "No such two-net");

	grbs_path_remove_2net(&ctx, tn);
	forget(name);
	return 0;
}

static int cmd_2net_append(char *args)
{
	char *name = args;
	grbs_2net_t *tn;

	shift_name(args, 0);
	obj_by_name(tn, name, "No such two-net");

	while(isspace(*args)) args++;
	return path_parse_append(tn, args);
}

static int cmd_2net_cleanup(char *args)
{
	grbs_addr_t *addr;

	for(addr = head; addr != NULL; addr = addr->user_data)
		grbs_path_cleanup_addr(&ctx, addr);

	uall_stacks_clean(&ctx.stk_addrs);

	return 0;
}

static int cmd_2net_markpt(char *args)
{
	char *name = args, *ptname;
	grbs_2net_t *tn;
	grbs_point_t *pt;

	shift_name(args, 0);
	obj_by_name(tn, name, "No such two-net");

	ptname = args;
	shift_name(args, 0);
	obj_by_name(pt, ptname, "no point");


	pt->temp_tn = tn;
	return 0;
}

static int cmd_imline(char *args)
{
	char *name = args, *pt1name, *pt2name;
	grbs_point_t *pt1, *pt2;
	grbs_2net_t *tn;
	grbs_line_t *l;

	pt1name = args;
	shift_name(args, 0);
	obj_by_name(pt1, pt1name, "no point1");

	pt2name = name = args;
	shift_name(args, 0);
	obj_by_name(pt2, pt2name, "no point2");

	tn = grbs_2net_new(&ctx, 0, 0);
	l = grbs_line_realize(&ctx, tn, pt1, pt2);
	l->immutable = 1;
	return 0;
}

static int cmd_geo_bicycle(char *args)
{
	char *name = args;
	double cx1, cy1, r1, cx2, cy2, r2, sr;
	double a[4];
	char *color;
	int pos, res, cross;
	FILE *f;

	shift_name(args, 0);
	obj_by_name(f, name, "geo_bicycle: no open svg");

	if (sscanf(args, "%lf %lf %lf %lf %lf %lf %lf %d %n", &cx1, &cy1, &r1, &cx2, &cy2, &r2, &sr, &cross, &pos) != 8) {
		fprintf(stderr, "syntax error in geo_bicycle; should be: svgname cx1 cy1 r1 cx2 cy2 r2 sr color\n");
		return -1;
	}
	color = args+pos;

	res = grbs_bicycle_angles(cx1, cy1, r1, cx2, cy2, r2, a, cross);
	if (res == 0) {
		int n, m;

		for(n = 0; n < 2; n++) {
			double x1, y1, x2, y2;
			m = cross ? (!n) : n;
			x1 = cx1 + cos(a[n+0]) * r1; y1 = cy1 + sin(a[n+0]) * r1;
			x2 = cx2 + cos(a[m+2]) * r2; y2 = cy2 + sin(a[m+2]) * r2;
			grbs_svg_wf_line(f, x1, y1, x2, y2, sr, color);
		}
	}
	else
		fprintf(stderr, "invalid bicycle\n");
	return 0;

}

static int cmd_echo(char *args)
{
	if (!CGI)
		printf("%s\n", args);
	return 0;
}

static int cmd_fail(char *cmd)
{
	char *args;
	int res;

	args = strpbrk(cmd, " \t");
	if (args != NULL) {
		*args = '\0';
		args++;
		while(isspace(*args)) args++;
	}
	else
		args = "";

	res = parse_cmd(cmd, args);
	if (res == ERR_SYNTAX)
		return res;
	if (res == 0) {
		fprintf(stderr, "command '%s' is expected to fail but did not fail\n", cmd);
		return -1;
	}
	return 0;
}

int brk = 0;
static int cmd_brk(char *args)
{
	brk = atoi(args);
	return 0;
}


static int parse_cmd(char *cmd, char *args)
{
	if (*cmd == '\0')                     return 0;

	if (strcmp(cmd, "point_new") == 0)    return cmd_point_new(args);
	if (strcmp(cmd, "point_chg") == 0)    return cmd_point_chg(args);
	if (strcmp(cmd, "svg_begin") == 0)    return cmd_svg_begin(args);
	if (strcmp(cmd, "svg_end") == 0)      return cmd_svg_end(args);
	if (strcmp(cmd, "fill_circle") == 0)  return cmd_fill_circle(args);
	if (strcmp(cmd, "wf_circle") == 0)    return cmd_wf_circle(args);
	if (strcmp(cmd, "fill_line") == 0)    return cmd_fill_line(args);
	if (strcmp(cmd, "wf_line") == 0)      return cmd_wf_line(args);
	if (strcmp(cmd, "fill_arc") == 0)     return cmd_fill_arc(args);
	if (strcmp(cmd, "wf_arc") == 0)       return cmd_wf_arc(args);
	if (strcmp(cmd, "draw") == 0)         return cmd_draw(args);
	if (strcmp(cmd, "dump") == 0)         return cmd_dump(args);
	if (strcmp(cmd, "2net_new") == 0)     return cmd_2net_new(args);
	if (strcmp(cmd, "2net_del") == 0)     return cmd_2net_del(args);
	if (strcmp(cmd, "2net_append") == 0)  return cmd_2net_append(args);
	if (strcmp(cmd, "2net_cleanup") == 0) return cmd_2net_cleanup(args);
	if (strcmp(cmd, "2net_markpt") == 0)  return cmd_2net_markpt(args);
	if (strcmp(cmd, "imline") == 0)       return cmd_imline(args);
	if (strcmp(cmd, "geo_bicycle") == 0)  return cmd_geo_bicycle(args);
	if (strcmp(cmd, "echo") == 0)         return cmd_echo(args);
	if (strcmp(cmd, "fail") == 0)         return cmd_fail(args);
	if (strcmp(cmd, "brk") == 0)          return cmd_brk(args);
	if (strcmp(cmd, "exit") == 0)         return ERR_EXIT;


	fprintf(stderr, "syntax error: unknown command %s\n", cmd);
	return ERR_SYNTAX;
}

int parse(char *line)
{
	char *cmd, *args, *end;

	while(isspace(*line)) line++;
	if (*line == '#')
		return 0;

	cmd = line;
	args = strpbrk(cmd, " \t\r\n");
	if (args != NULL) {
		*args = '\0';
		args++;
		while(isspace(*args)) args++;
		end = strpbrk(args, "\r\n");
		if (end != NULL)
			*end = '\0';
	}
	return parse_cmd(cmd, args);
}

int fparse(FILE *f)
{
	char line[1024];
	long lno = 0;
	int res;

	while(fgets(line, 1024, f) != NULL) {
		lno++;
		res = parse(line);
		if (res == ERR_EXIT)
			return 0;
		if (res != 0) {
			fprintf(stderr, " (in line %ld)\n", lno);
			return -1;
		}
	}
	return 0;
}

int main(int argc, char *argv[])
{
	int res, n;
	FILE *f;
	char *filename, *basename = NULL, bn[1024], *sep, *basename_end;
	int dump_svg = 0, dump_text = 0;

	f = stdin;
	for(n = 1; n < argc; n++) {
		char *cmd = argv[n], *arg = argv[n+1];
		while(*cmd == '-') cmd++;
		switch(*cmd) {
			case 'C': CGI = 1; break;
			case 'f':
				if (basename != NULL) {
					fprintf(stderr, "only one -f accepted\n");
					return -1;
				}
				n++;
				filename = arg;
				sep = strrchr(filename, '.');
				if (sep == NULL)
					sep = filename+strlen(filename);
				if ((sep - filename) > sizeof(bn)-8) {
					fprintf(stderr, "path '%s' is too long (%ld)\n", filename, (long)(sep - filename));
					return -1;
				}
				strncpy(bn, filename, sep-filename);
				bn[sep-filename] = '\0';
				basename = bn;
				basename_end = basename + (sep-filename);
				f = fopen(filename, "r");
				if (f == NULL) {
					fprintf(stderr, "can not open file '%s' for read\n", filename);
					return -1;
				}
				break;
			case 's': dump_svg = 1; break;
			case 'd': dump_text = 1; break;
		}
	}


	grbs_init(&ctx);
	htsp_init(&name2obj, strhash, strkeyeq);

	res = fparse(f);

	if (CGI && (res == 0)) {
		printf("Content-Type: image/svg+xml\n\n");
		grbs_draw_begin(&ctx, stdout);
		grbs_draw_points(&ctx, stdout);
		grbs_draw_wires(&ctx, stdout);
		grbs_draw_end(&ctx, stdout);
	}

	if (dump_svg) {
		if (basename != NULL) {
			FILE *fo;
		
			strcpy(basename_end, ".svg");
			fo = fopen(basename, "w");
			if (fo != NULL) {
				grbs_draw_begin(&ctx, fo);
				grbs_draw_points(&ctx, fo);
				grbs_draw_wires(&ctx, fo);
				grbs_draw_end(&ctx, fo);
				fclose(fo);
			}
			else
				fprintf(stderr, "Failed to open '%s' for write for -s\n", basename);
			*basename_end = '\0';
		}
		else
			fprintf(stderr, "Can't save svg on -s without -f\n");
	}

	if (dump_text) {
		if (basename != NULL) {
			FILE *fo;
		
			strcpy(basename_end, ".dump");
			fo = fopen(basename, "w");
			if (fo != NULL) {
				grbs_dump_points(&ctx, fo);
				grbs_dump_wires(&ctx, fo);
				fclose(fo);
			}
			else
				fprintf(stderr, "Failed to open '%s' for write for -d\n", basename);
			*basename_end = '\0';
		}
		else
			fprintf(stderr, "Can't save dump on -d without -f\n");
	}

	genht_uninit_deep(htsp, &name2obj, {
		free(htent->key);
	});
	grbs_uninit(&ctx);

	return res;
}
