/*
 * Serial LED Controller, IM Dendrite module (for ESP8266)
 * Created for Interplaymediumâ„¢ project (https://interplaymedium.org)
 * Copyright © 2016 Dmitry Shalnov [interplaymedium.org]
 * Licensed under the Apache License, Version 2.0
*/

#define SERIAL			0

#include "../../info"
#include "version.c"

#include <NTPClient.h>

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <EEPROM.h>

#define	LED_STRIPE		WS2811	// WS2812B // APA106 // type of LEDs		Supported LED chipsets: http://fastled.io/docs/3.1/md__r_e_a_d_m_e.html

// -------------------- FastLED settings  --------------

#define FASTLED_ESP8266_RAW_PIN_ORDER
#define FASTLED_INTERNAL				// to remove #pragma diagnostics messages

#include <FastLED.h>
					// Serial	// Switch 4 channel
#define DATA_PIN		3 	// 3		// 2			// DIn controller pin (GPIO2) TODO: switch to GPIO0 + pull down (to avoid blink on reset)
#define ENABLE_GATE_PIN		2	// 2		// 3			// common ping for SN74HC08 AND gate (to prevent blink on power on + pull up the level)
#define MAX_NUM_LEDS		256						// maximum number of leds in stripe
#define	DEF_NUM_LEDS		24						// default number of leds in stripe 
#define DEF_MAIN_DELAY		1						// default FX delay (in loop)

#define FULL_OUTPUT		2			// default quiet level

CRGB 		LEDStripe_RGB[ MAX_NUM_LEDS ];		// This is an array of leds.  One item for each led in your stripe.
CHSV		color_HSV;
String 		param;

uint8_t		paramCnt = 0;
uint8_t		JSONon = 0;

uint8_t		numberOfLEDs 	= DEF_NUM_LEDS;

uint16_t 	mainDelay = 0, mainDelayNew = 0;

unsigned char	FX_PhaseXDelay = 0, FX_PhaseYDelay = 0, FX_PhaseZDelay = 0;

// uint8_t	EXP[ 256 ];	// lookup table 

unsigned long 	timeStamp = 0;
uint8_t		changeColor = 0, quiet = FULL_OUTPUT, commandReboot = false;

// -------------------- EEPROM addresing ---------------

#define EEPROM_HOST		0

#define EEPROM_HOST_MAX_LEN	57	// Host name max length, can't be > 57 (could be 64, buth there is a bug in avahi https://github.com/lathiat/avahi/issues/273)
#define EEPROM_OTHER_MAX_LEN	13	// total size of other EEPROM parameters (summ of all EEPROM values below, included EEPROM_FLAG)


#define EEPROM_FLAG		EEPROM_HOST_MAX_LEN + 1	// EEPROM has been changed (avoid overlap: EEPROM_HOST_MAX_LEN)

#define EEPROM_numberOfLEDs	EEPROM_HOST_MAX_LEN + 2

#define EEPROM_HgradBegin	EEPROM_HOST_MAX_LEN + 3 //  (the order of defines matter)
#define EEPROM_SgradBegin	EEPROM_HOST_MAX_LEN + 4
#define EEPROM_VgradBegin	EEPROM_HOST_MAX_LEN + 5

#define EEPROM_HgradEnd		EEPROM_HOST_MAX_LEN + 6
#define EEPROM_SgradEnd		EEPROM_HOST_MAX_LEN + 7
#define EEPROM_VgradEnd		EEPROM_HOST_MAX_LEN + 8

#define EEPROM_PERIOD		EEPROM_HOST_MAX_LEN + 9

#define EEPROM_FX_PhaseX	EEPROM_HOST_MAX_LEN + 10
#define EEPROM_FX_PhaseY	EEPROM_HOST_MAX_LEN + 11
#define EEPROM_FX_PhaseZ	EEPROM_HOST_MAX_LEN + 12

#define EEPROM_mainDelayNew	EEPROM_HOST_MAX_LEN + 13


// ------------------- array of parameters ------------- (the order of defines matter)

#define NUMBER_OF_PARAM		10

#define H_GRAD_BEG		0 
#define S_GRAD_BEG		1
#define V_GRAD_BEG		2
#define H_GRAD_END		3
#define S_GRAD_END		4
#define V_GRAD_END		5
#define PERIOD			6
#define PHASE_X_INC		7
#define PHASE_Y_INC		8
#define PHASE_Z_INC		9

float	FX_Arr[ NUMBER_OF_PARAM ] 	= {};
float	FX_ArrNew[ NUMBER_OF_PARAM ] 	= {};
float	FX_Coeff[ NUMBER_OF_PARAM ]	= {};
float	Phase[ 3 ]			= {};

float	FX_CoeffMax 			= 1.0;

#define PHASE_X			0
#define PHASE_Y			1
#define PHASE_Z			2


// ------------------- server settings -----------------

#define HOST_DEFAULT		"im_serial_"
#define NTP_SERVER		"europe.pool.ntp.org"

char host[ EEPROM_HOST_MAX_LEN ];

// const char* ssid	= "axod_ap";
// const char* password	= "superstrongpassword"

const char* ssid	= IM_WIFI_SSID;
const char* password	= IM_WIFI_PASS;

ESP8266WebServer	server(80);

String 			hostDefURI;
String 			HTTPOut;
String			HTTPErr;

WiFiUDP 		ntpUDP;
NTPClient 		timeClient(ntpUDP, NTP_SERVER , 0, 60000);

// ---------------- EEPROM String r/w ------------------

void EEPROMStrRead( uint8_t addr, char * str ){
	for (uint8_t a = addr; a <  EEPROM_HOST_MAX_LEN; a++ ){
		str[ a ] = EEPROM.read( a );
		if (str[ a ] == 0) break;
	}
}

void EEPROMStrWrite( uint8_t addr, char * str ){
	uint8_t a = 0;
	for (a = addr; a <  EEPROM_HOST_MAX_LEN; a++ ){
		EEPROM.write( a, str[ a ] );
		if (str[ a ] == 0) break;
	}
	EEPROM.write( a, 0 );
}

// ---------------- misc -------------------------------

int str2HEX(const String str) {	
	return strtol( str.c_str(), 0, 16 );
}

String printNum(unsigned long num, int base, char sign) {

	char outbuf[12];

	int i = 12;
	int j = 0;
	int n;

	do {
		outbuf[i] = "0123456789ABCDEF"[num % base];
		i--;
		num = num/base;
	} while ( num > 0 );

	if ( sign != ' ' ){
		outbuf[0] = sign;
		++j;
	}

	while ( ++i < 13 ){
		outbuf[j++] = outbuf[i];
	}

	if ( j == 1 ) { outbuf[ 1 ] = outbuf[ 0 ]; outbuf[ 0 ] = '0'; ++j; }

	outbuf[j] = 0;

	return outbuf;

}

uint8_t is_number( const char *s ){

	while (*s) { 
		Serial.printf("s: %c\n", *s);
		if ( *s++ <48 || *s >57 ) return 0;
	}
	return true;
}

// ----------- explode for selected substring -----------

String expld( String str, unsigned int numb, char delimiter ){
	
	unsigned int cnt = 0, a = 0, p2 = 0, p1 = 0, lng = 0;

	lng = str.length();

	for ( a = 0; a < lng; a++ ){
		if ( str.charAt( a ) == delimiter ) { 
			p2 = p1;
			p1 = a;
			if ( cnt == numb ) break;
			cnt ++;
		}
	}

	if ( cnt < numb ) return "";

	if ( a == lng ) { 
		p2 = p1;
		p1 = lng;
	}

	if ( numb > 0 ) p2 ++;

	return str.substring(p2, p1);
	
}

// ---------------- FX ---------------------------------

void FX_Gradient_RT( uint8_t HgradBegin, uint8_t SgradBegin, uint8_t VgradBegin, uint8_t HgradEnd, uint8_t SgradEnd, uint8_t VgradEnd, uint8_t period, uint8_t phaseX, uint8_t phaseY, uint8_t phaseZ ) {

	unsigned int	i = 0;
	CHSV		color;

	#define 	Y_PHASE_SHIFT	128	// 128 // to avoid solid fill on phase = 0

	// NOTE! phaseZ is for color component only since it is cyclic

	for (i = 0; i < numberOfLEDs; i++) {

//		newHueColor = pow( (float)sin( (float)i*PI / (period * 2) ) ,2 ) * (gradEnd - gradBegin) + gradBegin;		// MEMO! floating point

 		if ( HgradEnd != HgradBegin ) color.h = ( ((int)cos8( (i + phaseX) * 128 / period ) - 128) * ((int)sin8( (phaseY + Y_PHASE_SHIFT) * 128 / period ) - 128) / 128 + 128 ) / ( 256 / ( HgradEnd - HgradBegin ) ) + HgradBegin; else color.h = HgradEnd;	
		if ( SgradEnd != SgradBegin ) color.s = ( ((int)cos8( (i + phaseX) * 128 / period ) - 128) * ((int)sin8( (phaseY + Y_PHASE_SHIFT) * 128 / period ) - 128) / 128 + 128 ) / ( 256 / ( SgradEnd - SgradBegin ) ) + SgradBegin; else color.s = SgradEnd;
		if ( VgradEnd != VgradBegin ) color.v = ( ((int)cos8( (i + phaseX) * 128 / period ) - 128) * ((int)sin8( (phaseY + Y_PHASE_SHIFT) * 128 / period ) - 128) / 128 + 128 ) / ( 256 / ( VgradEnd - VgradBegin ) ) + VgradBegin; else color.v = VgradEnd;

//		LEDStripe_RGB[ i ] = CHSV( color.h + phaseZ , color.s, color.v );

		color.h += phaseZ;
		hsv2rgb_spectrum(color, LEDStripe_RGB[ i ]);

	}
}

// -----------------------------------------------------
// --------------- Init --------------------------------
// -----------------------------------------------------

void setup(void) {

	HTTPOut.reserve(800); // lenght of Help message generally

	hostDefURI.reserve( EEPROM_HOST_MAX_LEN );
	hostDefURI = HOST_DEFAULT + WiFi.macAddress().substring(12, 14) + WiFi.macAddress().substring(15, 17); // 00:00:00:00:00:00

	// calculate lookup array 
 
//	for (unsigned int a = 1; a <= 255; a++) EXP[ a ] = round( pow( 255, (double)a / (255) ) ); // calculate exponential lookup array (for PWM etc..)

	// reset FX_Coeff (maybe not required)

	for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) FX_Coeff[ a ] = 1.0;

#if SERIAL == 1

	// Serial init

	Serial.begin(115200);
	Serial.println();
	Serial.println("Booting Sketch...");
#endif

	// read parameters from EEPROM (if it has been saved early)

	EEPROM.begin( EEPROM_HOST_MAX_LEN + EEPROM_OTHER_MAX_LEN );
	
	if ( EEPROM.read( EEPROM_FLAG ) == 1 ){

		EEPROMStrRead( EEPROM_HOST, host ); 

		numberOfLEDs 	= EEPROM.read( EEPROM_numberOfLEDs );
		mainDelayNew 	= EEPROM.read( EEPROM_mainDelayNew );

		for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) FX_ArrNew[ a ]  = EEPROM.read( EEPROM_HgradBegin + a );

		changeColor = true;

	} else {

		FX_ArrNew[ PERIOD ] = DEF_NUM_LEDS;
		strcpy( host, (char*)hostDefURI.c_str() ); // default URI and host name
	}

	// Fast LED RGB init

	pinMode(ENABLE_GATE_PIN, OUTPUT);
	digitalWrite( ENABLE_GATE_PIN, HIGH );

	delay(1000);  // 2000 // sanity check delay - allows reprogramming if accidently blowing power w/leds

	FastLED.addLeds<LED_STRIPE, DATA_PIN, GRB>( LEDStripe_RGB, numberOfLEDs );
	fill_solid( LEDStripe_RGB, numberOfLEDs, CRGB(0, 0, 0) );
	FastLED.show();

	// WiFi init 

	if ( host == "" ) strcpy( host, (char*)hostDefURI.c_str() );  // if it was changed without parameters by mistake

	WiFi.hostname( host );

//	WiFi.softAP(APssid, APpassword);
//	WiFi.mode(WIFI_AP);
//	WiFi.mode(WIFI_AP_STA); 

	WiFi.mode(WIFI_STA);
	WiFi.setAutoReconnect( true );
	WiFi.begin(ssid, password);

	// main events handlers

	if (WiFi.waitForConnectResult() == WL_CONNECTED) {

		MDNS.begin( host );

		// get time 

//		timeClient.setTimeOffset(0);
		timeClient.begin();
		timeClient.update();

		// start server 

		server.begin();
		MDNS.addService("http", "tcp", 80);

#if SERIAL == 1
		Serial.printf("Ready. Try \"curl %s/help\" to see the list of commands and parameters.\n", host);
#endif

		server.onNotFound( []() {

			server.sendHeader("Connection", "close");

			param 		= "";
//			HTTPOut		= "";

			// no command given (see handlers of commands below)

			if ( server.uri() != "/" )HTTPErr += "Command '" + server.uri() + "' not exist.\n";


			for ( uint8_t i=0; i < server.args(); i++ ){

				paramCnt = 0;

				// ------------- specific parameters -----------------

				// change number of leds in stripe 
	
				if ( server.argName(i) == "number" ) {

					// TODO make it in MAIN with dimming

					fill_solid( LEDStripe_RGB, numberOfLEDs, CRGB(0, 0, 0) );
					FastLED.show();

					numberOfLEDs 	= server.arg("number").toInt();
					FastLED.addLeds<LED_STRIPE, DATA_PIN, GRB>( LEDStripe_RGB, numberOfLEDs );

					paramCnt++;
				}

				// color gradient (hex)

				if ( server.argName(i) == "gradient" ) {

					param = server.arg("gradient");

					for ( uint8_t a = 0; a < 6; a++ ) FX_ArrNew[ H_GRAD_BEG + a ] = str2HEX( param.substring( a*2, a*2+2) );

					changeColor = true;
					paramCnt++;

				}

				// sin/cos period (dec)

				if ( server.argName(i) == "period" ) {

					FX_ArrNew[ PERIOD ] = server.arg("period").toInt(); 

					changeColor = true;
					paramCnt++; 
				}


				// pattern motion parameters (dec)

				if ( server.argName(i) == "slidex" ) { 
					FX_ArrNew [ PHASE_X_INC ] = (float)server.arg("slidex").toInt(); 
					changeColor = true;
					paramCnt++; 
				}

				if ( server.argName(i) == "slidey" ) { 
					FX_ArrNew [ PHASE_Y_INC ] = (float)server.arg("slidey").toInt(); 
					changeColor = true;
					paramCnt++; 
				}

				if ( server.argName(i) == "slidez" ) { 
					FX_ArrNew [ PHASE_Z_INC ] = (float)server.arg("slidez").toInt(); 
					changeColor = true;
					paramCnt++; 
				}


				// delay between frames

				if ( server.argName(i) == "delay" ) { 
					mainDelayNew = server.arg("delay").toInt(); 
//					changeColor = true;
					paramCnt++; 
				}

				// ------------- common parameters -------------------

				// debug data {Number R G B H S V}

				if ( server.argName(i) == "debug" ){ // || param != 0

					HTTPOut += "Stripe [debug]: ";

					for (unsigned int i = 0; i < numberOfLEDs; i++) { 
						color_HSV = rgb2hsv_approximate( LEDStripe_RGB[ i ] ); 
						HTTPOut += String( i ) + "\t" + String( LEDStripe_RGB[ i ].r ) + "\t" + String( LEDStripe_RGB[ i ].g ) + "\t" + String( LEDStripe_RGB[ i ].b ) + "\t" + String( color_HSV.h ) + "\t" + String( color_HSV.s ) + "\t" + String( color_HSV.v ) + "\n"; 
					} 

					paramCnt++;
				}

				// set time 

				if ( server.argName(i) == "time" ){ // || param != 0

					if ( is_number( server.arg("time").c_str() ) ) { 
						timeClient.update();
						timeStamp = strtol( server.arg("time").c_str(), 0, 10 );
					} else {
						HTTPErr += "Parameter 'time' must be specified in UNIX timestamp format.\n"; // 1631848103
					}

					paramCnt++;
				}

				// rename host 

				if ( server.argName(i) == "host" ) {

					if ( server.arg("host") == "" ) {

						HTTPErr += "Please specify new unit hostname.\n";

					} else if ( server.arg("host").length() > EEPROM_HOST_MAX_LEN ) { 

						HTTPErr += "Unit hostname can not exceed " + String( EEPROM_HOST_MAX_LEN ) + " symbols.\n";

					} else {

						strcpy( host, (char*)server.arg("host").c_str() ); // host = (char *)server.arg("host").c_str();
						WiFi.hostname( host );
						MDNS.begin( host );

						HTTPOut +=  "The host name has been changed to '" + String( host ) + "'. Do not forget to save current settings using the '/save' command.\n";
					}

					paramCnt++;
				}

				// save all parameters to EEPROM 

				if (server.argName(i) == "save" ){

					EEPROM.write( EEPROM_numberOfLEDs, numberOfLEDs );
					EEPROM.write( EEPROM_mainDelayNew, mainDelayNew );

					for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) EEPROM.write( EEPROM_HgradBegin + a, (uint8_t)roundf( FX_Arr[ a ] ) );

					EEPROMStrWrite( EEPROM_HOST, host );
					EEPROM.write( EEPROM_FLAG, 1 ); // EEPROM changed
					EEPROM.commit();

					HTTPOut +=  "The current settings are saved.\n";

					paramCnt++;
				}

				// reset to default parameters

				if ( server.argName(i) == "reset" ){

					numberOfLEDs		= DEF_NUM_LEDS;
					mainDelayNew		= DEF_MAIN_DELAY;

					for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) FX_ArrNew[ a ] = 0;

					FX_Arr[ PERIOD ]	= DEF_NUM_LEDS;
					FX_ArrNew[ PERIOD ]	= DEF_NUM_LEDS;

					strcpy( host, (char*)hostDefURI.c_str() );
					WiFi.hostname( host );

					server.begin();
						
					if ( quiet > 1 ) HTTPOut += "All parameters are set to default values. Don't forget to /save them.\n";

					paramCnt++;

					changeColor = true;

				}

				// reboot

				if ( server.argName(i) == "reboot" ){

					FX_ArrNew[ V_GRAD_BEG ] = 0;
					FX_ArrNew[ V_GRAD_END ] = 0;

					mainDelayNew = DEF_MAIN_DELAY;

					if ( quiet > 1 ) {

						server.send_P(200, "text/plain", PSTR("Unit rebooting...\n") );
						server.begin();
					}

					paramCnt++;

					changeColor = true;
					commandReboot = true;
				}

				// quiet, level of output

				if ( server.argName(i) == "quiet" ) {

					quiet = str2HEX( server.arg("quiet") );
					paramCnt++;
				} else {
					quiet = FULL_OUTPUT;
				}

				// json on

				if ( server.argName(i) == "json" ) {
					JSONon = true;
					paramCnt++;
				}

				if ( !paramCnt ) HTTPErr += "Parameter '" + server.argName(i) + "' not exist.\n";
			}


			// ---------------- host default return ------------------------

			if (HTTPErr != "" ) { 	

				// list of errors 

				String Err = "";

				paramCnt = 0;

				if (quiet > 0) { 

					if ( JSONon ) {

						HTTPOut += "{[";

						while ( 1 ){
							Err = expld( HTTPErr, paramCnt, '\n' );
							if ( Err != "" ) HTTPOut += "\"" + Err  + "\""; else break; 
							if ( expld( HTTPErr, paramCnt+1, '\n' ) != "" ) HTTPOut += ",";
							paramCnt ++;
						}

						HTTPOut += "]}";

					} else {

						while ( 1 ){
							Err = expld( HTTPErr, paramCnt, '\n' );
							if ( Err != "" ) HTTPOut += "Error: " + Err + "\n"; else break;
							paramCnt ++;
						}
//						HTTPOut += "See '/help' for details\n";
					}
				}

			} else {

				// standart output 

				if ( quiet > 1 && HTTPOut == "" ) { 

					if ( JSONon ){

						HTTPOut =  "{";
						HTTPOut += "\"MAC\":\"" 		+ String( WiFi.macAddress() ) + "\",\n";
						HTTPOut += "\"Host\":\"" 		+ String( host ) + "\",\n";
						HTTPOut += "\"Timestamp\":\""	 	+ String( timeClient.getEpochTime() ) + "\",\n";
						HTTPOut += "\"Target_timestamp\":\"" 	+ String( timeStamp ) + "\",\n";

						HTTPOut += "\"TNumber_of_LEDs\":\"" 	+ String( numberOfLEDs ) + "\",\n";

						HTTPOut += "\"SlideX:\":\""		+ String( (int)roundf( FX_ArrNew[ PHASE_X_INC ] ) ) + "\",\n";
						HTTPOut += "\"SlideY:\":\""		+ String( (int)roundf( FX_ArrNew[ PHASE_Y_INC ] ) ) + "\",\n";
						HTTPOut += "\"SlideZ:\":\"" 		+ String( (int)roundf( FX_ArrNew[ PHASE_Z_INC ] ) ) + "\",\n";

						HTTPOut += "\"Period:\":\""		+ String( (int)roundf( FX_ArrNew[ PERIOD ] ) ) + "\",\n";
						HTTPOut += "\"Delay:\":\""		+ String( mainDelayNew ) + "\",\n";

						HTTPOut += "\"Gradient_HSV:\":\"";

						for ( uint8_t a = 0; a < 6 ; a++ ) HTTPOut +=  String( printNum( FX_ArrNew[ a ], 16, ' ') );

						HTTPOut += "\",\n";

						HTTPOut += "}";

					} else { 

						HTTPOut =  "MAC:" 		+ String( WiFi.macAddress() ) + "\n";
						HTTPOut += "Host:" 		+ String( host ) + "\n";
						HTTPOut += "Timestamp:"		+ String( timeClient.getEpochTime() ) + "\n";
						HTTPOut += "Target timestamp:"	+ String( timeStamp ) + "\n";

						HTTPOut += "Number of LEDs:" 	+ String( numberOfLEDs ) + "\n";

						HTTPOut += "Slide X:" 		+ String( (int)roundf( FX_ArrNew[ PHASE_X_INC ] ) ) + "\n";
						HTTPOut += "Slide Y:" 		+ String( (int)roundf( FX_ArrNew[ PHASE_Y_INC ] ) ) + "\n";
						HTTPOut += "Slide Z:" 		+ String( (int)roundf( FX_ArrNew[ PHASE_Z_INC ] ) ) + "\n";

						HTTPOut += "Period:" 		+ String( (int)roundf( FX_ArrNew[ PERIOD ] ) ) + "\n";
						HTTPOut += "Delay:" 		+ String( mainDelayNew ) + "\n";

						HTTPOut += "Gradient (HSV):";

						for ( uint8_t a = 0; a < 6 ; a++ ) HTTPOut += String( printNum( roundf( FX_ArrNew[ a ] ), 16, ' ') ) + " ";				

						HTTPOut += "\n";
					
					}
				} 
			}

			server.send(200, "text/plain", HTTPOut );

			HTTPOut 	= "";
			HTTPErr		= "";
			JSONon		= 0;

		});

		// ui interface TODO xml inteface

		server.on("/ui", HTTP_GET, []() {
			HTTPOut = "User interface. Not yet implemented.\n";
			server.send(200, "text/plain", HTTPOut);
		});

		// help

		server.on("/help", HTTP_GET, []() {

			HTTPOut = 	"Interplay Medium ESP8266 Serial LED Controller. Version: " + String(VERSION) + "\n";
			HTTPOut += 	"Created by Dmitry Shalnov (c) 2017. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. \n\n";

			HTTPOut += 	"Parameters:\n";

			HTTPOut += 	"	?number= 	Number of LEDs in stripe (max MAX_NUM_LEDS)\n";
			HTTPOut += 	"	?gradient= 	Start and end colors of gradient (hex, 24bit/color in HLS format, e.g. 001122FFEECC)\n";
			HTTPOut += 	"	?period= 	Sine period (number of leds)\n";
			HTTPOut += 	"	?delay= 	Delay of state changing, integer\n";

			HTTPOut += 	"	?slidex= 	Step of X shift per cycle, signed integer\n";
			HTTPOut += 	"	?slidey= 	Step of Y shift per cycle, signed integer\n";
			HTTPOut += 	"	?slidez= 	Step of Z shift per cycle, signed integer\n";

			HTTPOut += 	"	?time= 		Target timestamp (when the changes will take effect, good for synch), decimal UNIX Timestamp\n\n";

			HTTPOut += 	"	?host= 		Rename the unit\n";
			HTTPOut += 	"	?quiet=	 	Level of output (0/empty = no output, 1 = OK/Error)\n\n";

			HTTPOut += 	"Commands:\n";

			HTTPOut += 	"	?save 		Save current settings\n";
			HTTPOut += 	"	?reset	 	Reset to initial settings\n";
			HTTPOut += 	"	?reboot 	Reboot unit\n";

			HTTPOut += 	"	\n";
			HTTPOut += 	"	/ui 		Output in XML UI format\n";
			HTTPOut += 	"	/json 		Output in JSON format\n";
			HTTPOut += 	"	/update 	Wireless update of firmware (see example below)\n";
			HTTPOut += 	"	/help	 	This help\n\n";

			HTTPOut += 	"Usage:  	curl " + String(host) + "?<parameter>=<value>\n";
			HTTPOut += 	"	  	curl " + String(host) + "/<command>\n";
			HTTPOut += 	"Examples:  	curl \"" + String(host) + "?gradient=ffffffddff00&period=30\" \n";
			HTTPOut += 	"	  	curl -F image=@firmware.bin " + String(host) + "/update \n";

			server.sendHeader("Connection", "close");
			server.send( 200, "text/plain", HTTPOut );

		});

		// firmware update

		server.on("/update", HTTP_POST, []() {

			server.sendHeader("Connection", "close");
			server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");

			ESP.restart();

			}, []() {

				HTTPUpload& upload = server.upload();

				if (upload.status == UPLOAD_FILE_START) {
				
#if SERIAL == 1
					Serial.setDebugOutput(true);
#endif
					WiFiUDP::stopAll();

#if SERIAL == 1
					Serial.printf("Update: %s\n", upload.filename.c_str());
#endif
					uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;

					if (!Update.begin(maxSketchSpace)) { //start with max available size
#if SERIAL == 1
						Update.printError(Serial);
#endif
					}
				} else if (upload.status == UPLOAD_FILE_WRITE) {
					if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
#if SERIAL == 1
						Update.printError(Serial);
#endif
					}
				} else if (upload.status == UPLOAD_FILE_END) {
					if (Update.end(true)) { //true to set the size to the current progress

						server.sendHeader("Connection", "close");
						server.send_P(200, "text/plain", PSTR("Success. Please wait until device replace firmware and boot up...\n") );
#if SERIAL == 1
						Serial.printf("Update Success: %u\nRebooting....\n", upload.totalSize); 
#endif
					} else {
						server.sendHeader("Connection", "close");
						server.send_P(200, "text/plain", PSTR("Something went wrong. Please reset device and try again.\n") );
#if SERIAL == 1
						Update.printError(Serial);
#endif
					}
#if SERIAL == 1
					Serial.setDebugOutput(false);
#endif
				}

				yield();
		});

	} else {
#if SERIAL == 1
		Serial.println("WiFi Failed");
#endif
	}
}

// -----------------------------------------------------
// ------------------- MAIN ----------------------------
// -----------------------------------------------------

// uint8_t rebootColorsCnt = 0;

void loop(void) {

	server.handleClient();
	MDNS.update();

	// reconnect

	if ( WiFi.status() != WL_CONNECTED ) {
#if SERIAL == 1
		Serial.printf(".");
#endif
		if (WiFi.waitForConnectResult() == WL_CONNECTED) {
#if SERIAL == 1
			Serial.println("Reconnected");
#endif
			MDNS.begin( host );

			timeClient.begin();
			timeClient.update();
		}

	} 

	if ( changeColor ) {

		// set float coefficents

		for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) {
			FX_Coeff[ H_GRAD_BEG + a ] = (float)FX_ArrNew[ H_GRAD_BEG + a ] - FX_Arr[ H_GRAD_BEG + a ];
			if ( abs( FX_Coeff[ H_GRAD_BEG + a ] ) > FX_CoeffMax ) FX_CoeffMax = abs( FX_Coeff[ H_GRAD_BEG + a ] );
		}

		for ( uint8_t a = 0; a < 6; a++ ) FX_Coeff[ H_GRAD_BEG + a ] = FX_Coeff[ H_GRAD_BEG + a ] / FX_CoeffMax;

		changeColor = false;

#if SERIAL == 1
		Serial.printf("FX_ArrNew: "); 
		for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) Serial.printf("%f ", FX_ArrNew[ a ] );
		Serial.printf("\n");
		Serial.printf("FX_Coeff: "); 
		for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) Serial.printf("%f ", FX_Coeff[ a ] );
		Serial.printf("\n");
#endif

	} else {

		if ( ++mainDelay == mainDelayNew /* && FX_Delay != 0 */ ) {

#if SERIAL == 1
//			Serial.printf("FX_Arr: "); 
//			for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) Serial.printf("%f ", FX_Arr[ a ] );
//			Serial.printf("\n");
//			Serial.printf("Phase: %f %f %f\n", Phase[ PHASE_X ], Phase[ PHASE_Y ], Phase[ PHASE_Z ] );
#endif

			mainDelay = 0;

			// fade out and reboot

			if ( commandReboot && roundf( FX_Arr[ V_GRAD_BEG ] ) == 0 && roundf( FX_Arr[ V_GRAD_END ]) == 0 ) {
				commandReboot = false;
#if SERIAL == 1
				Serial.printf("Reboot......\n"); 
#endif
				ESP.restart();
//				Serial.printf("Reboot.2...\n"); 
			}

			if ( timeClient.getEpochTime() > timeStamp ) {

				// change color 

				for ( uint8_t a = 0; a < NUMBER_OF_PARAM; a++ ) {
					if ( FX_ArrNew [ a ] != roundf( FX_Arr[ a ] ) ) { FX_Arr[ a ] += FX_Coeff[ a ]; } else FX_Arr[ a ] = (float)FX_ArrNew [ a ];
				}
		
				// change phase 

				for ( uint8_t a = 0; a < 3; a++ ) {

					if ( FX_Arr[ PHASE_X_INC + a ] == 0 ) { 
						// shift phase to 0 after slide. = 0
						if ( (uint8_t)round( Phase[ a ] ) != 0 ) Phase[ a ] -= FX_Coeff[ PHASE_X_INC + a ]/100; else Phase[ a ] = 0; 
					} else { 
						// routine shift phase
						Phase[ a ] += FX_Arr[ PHASE_X_INC + a ]/100;
					}
				}

			}

			if ( FX_Arr[ PERIOD ] == 0 ) FX_Arr[ PERIOD ] = 1;

			FX_Gradient_RT( roundf( FX_Arr[ H_GRAD_BEG ] ), roundf( FX_Arr[ S_GRAD_BEG ] ), roundf( FX_Arr[ V_GRAD_BEG ] ), roundf( FX_Arr[ H_GRAD_END ] ), roundf( FX_Arr[ S_GRAD_END ] ), roundf( FX_Arr[ V_GRAD_END ] ), roundf( FX_Arr[ PERIOD ] ), roundf( Phase[ PHASE_X ] ), roundf( Phase[ PHASE_Y ] ), roundf( Phase[ PHASE_Z ] ) );
			FastLED.show();
			delay(3);

		}
	}
}