/*
 *                            COPYRIGHT
 *
 *  camv-rnd - electronics-related CAM viewer
 *  Copyright (C) 2019,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 <librnd/core/error.h>
#include <librnd/core/safe_fs.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/math_helper.h>

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


typedef struct {
	/* static: set once in the header */
	vtc0_t tools;
	int fmat; /* format version */
	double metric_scale;
	unsigned int has_metric_scale:1;
	unsigned int inch:1;
	unsigned int inbody:1;
	unsigned int started:1; /* M48 detected */
	unsigned int got_unit:1;
	unsigned int leading_zero:1;

	/* dynamic */
	unsigned int rout:1;
	unsigned int tool_down:1;
	unsigned int eof:1; /* M30 */
	rnd_coord_t x, y; /* current pos */
	long tool;
	camv_layer_t *ly;

	/* parser */
	long lineno;
} exc_t;

static void exc_gen_line(exc_t *exc, rnd_coord_t x1, rnd_coord_t y1, rnd_coord_t x2, rnd_coord_t y2)
{
	camv_any_obj_t *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 = exc->tools.array[exc->tool];
	camv_obj_add_to_layer(exc->ly, o);
}


RND_INLINE rnd_coord_t exc2coord(exc_t *exc, double d)
{
	if (exc->inch)
		return RND_MIL_TO_COORD(d*1000.0);
	return RND_MM_TO_COORD(d);
}

static double exc_strtod(exc_t *exc, const char *str, char **end)
{
	const char *s;
	char tmp[32], *start, *o;
	int len, has_dot = 0, neg = 0;
	double res;

	/* copy digits and dots to start, which is in the middle of tmp so it
	   can be easily extended in both directions */
	start = o = tmp+16;
	if (*str == '-') {
		neg = 1;
		str++;
	}
	for(s = str, len = 0; (isdigit(*s) || (*s == '.')); s++,len++) {
		if (*s == '.')
			has_dot = 1;
		*o = *s;
		o++;
		if (len > 14) {
			rnd_message(RND_MSG_ERROR, "excellon: invalid coord '%s' (too long)\n", str);
			return 0;
		}
	}
	*end = (char *)s;
	*o = '\0';

	/* make sure we have exactly 6 digits */
	if (len < 6) {
		if (exc->leading_zero) {
			while(len < 6) {
				*o = '0';
				o++;
				len++;
			}
			*o = '\0';
		}
		else {
			while(len < 6) {
				start--;
				*start = '0';
				len++;
			}
		}
	}
	else if (len > 6)
		start[6] = '\0';


	res = strtod(start, NULL);
	if (neg)
		res = -res;

/*	printf("CNC C: '%s' -> '%s' -> %f end='%s'\n", str, start, res, *end);*/

	if (exc->inch && !has_dot) {
		/* old INCH format with no decimal point: 00.0000 format */
		res /= 10000.0;
	}
	else if (!exc->inch && exc->has_metric_scale && !has_dot) {
		/* METRIC,00.000 flexible format */
		res /= exc->metric_scale;
	}
	return res;
}

static int exc_load_tool(exc_t *exc, char **line, long idx)
{
	char *end;
	rnd_coord_t *td;
	double d = strtod(*line, &end);

	if ((*end != '\0') && (*end != 'F') && (*end != 'S')) { /* F is maybe feed rate, S is maybe spindle rpm */
		rnd_message(RND_MSG_ERROR, "excellon: invalid tool diameter '%s'\n", *line);
		return -1;
	}
	*line = end;
	td = vtc0_get(&exc->tools, idx, 1);
	if (td == NULL) {
		rnd_message(RND_MSG_ERROR, "excellon: failed to allocate tool id %ld\n", idx);
		return -1;
	}
	*td = exc2coord(exc, d);
	return 0;
}

static void expect_fmat(exc_t *exc, int fmat)
{
	if (exc->fmat == 0) return;
	if (exc->fmat != fmat)
		rnd_message(RND_MSG_ERROR, "excellon: WARNING: unexpected command for format in line %ld; format expected is %d, but file says it's %d \n", exc->lineno, fmat, exc->fmat);
}

static int exc_parse_header(exc_t *exc, char **line)
{
	if ((*line)[0] == '%') {
		(*line)++;
		exc->inbody = 1;
		return 0;
	}

	if (strncmp(*line, "FMAT,", 5) == 0) {
		char *end;

		exc->fmat = strtol((*line)+5, &end, 10);
		*line = end;
		if (!isspace(*end) && (*end != '\0')) {
			rnd_message(RND_MSG_ERROR, "excellon: invalid format version: %s\n", *line);
			return -1;
		}
		if ((exc->fmat < 0) || (exc->fmat > 2)) {
			rnd_message(RND_MSG_ERROR, "excellon: unknown format version: %s; please make a bugreport with this file\n", *line);
			return -1;
		}
		return 0;
	}

	if (strcmp(*line, "M95") == 0) {
		(*line) += 3;
		exc->inbody = 1;
		return 0;
	}

	if (strcmp(*line, "M48") == 0) {
		(*line) += 3;
		exc->started = 1;
		return 0;
	}

	if ((strcmp(*line, "M30") == 0) || (strcmp(*line, "M00") == 0) || (strcmp(*line, "M02") == 0)) {
		if ((*line)[2] == '2') expect_fmat(exc, 1);
		if ((*line)[2] == '0') expect_fmat(exc, 2);
		(*line) += 3;
		exc->eof = 1;
		return 0;
	}

	if (!exc->started) {
		rnd_message(RND_MSG_ERROR, "excellon: missing M48 before header comamnd %s\n", *line);
		return -1;
	}

	if (strncmp(*line, "METRIC", 6) == 0) {
		(*line) += 6;
		if (exc->got_unit)
			goto err_unit_dup;
		exc->inch = 0;
		exc->got_unit = 1;
		if ((*line)[0] == ',') {
			char *sep;
			int len, len2;
			(*line)++;
			sep = strchr(*line, '.');
			if (sep != NULL) {
				TODO("this expects newline");
				len = strlen(*line);
				len2 = len - (sep - (*line)) - 1;
				exc->has_metric_scale = 1;
				exc->metric_scale = pow(10, len2);
				(*line) += len;
				return 0;
			}
			else {
				rnd_message(RND_MSG_ERROR, "excellon: metric format specifier lacks decimal dot; this file is broken.\n(Assuming .2 but expect broken input)\n");
				exc->has_metric_scale = 1;
				exc->metric_scale = 100;
				(*line)--; /* so that *line points to ",LZ" again */
			}
		}
		return 0;
	}

	if (strncmp(*line, "INCH", 4) == 0) {
		(*line) += 4;
		if (exc->got_unit)
			goto err_unit_dup;


		exc->inch = 1;
		exc->got_unit = 1;
		return 0;
	}

	if (strncmp(*line, ",LZ", 3) == 0) {
		exc->leading_zero = 1;
		(*line) += 3;
		return 0;
	}

	if ((*line)[0] == 'T') {
		char *end;
		long idx;

		(*line)++;
		idx = strtol(*line, &end, 10);
		if (idx < 1) {
			rnd_message(RND_MSG_ERROR, "excellon: tool index %d out of range (0..99)\n", idx);
			return -1;
		}
		end = strchr(end, 'C'); /* skip over S and F */
		if ((end == NULL) || (*end != 'C')) {
			rnd_message(RND_MSG_ERROR, "excellon: tool separator 'C' is missing\n");
			return -1;
		}
		*line = end+1;
		return exc_load_tool(exc, line, idx);
	}

	rnd_message(RND_MSG_ERROR, "excellon: invalid line '%s' in header\n", *line);
	return -1;

	err_unit_dup:;
	rnd_message(RND_MSG_ERROR, "excellon: unit already set\n");
	return -1;
}

static int exc_parse_move(exc_t *exc, char **line, char endchar)
{
	double x = exc->x, y = exc->y;

	if (**line != 'Y') { /* X is optional, when missing, Y starts the line */
		if (**line != 'X') {
			rnd_message(RND_MSG_ERROR, "excellon: expected X\n");
			return -1;
		}
		(*line)++;
		x = exc_strtod(exc, *line, line);
		exc->x = exc2coord(exc, x);
	}

	if (**line != '\0') { /* Y is optional */
		if (**line != 'Y') {
			rnd_message(RND_MSG_ERROR, "excellon: expected Y (broken X coord?)\n");
			return -1;
		}
		(*line)++;
		y = exc_strtod(exc, *line, line);
		exc->y = exc2coord(exc, y);
	}

	if (endchar != '\0') { /* requested specific termination */
		if (**line != endchar) {
			rnd_message(RND_MSG_ERROR, "excellon: broken Y coord\n");
			return -1;
		}
	}

	return 0;
}

static int exc_parse_line(exc_t *exc, char **line)
{
	rnd_coord_t x0 = exc->x, y0 = exc->y;

	if (exc->tool == 0) {
		rnd_message(RND_MSG_ERROR, "excellon: can not drill: no tool selected\n");
		return -1;
	}

	if (exc_parse_move(exc, line, '\0') != 0)
		return -1;
	exc_gen_line(exc, x0, y0, exc->x, exc->y);
	return 0;
}

static int exc_parse_arc(exc_t *exc, char **line, int cw)
{
	rnd_coord_t x0 = exc->x, y0 = exc->y, r;
	double a;
	char *end;

	if (exc->tool == 0) {
		rnd_message(RND_MSG_ERROR, "excellon: can not drill: no tool selected\n");
		return -1;
	}

	if (exc_parse_move(exc, line, 'A') != 0)
		return -1;
	a = exc_strtod(exc, *line, &end);
	if (*end != '\0') {
		rnd_message(RND_MSG_ERROR, "excellon: broken A coord\n");
		return -1;
	}
	*line = end;
	r = exc2coord(exc, a);
	TODO("draw arc");
	return -1;
}

static int exc_parse_body(exc_t *exc, char **line)
{
	int code;

	if (exc->eof) {
		rnd_message(RND_MSG_ERROR, "excellon: command after M30: '%s'\n", *line);
		return -1;
	}

	code = ((*line)[1]-'0')*10 + ((*line)[2]-'0');

	if ((*line)[0] == 'G') {
		(*line)+=3;
		switch(code) {
			case 00: exc->rout = 1; return exc_parse_move(exc, line, '\0');
			case 05: expect_fmat(exc, 2); exc->rout = 0; return 0;
			case 81: expect_fmat(exc, 1); exc->rout = 0; return 0;
			case 01: return exc_parse_line(exc, line);
			case 85: return exc_parse_line(exc, line);
			case 02: return exc_parse_arc(exc, line, 1);
			case 03: return exc_parse_arc(exc, line, 0);
			case 90: return 0; /* ignore absolute mode */
			case 91:
				rnd_message(RND_MSG_ERROR, "excellon: increment mode (G91) is not supported. Please report this bug with the file.\n");
				return -1;

		}
		rnd_message(RND_MSG_ERROR, "excellon: invalid G-code: %d\n", code);
		return -1;
	}

	if ((*line)[0] == 'T') {
		char *end;
		long idx;
		(*line)++;
		idx = strtol(*line, &end, 10);
		if (*end == 'C') {
			end++;
			exc->inch = 1;
			if (exc->tools.used == 0)
				rnd_message(RND_MSG_ERROR, "excellon: old/broken drill file (no header); expect some drills missing or mislocated\n");
			if (exc_load_tool(exc, &end, idx) != 0) {
				rnd_message(RND_MSG_ERROR, "excellon: broken tool selection+specification\n");
				return -1;
			}
			if ((*end == 'F') || (*end == 'S'))
				*end = '\0';
		}
		if ((idx < 0) || (idx >= exc->tools.used)) {
			rnd_message(RND_MSG_ERROR, "excellon: tool index %d out of range (0..99)\n", idx);
			return -1;
		}
		if (*end != '\0') {
			rnd_message(RND_MSG_ERROR, "excellon: broken tool selection\n");
			return -1;
		}
		(*line) = end;
		if ((idx != 0) && (exc->tools.array[idx] == 0)) {
			rnd_message(RND_MSG_ERROR, "excellon: invalid tool selection: %d is not specified\n", idx);
			return -1;
		}
		exc->tool = idx;
		return 0;
	}

	if (((*line)[0] == 'X') || ((*line)[0] == 'Y')) {
		if (exc->tool == 0) {
			rnd_message(RND_MSG_ERROR, "excellon: can not drill: no tool selected\n");
			return -1;
		}
		if (exc_parse_move(exc, line, '\0') != 0) {
			rnd_message(RND_MSG_ERROR, "excellon: exc_parse_move() failed\n");
			return -1;
		}
		exc_gen_line(exc, exc->x, exc->y, exc->x, exc->y);
		return 0;
	}

	if ((*line)[0] == 'M') {
		(*line) += 3;
		switch(code) {
			case 15: exc->tool_down = 1; return 0;
			case 16: exc->tool_down = 0; return 0;
			case 17: exc->tool_down = 0; return 0;
			case 30: exc->eof = 1; return 0;
		}
	}

	return -1;
}

int camv_exc_load(camv_design_t *camv, const char *fn, FILE *f)
{
	char *end, *line, line_[1024];
	exc_t exc;
	int res;

	memset(&exc, 0, sizeof(exc));
	exc.ly = camv_layer_new();
	exc.ly->name = rnd_strdup(fn);
	camv_layer_invent_color(camv, exc.ly);
	camv_layer_append_to_design(camv, exc.ly);
	vtc0_init(&exc.tools);

	while((line = fgets(line_, sizeof(line_), f)) != NULL) {
		exc.lineno++;

		while(isspace(*line)) line++;
		end = strpbrk(line, "\n\r");
		if (end == NULL) {
			rnd_message(RND_MSG_ERROR, "%s:%ld: Every line needs to be terminated by a newline character\n", fn, exc.lineno);
			return -1;
		}
		*end = '\0';

		if (*line == '\0') continue; /* ignore empty lines */
		if (*line == ';')  continue; /* ignore comments */

		while(*line != '\0') {
			if (exc.inbody)
				res = exc_parse_body(&exc, &line);
			else
				res = exc_parse_header(&exc, &line);

			if (res != 0) {
				rnd_message(RND_MSG_ERROR, "(excellon error occured at %s:%ld)\n", fn, exc.lineno);
				goto quit;
			}
		}
	}

	quit:;
	vtc0_uninit(&exc.tools);
	return res;
}

static int camv_exc_test_load(camv_design_t *camv, const char *fn, FILE *f)
{
	char *s, *line, line_[1024];
	int bad = 0, had_m = 0;
	
	while((line = fgets(line_, sizeof(line_), f)) != NULL) {
		while(isspace(*line)) line++;
		if (*line == ';')
			continue;
		if ((line[0] == 'T') && !had_m) { /* drill file with no header: recognize tool selection: T1C.008F0S0 */
			s = line+1;
			while(isdigit(*s)) s++;
			if (s[0] == 'C') {
				if (((s[1] == '.') && isdigit(s[2])) || (isdigit(s[1])))
					return 1;
			}
		}
		if (line[0] == 'M')
			had_m = 1;
		if ((strncmp(line, "M48", 3) == 0) && (!isalnum(line[3])))
			return 1;
		bad++;
		if (bad > 16)
			return 0;
	}
	return 0;
}


static camv_io_t io_exc = {
	"excellon", 90,
	camv_exc_test_load,
	camv_exc_load,
	NULL
};


int pplg_check_ver_import_excellon(int ver_needed) { return 0; }

void pplg_uninit_import_excellon(void)
{
	camv_io_unreg(&io_exc);
}

int pplg_init_import_excellon(void)
{
	camv_io_reg(&io_exc);
	return 0;
}
