/*
 *                            COPYRIGHT
 *
 *  camv-rnd - electronics-related CAM viewer - low level gerber parser
 *  Copyright (C) 2019 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 <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <math.h>
#include <string.h>
#include <genht/hash.h>
#include <genht/htsp.h>
#include <genvector/gds_char.h>
#include "geparse.h"
#include "gedraw.h"
#include "gexpr.h"
#include "gex.tab.h"
#include <librnd/core/error.h>

#include <librnd/core/compat_misc.h>

/* valid polygon aperture corners */
#define POLY_MIN_CORNERS 3
#define POLY_MAX_CORNERS 16

/* valid coordinates can't take more than this number of characters in a string */
#define MAX_COORD_STRLEN 16

#define COMPILE(ctx, cmd_, field, arg) \
	do { \
		gedraw_inst_t *inst; \
		inst = gedraw_alloc(&((ctx)->draw), (ctx)->line, (ctx)->col); \
		inst->cmd = (cmd_); \
		inst->data.field = arg; \
	} while(0)

#define COMPILE0(ctx, cmd_) \
	do { \
		gedraw_inst_t *inst; \
		inst = gedraw_alloc(&((ctx)->draw), (ctx)->line, (ctx)->col); \
		inst->cmd = (cmd_); \
	} while(0)

#define READ_(_ctx_, _res_, allow_eof) \
	do { \
		for(;;) { \
			int _step_ = 0; \
			if (_ctx_->ungetc != 0) { \
				_res_ = _ctx_->ungetc; \
				_ctx_->ungetc = 0; \
			} \
			else { \
				_step_ = 1; \
				_res_ = ctx->get_char(ctx); \
				if ((_res_ < 32) && (_res_ != '\n') && (_res_ != '\r') && (_res_ != '\t')) { \
					ctx->errmsg = "Invalid character (low binary)"; \
					return GEP_ERROR; \
				} \
				else if (_res_ >= 127) { \
					ctx->errmsg = "Invalid character (high binary)"; \
					return GEP_ERROR; \
				} \
			} \
			if (_step_) \
				ctx->col++; \
			if (_res_ == EOF) { \
				if (!allow_eof) { \
					ctx->errmsg = "Invalid command (first character is EOF)"; \
					return GEP_ERROR; \
				} \
				break; \
			} \
			else if (_res_ == '\n') { \
				if (_step_) { \
					ctx->col = 0; \
					ctx->line++; \
				} \
			} \
			else if (_res_ == '\r') { \
			} \
			else \
				break; \
		} \
	} while(0)

#define UNGETC(_ctx_, _c_) \
	do { \
		if (_ctx_->ungetc != 0) { \
			ctx->errmsg = "Internal error: multiple UNGETC calls"; \
			return GEP_ERROR; \
		} \
		_ctx_->ungetc = _c_; \
	} while(0)

#define READ(_ctx_, _res_) READ_(_ctx_, _res_, 0)

#define READ_NUM(_ctx_, _res_) \
	do { \
		int __sign__ = 1, __digc__, __dig__; \
		_res_ = 0; \
		for(__digc__ = 0;;__digc__++) { \
			if (__digc__ > 10) { \
				_ctx_->errmsg = "Number too long"; \
				return GEP_ERROR; \
			} \
			READ(_ctx_, __dig__); \
			if (__dig__ == '-') { \
				if (__digc__ == 0) __sign__ = -1; \
				else break;\
			} \
			else if (__dig__ == '+') { \
				if (__digc__ == 0) __sign__ = +1; \
				else break;\
			} \
			else if (isdigit(__dig__)) \
				_res_ = _res_ * 10 + __dig__ - '0'; \
			else \
				break; \
		} \
		if (__sign__ < 0) \
			_res_ = -_res_; \
		UNGETC(_ctx_, __dig__); \
	} while(0)

/* read decimal into a char * buffer _res_; _len_inout_ is the maximum
  length as input and actual length as output */
#define READ_DEC_(_ctx_, _buf_, _len_inout_, _permit_decpt_) \
	do { \
		int __i__, __c__, __got_pt__ = 0; \
		for(__i__ = 0;;__i__++) { \
			if (__i__ >= _len_inout_-1) { \
				_ctx_->errmsg = "Number too long"; \
				return GEP_ERROR; \
			} \
			READ(_ctx_, __c__); \
			_buf_[__i__] = __c__; \
			if (__c__ == '-') { \
				if (__i__ != 0) \
					break;\
			} \
			else if (__c__ == '+') { \
				if (__i__ != 0) \
					break;\
			} \
			else if (_permit_decpt_ && (__c__ == '.')) { \
				if (__got_pt__ == 0) __got_pt__ = 1; \
				else break;\
			} \
			else if (!isdigit(__c__)) \
				break; \
		} \
		_buf_[__i__] = '\0'; \
		_len_inout_ = __i__; \
		UNGETC(_ctx_, __c__); \
	} while(0)

/* parse decimal into double _res_ */
#define READ_DEC(_ctx_, _res_) \
	do { \
		char __buf__[16]; \
		int __len__ = sizeof(__buf__); \
		READ_DEC_(_ctx_, __buf__, __len__, 1); \
		_res_ = strtod(__buf__, NULL); \
	} while(0)

#define READ_COORD(_ctx_, _res_) \
	do { \
		char __buf__[3*MAX_COORD_STRLEN], *__start__ = __buf__+MAX_COORD_STRLEN; \
		int __len__ = MAX_COORD_STRLEN, __need__ = _ctx_->cfmt_int + _ctx_->cfmt_fra, __save__, __has_sign__, __sign__; \
		long __int__; \
		double __fra__; \
		READ_DEC_(_ctx_, __start__, __len__, 0); \
		if (__need__ > 25) { \
			_ctx_->errmsg = "Coordinate format too long"; \
			return GEP_ERROR; \
		} \
		if ((_ctx_->cfmt_int < 1) || (_ctx_->cfmt_fra < 1)) { \
			_ctx_->errmsg = "Coordinate format too short"; \
			return GEP_ERROR; \
		} \
		__has_sign__ = (*__start__ == '-') || (*__start__ == '+'); \
		__sign__ = *__start__; \
		if (__len__  - __has_sign__ > __need__) { \
			_ctx_->errmsg = "Coordinate longer than format permits"; \
			return GEP_ERROR; \
		} \
		if (_ctx_->trailing_zero) { \
			while(__len__ < __need__) { \
				__buf__[__len__] = '0'; \
				__len__++; \
			} \
			__buf__[__len__] = '\0'; \
		} \
		else { \
			if (__has_sign__) \
				__start__++;\
			while(__len__ - __has_sign__ < __need__) { \
				__start__--; \
				*__start__ = '0'; \
				__len__++; \
			} \
			if (__has_sign__) {\
				 __start__--; \
				 *__start__ = __sign__; \
			} \
		} \
		__save__ = __start__[_ctx_->cfmt_int - 1 + __has_sign__]; \
		__start__[_ctx_->cfmt_int - 1 + __has_sign__] = '.'; \
		__fra__ = strtod(__start__ + _ctx_->cfmt_int + __has_sign__ - 1, NULL); \
		__start__[_ctx_->cfmt_int - 1 + __has_sign__] = __save__; \
		__start__[_ctx_->cfmt_int + __has_sign__] = '\0'; \
		__int__ = strtol(__start__, NULL, 10); \
		_res_ = ge_intfra_to_coord(_ctx_, __int__, __fra__, __has_sign__ && (__sign__ == '-')); \
	} while(0)

#define READ_ANGLE(_ctx_, _res_) READ_DEC(_ctx_, _res_)

#define READ_COORD_DEC(_ctx_, _res_) \
	do { \
		double __tmp__; \
		READ_DEC(_ctx_, __tmp__); \
		_res_ = ge_double_to_coord(_ctx_, __tmp__); \
	} while(0)


#define READ_UPCASE(_ctx_, _res_) \
	do { \
		READ(_ctx_, _res_); \
		if (!isupper(_res_)) { \
			_ctx_->errmsg = "Unexpected uppercase alpha character"; \
			return GEP_ERROR; \
		} \
	} while(0)

#define READ_CMDEND(_ctx_) \
	do { \
		for(;;) { \
			int __c__; \
			READ(_ctx_, __c__); \
			if (__c__ == '*') \
				break;\
			_ctx_->errmsg = "Unexpected character while looking for command terminator ('*')"; \
			return GEP_ERROR; \
		} \
	} while(0)

#define READ_LONGEND(_ctx_) \
	do { \
		for(;;) { \
			int __c__; \
			READ(_ctx_, __c__); \
			if (__c__ == '%') \
				break;\
			_ctx_->errmsg = "Unexpected character while looking for long command terminator ('%')"; \
			return GEP_ERROR; \
		} \
	} while(0)

#define READ_TILL_CMDEND(_ctx_) \
	do { \
		for(;;) { \
			int __c__; \
			READ(_ctx_, __c__); \
			if (__c__ == '*') \
				break;\
		} \
	} while(0)

#define READ_APERTURE(_ctx_, _aid_, _allow_empty_) \
	do { \
		int __c__; \
		READ(_ctx_, __c__); \
		if (__c__ != '*') { \
			if (__c__ != 'D') { \
				_ctx_->errmsg = "Expected 'D' for aperture ID"; \
				return GEP_ERROR; \
			} \
			READ_NUM(_ctx_, _aid_); \
		} \
		else { \
			if (!_allow_empty_) { \
				_ctx_->errmsg = "Expected 'D' for aperture ID - empty aperture not supported in this context"; \
				return GEP_ERROR; \
			} \
			else \
				_aid_ = -42; \
		} \
	} while(0)

/* Append a macro name into a gds string, utnil endchr or '*' (endchr is not consumed) */
#define READ_NAME_GDS(_ctx_, dst, endchr) \
do { \
	for(;;) { \
		READ(ctx, c); \
		if ((c == endchr) || (c == '*')) {\
			UNGETC(_ctx_, c); \
			break; \
		} \
		gds_append(&name, c); \
	} \
} while(0)

static double ge_round(double x)
{
	return rnd_round(x);
}

ge_coord_t ge_double_to_coord(geparse_ctx_t *ctx, double d)
{
	switch(ctx->unit) {
		case GEU_INCH:
			return ge_round(d * 25400000.0);
		case GEU_MM:
		default:
			return ge_round(d * 1000000.0);
	}
}

ge_coord_t ge_intfra_to_coord(geparse_ctx_t *ctx, long int_part, double fra_part, int is_neg)
{
	ge_coord_t c;

	if (int_part < 0)
		int_part = -int_part;
	c = ge_double_to_coord(ctx, (double)int_part + fra_part); /* lazy approach, slow but still accurate enough because of the magnitude of i and f */
	return is_neg ? -c : c;
}

static ge_parse_res_t geparse_aperture_spec_shaped(geparse_ctx_t *ctx, long aid, int shape)
{
	int c;
	gedraw_inst_t *inst = gedraw_alloc(&ctx->draw, ctx->line, ctx->col);

	inst->data.aper.id = aid;
	switch(shape) {
		case 'C':
			inst->data.aper.shape = GEA_CIRC;
			READ_COORD_DEC(ctx, inst->data.aper.data.circ.dia);
			READ(ctx, c);
			if (c == '*') /* it seems a few arguments at the end are optional; especially rotation for '1' (circle) may be missing */
				break;
			if (c != 'X') {
				ctx->errmsg = "unexpected character in circle aperture def";
				return GEP_ERROR;
			}
			READ_COORD_DEC(ctx, inst->data.aper.hole);

			READ(ctx, c);
			if (c == '*')
				break;
			ctx->errmsg = "unexpected character at the end of a circle aperture def";
			return GEP_ERROR;

		case 'R':
			inst->data.aper.shape = GEA_RECT;
			rect_cheat:;
			READ_COORD_DEC(ctx, inst->data.aper.data.rect.xs);
			READ(ctx, c);
			if (c != 'X') {
				ctx->errmsg = "unexpected character in rectangle aperture def after x-size";
				return GEP_ERROR;
			}
			READ_COORD_DEC(ctx, inst->data.aper.data.rect.ys);
			READ(ctx, c);
			if (c == '*')
				break;
			if (c != 'X') {
				ctx->errmsg = "unexpected character in rectangle aperture def";
				return GEP_ERROR;
			}
			READ_COORD_DEC(ctx, inst->data.aper.hole);
			READ(ctx, c);
			if (c == '*')
				break;
			ctx->errmsg = "unexpected character at the end of a circle aperture def";
			return GEP_ERROR;


		case 'O':
			inst->data.aper.shape = GEA_OBLONG;
			goto rect_cheat; /* we can do this as long as the struct for the two are the same */
			break;

		case 'P':
			inst->data.aper.shape = GEA_POLY;
			READ_COORD_DEC(ctx, inst->data.aper.data.poly.dia);
			READ(ctx, c);
			if (c != 'X') {
				ctx->errmsg = "unexpected character in polygon aperture def after diameter";
				return GEP_ERROR;
			}
			READ_NUM(ctx, inst->data.aper.data.poly.corners);
			READ(ctx, c);
			if (c == '*')
				goto poly_fin;
			if (c != 'X') {
				ctx->errmsg = "unexpected character in polygon aperture def (at angle)";
				return GEP_ERROR;
			}
			READ_ANGLE(ctx, inst->data.aper.data.poly.rot);
			READ(ctx, c);
			if (c == '*')
				goto poly_fin;
			if (c != 'X') {
				ctx->errmsg = "unexpected character in polygon aperture def (at hole)";
				return GEP_ERROR;
			}
			READ_COORD_DEC(ctx, inst->data.aper.hole);
			READ(ctx, c);

			poly_fin:;
			if (inst->data.aper.data.poly.corners < POLY_MIN_CORNERS) {
				ctx->errmsg = "invalid polygon aperture: too few corners";
				return GEP_ERROR;
			}
			if (inst->data.aper.data.poly.corners > POLY_MAX_CORNERS) {
				ctx->errmsg = "invalid polygon aperture: too many corners";
				return GEP_ERROR;
			}
			break;
		default:
			ctx->errmsg = "unsupported aperture shape (character)";
			return GEP_ERROR;
	}
	READ_LONGEND(ctx);
	inst->cmd = GEC_APER_DEF;
	return GEP_NEXT;
}

/*** expression parsing ***/
int gexerror(ge_expr_prglist_t *ctx_, const char *msg)
{
	geparse_ctx_t *ctx = ctx_->parent;
	ctx->errmsg = msg;
	return -1;
}

int gexlex(YYSTYPE *lval, ge_expr_prglist_t *ctx_)
{
	geparse_ctx_t *ctx = ctx_->parent;
	int c;

	READ(ctx, c);
	switch(c) {
		case ',':
		case '%':
		case '*':
			UNGETC(ctx, c);
			return 0;
		case 'X':
			return 'x'; /* accept uppercase X for multiplication */
		case '$':
			READ_NUM(ctx, lval->idx);
			return T_PARAM;
		case '0': case '1': case '2': case '3': case '4': case '5':
		case '6': case '7': case '8': case '9': case '.':
			UNGETC(ctx, c);
			READ_DEC(ctx, lval->num);
			return T_NUM;
	}
	return c;
}

TODO("switch over from byacc to byaccic and have a .h")
int gexparse(ge_expr_prglist_t *lst);

static int geparse_macro_expr(geparse_ctx_t *ctx, ge_expr_prg_t **res) {
	int r;
	ge_expr_prglist_t lst;

	lst.first = lst.last = NULL;
	lst.parent = ctx;

	r = gexparse(&lst);
	if (r == 0)
		*res = lst.first;
	else
		*res = NULL;

	return 0;
}

static ge_macro_line_t *macro_append(geparse_ctx_t *ctx, ge_aper_macro_t *am)
{
	ge_macro_line_t *line = calloc(sizeof(ge_macro_line_t), 1);
	if (am->last != NULL) {
		am->last->next = line;
		am->last = line;
	}
	else
		am->line1 = am->last = line;

	return line;
}

static int geparse_macro_aperture_line(geparse_ctx_t *ctx, ge_aper_macro_t *am, int code, int argc)
{
	int c, a;
	ge_macro_line_t *line = macro_append(ctx, am);

	line->op = code;

	for(a = 0; a < argc; a++) {
		ge_expr_prg_t *e;
		READ(ctx, c);
		if (c == '*')
			return 0;
		if ((a < argc-1) && (c != ',')) {
			ctx->errmsg = "macro aperture: missing comma (not enough parameters?)";
			return -1;
		}
		geparse_macro_expr(ctx, &e);
		vtp0_append(&line->operand, e);
		if ((a == argc-1) && (c == '*'))
			return 0;
	}
	READ_CMDEND(ctx);
	return 0;
}

static int geparse_macro_aperture_poly(geparse_ctx_t *ctx, ge_aper_macro_t *am, int code)
{
	int c, a, numpts;
	double er;
	ge_expr_prg_t *e;
	vtd0_t allzero;
	ge_macro_line_t *line = macro_append(ctx, am);

	line->op = code;

	for(a = 0; a < 2; a++) {
		READ(ctx, c);
		if (c != ',') {
			ctx->errmsg = "macro aperture: missing comma (not enough parameters?)";
			return -1;
		}
		geparse_macro_expr(ctx, &e);
		vtp0_append(&line->operand, e);
	}

	memset(&allzero, 0, sizeof(allzero));
	if (gex_eval(e, &allzero, &er) != 0) {
		ctx->errmsg = "macro aperture: failed to evaluate number of polygon points";
		return -1;
	}
	numpts = rnd_round(er);

	for(a = 0; a < numpts*2+3; a++) {
		READ(ctx, c);
		if (c != ',') {
			ctx->errmsg = "macro aperture: missing comma (not enough points?)";
			return -1;
		}
		geparse_macro_expr(ctx, &e);
		vtp0_append(&line->operand, e);
	}

	READ_CMDEND(ctx);
	return 0;
}

static int geparse_macro_aperture_set(geparse_ctx_t *ctx, ge_aper_macro_t *am)
{
	ge_expr_prg_t *e;
	int idx, c;
	ge_macro_line_t *line = macro_append(ctx, am);

	READ_NUM(ctx, idx);
	READ(ctx, c);
	if (c != '=')
		return -1;
	line->op = GEMO_SET;
	line->idx = idx;
	geparse_macro_expr(ctx, &e);
	vtp0_append(&line->operand, e);
	READ_CMDEND(ctx);
	return 0;
}

static ge_parse_res_t geparse_macro_aperture(geparse_ctx_t *ctx)
{
	gedraw_inst_t *inst;
	ge_aper_macro_t *am;
	int c = 0;
	gds_t name;

	/* read the name and check for dups */
	gds_init(&name);
	READ_NAME_GDS(ctx, &name, '*');
	READ(ctx, c); /* remvoe the '*' */
	if (htsp_has(ctx->macros, name.array)) {
		gds_uninit(&name);
		ctx->errmsg = "macro aperture: duplicate macro name";
		return GEP_ERROR;
	}

	am = calloc(sizeof(ge_aper_macro_t), 1);
	inst = gedraw_alloc(&ctx->draw, ctx->line, ctx->col);
	inst->data.aper.data.macro.name = name.array;
	inst->cmd = GEC_MACRO_DEF;
	inst->data.aper.shape = GEA_MACRO;
	inst->data.aper.data.macro.am = am;

	htsp_set(ctx->macros, name.array, am);
	/* do _not_ call gds_uninit(&name): alloction is now owned by the hash table */

	for(;;) {
		int code, lineres;
		READ(ctx, c);
		switch(c) {
			case '$':
				if (geparse_macro_aperture_set(ctx, am) != 0)
					return GEP_ERROR;
				continue;
			case '%':
				return GEP_NEXT; /* long command termination */
			default:
				UNGETC(ctx, c); /* would be a numeric code */
		}
		READ_NUM(ctx, code);
		switch(code) {
			case 0: /* comment */
				READ_TILL_CMDEND(ctx);
				lineres = 0;
				break;
			case GEMO_CIRC:    lineres = geparse_macro_aperture_line(ctx, am, code, 5); break;
			case GEMO_POLY:    lineres = geparse_macro_aperture_poly(ctx, am, code); break;
			case GEMO_REGPOLY: lineres = geparse_macro_aperture_line(ctx, am, code, 6); break;
			case GEMO_MOIRE:   lineres = geparse_macro_aperture_line(ctx, am, code, 9); break;
			case GEMO_THERM:   lineres = geparse_macro_aperture_line(ctx, am, code, 6); break;
			case GEMO_LINE_XY: lineres = geparse_macro_aperture_line(ctx, am, code, 7); break;
			case GEMO_LINE_WH: lineres = geparse_macro_aperture_line(ctx, am, code, 6); break;
			default:
				ctx->errmsg = "macro aperture: unknown primtive code";
				return GEP_ERROR;
		}
		if (lineres != 0)
			return GEP_ERROR;
	}
}

#define CMD(c1, c2) ((((unsigned int)c1)<<8) | (((unsigned int)c2)))

static ge_parse_res_t geparse_aperture_add(geparse_ctx_t *ctx)
{
	int c1, c2, c;
	long aid;

	READ_APERTURE(ctx, aid, 1);
	if (aid == -42) {
		READ_LONGEND(ctx);
		return GEP_NEXT;
	}
	if (aid < 10) {
		ctx->errmsg = "invalid aperture ID: must be at least 10";
		return GEP_ERROR;
	}
	READ(ctx, c1);
	READ(ctx, c2);
	if (c2 != ',') { /* named macro */
		char sep;
		gds_t name;
		htsp_entry_t *he;
		ge_aper_macro_t *am;
		gedraw_inst_t *inst;

		gds_init(&name);
		gds_append(&name, c1);
		gds_append(&name, c2);
		READ_NAME_GDS(ctx, &name, ',');

		he = htsp_getentry(ctx->macros, name.array);
		if (he == NULL) {
			ctx->errmsg = "named aperture not found";
			return GEP_ERROR;
		}
		am = he->value;
		gds_uninit(&name);

		inst = gedraw_alloc(&ctx->draw, ctx->line, ctx->col);
		READ(ctx, sep);
		while(sep != '*') {
			double d;
			READ_DEC(ctx, d);
			vtd0_append(&inst->data.aper.data.macro.param, d);
			READ(ctx, sep);
			if ((sep != 'X') && (sep != '*')) {
				vtd0_uninit(&inst->data.aper.data.macro.param);
				ctx->errmsg = "aperture macro: invalid parameter separator: expected X or *";
				return GEP_ERROR;
			}
		}

		inst->cmd = GEC_APER_DEF;
		inst->data.aper.shape = GEA_MACRO;
		inst->data.aper.id = aid;
		inst->data.aper.data.macro.name = he->key;
		inst->data.aper.data.macro.am = am;
	
		READ_LONGEND(ctx);
		return GEP_NEXT;
	}

	/* simple, non-macro aperture */
	return geparse_aperture_spec_shaped(ctx, aid, c1);
}

static ge_parse_res_t geparse_set_unit(geparse_ctx_t *ctx)
{
	int c1, c2;

	READ_UPCASE(ctx, c1);
	READ_UPCASE(ctx, c2);
	switch(CMD(c1, c2)) {
		case CMD('I', 'N'):
			ctx->unit = GEU_INCH;
			READ_CMDEND(ctx);
			break;
		case CMD('M', 'M'):
			ctx->unit = GEU_MM;
			READ_CMDEND(ctx);
			break;
		default:
			ctx->errmsg = "invalid unit for %%MO";
			return GEP_ERROR;
	}

	READ_LONGEND(ctx);
	return GEP_NEXT;
}

static ge_parse_res_t geparse_steprep(geparse_ctx_t *ctx)
{
	int c;

	gedraw_inst_t *inst = gedraw_alloc(&ctx->draw, ctx->line, ctx->col);
	inst->cmd = GEC_STEPREP;
	READ(ctx, c);
	switch(c) {
		case 'X':
			inst->data.steprep.end = 0;
			READ_NUM(ctx, inst->data.steprep.x);
			READ(ctx, c); if (c != 'Y') goto steprep_err;
			READ_NUM(ctx, inst->data.steprep.y);
			READ(ctx, c); if (c != 'I') goto steprep_err;
			READ_COORD_DEC(ctx, inst->data.steprep.i);
			READ(ctx, c); if (c != 'J') goto steprep_err;
			READ_COORD_DEC(ctx, inst->data.steprep.j);
			READ(ctx, c); if (c != '*') goto steprep_err;
			break;
		case '*':
			inst->data.steprep.end = 1;
			break;
		default:
			ctx->errmsg = "unsupported SR parameter: must be empty or must start with X";
			return GEP_ERROR;
	}
	READ_LONGEND(ctx);
	return GEP_NEXT;

	steprep_err:;
	ctx->errmsg = "invalid %SR argument format";
	return GEP_ERROR;
}

static ge_parse_res_t geparse_coordfmt(geparse_ctx_t *ctx)
{
	int c, x1, y1, x2, y2;

	READ(ctx, c);
	switch(c) {
		case 'L': ctx->trailing_zero = 0; break;
		case 'T': ctx->trailing_zero = 1; break;
		default:
			ctx->errmsg = "unsupported coord format: %FS zero padding character must be 'L' or 'T'";
			return GEP_ERROR;
	}
	READ(ctx, c);
	switch(c) {
		case 'A': COMPILE(ctx, GEC_SET_RELCRD, on, 0); break;
		case 'I': COMPILE(ctx, GEC_SET_RELCRD, on, 1); break;
		default:
			ctx->errmsg = "unsupported coord format: %FS coord reference character should be 'A' or 'I'";
			return GEP_ERROR;
	}

	READ(ctx, c);

	/* N2 is in Orcad Layout Plus 9.2. exported gervers */
	if (c == 'N') {
		/* probably a number -> ignore */
		do {
			READ(ctx, c);
		} while(isdigit(c));
	}

	/* back to normal: X, digit, digit, Y, digit, digit */
	if (c != 'X') goto bad_cmd;

	READ(ctx, x1); if (!isdigit(x1)) goto bad_num;
	READ(ctx, x2); if (!isdigit(x2)) goto bad_num;
	READ(ctx, c); if (c != 'Y') goto bad_cmd;
	READ(ctx, y1); if (!isdigit(y1)) goto bad_num;
	READ(ctx, y2); if (!isdigit(y2)) goto bad_num;
	READ_CMDEND(ctx);
	if ((x1 != y1) || (x2 != y2)) {
		ctx->errmsg = "unsupported coord format: %FS X and Y should be the same";
		return GEP_ERROR;
	}
	ctx->cfmt_int = x1 - '0';
	ctx->cfmt_fra = x2 - '0';

	READ_LONGEND(ctx);
	return GEP_NEXT;

	bad_num:;
	ctx->errmsg = "expected digit";
	return GEP_ERROR;

	bad_cmd:;
	ctx->errmsg = "missing X or Y in coord format spec";
	return GEP_ERROR;
}

ge_parse_res_t geparse_long_cmd(geparse_ctx_t *ctx)
{
	int c1, c2, c;

	READ_UPCASE(ctx, c1);
	READ_UPCASE(ctx, c2);
	switch(CMD(c1, c2)) {
		case CMD('M', 'O'):
			return geparse_set_unit(ctx);

		case CMD('S', 'R'):
			return geparse_steprep(ctx);

		case CMD('I', 'P'): /* image polarity - pos or neg */
			READ(ctx, c);
			switch(c) {
				case 'P': /* positive is the default setup, no need to do anything */ break;
				case 'N': rnd_message(RND_MSG_WARNING, "IPNEG: negative image polarity is not yet supported:\nthe image will be displayed in inverse.\n"); break;
				default: rnd_message(RND_MSG_WARNING, "Invalid IP arg, assuming POS\n");
			}
			READ_TILL_CMDEND(ctx);
			break;

		case CMD('L', 'P'):
			READ(ctx, c);
			switch(c) {
				case 'C': COMPILE(ctx, GEC_SET_POLCLR, on, 1); break;
				case 'D': COMPILE(ctx, GEC_SET_POLCLR, on, 0); break;
			}
			READ_CMDEND(ctx);
			COMPILE0(ctx, GEC_DO);
			break;

		case CMD('F', 'S'): /* coord format */
			return geparse_coordfmt(ctx);

		case CMD('A', 'D'): /* add aperture */
			return geparse_aperture_add(ctx);

		case CMD('A', 'M'): /* add macro aperture, up to and including the terminator % */
			return geparse_macro_aperture(ctx);

		case CMD('L', 'N'): /* layer name? -> ignore */
			READ_TILL_CMDEND(ctx);
			break;

		case CMD('I', 'N'): /* design name? -> ignore */
			READ_TILL_CMDEND(ctx);
			break;

		/* these are just overly fancy comments really -> ignore */
		case CMD('T', 'F'):
		case CMD('T', 'A'):
		case CMD('T', 'D'):
		case CMD('T', 'O'):
			READ_TILL_CMDEND(ctx);
			break;

		/* offset and scale factor -> ignore for now */
		case CMD('O', 'F'):
		case CMD('S', 'F'):
			READ_TILL_CMDEND(ctx);
			break;


		/* unknown -> ignore */
		case CMD('I', 'C'): /* Orcad Layout Plus 9.2 */
		case CMD('A', 'S'): /* Orcad Layout Plus 9.2 */
			READ_TILL_CMDEND(ctx);
			break;

		default:
			ctx->errmsg = "unrecognized long command";
			return GEP_ERROR;
	}

	READ_LONGEND(ctx);
	return GEP_NEXT;
}
#undef CMD

#define CMD(chr, code) ((((unsigned int)chr)<<8)+code)
ge_parse_res_t geparse_short_cmd(geparse_ctx_t *ctx, int cmd)
{
	ge_coord_t crd;
	long code;
	int c;
	

	switch(cmd) { /* single char short commands */
		case '*': COMPILE0(ctx, GEC_DO); return GEP_NEXT;
		case 'X': READ_COORD(ctx, crd); COMPILE(ctx, GEC_SET_X, coord, crd); return GEP_NEXT;
		case 'Y': READ_COORD(ctx, crd); COMPILE(ctx, GEC_SET_Y, coord, crd); return GEP_NEXT;
		case 'I': READ_COORD(ctx, crd); COMPILE(ctx, GEC_SET_I, coord, crd); return GEP_NEXT;
		case 'J': READ_COORD(ctx, crd); COMPILE(ctx, GEC_SET_J, coord, crd); return GEP_NEXT;
		case 'D':
			select_aper:;
			READ_NUM(ctx, code);
			if (code < 10)
				goto d_cmd; /* these are not apertures but commands; code is already parsed */
			READ_CMDEND(ctx);
			COMPILE(ctx, GEC_APER_SEL, id, code);
			return GEP_NEXT;
	}

	READ_NUM(ctx, code);
	d_cmd:;
	code += ((unsigned int)cmd) << 8;
	switch(code) {
		case CMD('G', 01): COMPILE(ctx, GEC_SET_INTERP, interp, GEI_LIN); return GEP_NEXT;
		case CMD('G', 02): COMPILE(ctx, GEC_SET_INTERP, interp, GEI_CW); return GEP_NEXT;
		case CMD('G', 03): COMPILE(ctx, GEC_SET_INTERP, interp, GEI_CCW); return GEP_NEXT;
		case CMD('G', 04): READ_TILL_CMDEND(ctx); return GEP_NEXT;
		case CMD('G', 36): READ_CMDEND(ctx); COMPILE(ctx, GEC_SET_POLY, on, 1); return GEP_NEXT;
		case CMD('G', 37): READ_CMDEND(ctx); COMPILE(ctx, GEC_SET_POLY, on, 0); return GEP_NEXT;
		case CMD('G', 54):
			READ(ctx, c);
			if (c != 'D') {
				ctx->errmsg = "G54 requires a 'D'\n";
				return GEP_ERROR;
			}
			goto select_aper;
		case CMD('G', 70): READ_CMDEND(ctx); ctx->unit = GEU_INCH; return GEP_NEXT;
		case CMD('G', 71): READ_CMDEND(ctx); ctx->unit = GEU_MM; return GEP_NEXT;
		case CMD('G', 74): READ_CMDEND(ctx); COMPILE(ctx, GEC_SET_QUADR, interp, GEQ_SINGLE); return GEP_NEXT;
		case CMD('G', 75): READ_CMDEND(ctx); COMPILE(ctx, GEC_SET_QUADR, interp, GEQ_MULTI); return GEP_NEXT;
		case CMD('G', 90): COMPILE(ctx, GEC_SET_RELCRD, on, 0); return GEP_NEXT;
		case CMD('G', 91): COMPILE(ctx, GEC_SET_RELCRD, on, 1); return GEP_NEXT;
		case CMD('D', 01): COMPILE0(ctx, GEC_DRAW); return GEP_NEXT;
		case CMD('D', 02): COMPILE0(ctx, GEC_MOVE); return GEP_NEXT;
		case CMD('D', 03): COMPILE0(ctx, GEC_FLASH); return GEP_NEXT;
		case CMD('M', 02): READ_CMDEND(ctx); ctx->at_end = 1; return GEP_END;
	}
	ctx->errmsg = "unknown short command code\n";
	return GEP_ERROR;
}
#undef CMD


static ge_parse_res_t geparse_(geparse_ctx_t *ctx)
{
	if (ctx->at_end)
		return GEP_END;
	if (ctx->line == 0)
		ctx->line = 1;
	for(;;) {
		int c;
		
		READ(ctx, c);
		switch(c) {
			case EOF:
				if (ctx->cmd_cnt == 0) {
					ctx->errmsg = "EOF before the first command";
					return GEP_ERROR;
				}
				return GEP_END;
			case '\n':
			case '\r':
				break;
			case '%':
				ctx->cmd_cnt++;
				return geparse_long_cmd(ctx);
			case 'D':
			case 'G':
			case 'M':
			case 'X':
			case 'Y':
			case 'I':
			case 'J':
			case '*':
				ctx->cmd_cnt++;
				return geparse_short_cmd(ctx, c);
			default:
				ctx->errmsg = "Invalid command (first character)";
				return GEP_ERROR;
		}
	}
}

ge_parse_res_t geparse(geparse_ctx_t *ctx)
{
	if (ctx->macros == NULL)
		ctx->macros = htsp_alloc(strhash, strkeyeq);
	return geparse_(ctx);
}

static void geparse_free_macro_aper(ge_aper_macro_t *am)
{
	ge_macro_line_t *l, *next;
	for(l = am->line1; l != NULL; l = next) {
		int a;
		next = l->next;
		for(a = 0; a < l->operand.used; a++)
			gex_free_prg(l->operand.array[a]);
		free(l->operand.array);
		free(l);
	}
	free(am->argv);
	free(am);
}

void geparse_free(geparse_ctx_t *ctx)
{
	gedraw_free(&ctx->draw);
	if (ctx->macros != NULL) {
		htsp_entry_t *e;
		for(e = htsp_first(ctx->macros); e != NULL; e = htsp_next(ctx->macros, e)) {
			free(e->key); /* name */
			geparse_free_macro_aper(e->value);
		}
		htsp_free(ctx->macros);
	}
}

