/*
  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 3 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, see <http://www.gnu.org/licenses/>. 
*/

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>

#include "spi.h"

#define BAIL(...) { printf( __VA_ARGS__); return 1; }

bool stop_request = false;
static void sigint(int dummy) {
	if(stop_request) {
		exit(1);
	} else stop_request = true;
}

typedef struct {
	const char *id;
	const char *desc;
	uint8_t size;
	void (*pfget)(uint32_t);
	const char *params;
	bool (*pfset)(char *, uint32_t *);
} centry;

// Device targeting
static uint8_t address = 0xFF;			// Device address (0xFF = none)
static char info[4];					// Device information
static uint8_t configuration[32];		// Configuration in memory
static uint8_t configuration_dev[32];	// Configuration in device
static uint8_t configuration_size;		// Configuration byte count
static centry *cdesc;					// Pointer to configuration descriptor

// GENERIC translators
static void ms_get(uint32_t v) { printf("%ims", v); }
static bool ms_set(char *i, uint32_t *v) { *v = strtoul(i, NULL, 10); return *v >= 0 && *v <= 32767; }
static void resp_get(uint32_t v) { printf(v == 0x00 ? "no response" : v == 0x80 ? "force off" : v == 0x81 ? "force on" : "unknown"); }
static bool resp_set(char *i, uint32_t *v) { *v = *i == 'x' ? 0x00 : *i == '0' ? 0x80 : *i == '1' ? 0x81 : 0xFF; return *v != 0xFF; }
static void rmstate_get(uint32_t v) { printf(v == 0x00 ? "off" : v == 0x01 ? "on" : v == 0x80 ? "pwm" : "unknown"); }
static bool rmstate_set(char *i, uint32_t *v) { *v = *i == 'p' ? 0x80 : *i == '0' ? 0x00 : *i == '1' ? 0x01 : 0xFF; return *v != 0xFF; }

// == RELAY board configuration ======================================
static void rperiod_get(uint32_t v) { printf("%isec", v); }
static bool rperiod_set(char *i, uint32_t *v) { *v = strtoul(i, NULL, 10); return *v >= 1 && *v <= 60; }
static centry rconfig[] = {
	{"state",  "Power on state", 1, rmstate_get, "p=pwm/0=off/1=on",                         rmstate_set},
	{"period", "PWM period",     1, rperiod_get, "seconds, 0-60",                            rperiod_set},
	{"atime",  "Comm timeout",   2, ms_get,      "0=disable/milliseconds, 1-32767",          ms_set     },
	{"aresp",  "Alarm response", 1, resp_get,    "x=no response/0=force off/1=force on",     resp_set   },
	{NULL}
};

// == MOSFET board configuration =====================================
static void mfreq_get(uint32_t v) { printf("%ihz", v * 10); }
static bool mfreq_set(char *i, uint32_t *v) { *v = strtoul(i, NULL, 10) / 10; return *v >= 1 && *v <= 250; }
static void mmode_get(uint32_t v) { printf(v == 0x00 ? "alarm disabled" : v == 0x01 ? "mAmps < level" : v == 0x02 ? "mAmps >= level" : "unknown"); }
static bool mmode_set(char *i, uint32_t *v) { *v = *i == '0' ? 0x00 : *i == 'a' ? 0x01 : *i == 'b' ? 0x02 : 0xFF; return *v != 0xFF; }
static void mlevel_get(uint32_t v) { printf("%imA", v); }
static bool mlevel_set(char *i, uint32_t *v) { *v = strtoul(i, NULL, 10); return *v >= 0 && *v <= 32767; }
static centry mconfig[] = {
	{"state",  "Power on state", 1, rmstate_get,  "p=pwm/0=off/1=on",                        rmstate_set},
	{"freq",   "PWM frequency",  1, mfreq_get,    "hz, 10-2500",                             mfreq_set  },
	{"atime",  "Comm timeout",   2, ms_get,       "0=disable/milliseconds, 1-32767",         ms_set     },
	{"amode",  "Alarm mode",     1, mmode_get,    "0=disabled/a=mAmps<level/b=,Amps>=level", mmode_set  },
	{"alevel", "Alarm level",    2, mlevel_get,   "mAmps, 0-32767",                          mlevel_set },
	{"adelay", "Alarm delay",    2, ms_get,       "milliseconds, 0-32767",                   ms_set     },
	{"aresp",  "Alarm response", 1, resp_get,     "x=no response/0=force off/1=force on",    resp_set   },
	{NULL}
};

// == CONSUMPTION board configuration ================================
static void crange_get(uint32_t v) { printf("0.333V=%iA", v); }
static bool crange_set(char *i, uint32_t *v) { *v = strtoul(i, NULL, 10); return *v >= 1 && *v <= 50; }
static centry cconfig[] = {
	{"range",  "Range",          1, crange_get,   "amperes@0.333V, 1-50",                    crange_set },
	{"atime",  "Comm timeout",   2, ms_get,       "0=disable/milliseconds, 1-32767",         ms_set     },
	{"alevel", "Alarm level",    2, mlevel_get,   "mAmps, 0-32767",                          mlevel_set },
	{"amode",  "Alarm mode",     1, mmode_get,    "0=disabled/a=mAmps<level/b=,Amps>=level", mmode_set  },
	{"adelay", "Alarm delay",    2, ms_get,       "milliseconds, 0-32767",                   ms_set     },
	{NULL}
};

// == INPUT board configuration ======================================
static void imode_get(uint32_t v) {
	switch(v) {
		case 0x00: printf("Analog, voltage, 0-3.3V" ); break;
		case 0x01: printf("Analog, voltage, 0-12V"  ); break;
		case 0x02: printf("Analog, voltage, 0-24V"  ); break;
		case 0x03: printf("Analog, resistive sensor"); break;
		case 0x10: printf("Digital, DC, 0-3.3V"     ); break;
		case 0x11: printf("Digital, DC, 0-12V"      ); break;
		case 0x12: printf("Digital, DC, 0-24V"      ); break;
		case 0x20: printf("Digital, AC, 0-3.3V"     ); break;
		case 0x21: printf("Digital, AC, 0-12V"      ); break;
		case 0x22: printf("Digital, AC, 0-24V"      ); break;
		default: printf("Unknown");
	}
}
static bool imode_set(char *i, uint32_t *v) {
	if(!strcmp(i, "v3" )) { *v = 0x00; return true; }
	if(!strcmp(i, "v12")) { *v = 0x01; return true; }
	if(!strcmp(i, "v24")) { *v = 0x02; return true; }
	if(!strcmp(i, "r" ))  { *v = 0x03; return true; }
	if(!strcmp(i, "d3"))  { *v = 0x10; return true; }
	if(!strcmp(i, "d12")) { *v = 0x11; return true; }
	if(!strcmp(i, "d24")) { *v = 0x12; return true; }
	if(!strcmp(i, "f3"))  { *v = 0x20; return true; }
	if(!strcmp(i, "f12")) { *v = 0x21; return true; }
	if(!strcmp(i, "f24")) { *v = 0x22; return true; }
	if(!strcmp(i, "?"))   {
		printf("r\tResistive sensor\n");
		printf("v3\tAnalog, voltage, 0-3.3V\n");
		printf("v12\tAnalog, voltage, 0-12V\n");
		printf("v24\tAnalog, voltage, 0-24V\n");
		printf("d3\tDigital, 0-3.3V\n");
		printf("d12\tDigital, 0-12V\n");
		printf("d24\tDigital, 0-24V\n");
		printf("f3\tDigital with AC filter, 0-3.3V\n");
		printf("f12\tDigital with AC filter, 0-12V\n");
		printf("f24\tDigital with AC filter, 0-24V\n");
	}
	return false;
}
static void ibias_get(uint32_t v) { printf("%i", v); }
static bool ibias_set(char *i, uint32_t *v) { *v = strtoul(i, NULL, 10); return *v >= 0 && *v <= 1023; }
static void igain_get(uint32_t v) { printf("%i", v); }
static bool igain_set(char *i, uint32_t *v) { *v = strtoul(i, NULL, 10); return *v >= 1 && *v <= 13; }
static centry iconfig[] = {
	{"mode",   "Mode",           1, imode_get,    "type ? for list",                         imode_set  },
	{"bias",   "Bias",           2, ibias_get,    "bias, 0-1023 - see manual",               ibias_set  },
	{"gain",   "Gain",           1, igain_get,    "gain, 1-13 - see manual",                 igain_set  },
	{"atime",  "Comm timeout",   2, ms_get,       "0=disable/milliseconds, 1-32767",         ms_set     },
	{"amode",  "Alarm mode",     1, mmode_get,    "0=disabled/a=mAmps<level/b=,Amps>=level", mmode_set  },
	{"alevel", "Alarm level",    2, mlevel_get,   "mAmps, 0-32767",                          mlevel_set },
	{"adelay", "Alarm delay",    2, ms_get,       "milliseconds, 0-32767",                   ms_set     },
	{NULL}
};

// Display configuration to user
static void showconfig() {
	centry *c = cdesc;
	uint8_t *pc = configuration;
	uint32_t vc;
	uint8_t n;
	if(c) {
		while(c->id) {
			for(vc = n = 0; n < c->size; n++) {
				vc = (vc << 8) + *pc++;
			}
			printf("%s\t%s\t", c->id, c->desc);
			c->pfget(vc);
			printf("\n");
			c++;
		}
	}
}

// Read configuration from device
static void readconfig() {
	if(configuration_size) {
		spi_write(address, (char[]){0x21}, 1);
		spi_read(address, configuration, configuration_size);
		memcpy(configuration_dev, configuration, sizeof(configuration));
	}
}

// Get configuration parameter
static uint32_t getconfig(const char *id) {
	centry *c = cdesc;
	uint8_t *pc = configuration_dev;
	uint32_t vc;
	uint8_t n;
	while(c->id) {
		if(!strcmp(c->id, id)) {
			for(vc = n = 0; n < c->size; n++) {
				vc = (vc << 8) + *pc++;
			}
			return vc;
		}
		pc += c->size;
		c++;
	}
	printf("Internal error\n");
	return 0;
}


// Enumerate devices (will reset targeting)
int enumerate(int argc, char *argv[]) {
	uint8_t buf[32], id;
	uint8_t n;
	bool ok;
	spi_write(0x00, "\x80", 1);         // Set IDENT mode
	spi_setflags(0x01);					// Target first device
	
	for(n = 0; n < 127; n++) {
		spi_write(0x00, "\x85\x7F", 2); // Set temporary address 7F
		spi_write(0x7F, "\x83", 1);     // Issue READ ID
		spi_read(0x7F, &id, 1);         // Read response
		spi_write(0x7F, "\x00", 1);     // Issue READ DEVINFO
		ok = spi_read(0x7F, buf, 4);    // Read response
		if(!ok || buf[0] == 0x00 || buf[0] == 0xFF) break;
		printf("Device #%02X is ", n);
		switch(buf[0]) {
			case 'R': printf("RELAY  "); break;
			case 'M': printf("MOSFET "); break;
			case 'I': printf("INPUT  "); break;
			case 'C': printf("CURRENT"); break;
			case 'S': printf("SHIELD "); break;
			default:  printf("unknown (%02X)", buf[0]); break;
		}
		printf(" v%c.%c.%c, ID is ", buf[1], buf[2], buf[3]);
		if(id == 0x00) {
			printf("NOT SET\n");
		} else {
			printf("%02X\n", id);
		}
		spi_write(0x7F, "\x82", 1);     // Target next device
		spi_delay();
	}
	address = 0xFF;
	spi_write(0x00, "\x81", 1);         // Clear IDENT mode
	spi_setflags(0x00);
	printf("Found %i devices\n\n", n);
	return 0;
}

// Target device by location or ID
int target(int argc, char *argv[]) {
	uint8_t id;
	uint8_t n;
	if(argc != 2 || (argv[1][0] != '#' && (argv[1][0] < '1' && argv[1][0] > '9')))
		BAIL("To target a device by location:\n\ttarget #nn\nTo target a device by ID:\n\ttarget nn\n");
	bool ok;
	if(argv[1][0] == '#') {
		uint8_t index = strtoul(&argv[1][1], NULL, 16);
		spi_write(0x00, "\x80", 1);         // Set IDENT mode
		spi_setflags(0x01);					// Target first device
		for(n = 0; n <= index; n++) {
			if(n) {
				spi_write(0x7F, "\x82", 1); // Target next device
				spi_delay();
			}
			printf("\rTargetting... #%02X", n);
			spi_write(0x00, "\x85\x7F", 2); // Set temporary address 7F
			spi_write(0x7F, "\x83", 1);     // Issue READ ID
			spi_read(0x7F, &id, 1);         // Read response
		}
		printf("\n");
		address = 0x7F;
	} else {
		address = strtoul(argv[1], NULL, 16);
		spi_write(address, "\x83", 1);     // Issue READ ID
		spi_read(address, &id, 1);         // Read response
	}

	spi_write(address, "\x00", 1);         // Issue READ DEVINFO
	ok = spi_read(address, info, 4);       // Read response
	printf("Targetting ");
	cdesc = NULL;
	switch(ok ? info[0] : '?') {
		case 'R': printf("RELAY  "); cdesc = rconfig; break;
		case 'M': printf("MOSFET "); cdesc = mconfig; break;
		case 'I': printf("INPUT  "); cdesc = iconfig; break;
		case 'C': printf("CURRENT"); cdesc = cconfig; break;
		case 'S': printf("SHIELD "); cdesc = NULL; break; // TODO
		default:  printf("failed\n"); ok = false; break;
	}
	if(ok) {
		centry *c = cdesc;
		configuration_size = 0;
		if(c) {
			while(c->id) {
				configuration_size += c->size;
				c++;
			}
		}
		printf(" v%c.%c.%c, ID is ", info[1], info[2], info[3]);
		if(id == 0x00) {
			printf("NOT SET\n");
		} else {
			printf("%02X\n", id);
		}
		readconfig();
	} else {
		address = 0xFF;
		spi_write(0x00, "\x81", 1);         // Clear IDENT mode
		spi_setflags(0x00);
	}

	return 0;
}

// Change ID
int setid(int argc, char *argv[]) {
	if(address == 0xFF) BAIL("Target a device first\n");
	if(argc != 2)       BAIL("To program a new ID into targeted device:\n\tsetid nn\n");
	uint8_t newaddr = strtoul(argv[1], NULL, 16);
	spi_write(address, (char[]){0x84, newaddr}, 2);         // Clear IDENT mode
	address = newaddr;
	
	return 0;
}

// Manipulate configuration
int config(int argc, char *argv[]) {
	uint8_t buf[sizeof(configuration) + 1] = {0x20};
	if(address == 0xFF) BAIL("Target a device first\n");
	if(argc == 2) {
		if(!strcmp(argv[1], "read")) {
			printf("Reading configuration... ");
			readconfig();
			printf("\n");
			showconfig();
		} else if(!strcmp(argv[1], "write")) {
			printf("Writing %i bytes... ", configuration_size);
			memcpy(&buf[1], configuration,  configuration_size);
			spi_write(address, buf, configuration_size + 1);
			memcpy(configuration_dev, configuration, sizeof(configuration));
			printf("done\n");
		} else if(!strcmp(argv[1], "show")) {
			showconfig();
		} else {
			centry *c = cdesc;
			if(c) {
				uint8_t *pc = configuration;
				uint32_t vc;
				while(c->id) {
					if(!strcmp(c->id, argv[1])) {
						char new[32];
						while(1) {
							printf("%s (%s): ", c->desc, c->params);
							fgets(new, sizeof(new), stdin);
							if(new[strlen(new) - 1] == '\n') new[strlen(new) - 1] = 0;
							if(new[0] == '\n') break;
							if(c->pfset(new, &vc)) {
								uint8_t n;
								for(n = 1; n <= c->size; n++) {
									pc[c->size - n] = vc & 0xFF;
									vc >>= 8;
								}
								break;
							}
						}
						break;
					}
					pc += c->size;
					c++;
				}
				if(!c->id) {
					BAIL("Unknown parameter %s\n", argv[1]);
				}
			}
		}
	} else {
		BAIL("To read, write or show configuration:\n\tconfig read/write/show\nTo change configuration:\n\tconfig [parameter]\n");
	}
	return 0;
}

// Readout
int get(int argc, char *argv[]) {
	uint16_t value;
	uint8_t buf[2];
	if(address == 0xFF) BAIL("Target a device first\n");
	printf("Continous read, press Ctrl+C to stop\n");
	stop_request = false;
    signal(SIGINT, sigint);	
	while(!stop_request) {
		printf("\rReadout: ");
		if(info[0] == 'M') {
			spi_write(address, (uint8_t[]){0x01}, 1);
			spi_read(address, buf, 2);
			value = (buf[0] << 8) | buf[1];
			printf("%imA", value);
		} else if(info[0] == 'C') {
			spi_write(address, (uint8_t[]){0x01}, 1);
			spi_read(address, buf, 2);
			value = (buf[0] << 8) | buf[1];
			printf("%imA", value);
		} else if(info[0] == 'I') {
			uint8_t mode = getconfig("mode");
			if(mode & 0xF0) {
				spi_write(address, (uint8_t[]){0x02}, 1); // Read digital
				spi_read(address, buf, 1);
				if(buf[0] == 0x80) printf("LOW");
				else if(buf[1] == 0x81) printf("HIGH");
				else printf("unknown");
			} else {
				spi_write(address, (uint8_t[]){0x01}, 1); // Read analog
				spi_read(address, buf, 2);
				value = (buf[0] << 8) | buf[1];
				double bias = (double)getconfig("bias") / 310.0;
                double gain = 1 << (getconfig("gain") - 1);
				double calc = value;
				calc = ((calc / 310.0)  + bias) / gain;
				if(mode == 0x01) calc *= 6.0;
				else if(mode == 0x02) calc *= 11.0;
				else if(mode == 0x03) calc = -(calc - 3.3) / calc * 10000.0;
				printf("%1.3f", calc);
				if(mode == 0x03) printf("ohm"); else printf("V");
				
			}
		} else {
			BAIL("Targeted device has no input\n");
		}
		printf("        ");
	}
	printf("\n");
	return 0;
}

// Output
int set(int argc, char *argv[]) {
	if(address == 0xFF) BAIL("Target a device first\n");
	uint8_t period;
	if(info[0] == 'R' || info[0] == 'M') {
			uint8_t offset = info[0] == 'M' ? 2 : 0;
			if(argc == 2) {
				if(!strcmp(argv[1], "on")) {
					spi_write(address, (uint8_t[]){0x01 + offset, 0x01}, 2);
					return 0;
				} else if(!strcmp(argv[1], "off")) {
					spi_write(address, (uint8_t[]){0x01 + offset, 0x00}, 2);
					return 0;
				} else {
					if(argv[1][strlen(argv[1]) - 1] == '%') {
						period = (strtoul(argv[1], NULL, 10) * 255) / 100;
						spi_write(address, (uint8_t[]){0x03 + offset, period}, 2);
						spi_write(address, (uint8_t[]){0x01 + offset, 0x80}, 2);
						return 0;
					}
				}
			}
			BAIL("To set on/off:\n\tset on/off\nTo set PWM:\n\tset n%%\n");
	}
	BAIL("Targeted device has no output\n");
}
