/*
 *                            COPYRIGHT
 *
 *  camv-rnd - electronics-related CAM viewer
 *  Copyright (C) 2024 Tibor 'Igor2' Palinkas
 *
 *  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/camv-rnd
 *    lead developer: http://repo.hu/projects/camv-rnd/contact.html
 *    mailing list: pcb-rnd (at) list.repo.hu (send "subscribe")
 */

#include <librnd/config.h>

#include <stdio.h>
#include <ctype.h>
#include <assert.h>
#include <math.h>

#include <librnd/core/error.h>
#include <librnd/core/safe_fs.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/math_helper.h>
#include <librnd/core/rotate.h>
#include <librnd/font/font.h>

#include "data.h"
#include "plug_io.h"
#include "obj_any.h"

#ifdef M_PI
#	define H_PI M_PI
#else
#	define H_PI 3.141592654
#endif

#define HPGL_FONT_SCALE 8.0

typedef struct {
	rnd_coord_t x, y;
	unsigned up:1;
	unsigned ct:1;
	unsigned iw:1; /* 1 if input window is actiev */
	unsigned relative:1;

	unsigned page;
	int pen;

	/* transformation */
	double scx, scy, ox, oy;
	double text_rotdeg, text_scx, text_scy;
	rnd_coord_t iwx1, iwy1, iwx2, iwy2; /* window in absolute rendering units */

	/* text (labels) */
	int labterm;

	/* temporary storage for IP+SC */
	long p1x, p1y, p2x, p2y, u1x, u1y, u2x, u2y;

	vtp0_t layers;
	const char *fn;
	camv_design_t *camv;
} hpgl_st_t;

static void hpgl_uninit(hpgl_st_t *st)
{
	vtp0_uninit(&st->layers);
	st->fn = NULL;
	st->camv = NULL;
}

static void hpgl_reset(hpgl_st_t *st, int df)
{
	if (!df) {
		st->x = st->y = 0;
		st->pen = 0;
		st->labterm = '\3';
	}

	st->scx = st->scy = 1;
	st->ox = st->oy = 0;
	st->up = 1;
	st->ct = 0;
	st->iw = 0;
	st->text_rotdeg = 0;
	st->text_scx = st->text_scy = 1.0;
}

static void hpgl_set_scale(hpgl_st_t *st)
{
	st->scx = (double)(st->p2x - st->p1x) / (double)(st->u2x - st->u1x);
	st->scy = (double)(st->p2y - st->p1y) / (double)(st->u2y - st->u1y);
	st->ox = + st->u1x * st->scx + st->p1x;
	st->oy = + st->u1y * st->scy + st->p1y;
}

void hpgl_page_feed(hpgl_st_t *st)
{
	st->page++;
	st->x = st->y = 0;
	vtp0_uninit(&st->layers);
}

static camv_layer_t *hpgl_get_layer(hpgl_st_t *st)
{
	camv_layer_t *ly;

	if (st->pen >= st->layers.used)
		vtp0_enlarge(&st->layers, st->pen);

	assert(st->pen < st->layers.used);

	if (st->layers.array[st->pen] == NULL) {
		ly = camv_layer_new();
		if (st->page == 0)
			ly->name = rnd_strdup_printf("%s:pen_%d", st->fn, st->pen);
		else
			ly->name = rnd_strdup_printf("%s:pg_%u_pen_%d", st->fn, st->page, st->pen);
		camv_layer_invent_color(st->camv, ly);
		st->layers.array[st->pen] = ly;
		camv_layer_append_to_design(st->camv, ly);
	}
	else
		ly = st->layers.array[st->pen];

	return ly;
}

#define TX(x) (RND_MM_TO_COORD((double)x + st->ox) * 0.025 * st->scx)
#define TY(y) (RND_MM_TO_COORD((double)y + st->oy) * 0.025 * st->scy)

#define TPX(x) (RND_MM_TO_COORD((double)x) * 0.025)
#define TPY(y) (RND_MM_TO_COORD((double)y) * 0.025)

#define RTPX(x) (RND_COORD_TO_MM((double)x) / 0.025)
#define RTPY(y) (RND_COORD_TO_MM((double)y) / 0.025)

static double line_y_for_x(double x1, double y1, double x2, double y2, double x_at)
{
	double dx = x2 - x1, dy = y2 - y1, m = dy/dx;
	return y1 + (x_at - x1) * m;
}

static double line_x_for_y(double x1, double y1, double x2, double y2, double y_at)
{
	double dx = x2 - x1, dy = y2 - y1, m = dx/dy;
	return x1 + (y_at - y1) * m;
}

static void hpgl_gen_line_transformed(hpgl_st_t *st, double x1, double y1, double x2, double y2)
{
	camv_layer_t *ly = hpgl_get_layer(st);
	camv_any_obj_t *o;

	if (st->iw) {
		/* clip to input window; window coords are ordered, line coords are not */

		/* left vertical edge of iw */
		if ((x1 < st->iwx1) && (x2 < st->iwx1))
			return;
		if (x1 < x2) {
			if ((st->iwx1 > x1) && (st->iwx1 < x2)) {
				y1 = line_y_for_x(x1, y1, x2, y2, st->iwx1);
				x1 = st->iwx1;
			}
		}
		else {
			if ((st->iwx1 > x2) && (st->iwx1 < x1)) {
				y2 = line_y_for_x(x1, y1, x2, y2, st->iwx1);
				x2 = st->iwx1;
			}
		}

		/* right vertical edge of iw */
		if ((x1 > st->iwx2) && (x2 > st->iwx2))
			return;
		if (x1 < x2) {
			if ((st->iwx2 > x1) && (st->iwx2 < x2)) {
				y2 = line_y_for_x(x1, y1, x2, y2, st->iwx2);
				x2 = st->iwx2;
			}
		}
		else {
			if ((st->iwx2 > x2) && (st->iwx2 < x1)) {
				y1 = line_y_for_x(x1, y1, x2, y2, st->iwx2);
				x1 = st->iwx2;
			}
		}

		/* bottom horizontal edge of iw */
		if ((y1 < st->iwy1) && (y2 < st->iwy1))
			return;
		if (y1 < y2) {
			if ((st->iwy1 > y1) && (st->iwx1 < y2)) {
				x1 = line_x_for_y(x1, y1, x2, y2, st->iwy1);
				y1 = st->iwy1;
			}
		}
		else {
			if ((st->iwy1 > y2) && (st->iwy1 < y1)) {
				x2 = line_y_for_x(x1, y1, x2, y2, st->iwy1);
				y2 = st->iwy1;
			}
		}

		/* top horizontal edge of iw */
		if ((y1 > st->iwy2) && (y2 > st->iwy2))
			return;
		if (y1 < y2) {
			if ((st->iwy2 > y1) && (st->iwx2 < y2)) {
				x2 = line_x_for_y(x1, y1, x2, y2, st->iwy2);
				y2 = st->iwy2;
			}
		}
		else {
			if ((st->iwy2 > y2) && (st->iwy2 < y1)) {
				x1 = line_y_for_x(x1, y1, x2, y2, st->iwy2);
				y1 = st->iwy2;
			}
		}


	}

	o = (camv_any_obj_t *)camv_line_new();
	o->line.x1 = x1; o->line.y1 = y1;
	o->line.x2 = x2; o->line.y2 = y2;
	o->line.thick = RND_MM_TO_COORD(0.25);
	camv_obj_add_to_layer(ly, o);
}

static void hpgl_gen_line_dbl(hpgl_st_t *st, double x1, double y1, double x2, double y2)
{
	x1 = TX(x1); y1 = TX(y1);
	x2 = TX(x2); y2 = TX(y2);

	hpgl_gen_line_transformed(st, x1, y1, x2, y2);
}


static void hpgl_gen_line(hpgl_st_t *st, long x1, long y1, long x2, long y2)
{
	hpgl_gen_line_dbl(st, x1, y1, x2, y2);
}

static void hpgl_arc(hpgl_st_t *st, long cx, long cy, double radius, double astart, double delta, double resolution, int set_st_xy)
{
	double a, step, lx, ly, x, y;
	long n, v;

	/* figure angle step to fulfill CT tolerance criteria */
	if (resolution == 0)
		resolution = 5.0;
	if (resolution < 0.5)
		resolution = 0.5;

	/* based on hp2xx */
	step = resolution * M_PI / 180.0;
	if (st->ct)
		step = 15 * acos((radius - step) / radius);

	if (delta < 0)
		step = -step;

	v = delta/step;
	if (v < 0)
		v = -v;

	/* draw first point and subsequent points with stepping */
	for(n = 0, a = astart; n <= v; n++, a+=step) {
		x = cx + cos(a) * radius;
		y = cy + sin(a) * radius;
		if (n > 0)
			hpgl_gen_line_dbl(st, lx, ly, x, y);
		lx = x;
		ly = y;
	}

	/* draw last point */
	a = astart + delta;
	x = cx + cos(a) * radius;
	y = cy + sin(a) * radius;
	if ((x != lx) && (y != ly))
		hpgl_gen_line_dbl(st, lx, ly, x, y);

	if (set_st_xy) {
		st->x = x;
		st->y = y;
	}
}

static void hpgl_gen_circle(hpgl_st_t *st, long radius, double resolution)
{
	hpgl_arc(st, st->x, st->y, radius, 0, 2*H_PI, resolution, 0);
}

static void hpgl_gen_arc(hpgl_st_t *st, long cx, long cy, double adelta, double resolution, int relative)
{
	double radius, dx, dy, astart;

	if (relative) {
		cx += st->x;
		cy += st->y;
	}

	dx = cx - st->x;
	dy = cy - st->y;
	radius = sqrt(dx*dx + dy*dy);

	astart = atan2(dy, dx);

	hpgl_arc(st, cx, cy, radius, astart + H_PI, adelta * H_PI / 180, resolution, 1);
}

static void hpgl_text_draw_atom_cb(void *cb_ctx, const rnd_glyph_atom_t *a)
{
	hpgl_st_t *st = cb_ctx;

	switch(a->type) {
		case RND_GLYPH_LINE:
			hpgl_gen_line_transformed(st, a->line.x1, a->line.y1, a->line.x2, a->line.y2);
			break;
		case RND_GLYPH_ARC:
			rnd_message(RND_MSG_ERROR, "arc in font not yet supported\n");
			break;
		case RND_GLYPH_POLY:
			rnd_message(RND_MSG_ERROR, "poly in font not yet supported\n");
			break;
	}
}

extern rnd_font_t *camv_font;
static void hpgl_gen_label(hpgl_st_t *st, char *str, int dry, long mx, long my)
{
	double scale = HPGL_FONT_SCALE, rotr;
	rnd_coord_t ox, oy, cx, cy;
	rnd_coord_t bbx[4], bby[4];

	rnd_font_string_bbox(bbx, bby, camv_font, (unsigned char *)str, 0, 0, scale * st->text_scx, scale * st->text_scy, 0, RND_FONT_MIRROR_Y, 0, 0);

	ox = TX(st->x);
	oy = TX(st->y);

	cx = ox;
	cy = oy - bby[2] * my;

	if (st->text_rotdeg != 0) {
		rotr = -st->text_rotdeg / 180.0 * H_PI;
		rnd_rotate(&cx, &cy, ox, oy, cos(rotr), sin(rotr));
	}

	if (!dry)
		rnd_font_draw_string(camv_font, (unsigned char *)str, cx, cy, scale * st->text_scx, scale * st->text_scy, st->text_rotdeg, RND_FONT_MIRROR_Y, 0, 0, 0, RND_FONT_TINY_HIDE, hpgl_text_draw_atom_cb, st);

	if (st->text_rotdeg != 0) {
		cx = st->x + RTPX(bbx[2] * mx);
		cy = st->y;
		rnd_rotate(&cx, &cy, st->x, st->y, cos(rotr), sin(rotr));
		st->x = cx;
		st->y = cy;
	}
	else {
		st->x = st->x + RTPX(bbx[2] * mx);
		st->y = st->y;
	}
}

static void hpgl_text_rot(hpgl_st_t *st, long dx, long dy, int relative)
{
	if (!relative) {
		dx = dx - st->x;
		dy = dy - st->y;
	}
	st->text_rotdeg = atan2(dy, dx) * 180.0 / H_PI;
}


#define INST(a,b) (((unsigned)a << 8) | ((unsigned)b))

static int hpgl_parse_cmd(hpgl_st_t *st, char *buf, int len)
{
	long x, y;
	int draw_line = !st->up, relative = 0, slen;

	if (buf[0] == '\0')
		return 0;

	switch(INST(buf[0], buf[1])) {
		case INST('I', 'N'): /* "IN": initialize */
			hpgl_reset(st, 0);
			return 0;

		case INST('D', 'F'): /* "DF": defaults */
			hpgl_reset(st, 1);
			return 0;

		case INST('P', 'U'): /* "PU": pen up then move */
			st->up = 1;
			relative = st->relative;
			draw_line = 0;
			buf+=2;
			goto maybe_move;

		case INST('P', 'D'): /* "PD": pen down then move */
			st->up = 0;
			relative = st->relative;
			draw_line = 1;
			buf+=2;
			goto maybe_move;

		case INST('P', 'A'): /* "PA": linear move absolute */
			buf+=2;
			st->relative = 0;
			goto move;

		case INST('P', 'R'): /* "PA": linear move relative */
			relative = 1;
			st->relative = 1;
			buf+=2;
			goto move;

		case INST('S', 'P'): /* "SP": select pen */
			{
				char *end;

				st->pen = strtol(buf+2, &end, 10);
				if (*end != '\0') {
					rnd_message(RND_MSG_ERROR, "camv_hpgl: invalid pen selection: '%s'\n", buf);
					return -1;
				}
			}
			return 0;

		case INST('A', 'A'): /* "AA": arc absolute */
		case INST('A', 'R'): /* "AR": arc relative */
			{
				int r;
				long cx, cy;
				double ang, res = 0;

				r = sscanf(buf+2, "%ld,%ld,%lf,%lf", &cx, &cy, &ang, &res);
				if ((r < 3) || (r > 4)) {
					rnd_message(RND_MSG_ERROR, "camv_hpgl: CI needs to have 1 or 2 arguments '%s'\n", buf);
					return -1;
				}

				hpgl_gen_arc(st, cx, cy, ang, res, (buf[1] == 'R'));
			}
			return 0;

		case INST('I', 'P'): /* "IP": translate/scale: plotter window */
			buf += 2;
			if (sscanf(buf, "%ld,%ld,%ld,%ld", &st->p1x, &st->p1y, &st->p2x, &st->p2y) != 4) {
				rnd_message(RND_MSG_ERROR, "Failed to read coord quad for IP: '%s'\n", buf);
				return -1;
			}
			return 0;

		case INST('S', 'C'): /* "SC": translate/scale: user window */
			buf += 2;
			if (sscanf(buf, "%ld,%ld,%ld,%ld", &st->u1x, &st->u2x, &st->u1y, &st->u2y) != 4) {
				rnd_message(RND_MSG_ERROR, "Failed to read coord quad for SC: '%s'\n", buf);
				return -1;
			}
			hpgl_set_scale(st);
			return 0;

		case INST('I', 'W'): /* "IW": input window */
			{
				long x1, y1, x2, y2;
				buf += 2;
				if (sscanf(buf, "%ld,%ld,%ld,%ld", &x1, &y1, &x2, &y2) != 4) {
					rnd_message(RND_MSG_ERROR, "Failed to read coord quad for IW: '%s'\n", buf);
					return -1;
				}
				st->iwx1 = TPX(RND_MIN(x1, x2));
				st->iwy1 = TPY(RND_MIN(y1, y2));
				st->iwx2 = TPX(RND_MAX(x1, x2));
				st->iwy2 = TPY(RND_MAX(y1, y2));
				st->iw = 1;
			}
			return 0;

		case INST('L', 'T'): /* "LT": line type */
			rnd_message(RND_MSG_ERROR, "camv_hpgl: LT (line type) is not yet supported - drawing all lines as solid\n");
			return 0;

		case INST('C', 'T'): /* "CT": Chord Tolerance */
			{
				char *end;
				st->ct = strtol(buf+2, &end, 10);
				if ((*end != '\0') || !((st->ct == 0) || (st->ct == 1))) {
					rnd_message(RND_MSG_ERROR, "camv_hpgl: invalid chord tolerance selection: '%s'\n", buf);
					return -1;
				}
			}
			return 0;

		case INST('C', 'I'): /* "CI": Circle */
			{
				long r, rad;
				double res = 0;

				r = sscanf(buf+2, "%ld,%lf", &rad, &res);
				if ((r != 1) && (r != 2)) {
					rnd_message(RND_MSG_ERROR, "camv_hpgl: CI needs to have 1 or 2 arguments '%s'\n", buf);
					return -1;
				}

				hpgl_gen_circle(st, rad, res);
			}
			return 0;

		case INST('R', 'O'): /* "RO": rotate coord system */
			rnd_message(RND_MSG_ERROR, "camv_hpgl: RO (rotate) is not yet supported\n");
			return -1;

		case INST('P', 'G'): /* "PG": page feed */
			hpgl_page_feed(st);
			return 0;

		case INST('C', 'S'): /* "CS": configure standard charset */
		case INST('C', 'A'): /* "CA": configure alternate charset */
		case INST('S', 'S'): /* "CS": select standard charset */
		case INST('S', 'A'): /* "CA": select alternate charset */
			rnd_message(RND_MSG_ERROR, "camv_hpgl: ignored: '%s' - always using ASCII\n", buf);
			return 0;

		case INST('D', 'T'): /* "DT": define label terminator */
			st->labterm = buf[2];
			return 0;

		case INST('D', 'I'): /* "DI": text rotation, absolute */
		case INST('D', 'R'): /* "DR": text rotation, relative */
			{
				long dx, dy;

				if (sscanf(buf+2, "%ld,%ld", &dx, &dy) != 2) {
					rnd_message(RND_MSG_ERROR, "Failed to read coord pair for '%s'\n", buf);
					return -1;
				}

				hpgl_text_rot(st, dx, dy, (buf[1] == 'R'));
			}
			return 0;

		case INST('L', 'B'): /* "LB": label */
			buf+=2;
			hpgl_gen_label(st, buf, 0, 1, 1);
			return 0;

		case INST('C', 'P'): /* "CP": character plot (move but don't draw chars) */
			{
				long dx, dy;

				if (sscanf(buf+2, "%ld,%ld", &dx, &dy) != 2) {
					rnd_message(RND_MSG_ERROR, "Failed to read coord pair for '%s'\n", buf);
					return -1;
				}

				hpgl_gen_label(st, "M", 1, dx, dy);
			}
			return 0;

		case INST('S', 'I'): /* "SI": absolute character size in cm */
		case INST('S', 'R'): /* "SR": relative character size */
			{
				double sx = 1.0, sy = 1.0;
				int r;

				r = sscanf(buf+2, "%lf,%lf", &sx, &sy);
				if (r > 0) sx /= 0.1879; /* default char width from spec, in cm */
				if (r > 1) sy /= 0.2690; /* default char height from spec, in cm */

				if (buf[1] == 'R') {
					st->text_scx *= sx;
					st->text_scy *= sy;
				}
				else {
					st->text_scx = sx;
					st->text_scy = sy;
				}
			}
			return 0;

		case INST('V', 'S'): return 0; /* "VS": velocity in cm/s - ignore */


		default:
			rnd_message(RND_MSG_ERROR, "camv_hpgl: unsupported instruction: '%s'\n", buf);
	}
	return -1;

	maybe_move:;
	if (buf[0] == '\0')
		return 0;

	move:;
	while(*buf != '\0') {
		if (sscanf(buf, "%ld,%ld%n", &x, &y, &slen) != 2) {
			rnd_message(RND_MSG_ERROR, "Failed to read coord pair for move: '%s'\n", buf);
			return -1;
		}

		if (relative) {
			x += st->x;
			y += st->y;
		}

		if (draw_line)
			hpgl_gen_line(st, st->x, st->y, x, y);

		st->x = x;
		st->y = y;

		buf += slen;
		if (*buf == ',')
			buf++;
	}

	return 0;
}

static int camv_hpgl_load(camv_design_t *camv, const char *fn, FILE *f)
{
	char buf[1024];
	int len = 0;
	hpgl_st_t st = {0};


	st.fn = fn;
	st.camv = camv;
	hpgl_reset(&st, 0);

	for(;;) {
		int inlabel, isterm, c;
		
		c = fgetc(f);

		inlabel = ((len >= 2) && (buf[0] == 'L') && (buf[1] == 'B'));

		if (!inlabel && isspace(c))
			continue;

		if (c == EOF) {
			if (len != 0)
				rnd_message(RND_MSG_ERROR, "camv_hpgl: premature EOF, file is probably truncated\n");
			hpgl_uninit(&st);
			return 0;
		}

		/* special case: labels are terminated differently, in a configurable way */
		isterm = 0;
		if (inlabel) {
			if (c == st.labterm)
				isterm = 1;
		}
		else if (c == ';')
			isterm = 1;

		if (isterm) {
			int res;

			buf[len++] = '\0';
			res = hpgl_parse_cmd(&st, buf, len);
			if (res != 0) {
				hpgl_uninit(&st);
				return res;
			}
			len = 0;
			continue;
		}

		buf[len++] = c;
		if (len > sizeof(buf)-2) {
			rnd_message(RND_MSG_ERROR, "camv_hpgl: command too long\n");
			hpgl_uninit(&st);
			return -1;
		}
	}

	hpgl_uninit(&st);
	return -1;
}

static int camv_hpgl_test_load(camv_design_t *camv, const char *fn, FILE *f)
{
	char buff[256], *s;
	int score = 0, len;
	len = fread(buff, 1, sizeof(buff)-4, f);

	if (len < 3)
		return 0;

	if (memcmp(buff, "IN;", 3) == 0)
		return 1;

	buff[len] = '\0';
	for(s = buff; *s != '\0'; s++) {
		if ((s[0] == 'P') && ((s[1] == 'U') || (s[1] == 'D') || (s[1] == 'A') || (s[1] == 'R'))) {
			if ((s[2] == ';') || isdigit(s[2]))
				score++;
		}
		if ((s[0] == 'S') && (s[1] == 'P') && isdigit(s[2]))
			score++;
		if (score > 4)
			return 1;
	}

	return 0;
}


static camv_io_t io_hpgl = {
	"hpgl", 90,
	camv_hpgl_test_load,
	camv_hpgl_load,
	NULL
};


int pplg_check_ver_import_hpgl(int ver_needed) { return 0; }

void pplg_uninit_import_hpgl(void)
{
	camv_io_unreg(&io_hpgl);
}

int pplg_init_import_hpgl(void)
{
	camv_io_reg(&io_hpgl);
	return 0;
}
