#ifndef COMMANDS_H
#define COMMANDS_H
#include "kbdparser.h"
#include <Arduino.h>
#include <EEPROM.h>
#include <avr/wdt.h>

static const char hostname_prefix[] = "\e[35;1m";           //! hostname prefix
static const char hostname_default[] PROGMEM = "arduino";   //! default hostname hardcoded in flash
static const char hostname_sufix[] = "\e[34m>\e[0m ";       //! hostname sufix
#define HOSTNAME_MAX_SIZE 24                                //! hostname max size to store in EEPROM
char hostname[HOSTNAME_MAX_SIZE];                           //! current hostname
bool com_run = false;                                       //! command is running
bool cancel = false;                                        //! interrupt and cancel current command

//##################################//
//##         EEPROM TABLE         ##//
//##------------------------------##//
//##     0     ##    magic flag   ##//
//##   1 - 24  ##    hostname     ##//
//##     25    ##    heartbeat    ##//
//##  26 - 27  ##       delay     ##//
//##################################//
#define EEPROM_MAGIC_FLAG 33
int led_heart = 1;                                          //! led heartbeat on waiting, 0 to off
unsigned int com_delay = 1000;                                       //! command delay in command running loop

struct ComArgs {
	String arg;
};
typedef void (*ComSlot)(ComArgs);
static const char pin_error[] PROGMEM  = ": error, invalid pin number";
static const char delay_error[] PROGMEM  = "delay: error, invalid delay";
static const char pwm_error[] PROGMEM  = "pwm: error, invalid value, must be from 0 to 255";
static const char hex_error[] PROGMEM  = "sendhex: error, invalid argument, must be from 00 to FF per byte wihout any separator";
#ifdef SERIAL_PORT_USBVIRTUAL
static const char serial_error[] PROGMEM  = "serial: error, invalid speed";
#else
static const char serial_error[] PROGMEM  = "Not implemented in different from leonardo/micro boards";
#endif

ComSlot bg_com = 0;       //! backgroung command
ComArgs bg_arg;           //! argumenst for background command
bool bg_printing = false; //! set true to update line while background command printing

enum ComCfg {
	InvalidCfg = 0,
	NoArgs = 1,
	HasArgs = 2,
	AnyArgs = 3,
	IsBackground = 4
};
typedef PIFlags<ComCfg> ComFlag;

struct ArduinoCommand {
	String comm;
	ComSlot slot;
	ComFlag flag;
};

void help(ComArgs);
void echo(ComArgs a);
void cdelay(ComArgs a);
void blink(ComArgs a);
void high(ComArgs a);
void low(ComArgs a);
void watch(ComArgs a);
void heart(ComArgs);
void start_ser(ComArgs a);
void send(ComArgs a);
void sendhex(ComArgs a);
void recv(ComArgs);
void recvhex(ComArgs);
void adc(ComArgs a);
void tty(ComArgs a);
void nobg(ComArgs);
void bgwatch(ComArgs a);
void name(ComArgs a);
void save(ComArgs);
void load(ComArgs);
void bgrecv(ComArgs);
void bgrecvhex(ComArgs);
void exit(ComArgs);
void pwm(ComArgs a1a2);

#define DESC_CANCEL "; press 'Esc' or 'Q' to finish"
static const char desc__adc[] PROGMEM = "Get ADC value on pin 'N' every 'DELAY'; usage: 'adc N'" DESC_CANCEL;
static const char desc__bgrecv[] PROGMEM = "'recv' in backgroung";
static const char desc__bgrecvhex[] PROGMEM = "'recvhex' in backgroung";
static const char desc__bgwatch[] PROGMEM = "'watch' in background if pin change; usage: 'bgwatch N'; to cancel run 'nobg'";
static const char desc__blink[] PROGMEM = "Blink pin 'N', invert pin value on 'DELAY'; usage: 'blink N'";
static const char desc__delay[] PROGMEM = "Set 'DELAY' to 'N' msec; usage: 'delay N'";
static const char desc__echo[] PROGMEM = "Just echo for testing";
static const char desc__exit[] PROGMEM = "Restart session";
static const char desc__heart[] PROGMEM = "Switch on/off heartbeat LED on waiting";
static const char desc__help[] PROGMEM = "Print this help";
static const char desc__high[] PROGMEM = "Set pin 'N' to HIGH; usage: 'high N'";
static const char desc__load[] PROGMEM = "Load settings from EEPROM";
static const char desc__low[] PROGMEM = "Set pin 'N' to LOW; usage: 'low N'";
static const char desc__name[] PROGMEM = "Change Arduino name";
static const char desc__nobg[] PROGMEM = "Cancel all backgroung tasks";
static const char desc__pwm[] PROGMEM = "Start PWM with value 'V' (from 0(0%) to 255(100%)) on pin 'N'; usage 'pwm N V'";
static const char desc__recv[] PROGMEM = "Start receiving on UART; usage: 'recv'" DESC_CANCEL;
static const char desc__recvhex[] PROGMEM = "'recv' in HEX mode" DESC_CANCEL;
static const char desc__save[] PROGMEM = "Save settings to EEPROM";
static const char desc__send[] PROGMEM = "Send string 'CMD' to UART; usage: 'send CMD'";
static const char desc__sendhex[] PROGMEM = "'send' in hex mode; usage example: send 313D0D0A";
static const char desc__serial[] PROGMEM = "Init UART on speed 'S'; usage: 'serial S'";
static const char desc__tty[] PROGMEM = "Enter in tty mode on UART, like transparent gateway; to exit press 'Esc' then '~' then '.'";
static const char desc__watch[] PROGMEM = "Watch pin 'N' state and out value every 'DELAY'; usage: 'watch N'" DESC_CANCEL;
static const char desc__[] PROGMEM = "";

static const char * const com_descs[] PROGMEM = {
	desc__adc,
	desc__bgrecv,
	desc__bgrecvhex,
	desc__bgwatch,
	desc__blink,
	desc__delay,
	desc__echo,
	desc__exit,
	desc__heart,
	desc__help,
	desc__high,
	desc__load,
	desc__low,
	desc__name,
	desc__nobg,
	desc__pwm,
	desc__recv,
	desc__recvhex,
	desc__save,
	desc__send,
	desc__sendhex,
	desc__serial,
	desc__tty,
	desc__watch,
	/// end
	desc__
};
const ArduinoCommand coms[] = {
	{String("adc"), adc ,HasArgs},
	{String("bgrecv"), bgrecv ,NoArgs | IsBackground},
	{String("bgrecvhex"), bgrecvhex ,NoArgs | IsBackground},
	{String("bgwatch"), bgwatch ,HasArgs | IsBackground},
	{String("blink"), blink ,HasArgs},
	{String("delay"), cdelay ,AnyArgs},
	{String("echo"), echo ,HasArgs},
	{String("exit"), exit ,NoArgs},
	{String("heart"), heart ,NoArgs},
	{String("help"), help ,NoArgs},
	{String("high"), high ,HasArgs},
	{String("load"), load ,NoArgs},
	{String("low"), low ,HasArgs},
	{String("name"), name ,AnyArgs},
	{String("nobg"), nobg ,NoArgs},
	{String("pwm"), pwm ,HasArgs},
	{String("recv"), recv ,NoArgs},
	{String("recvhex"), recvhex ,NoArgs},
	{String("save"), save ,NoArgs},
	{String("send"), send ,HasArgs},
	{String("sendhex"), sendhex ,HasArgs},
	{String("serial"), start_ser ,HasArgs},
	{String("tty"), tty ,NoArgs},
	{String("watch"), watch ,HasArgs},
	/// end
	{String(), 0, InvalidCfg}
};

bool checkPin(int pin, int from, int to, String cm);

void blink(ComArgs a) {
	int n;
	int ep = 13;
	int sp = 0;
	if (a.arg[0] == 'A' && a.arg.length() > 1) {
		a.arg = a.arg.substring(1, a.arg.length());
		ep = 5;
		sp = 18;
	}
	n = a.arg.toInt();
	if (n == 0 && !(a.arg.length() == 1 && a.arg[0] == '0')) n = -1;
	if (checkPin(n, 0, ep, "blink")) {
		pinMode(n+sp, OUTPUT);
		byte cv = digitalRead(n+sp);
		digitalWrite(n+sp, cv == 0 ? HIGH : LOW);
		for (int i=0; i<com_delay; i++) {
			if (Serial.available() > 0) keyHandler();
			if (cancel) break;
			delay(1);
		}
		digitalWrite(n+sp, cv == 0 ? LOW : HIGH);
	}
}

void echo(ComArgs a) {
	Serial.println(a.arg);
}

void help(ComArgs) {
	char buffer_desc[128];
	for (int i=0; ; i++) {
		if (coms[i].slot == 0) break;
		Serial.print("\e[1m");
		Serial.print(coms[i].comm);
		Serial.print("\e[0m");
		Serial.print(" - ");
		strcpy_P(buffer_desc, (char*)pgm_read_word(&(com_descs[i])));
		Serial.println(buffer_desc);
	}
}

void cdelay(ComArgs a) {
	if (a.arg.length() == 0) {
		Serial.print("current delay: ");
		Serial.println(com_delay);
		return;
	}
	int n = a.arg.toInt();
	if (n > 0 && n < 300000) {
		Serial.print("old delay: ");
		Serial.println(com_delay);
		com_delay = n;
		Serial.print("new delay: ");
		Serial.println(com_delay);
	} else {
		int len = strlen_P(delay_error);
		for (int k=0; k<len; k++)
			Serial.write(pgm_read_byte_near(delay_error + k));
		Serial.println();
	}
}

void high(ComArgs a) {
	int n;
	int ep = 13;
	int sp = 0;
	if (a.arg[0] == 'A' && a.arg.length() > 1) {
		a.arg = a.arg.substring(1, a.arg.length());
		ep = 5;
		sp = 18;
	}
	n = a.arg.toInt();
	if (n == 0 && !(a.arg.length() == 1 && a.arg[0] == '0')) n = -1;
	if (checkPin(n, 0, ep, "high")) {
		pinMode(n+sp, OUTPUT);
		digitalWrite(n+sp, HIGH);
	}
}

void low(ComArgs a) {
	int n;
	int ep = 13;
	int sp = 0;
	if (a.arg[0] == 'A' && a.arg.length() > 1) {
		a.arg = a.arg.substring(1, a.arg.length());
		ep = 5;
		sp = 18;
	}
	n = a.arg.toInt();
	if (n == 0 && !(a.arg.length() == 1 && a.arg[0] == '0')) n = -1;
	if (checkPin(n, 0, ep, "high")) {
		pinMode(n+sp, OUTPUT);
		digitalWrite(n+sp, LOW);
	}
}

void watch(ComArgs a) {
	int n;
	int ep = 13;
	int sp = 0;
	if (a.arg[0] == 'A' && a.arg.length() > 1) {
		a.arg = a.arg.substring(1, a.arg.length());
		ep = 5;
		sp = 18;
	}
	n = a.arg.toInt();
	if (n == 0 && !(a.arg.length() == 1 && a.arg[0] == '0')) n = -1;
	if (!checkPin(n, 0, ep, "watch")) return;
	com_run = true;
	pinMode(n+sp, INPUT_PULLUP);
	Serial.print("Pin ");
	Serial.print(n+sp);
	Serial.print(":  ");
	hideCursor();
	while(!cancel) {
		Serial.print("\e[D");
		Serial.print(digitalRead(n+sp));
		for (int i=0; i<com_delay; i++) {
			if (Serial.available() > 0) keyHandler();
			if (cancel) break;
			delay(1);
		}
	}
	showCursor();
	Serial.println();
	com_run = false;
}

void heart(ComArgs) {
	digitalWrite(LED_BUILTIN, LOW);
	if (led_heart > 0) led_heart = 0;
	else led_heart = 1;
}

void start_ser(ComArgs a) {
#ifdef SERIAL_PORT_USBVIRTUAL
	long n = a.arg.toInt();
	if (n < 8 || n > 4000000) {
		int len = strlen_P(serial_error);
		for (int k=0; k<len; k++)
			Serial.write(pgm_read_byte_near(serial_error + k));
		Serial.println();
		return;
	}
	Serial1.begin(n);
#else 
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
#endif
}

void send(ComArgs a) {
#ifdef SERIAL_PORT_USBVIRTUAL
	Serial1.println(a.arg);
#else 
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
#endif
}

void sendhex(ComArgs a) {
#ifdef SERIAL_PORT_USBVIRTUAL
	if (a.arg.length() % 2 != 0) {
		int len = strlen_P(hex_error);
		for (int k=0; k<len; k++)
			Serial.write(pgm_read_byte_near(hex_error + k));
		Serial.println();
		return;
	}
	byte ar[a.arg.length() / 2];
	for (int i=0; i<a.arg.length()/2; i++) {
		char h0 = toUpperCase(a.arg[i*2]);
		char h1 = toUpperCase(a.arg[i*2+1]);
		if (!isHexadecimalDigit(h0) || !isHexadecimalDigit(h1)) {
			int len = strlen_P(hex_error);
			for (int k=0; k<len; k++)
				Serial.write(pgm_read_byte_near(hex_error + k));
			Serial.println();
			return;
		}
		ar[i] = ((h0 - (h0 > '9' ? 'A'- 10 : '0')) << 4) | (h1 - (h1 > '9' ? 'A' - 10 : '0'));
	}
	for (int i=0; i<a.arg.length()/2; i++)
		Serial1.write(ar[i]);
#else
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
#endif
}

void recv(ComArgs) {
#ifdef SERIAL_PORT_USBVIRTUAL
	com_run = true;
	while(!cancel) {
		if (Serial1.available() > 0) Serial.write(Serial1.read());
		if (Serial.available() > 0) keyHandler();
	}
	Serial.println();
	com_run = false;
#else 
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
#endif
}

void recvhex(ComArgs) {
#ifdef SERIAL_PORT_USBVIRTUAL
	com_run = true;
	while(!cancel) {
		if (Serial1.available() > 0) {
			byte v = Serial1.read();
			if (v < 16) Serial.print("0");
			Serial.print(v, HEX);
			Serial.print(" ");
		}
		if (Serial.available() > 0) keyHandler();
	}
	Serial.println();
	com_run = false;
#else
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
#endif
}

void adc(ComArgs a) {
	if (a.arg[0] == 'A' && a.arg.length() > 1)
		a.arg = a.arg.substring(1, a.arg.length());
	int n = a.arg.toInt();
	if (n == 0 && !(a.arg.length() == 1 && a.arg[0] == '0')) n = -1;
	if (!checkPin(n, 0, 5, "adc")) return;
	com_run = true;
	Serial.print("ADC ");
	Serial.print(n);
	Serial.print(":     ");
	hideCursor();
	while(!cancel) {
		Serial.print("\e[4D\e[K");
		String s = String(analogRead(n));
		while (s.length() < 4) s = " " + s;
		Serial.print(s);
		for (int i=0; i<com_delay; i++) {
			if (Serial.available() > 0) keyHandler();
			if (cancel) break;
			delay(1);
		}
	}
	showCursor();
	Serial.println();
	com_run = false;
}

void tty(ComArgs a) {
#ifdef SERIAL_PORT_USBVIRTUAL
	char ek[] = "\e~.";
	int kn = 0;
	bool exit = false;
	while (!exit) {
		while (Serial.available()) {
			char v = Serial.read();
			if (v == ek[kn]) {
				kn++;
				if (kn == sizeof(ek)-1) exit = true;
			} else kn = 0;
			Serial1.write(v);
		}
		while (Serial1.available())
			Serial.write(Serial1.read());
	}
	Serial.println();
#else 
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
#endif
}

bool watchbg_first;
byte watchbg_lastval;
void bgwatch(ComArgs a) {
	int n;
	int ep = 13;
	int sp = 0;
	if (a.arg[0] == 'A' && a.arg.length() > 1) {
		a.arg = a.arg.substring(1, a.arg.length());
		ep = 5;
		sp = 18;
	}
	n = a.arg.toInt();
	if (n == 0 && !(a.arg.length() == 1 && a.arg[0] == '0')) n = -1;
	if (!checkPin(n, 0, ep, "watchbg")) {
		nobg(a);
		return;
	}
	byte v = digitalRead(n+sp);
	if (watchbg_first) {
		watchbg_first = false;
		pinMode(n, INPUT_PULLUP);
		Serial.println();
		Serial.print("Pin ");
		Serial.print(n+sp);
		Serial.print(":  ");
		Serial.print(v);
		watchbg_lastval = v;
		bg_printing = true;
	} else {
		if (v != watchbg_lastval) {
			watchbg_lastval = v;
			Serial.println();
			Serial.print("Pin ");
			Serial.print(n+sp);
			Serial.print(":  ");
			Serial.print(v);
			bg_printing = true;
		}
	}
}

void nobg(ComArgs) {
	bg_com = 0;
	watchbg_first = true;
	bg_printing = true;
}

void name(ComArgs a) {
	if (a.arg.length() == 0) {
		Serial.print("current name: ");
		Serial.println(hostname);
		return;
	}
	a.arg.toCharArray(hostname, HOSTNAME_MAX_SIZE);
}

void save(ComArgs) {
	EEPROM.write(0, EEPROM_MAGIC_FLAG);
	for (int k=0; k<HOSTNAME_MAX_SIZE; k++)
		EEPROM.write(k+1, hostname[k]);
	if (led_heart == 0) EEPROM.write(25, 0);
	else EEPROM.write(25, 1);
	byte h = com_delay / 256;
	byte l = com_delay % 256;
	EEPROM.write(26, h);
	EEPROM.write(27, l);
}

void load(ComArgs) {
	byte e0 = EEPROM.read(0);
	byte e1 = EEPROM.read(1);
	if (e0 == EEPROM_MAGIC_FLAG && e1 > 0x20 && e1 < 0x7f) {
		for (int k=0; k<HOSTNAME_MAX_SIZE; k++)
			hostname[k] = EEPROM.read(k+1);
		led_heart = (EEPROM.read(25) == 0 ? 0 : 1);
		e0 = EEPROM.read(26);
		e1 = EEPROM.read(27);
		com_delay = int(e0)*256 + e1;
	} else {
		int len = strlen_P(hostname_default);
		for (int k=0; k<len; k++)
			hostname[k] = pgm_read_byte_near(hostname_default + k);
		hostname[len] = 0;
		led_heart = 1;
		com_delay = 1000;
	}
}

void bgrecv(ComArgs) {
#ifdef SERIAL_PORT_USBVIRTUAL
	if (Serial1.available() > 0) {
		Serial.println();
		Serial.print("Reading : ");
		while (Serial1.available() > 0) Serial.write(Serial1.read());
		bg_printing = true;
	} else bg_printing = false;
#else 
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
	nobg(ComArgs());
#endif
}

void bgrecvhex(ComArgs) {
#ifdef SERIAL_PORT_USBVIRTUAL
	if (Serial1.available() > 0) {
		Serial.println();
		Serial.print("Reading : ");
		while (Serial1.available() > 0) {
			byte v = Serial1.read();
			if (v < 16) Serial.print("0");
			Serial.print(v, HEX);
			Serial.print(" ");
		}
		bg_printing = true;
	} else bg_printing = false;
#else
	int len = strlen_P(serial_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(serial_error + k));
	Serial.println();
	nobg(ComArgs());
#endif
}

void exit(ComArgs) {
#ifdef SERIAL_PORT_USBVIRTUAL
	  wdt_disable();
	  wdt_enable(WDTO_4S);
#endif
	  com_run = true;
	  Serial.print("Reboot after: 4... ");
	  for (int i=0; i<1000; i++) {
		  if (Serial.available() > 0) keyHandler();
		  if (cancel) {
#ifdef SERIAL_PORT_USBVIRTUAL
			  wdt_disable();
#endif
			  Serial.println();
			  com_run = false;
			  return;
		  }
		  delay(1);
	  }
	  Serial.print("3... ");
	  for (int i=0; i<1000; i++) {
		  if (Serial.available() > 0) keyHandler();
		  if (cancel) {
#ifdef SERIAL_PORT_USBVIRTUAL
			  wdt_disable();
#endif
			  Serial.println();
			  com_run = false;
			  return;
		  }
		  delay(1);
	  }
	  Serial.print("2... ");
	  for (int i=0; i<1000; i++) {
		  if (Serial.available() > 0) keyHandler();
		  if (cancel) {
#ifdef SERIAL_PORT_USBVIRTUAL
			  wdt_disable();
#endif
			  Serial.println();
			  com_run = false;
			  return;
		  }
		  delay(1);
	  }
	  Serial.print("1... bye bye!");
#ifdef SERIAL_PORT_USBVIRTUAL
	  while (1) {;}
#else
	  com_run = false;
	  delay(100);
	  asm volatile ("jmp 0x0000");
#endif
}

void pwm(ComArgs a1a2) {
	String a1 = a1a2.arg.substring(0, a1a2.arg.indexOf(' '));
	String a2 = a1a2.arg.substring(a1.length());
	a1.trim(); a2.trim();
	int n = a1.toInt();
	int v = a2.toInt();
	if (n == 0 && !(a1.length() == 1 && a1[0] == '0')) n = -1;
	if (v == 0 && !(a2.length() == 1 && a2[0] == '0')) v = -1;
	if (v < 0 || v > 255) {
		int len = strlen_P(pwm_error);
		for (int k=0; k<len; k++)
			Serial.write(pgm_read_byte_near(pwm_error + k));
		Serial.println();
		return;
	}
	if (checkPin(n, 0, 13, "pwm")) {
		pinMode(n, OUTPUT);
		analogWrite(n, v);
	}
}

//==================================================

bool checkPin(int pin, int from, int to, String cm) {
	if (pin >= from && pin <=to) return true;
	Serial.print(cm);
	int len = strlen_P(pin_error);
	for (int k=0; k<len; k++)
		Serial.write(pgm_read_byte_near(pin_error + k));
	Serial.println();
	return false;
}

#endif // COMMANDS_H
