/*
 * EspFirmwareUpload.cpp
 *
 *  Created on: 15 Apr 2016
 *      Author: David
 */

#include "WifiFirmwareUploader.h"

#include "Network.h"
#include "Platform.h"
#include "RepRap.h"
#include "Storage/FileStore.h"

// ESP8266 command codes
const uint8_t ESP_FLASH_BEGIN = 0x02;
const uint8_t ESP_FLASH_DATA = 0x03;
const uint8_t ESP_FLASH_END = 0x04;
const uint8_t ESP_MEM_BEGIN = 0x05;
const uint8_t ESP_MEM_END = 0x06;
const uint8_t ESP_MEM_DATA = 0x07;
const uint8_t ESP_SYNC = 0x08;
const uint8_t ESP_WRITE_REG = 0x09;
const uint8_t ESP_READ_REG = 0x0a;

// MAC address storage locations
const uint32_t ESP_OTP_MAC0 = 0x3ff00050;
const uint32_t ESP_OTP_MAC1 = 0x3ff00054;
const uint32_t ESP_OTP_MAC2	= 0x3ff00058;
const uint32_t ESP_OTP_MAC3 = 0x3ff0005c;

const size_t EspFlashBlockSize = 0x0400;			// 1K byte blocks

const uint8_t ESP_IMAGE_MAGIC = 0xe9;
const uint8_t ESP_CHECKSUM_MAGIC = 0xef;

const uint32_t ESP_ERASE_CHIP_ADDR = 0x40004984;	// &SPIEraseChip
const uint32_t ESP_SEND_PACKET_ADDR = 0x40003c80;	// &send_packet
const uint32_t ESP_SPI_READ_ADDR = 0x40004b1c;		// &SPIRead
const uint32_t ESP_UNKNOWN_ADDR = 0x40001121;		// not used
const uint32_t ESP_USER_DATA_RAM_ADDR = 0x3ffe8000;	// &user data ram
const uint32_t ESP_IRAM_ADDR = 0x40100000;			// instruction RAM
const uint32_t ESP_FLASH_ADDR = 0x40200000;			// address of start of Flash
const uint32_t ESP_FLASH_READ_STUB_BEGIN = IRAM_ADDR + 0x18;

// Messages corresponding to result codes, should make sense when followed by " error"
const char *resultMessages[] =
{
	"no",
	"timeout",
	"comm write",
	"connect",
	"bad reply",
	"file read",
	"empty file",
	"response header",
	"slip frame",
	"slip state",
	"slip data"
};

// A note on baud rates.
// The ESP8266 supports 921600, 460800, 230400, 115200, 74880 and some lower baud rates.
// 921600b is not reliable because even though it sometimes succeeds in connecting, we get a bad response during uploading after a few blocks.
// Probably our UART ISR cannot receive bytes fast enough, perhaps because of the latency of the system tick ISR.
// 460800b doesn't always manage to connect, but if it does then uploading appears to be reliable.
// 230400b always manages to connect.
static const uint32_t uploadBaudRates[] = { 460800, 230400, 115200, 74880 };

WifiFirmwareUploader::WifiFirmwareUploader(UARTClass& port)
	: uploadPort(port), uploadFile(nullptr), state(UploadState::idle)
{
}

bool WifiFirmwareUploader::IsReady() const
{
	return state == UploadState::idle;
}

void WifiFirmwareUploader::MessageF(const char *fmt, ...)
{
	va_list vargs;
	va_start(vargs, fmt);
	reprap.GetPlatform()->MessageF(FIRMWARE_UPDATE_MESSAGE, fmt, vargs);
	va_end(vargs);
}

void WifiFirmwareUploader::flushInput()
{
	while (uploadPort.available() != 0)
	{
		(void)uploadPort.read();
	}
}

// Extract 1-4 bytes of a value in little-endian order from a buffer beginning at a specified offset
uint32_t WifiFirmwareUploader::getData(unsigned byteCnt, const uint8_t *buf, int ofst)
{
	uint32_t val = 0;

	if (buf && byteCnt)
	{
		unsigned int shiftCnt = 0;
		if (byteCnt > 4)
			byteCnt = 4;
		do
		{
			val |= (uint32_t)buf[ofst++] << shiftCnt;
			shiftCnt += 8;
		} while (--byteCnt);
	}
	return(val);
}

// Put 1-4 bytes of a value in little-endian order into a buffer beginning at a specified offset.
void WifiFirmwareUploader::putData(uint32_t val, unsigned byteCnt, uint8_t *buf, int ofst)
{
	if (buf && byteCnt)
	{
		if (byteCnt > 4)
		{
			byteCnt = 4;
		}
		do
		{
			buf[ofst++] = (uint8_t)(val & 0xff);
			val >>= 8;
		} while (--byteCnt);
	}
}

// Read a byte optionally performing SLIP decoding.  The return values are:
//
//	2 - an escaped byte was read successfully
//	1 - a non-escaped byte was read successfully
//	0 - no data was available
//   -1 - the value 0xc0 was encountered (shouldn't happen)
//   -2 - a SLIP escape byte was found but the following byte wasn't available
//   -3 - a SLIP escape byte was followed by an invalid byte
int WifiFirmwareUploader::ReadByte(uint8_t& data, bool slipDecode)
{
	if (uploadPort.available() == 0)
	{
		return(0);
	}

	// at least one byte is available
	data = uploadPort.read();
	if (!slipDecode)
	{
		return(1);
	}

	if (data == 0xc0)
	{
		// this shouldn't happen
		return(-1);
	}

	// if not the SLIP escape, we're done
	if (data != 0xdb)
	{
		return(1);
	}

	// SLIP escape, check availability of subsequent byte
	if (uploadPort.available() == 0)
	{
		return(-2);
	}

	// process the escaped byte
	data = uploadPort.read();
	if (data == 0xdc)
	{
		data = 0xc0;
		return(2);
	}

	if (data == 0xdd)
	{
		data = 0xdb;
		return(2);
	}
	// invalid
	return(-3);
}

// When we write a sync packet, there must be no gaps between most of the characters.
// So use this function, which does a block write to the UART buffer in the latest CoreNG.
void WifiFirmwareUploader::writePacketRaw(const uint8_t *buf, size_t len)
{
	uploadPort.write(buf, len);
}

// Write a byte to the serial port optionally SLIP encoding. Return the number of bytes actually written.
inline void WifiFirmwareUploader::WriteByteRaw(uint8_t b)
{
	uploadPort.write(b);
}

// Write a byte to the serial port optionally SLIP encoding. Return the number of bytes actually written.
inline void WifiFirmwareUploader::WriteByteSlip(uint8_t b)
{
	if (b == 0xC0)
	{
		WriteByteRaw(0xDB);
		WriteByteRaw(0xDC);
	}
	else if (b == 0xDB)
	{
		WriteByteRaw(0xDB);
		WriteByteRaw(0xDD);
	}
	else
	{
		uploadPort.write(b);
	}
}

// Wait for a data packet to be returned.  If the body of the packet is
// non-zero length, return an allocated buffer indirectly containing the
// data and return the data length. Note that if the pointer for returning
// the data buffer is NULL, the response is expected to be two bytes of zero.
//
// If an error occurs, return a negative value.  Otherwise, return the number
// of bytes in the response (or zero if the response was not the standard "two bytes of zero").
WifiFirmwareUploader::EspUploadResult WifiFirmwareUploader::readPacket(uint8_t op, uint32_t *valp, size_t& bodyLen, uint32_t msTimeout)
{
	enum class PacketState
	{
		begin = 0,
		header,
		body,
		end,
		done
	};

	const size_t headerLength = 8;

	uint32_t startTime = millis();
	uint8_t hdr[headerLength];
	uint16_t hdrIdx = 0;
	bodyLen = 0;
	uint16_t bodyIdx = 0;
	uint8_t respBuf[2];

	// wait for the response
	uint16_t needBytes = 1;
	PacketState state = PacketState::begin;
	while (state != PacketState::done)
	{
		uint8_t c;
		EspUploadResult stat;

		if (millis() - startTime > msTimeout)
		{
			return(EspUploadResult::timeout);
		}

		if (uploadPort.available() < needBytes)
		{
			// insufficient data available
			// preferably, return to Spin() here
			continue;
		}

		// sufficient bytes have been received for the current state, process them
		switch(state)
		{
		case PacketState::begin:	// expecting frame start
		case PacketState::end:		// expecting frame end
			c = uploadPort.read();
			if (c != 0xc0)
			{
				return EspUploadResult::slipFrame;
			}
			if (state == PacketState::begin)
			{
				state = PacketState::header;
				needBytes = 2;
			}
			else
			{
				state = PacketState::done;
			}
			break;

		case PacketState::header:	// reading an 8-byte header
		case PacketState::body:		// reading the response body
			{
				int rslt;
				// retrieve a byte with SLIP decoding
				rslt = ReadByte(c, true);
				if (rslt != 1 && rslt != 2)
				{
					// some error occurred
					stat = (rslt == 0 || rslt == -2) ? EspUploadResult::slipData : EspUploadResult::slipFrame;
					return stat;
				}
				else if (state == PacketState::header)
				{
					//store the header byte
					hdr[hdrIdx++] = c;
					if (hdrIdx >= headerLength)
					{
						// get the body length, prepare a buffer for it
						bodyLen = (uint16_t)getData(2, hdr, 2);

						// extract the value, if requested
						if (valp != nullptr)
						{
							*valp = getData(4, hdr, 4);
						}

						if (bodyLen != 0)
						{
							state = PacketState::body;
						}
						else
						{
							needBytes = 1;
							state = PacketState::end;
						}
					}
				}
				else
				{
					// Store the response body byte, check for completion
					if (bodyIdx < ARRAY_SIZE(respBuf))
					{
						respBuf[bodyIdx] = c;
					}
					++bodyIdx;
					if (bodyIdx >= bodyLen)
					{
						needBytes = 1;
						state = PacketState::end;
					}
				}
			}
			break;

		default:		// this shouldn't happen
			return EspUploadResult::slipState;
		}
	}

	// Extract elements from the header
	const uint8_t resp = (uint8_t)getData(1, hdr, 0);
	const uint8_t opRet = (uint8_t)getData(1, hdr, 1);
	// Sync packets often provoke a response with a zero opcode instead of ESP_SYNC
	if (resp != 0x01 || opRet != op)
	{
//debugPrintf("resp %02x %02x\n", resp, opRet);
		return EspUploadResult::respHeader;
	}

	return EspUploadResult::success;
}

// Send a block of data performing SLIP encoding of the content.
inline void WifiFirmwareUploader::writePacket(const uint8_t *data, size_t len)
{
	while (len != 0)
	{
		WriteByteSlip(*data++);
		--len;
	}
}

// Send a packet to the serial port while performing SLIP framing. The packet data comprises a header and an optional data block.
// A SLIP packet begins and ends with 0xc0.  The data encapsulated has the bytes
// 0xc0 and 0xdb replaced by the two-byte sequences {0xdb, 0xdc} and {0xdb, 0xdd} respectively.
void WifiFirmwareUploader::writePacket(const uint8_t *hdr, size_t hdrLen, const uint8_t *data, size_t dataLen)
{
	WriteByteRaw(0xc0);				// send the packet start character
	writePacket(hdr, hdrLen);		// send the header
	writePacket(data, dataLen);		// send the data block
	WriteByteRaw(0xc0);				// send the packet end character
}

// Send a packet to the serial port while performing SLIP framing. The packet data comprises a header and an optional data block.
// This is like writePacket except that it does a fast block write for both the header and the main data with no SLIP encoding. Used to send sync commands.
void WifiFirmwareUploader::writePacketRaw(const uint8_t *hdr, size_t hdrLen, const uint8_t *data, size_t dataLen)
{
	WriteByteRaw(0xc0);				// send the packet start character
	writePacketRaw(hdr, hdrLen);	// send the header
	writePacketRaw(data, dataLen);	// send the data block in raw mode
	WriteByteRaw(0xc0);				// send the packet end character
}

// Send a command to the attached device together with the supplied data, if any.
// The data is supplied via a list of one or more segments.
void WifiFirmwareUploader::sendCommand(uint8_t op, uint32_t checkVal, const uint8_t *data, size_t dataLen)
{
	// populate the header
	uint8_t hdr[8];
	putData(0, 1, hdr, 0);
	putData(op, 1, hdr, 1);
	putData(dataLen, 2, hdr, 2);
	putData(checkVal, 4, hdr, 4);

	// send the packet
	flushInput();
	if (op == ESP_SYNC)
	{
		writePacketRaw(hdr, sizeof(hdr), data, dataLen);
	}
	else
	{
		writePacket(hdr, sizeof(hdr), data, dataLen);
	}
}

// Send a command to the attached device together with the supplied data, if any, and get the response
WifiFirmwareUploader::EspUploadResult WifiFirmwareUploader::doCommand(uint8_t op, const uint8_t *data, size_t dataLen, uint32_t checkVal, uint32_t *valp, uint32_t msTimeout)
{
	sendCommand(op, checkVal, data, dataLen);
	size_t bodyLen;
	EspUploadResult stat = readPacket(op, valp, bodyLen, msTimeout);
	if (stat == EspUploadResult::success && bodyLen != 2)
	{
		stat = EspUploadResult::badReply;
	}

	return stat;
}

// Send a synchronising packet to the serial port in an attempt to induce
// the ESP8266 to auto-baud lock on the baud rate.
WifiFirmwareUploader::EspUploadResult WifiFirmwareUploader::Sync(uint16_t timeout)
{
	uint8_t buf[36];

	// compose the data for the sync attempt
	memset(buf, 0x55, sizeof(buf));
	buf[0] = 0x07;
	buf[1] = 0x07;
	buf[2] = 0x12;
	buf[3] = 0x20;

	EspUploadResult stat = doCommand(ESP_SYNC, buf, sizeof(buf), 0, nullptr, timeout);

	// If we got a response other than sync, discard it and wait for a sync response. This happens at higher baud rates.
	for (int i = 0; i < 10 && stat == EspUploadResult::respHeader; ++i)
	{
		size_t bodyLen;
		stat = readPacket(ESP_SYNC, nullptr, bodyLen, timeout);
	}

	if (stat == EspUploadResult::success)
	{
		// Read and discard additional replies
		for (;;)
		{
			size_t bodyLen;
			EspUploadResult rc = readPacket(ESP_SYNC, nullptr, bodyLen, defaultTimeout);
			if (rc != EspUploadResult::success || bodyLen != 2)
			{
				break;
			}
		}
	}
//DEBUG
//	else debugPrintf("stat=%d\n", (int)stat);
	return stat;
}

// Send a command to the device to begin the Flash process.
WifiFirmwareUploader::EspUploadResult WifiFirmwareUploader::flashBegin(uint32_t addr, uint32_t size)
{
	// determine the number of blocks represented by the size
	uint32_t blkCnt;
	blkCnt = (size + EspFlashBlockSize - 1) / EspFlashBlockSize;

	// ensure that the address is on a block boundary
	addr &= ~(EspFlashBlockSize - 1);

	// begin the Flash process
	uint8_t buf[16];
	putData(size, 4, buf, 0);
	putData(blkCnt, 4, buf, 4);
	putData(EspFlashBlockSize, 4, buf, 8);
	putData(addr, 4, buf, 12);

	uint32_t timeout = (size != 0) ? eraseTimeout : defaultTimeout;
	return doCommand(ESP_FLASH_BEGIN, buf, sizeof(buf), 0, nullptr, timeout);
}

// Send a command to the device to terminate the Flash process
WifiFirmwareUploader::EspUploadResult WifiFirmwareUploader::flashFinish(bool reboot)
{
	uint8_t buf[4];
	putData(reboot ? 0 : 1, 4, buf, 0);
	return doCommand(ESP_FLASH_END, buf, sizeof(buf), 0, nullptr, defaultTimeout);
}

// Compute the checksum of a block of data
uint16_t WifiFirmwareUploader::checksum(const uint8_t *data, uint16_t dataLen, uint16_t cksum)
{
	if (data != NULL)
	{
		while (dataLen--)
		{
			cksum ^= (uint16_t)*data++;
		}
	}
	return(cksum);
}

WifiFirmwareUploader::EspUploadResult WifiFirmwareUploader::flashWriteBlock(uint16_t flashParmVal, uint16_t flashParmMask)
{
	const uint32_t blkSize = EspFlashBlockSize;

	// Allocate a data buffer for the combined header and block data
	const uint16_t hdrOfst = 0;
	const uint16_t dataOfst = 16;
	const uint16_t blkBufSize = dataOfst + blkSize;
	uint32_t blkBuf32[blkBufSize/4];
	uint8_t * const blkBuf = reinterpret_cast<uint8_t*>(blkBuf32);

	// Prepare the header for the block
	putData(blkSize, 4, blkBuf, hdrOfst + 0);
	putData(uploadBlockNumber, 4, blkBuf, hdrOfst + 4);
	putData(0, 4, blkBuf, hdrOfst + 8);
	putData(0, 4, blkBuf, hdrOfst + 12);

	// Get the data for the block
	size_t cnt = uploadFile->Read(reinterpret_cast<char *>(blkBuf + dataOfst), blkSize);
	if (cnt != blkSize)
	{
		if (uploadFile->Position() == fileSize)
		{
			// partial last block, fill the remainder
			memset(blkBuf + dataOfst + cnt, 0xff, blkSize - cnt);
		}
		else
		{
			return EspUploadResult::fileRead;
		}
	}

	// Patch the flash parameters into the first block if it is loaded at address 0
	if (uploadBlockNumber == 0 && uploadAddress == 0 && blkBuf[dataOfst] == ESP_IMAGE_MAGIC && flashParmMask != 0)
	{
		// update the Flash parameters
		uint32_t flashParm = getData(2, blkBuf + dataOfst + 2, 0) & ~(uint32_t)flashParmMask;
		putData(flashParm | flashParmVal, 2, blkBuf + dataOfst + 2, 0);
	}

	// Calculate the block checksum
	uint16_t cksum = checksum(blkBuf + dataOfst, blkSize, ESP_CHECKSUM_MAGIC);
	EspUploadResult stat;
	for (int i = 0; i < 3; i++)
	{
		if ((stat = doCommand(ESP_FLASH_DATA, blkBuf, blkBufSize, cksum, nullptr, blockWriteTimeout)) == EspUploadResult::success)
		{
			break;
		}
	}

	return stat;
}

void WifiFirmwareUploader::Spin()
{
	switch (state)
	{
	case UploadState::resetting:
		if (connectAttemptNumber == ARRAY_SIZE(uploadBaudRates) * retriesPerBaudRate)
		{
			// Time to give up
			Network::ResetWiFi();
			uploadResult = EspUploadResult::connect;
			state = UploadState::done;
		}
		else
		{
			// Reset the serial port at the new baud rate. Also reset the ESP8266.
			const uint32_t baud = uploadBaudRates[connectAttemptNumber/retriesPerBaudRate];
			if (connectAttemptNumber % retriesPerBaudRate == 0)
			{
				// First attempt at this baud rate
				MessageF("Trying to connect at %u baud: ", baud);
			}
			uploadPort.begin(baud);
			uploadPort.setInterruptPriority(1);				// we are going to move data at seriously high speeds
			Network::ResetWiFiForUpload(false);
			lastAttemptTime = lastResetTime = millis();
			state = UploadState::connecting;
		}
		break;

	case UploadState::connecting:
		if (millis() - lastAttemptTime >= connectAttemptInterval && millis() - lastResetTime >= resetDelay)
		{
			// Attempt to establish a connection to the ESP8266.
			EspUploadResult res = Sync(syncTimeout);
			lastAttemptTime = millis();
			if (res == EspUploadResult::success)
			{
				// Successful connection
//				MessageF(" success on attempt %d\n", (connectAttemptNumber % retriesPerBaudRate) + 1);
				MessageF(" success\n");
				state = UploadState::erasing;
			}
			else
			{
				// This attempt failed
				++connectAttemptNumber;
				if (connectAttemptNumber % retriesPerReset == 0)
				{
					if (connectAttemptNumber % retriesPerBaudRate == 0)
					{
						MessageF(" failed\n");
					}
					state = UploadState::resetting;		// try a reset and a lower baud rate
				}
			}
		}
		break;

	case UploadState::erasing:
		if (millis() - lastAttemptTime >= blockWriteInterval)
		{
			const uint32_t sectorsPerBlock = 16;
			const uint32_t sectorSize = 4096;
			const uint32_t numSectors = (fileSize + sectorSize - 1)/sectorSize;
			const uint32_t startSector = uploadAddress/sectorSize;
			uint32_t headSectors = sectorsPerBlock - (startSector % sectorsPerBlock);

			if (numSectors < headSectors)
			{
				headSectors = numSectors;
			}
	        const uint32_t eraseSize = (numSectors < 2 * headSectors)
	        									? (numSectors + 1) / 2 * sectorSize
	        									: (numSectors - headSectors) * sectorSize;

			MessageF("Erasing %u bytes...\n", fileSize);
			uploadResult = flashBegin(uploadAddress, eraseSize);
			if (uploadResult == EspUploadResult::success)
			{
				MessageF("Uploading file...\n");
				uploadBlockNumber = 0;
				uploadNextPercentToReport = percentToReportIncrement;
				lastAttemptTime = millis();
				state = UploadState::uploading;
			}
			else
			{
				MessageF("Erase failed\n");
				state = UploadState::done;
			}
		}
		break;

	case UploadState::uploading:
		// The ESP needs several milliseconds to recover from one packet before it will accept another
		if (millis() - lastAttemptTime >= blockWriteInterval)
		{
			const uint32_t blkCnt = (fileSize + EspFlashBlockSize - 1) / EspFlashBlockSize;
			if (uploadBlockNumber < blkCnt)
			{
				uploadResult = flashWriteBlock(0, 0);
				lastAttemptTime = millis();
				if (uploadResult != EspUploadResult::success)
				{
					MessageF("Flash block upload failed\n");
					state = UploadState::done;
				}
				const unsigned int percentComplete = (100 * uploadBlockNumber)/blkCnt;
				++uploadBlockNumber;
				if (percentComplete >= uploadNextPercentToReport)
				{
					MessageF("%u%% complete\n", percentComplete);
					uploadNextPercentToReport += percentToReportIncrement;
				}
			}
			else
			{
				state = UploadState::done;
			}
		}
		break;

	case UploadState::done:
		uploadFile->Close();
		uploadPort.end();					// disable the port, it has a high interrupt priority
		if (uploadResult == EspUploadResult::success)
		{
			reprap.GetPlatform()->Message(FIRMWARE_UPDATE_MESSAGE, "Upload successful\n");
			if (restartOnCompletion)
			{
				reprap.GetNetwork()->Start();
			}
			else
			{
				reprap.GetNetwork()->ResetWiFi();
			}
		}
		else
		{
			reprap.GetPlatform()->MessageF(FIRMWARE_UPDATE_MESSAGE, "Error: Installation failed due to %s error\n", resultMessages[(size_t)uploadResult]);
			// Not safe to restart the network
			reprap.GetNetwork()->ResetWiFi();
		}
		state = UploadState::idle;
		break;

	default:
		break;
	}
}

// Try to upload the given file at the given address
void WifiFirmwareUploader::SendUpdateFile(const char *file, const char *dir, uint32_t address)
{
	Platform * const platform = reprap.GetPlatform();
	uploadFile = platform->GetFileStore(dir, file, false);
	if (uploadFile == nullptr)
	{
		platform->MessageF(FIRMWARE_UPDATE_MESSAGE, "Failed to open file %s\n", file);
		return;
	}

	fileSize = uploadFile->Length();
	if (fileSize == 0)
	{
		uploadFile->Close();
		platform->MessageF(FIRMWARE_UPDATE_MESSAGE, "Upload file is empty %s\n", file);
		return;
	}

	// Stop the network
	Network *network = reprap.GetNetwork();
	restartOnCompletion = network->IsEnabled();
	network->Stop();

	// Set up the state so that subsequent calls to Spin() will attempt the upload
	uploadAddress = address;
	connectAttemptNumber = 0;
	state = UploadState::resetting;
}

// End
