/*
Copyright (c) 2016, Blue Brain Project
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "nmodlconf.h"
/* /local/src/master/nrn/src/modlunit/units.c,v 1.5 1997/11/24 16:19:13 hines Exp */
/* Mostly from Berkeley */
#include	<stdio.h>
#include	<stdlib.h>
#include	<signal.h>
#include	<string.h>
#include	"units.h"
#include	<assert.h>

#ifndef USE_LEGACY_UNITS
#define USE_LEGACY_UNITS 0
#endif

int	unitonflag = 1;
static int	UnitsOn = 0;
extern double	fabs();
extern void diag();
#define	IFUNITS	{if (!UnitsOn) return;}
#define OUTTOLERANCE(arg1,arg2) (fabs(arg2/arg1 - 1.) > 1.e-5)

#define	NTAB	601
/* if MODLUNIT environment variable not set then look in the following places*/
#if MAC
static char	*dfile	= ":lib:nrnunits.lib";
#else
#if defined(NEURON_DATA_DIR)
static char	*dfile	= NEURON_DATA_DIR"/lib/nrnunits.lib";
#else
static char	*dfile	= "/usr/lib/units";
#endif
#endif
#if defined(__TURBOC__) || defined(__GO32__)
static char	*dfilealt	= "/nrn/lib/nrnunits.lib";
#else
#if MAC
static char *dfilealt = "::lib:nrnunits.lib";
#else
static char	*dfilealt	= "../../share/lib/nrnunits.lib";
#endif
#endif
static char	*unames[NDIM];
static double	getflt();
static void fperr();
static int	lookup();
static struct	table	*hash();

static void chkfperror();
static void units();
static int pu();
static int convr();
static void init();
static int get();

extern void Unit_push(char*);

static struct table
{
	double	factor;
#if -1 == '\377'
	char	dim[NDIM];
#else
	signed char	dim[NDIM];
#endif
	char	*name;
} table[NTAB];
static char	names[NTAB*10];
static struct prefix
{
	double	factor;
	char	*pname;
} prefix[] = 
{
	1e-18,	"atto",
	1e-15,	"femto",
	1e-12,	"pico",
	1e-9,	"nano",
	1e-6,	"micro",
	1e-3,	"milli",
	1e-2,	"centi",
	1e-1,	"deci",
	1e1,	"deka",
	1e2,	"hecta",
	1e2,	"hecto",
	1e3,	"kilo",
	1e6,	"mega",
	1e6,	"meg",
	1e9,	"giga",
	1e12,	"tera",
	0.0,	0
};
static FILE	*inpfile;
static int	fperrc;
static int	peekc;
static int	dumpflg;

static char *pc;

static int Getc(inp)
	FILE *inp;
{
	if (inp != stdin) {
#if MAC
		int c = getc(inp);
		if (c == '\r') { c  = '\n';}
		return c;
#else
		return getc(inp);
#endif
	}else if (pc && *pc) {
		return (int)(*pc++);
	}else{
		return (int)('\n');
	}
}

#define UNIT_STK_SIZE	20
static struct unit unit_stack[UNIT_STK_SIZE], *usp;


void unit_pop() {
	IFUNITS
	assert(usp >= unit_stack);
	--usp;
}

void unit_swap() { /*exchange top two elements of stack*/
	struct unit *up;
	int i, j;
	double d;
	
	IFUNITS
	assert(usp > unit_stack);
	up = usp -1;

	d = usp->factor;
	usp->factor = up->factor;
	up->factor = d;

	for (i=0; i<NDIM; i++) {
		j = usp->dim[i];
		usp->dim[i] = up->dim[i];
		up->dim[i] = j;
	}
}	
	
double
unit_mag() {	/* return unit magnitude that is on stack */
	return usp->factor;
}

void unit_mag_mul(d)
	double d;
{
	usp->factor *= d;
}

void punit() {
	struct unit *	i;
	for (i=usp; i!=unit_stack-1; --i) {
		printf("%s\n", Unit_str(i));
	}
}

void ucopypop(up)
	struct unit *up;
{
	int i;
	for (i=0; i<NDIM; i++) {
		up->dim[i] = usp->dim[i];
	}
	up->factor = usp->factor;
	up->isnum = usp->isnum;
	unit_pop();
}

void ucopypush(up)
	struct unit *up;
{
	int i;
	Unit_push("");
	for (i=0; i<NDIM; i++) {
		usp->dim[i] = up->dim[i];
	}
	usp->factor = up->factor;
	usp->isnum = up->isnum;
}

void Unit_push(string)
	char *string;
{
	IFUNITS
	assert(usp < unit_stack + (UNIT_STK_SIZE - 1));
	++usp;
	pc = string;
	if (string) {
		usp->isnum = 0;
	}else{
		pc = "";
		usp->isnum = 1;
	}
	convr(usp);
/*printf("unit_push %s\n", string); units(usp);*/
}

void unit_push_num(d)
	double d;
{
	Unit_push("");
	usp->factor = d;
}

void unitcheck(s)
	char *s;
{
	Unit_push(s);
	unit_pop();
}

char *
unit_str() {
	/* return top of stack as units string */
	if (!UnitsOn) return "";
	return Unit_str(usp);
}

void install_units(s1, s2) /* define s1 as s2 */
	char *s1, *s2;
{
	struct table *tp;
	int i;
	
	IFUNITS
	Unit_push(s2);
	tp = hash(s1);
	if (tp->name) {
		printf("Redefinition of units (%s) to:", s1);
		units(usp);
		printf(" is ignored.\nThey remain:");
		Unit_push(s1); units(usp);		
		diag("Units redefinition", (char *)0);
	}
	tp->name = s1;
	tp->factor = usp->factor;
	for (i=0; i<NDIM; i++) {
		tp->dim[i] = usp->dim[i];
	}
	unit_pop();
}

void check_num()
{
	struct unit * up = usp -1;
	/*EMPTY*/
	if (up->isnum && usp->isnum) {
		;
	} else {
		up->isnum = 0;
	}
}
		
void unit_mul() {
	/* multiply next element by top of stack and leave on stack */
	struct unit *up;
	int i;
	
	IFUNITS
	assert(usp > unit_stack);
	check_num();
	up = usp -1;
	for (i=0; i<NDIM; i++) {
		up->dim[i] += usp->dim[i];
	}
	up->factor *= usp->factor;
	unit_pop();
}

void unit_div() {
	/* divide next element by top of stack and leave on stack */
	struct unit *up;
	int i;
	
	IFUNITS
	assert(usp > unit_stack);
	check_num();
	up = usp -1;
	for (i=0; i<NDIM; i++) {
		up->dim[i] -= usp->dim[i];
	}
	up->factor /= usp->factor;
	unit_pop();
}

void Unit_exponent(val)
	int val;
{
	/* multiply top of stack by val and leave on stack */
	int i;
	double d;
	
	IFUNITS
	assert(usp >= unit_stack);
	for (i=0; i<NDIM; i++) {
		usp->dim[i] *= val;
	}
	d = usp->factor;
	for (i=1; i<val; i++) {
		usp->factor *= d;
	}
}

int
unit_cmp_exact() { /* returns 1 if top two units on stack are same */
	struct unit *up;
	int i;

	{if (!UnitsOn) return 0;}
	up = usp - 1;
   if (unitonflag) {
	if (up->dim[0] == 9 || usp->dim[0] == 9) {
		return 1;
	}
	for (i=0; i<NDIM; i++) {
		if (up->dim[i] != usp->dim[i]) {
			chkfperror();
			return 0;
		}
	}
	if (OUTTOLERANCE(up->factor, usp->factor)) {
		chkfperror();
		return 0;
	}
   }
	return 1;
}

/* ARGSUSED */
static void print_unit_expr(i) int i; {}

void Unit_cmp() {
	/*compares top two units on stack. If not conformable then
	gives error. If not same factor then gives error.
	*/
	struct unit *up;
	int i;
	
	IFUNITS
	assert(usp > unit_stack);
	up = usp - 1;
	if (usp->isnum) {
		unit_pop();
		return;
	}
	if (up->isnum) {
		for (i=0; i < NDIM; i++) {
			up->dim[i] = usp->dim[i];
		}
		up->factor = usp->factor;
		up->isnum = 0;
	}
if (unitonflag && up->dim[0] != 9 && usp->dim[0] != 9) {
	for (i=0; i<NDIM; i++) {
		if (up->dim[i] != usp->dim[i]) {
			chkfperror();
			print_unit_expr(2);
			fprintf(stderr, "\nunits:");
			units(usp);
			print_unit_expr(1);
			fprintf(stderr, "\nunits:");
			units(up);
diag("The units of the previous two expressions are not conformable","\n");
		}
	}
	if (OUTTOLERANCE(up->factor, usp->factor)) {
		chkfperror();
fprintf(stderr, "The previous primary expression with units: %s\n\
is missing a conversion factor and should read:\n  (%g)*(",
Unit_str(usp), usp->factor/up->factor);
		print_unit_expr(2);
		diag(")\n", (char *)0);
	}
}
	unit_pop();
	return;
}

int unit_diff() {
	/*compares top two units on stack. If not conformable then
	return 1, if not same factor return 2 if same return 0
	*/
	struct unit *up;
	int i;
	
	if (!UnitsOn) return 0;
	assert(usp > unit_stack);
	up = usp - 1;
	if (up->dim[0] == 9 || usp->dim[0] == 9) {
		unit_pop();
		return 0;
	}
	if (usp->isnum) {
		unit_pop();
		return 0;
	}
	if (up->isnum) {
		for (i=0; i < NDIM; i++) {
			up->dim[i] = usp->dim[i];
		}
		up->factor = usp->factor;
		up->isnum = 0;
	}
	for (i=0; i<NDIM; i++) {
		if (up->dim[i] != usp->dim[i]) {
			return 1;
		}
	}
	if (OUTTOLERANCE(up->factor, usp->factor)) {
		return 2;
	}
	unit_pop();
	return 0;
}

static void chkfperror()
{
	if (fperrc) {
		diag("underflow or overflow in units calculation", (char *)0);
	}
}

void dimensionless() {
	/* ensures top element is dimensionless */
	int i;
	
	IFUNITS
	assert(usp >= unit_stack);
	if (usp->dim[0] == 9) {return;}
	for (i=0; i<NDIM; i++) {
		if (usp->dim[i] != 0) {
			units(usp);
diag("The previous expression is not dimensionless", (char *)0);
		}
	}
}

void unit_less() {
	/* ensures top element is dimensionless  with factor 1*/
	IFUNITS
	if (unitonflag) {
		dimensionless();
		if (usp->dim[0]!=9 && OUTTOLERANCE(usp->factor,1.0)) {
fprintf(stderr, "The previous expression needs the conversion factor (%g)\n",
1./(usp->factor));
			diag("", (char *)0);
		}
	}
	unit_pop();
}

void unit_stk_clean() {
	IFUNITS
	usp = unit_stack - 1;
}
	
#if MODL||NMODL||HMODL||SIMSYS
extern void unit_init();

void modl_units() {
	static int first=1;
	unitonflag = 1;
	if (first) {
		unit_init();
		first = 0;
	}
}

#endif

void unit_init() {
	char* s;
	char buf[256];
	inpfile = (FILE*)0;
	UnitsOn = 1;
	s = getenv("MODLUNIT");
	if (s) {
		if ((inpfile = fopen(s, "r")) == (FILE *)0) {
diag("Bad MODLUNIT environment variable. Cant open:", s);
		}
	}
	if (!inpfile) {
fprintf(stderr, "Set a MODLUNIT environment variable path to the units table file\n");
fprintf(stderr, "Cant open units table in either of:\n%s\n", buf);
			diag(dfile, dfilealt);
	}
	signal(8, fperr);
	init();
	unit_stk_clean();
}

#if 0
void main(argc, argv)
char *argv[];
{
	register i;
	register char *file;
	struct unit u1, u2;
	double f;

	if(argc>1 && *argv[1]=='-') {
		argc--;
		argv++;
		dumpflg++;
	}
	file = dfile;
	if(argc > 1)
		file = argv[1];
	if ((inpfile = fopen(file, "r")) == NULL) {
		printf("no table\n");
		exit(1);
	}
	signal(8, fperr);
	init();

loop:
	fperrc = 0;
	printf("you have: ");
	if(convr(&u1))
		goto loop;
	if(fperrc)
		goto fp;
loop1:
	printf("you want: ");
	if(convr(&u2))
		goto loop1;
	for(i=0; i<NDIM; i++)
		if(u1.dim[i] != u2.dim[i])
			goto conform;
	f = u1.factor/u2.factor;
	if(fperrc)
		goto fp;
	printf("\t* %e\n", f);
	printf("\t/ %e\n", 1./f);
	goto loop;

conform:
	if(fperrc)
		goto fp;
	printf("conformability\n");
	units(&u1);
	units(&u2);
	goto loop;

fp:
	printf("underflow or overflow\n");
	goto loop;
}
#endif

static void units(up)
struct unit *up;
{
	printf("\t%s\n", Unit_str(up));
}

static char *ucp;
char *Unit_str(up)
struct unit *up;
{
	register struct unit *p;
	register int f, i;
	static char buf[256];

	p = up;
	sprintf(buf, "%g ", p->factor);
	{int seee=0; for (ucp=buf; *ucp; ucp++) {
		if (*ucp == 'e') seee=1;
		if (seee) *ucp = ucp[1];
	} if (seee) ucp--;}
	f = 0;
	for(i=0; i<NDIM; i++)
		f |= pu(p->dim[i], i, f);
	if(f&1) {
		*ucp++ = '/';
		f = 0;
		for(i=0; i<NDIM; i++)
			f |= pu(-p->dim[i], i, f);
	}
	*ucp = '\0';
	return buf;
}

static int pu(int u, int i, int f)
{

	if(u > 0) {
		if(f&2)
			*ucp++ = '-';
		if(unames[i]) {
			sprintf(ucp,"%s", unames[i]);
			ucp += strlen(ucp);
		} else{
			sprintf(ucp,"*%c*", i+'a');
			ucp += strlen(ucp);
		}
		if(u > 1)
			*ucp++ = (u+'0');
			return(2);
	}
	if(u < 0)
		return(1);
	return(0);
}

static int convr(up)
struct unit *up;
{
	register struct unit *p;
	register int c;
	register char *cp;
	char name[20];
	int den, err;

	p = up;
	for(c=0; c<NDIM; c++)
		p->dim[c] = 0;
	p->factor = getflt();
	if(p->factor == 0.) {
		p->factor = 1.0;
	}
	err = 0;
	den = 0;
	cp = name;

loop:
	switch(c=get()) {

	case '1':
	case '2':
	case '3':
	case '4':
	case '5':
	case '6':
	case '7':
	case '8':
	case '9':
	case '-':
	case '/':
	case ' ':
	case '\t':
	case '\n':
		if(cp != name) {
			*cp++ = 0;
			cp = name;
			err |= lookup(cp, p, den, c);
		}
		if(c == '/')
			den++;
		if(c == '\n')
			return(err);
		goto loop;
	}
	*cp++ = c;
	goto loop;
}

static int lookup(char* name, struct unit* up, int den, int c) {
	register struct unit *p;
	register struct table *q;
	register int i;
	char *cp1, *cp2;
	double e;

	p = up;
	e = 1.0;
loop:
	q = hash(name);
	if(q->name) {
		l1:
		if(den) {
			p->factor /= q->factor*e;
			for(i=0; i<NDIM; i++)
				p->dim[i] -= q->dim[i];
		} else {
			p->factor *= q->factor*e;
			for(i=0; i<NDIM; i++)
				p->dim[i] += q->dim[i];
		}
		if(c >= '2' && c <= '9') {
			c--;
			goto l1;
		}
		return(0);
	}
	for(i=0; (cp1 = prefix[i].pname) != 0; i++) {
		cp2 = name;
		while(*cp1 == *cp2++)
			if(*cp1++ == 0) {
				cp1--;
				break;
			}
		if(*cp1 == 0) {
			e *= prefix[i].factor;
			name = cp2-1;
			goto loop;
		}
	}
	/*EMPTY*/
	for(cp1 = name; *cp1; cp1++);
	if(cp1 > name+1 && *--cp1 == 's') {
		*cp1 = 0;
		goto loop;
	}
	fprintf(stderr, "Need declaration in UNITS block of the form:\n\
	(%s)	(units)\n", name);
	diag("Cannot recognize the units: ", name);
/*	printf("cannot recognize %s\n", name);*/
	return(1);
}

static int equal(s1, s2)
char *s1, *s2;
{
	register char *c1, *c2;

	c1 = s1;
	c2 = s2;
	while(*c1++ == *c2)
		if(*c2++ == 0)
			return(1);
	return(0);
}

static void init()
{
	register char *cp;
	register struct table *tp, *lp;
	int c, i, f, t;
	char *np;

	cp = names;
	for(i=0; i<NDIM; i++) {
		np = cp;
		*cp++ = '*';
		*cp++ = i+'a';
		*cp++ = '*';
		*cp++ = 0;
		lp = hash(np);
		lp->name = np;
		lp->factor = 1.0;
		lp->dim[i] = 1;
	}
	lp = hash("");
	lp->name = cp-1;
	lp->factor = 1.0;

l0:
	c = get();
	if(c == 0) {
#if 0
		printf("%d units; %d bytes\n\n", i, cp-names);
#endif
		if(dumpflg)
		for(tp = table; tp < table+NTAB; tp++) {
			if(tp->name == 0)
				continue;
			printf("%s", tp->name);
			units((struct unit *)tp);
		}
		fclose(inpfile);
		inpfile = stdin;
		return;
	}
	if(c == '/') {
		while(c != '\n' && c != 0)
			c = get();
		goto l0;
	}
	if(c == '\n')
		goto l0;
	np = cp;
	while(c != ' ' && c != '\t') {
		*cp++ = c;
		c = get();
		if (c==0)
			goto l0;
		if(c == '\n') {
			*cp++ = 0;
			tp = hash(np);
			if(tp->name)
				goto redef;
			tp->name = np;
			tp->factor = lp->factor;
			for(c=0; c<NDIM; c++)
				tp->dim[c] = lp->dim[c];
			i++;
			goto l0;
		}
	}
	*cp++ = 0;
	lp = hash(np);
	if(lp->name)
		goto redef;
	convr((struct unit *)lp);
	lp->name = np;
	f = 0;
	i++;
	if(lp->factor != 1.0)
		goto l0;
	for(c=0; c<NDIM; c++) {
		t = lp->dim[c];
		if(t>1 || (f>0 && t!=0))
			goto l0;
		if(f==0 && t==1) {
			if(unames[c])
				goto l0;
			f = c+1;
		}
	}
	if(f>0)
		unames[f-1] = np;
	goto l0;

redef:
	printf("redefinition %s\n", np);
	goto l0;
}

#if USE_LEGACY_UNITS == 0
/*
  Convert str to double using atof.
  Allows consistency with BlueBrain/nmodl
*/

static double getflt() {
	int c;
	char str[100];
	char* cp;
	double d_modern;

	cp = str;
	do
		c = get();
	while(c == ' ' || c == '\t');

l1:
	if(c >= '0' && c <= '9') {
		*cp++ = c;
		c = get();
		goto l1;
	}
	if(c == '.') {
		*cp++ = c;
		c = get();
		goto l1;
	}
	if(c == '+' || c == '-') {
		*cp++ = 'e';
		*cp++ = c;
		c = get();
		while(c >= '0' && c <= '9') {
			*cp++ = c;
			c = get();
		}
	}
	*cp = '\0';
	d_modern = atof(str);
	if(c == '|') {
		d_modern /= getflt();
		return d_modern;
	}
	peekc = c;
	return(d_modern);
}

#else /* USE_LEGACY_UNITS == 1 */

static double
getflt()
{
	register int c, i, dp;
	double d, e;
	int f;

	d = 0.;
	dp = 0;
	do
		c = get();
	while(c == ' ' || c == '\t');

l1:
	if(c >= '0' && c <= '9') {
		d = d*10. + c-'0';
		if(dp)
			dp++;
		c = get();
		goto l1;
	}
	if(c == '.') {
		dp++;
		c = get();
		goto l1;
	}
	if(dp)
		dp--;
	if(c == '+' || c == '-') {
		f = 0;
		if(c == '-')
			f++;
		i = 0;
		c = get();
		while(c >= '0' && c <= '9') {
			i = i*10 + c-'0';
			c = get();
		}
		if(f)
			i = -i;
		dp -= i;
	}
	e = 1.;
	i = dp;
	if(i < 0)
		i = -i;
	while(i--)
		e *= 10.;
	if(dp < 0)
		d *= e; else
		d /= e;
	if(c == '|')
		return(d/getflt());
	peekc = c;
	return(d);
}

#endif /* USE_LEGACY_UNITS == 1 */

static int get()
{
	register int c;

	/*SUPPRESS 560*/
	if((c=peekc) != 0) {
		peekc = 0;
		return(c);
	}
	c = Getc(inpfile);
	if (c == '\r') { c = Getc(inpfile); }
	if (c == EOF) {
		if (inpfile == stdin) {
			printf("\n");
			exit(0);
		}
		return(0);
	}
	return(c);
}

static struct table *
hash(name)
char *name;
{
	register struct table *tp;
	register char *np;
	register unsigned h;

	h = 0;
	np = name;
	while(*np)
		h = h*57 + *np++ - '0';
	if( ((int)h)<0) h= -(int)h;
	h %= NTAB;
	tp = &table[h];
l0:
	if(tp->name == 0)
		return(tp);
	if(equal(name, tp->name))
		return(tp);
	tp++;
	if(tp >= table+NTAB)
		tp = table;
	goto l0;
}

static void fperr(sig) int sig;
{

	signal(8, fperr);
	fperrc++;
}

void nrnunit_dynamic_str(char* buf, const char* name, char* u1, char* u2) {
  double u;
  Unit_push(u1);
  Unit_push(u2);
  unit_div();
  u = unit_mag();
  unit_pop();

#if USE_LEGACY_UNITS
  sprintf(buf, "\nstatic double %s = %g;\n", name, u);
#else
  /* compiler can't read %a format til c++17 (or c99) */
  sprintf(buf, "\nstatic double %s = /* %a; */ %.18g;\n", name, u, u);
#endif
}


