/*
 * Remote Laboratory FPGA Programming Part
 *
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * (c) 2012-2013 Timothy Pearson
 * Raptor Engineering
 * http://www.raptorengineeringinc.com
 */

#include "define.h"
#include "part.h"

#include <tdeaboutdata.h>   //::createAboutData()
#include <tdeaction.h>
#include <tdelocale.h>
#include <ktextedit.h>
#include <tdefiledialog.h>
#include <tdemessagebox.h>  //::start()
#include <tdeparts/genericfactory.h>
#include <kstatusbar.h>
#include <kstdaction.h>
#include <knuminput.h>
#include <kmdcodec.h>
#include <kurlrequester.h>
#include <tqfile.h>        //encodeName()
#include <tqtimer.h>       //postInit() hack
#include <tqvbox.h>
#include <tqsocket.h>
#include <tqmutex.h>
#include <tqeventloop.h>
#include <tqapplication.h>
#include <tqgroupbox.h>
#include <tqcheckbox.h>
#include <tqpushbutton.h>
#include <tqprogressbar.h>
#include <unistd.h>       //access()
#include <stdint.h>

#include <tqpainter.h>

#include "tracewidget.h"
#include "floatspinbox.h"
#include "layout.h"

#define NETWORK_COMM_TIMEOUT_MS 15000

enum connectionModes {
	ModeIdle					= 0,
	ModeProgramming					= 1
};

enum connectionStates {
	ModeIdle_StateStatusRequest			= 0,
	ModeIdle_StateProcessStatus			= 1,
	ModeIdle_StateDelay				= 2,
	ModeIdle_StateGetLogMessages			= 3,
	ModeProgramming_StateReadFile			= 100,
	ModeProgramming_StateInitProgramming		= 101,
	ModeProgramming_StateWaitForCompletion		= 102,
	ModeProgramming_StateRequestStatus		= 103
};

namespace RemoteLab {

typedef KParts::GenericFactory<RemoteLab::FPGAProgramPart> Factory;
#define CLIENT_LIBRARY "libremotelab_fpgaprogrammer"
K_EXPORT_COMPONENT_FACTORY(libremotelab_fpgaprogrammer, RemoteLab::Factory)

FPGAProgramPart::FPGAProgramPart(TQWidget *parentWidget, const char *widgetName, TQObject *parent, const char *name, const TQStringList&)
	: RemoteInstrumentPart( parent, name ), m_base(NULL), m_commHandlerState(0), m_connectionActiveAndValid(false), m_tickerState(0)
{
	// Initialize important base class variables
	m_clientLibraryName = CLIENT_LIBRARY;

	// Initialize mutex
	m_connectionMutex = new TQMutex(false);

	// Initialize kpart
	setInstance(Factory::instance());
	setWidget(new TQVBox(parentWidget, widgetName));

	// Create timers
	m_forcedUpdateTimer = new TQTimer(this);
	connect(m_forcedUpdateTimer, SIGNAL(timeout()), this, SLOT(mainEventLoop()));
	m_updateTimeoutTimer = new TQTimer(this);
	connect(m_updateTimeoutTimer, SIGNAL(timeout()), this, SLOT(mainEventLoop()));
	m_pingDelayTimer = new TQTimer(this);
	connect(m_pingDelayTimer, SIGNAL(timeout()), this, SLOT(mainEventLoop()));

	// Create widgets
	m_base = new FPGAProgramBase(widget());

	// Load configuration
	m_config = new KSimpleConfig("ulab_client_part_fpgaprogrammer.conf", false);
	m_config->setGroup("UI");
	m_base->programmingInputFile->setURL(m_config->readPathEntry("programmingInputFile", ""));

	// Initialize widgets
	m_base->setMinimumSize(500,350);
	m_base->programmingLogBox->setReadOnly(true);
	connect(m_base->programRunButton, SIGNAL(clicked()), this, SLOT(programRunButtonClicked()));
	connect(m_base->clearProgrammingLogButton, SIGNAL(clicked()), this, SLOT(clearProgrammingLogBox()));
	connect(m_base->saveProgrammingLogButton, SIGNAL(clicked()), this, SLOT(saveProgrammingLogBox()));
	connect(m_base->programmingInputFile, SIGNAL(textChanged(const TQString &)), this, SLOT(processLockouts()));

	TQTimer::singleShot(0, this, TQT_SLOT(postInit()));
}

FPGAProgramPart::~FPGAProgramPart() {
	// Save field state for restoration on next load
	m_config->setGroup("UI");
	m_config->writeEntry("programmingInputFile", m_base->programmingInputFile->url());
	m_config->sync();
	delete m_config;

	if (m_connectionMutex->locked()) {
		printf("[WARNING] Exiting when data transfer still in progress!\n\r"); fflush(stdout);
	}

	disconnectFromServer();
	delete m_connectionMutex;
}

void FPGAProgramPart::processLockouts() {
	TQWidget* mainWidget = widget();
	if (mainWidget) {
		if ((m_socket) && (m_socket->state() == TQSocket::Connected) && (connToServerState > 0) && (connToServerConnecting == false)) {
			mainWidget->setEnabled(true);
		}
		else {
			mainWidget->setEnabled(false);
		}
	}

	if ((m_base->programmingInputFile->url() != "") && (m_commHandlerMode == 0) && (m_connectionActiveAndValid == true)) {
		m_base->programRunButton->setEnabled(true);
	}
	else {
		m_base->programRunButton->setEnabled(false);
	}

	if (m_commHandlerMode == 1) {
		m_base->programmingInputFile->setEnabled(false);
	}
	else {
		m_base->programmingInputFile->setEnabled(true);
	}

	if ((m_connectionActiveAndValid == true) && (m_commHandlerMode == 0)) {
		m_base->programmingStatusLabel->setText(i18n("Ready"));
		m_base->programmingProgressBar->setTotalSteps(1);
		m_base->programmingProgressBar->setProgress(-1);
	}
}

void FPGAProgramPart::resizeToHint() {
	resize(widget()->sizeHint());
}

void FPGAProgramPart::connectionClosed() {
	closeURL();
}

void FPGAProgramPart::postInit() {
	setUsingFixedSize(false);
	processLockouts();
}

bool FPGAProgramPart::openURL(const KURL &url) {
	int ret;
	ret = connectToServer(url.url());
	processLockouts();
	return (ret != 0);
}

bool FPGAProgramPart::closeURL() {
	disconnectFromServer();
	m_url = KURL();
	return true;
}

void FPGAProgramPart::disconnectFromServerCallback() {
	m_forcedUpdateTimer->stop();
	m_updateTimeoutTimer->stop();
}

void FPGAProgramPart::connectionFinishedCallback() {
	connect(m_socket, SIGNAL(readyRead()), m_socket, SLOT(processPendingData()));
	m_socket->processPendingData();
	connect(m_socket, SIGNAL(newDataReceived()), this, SLOT(mainEventLoop()));
	m_tickerState = 0;
	m_commHandlerState = ModeIdle_StateStatusRequest;
	m_commHandlerMode = ModeIdle;
	m_socket->setDataTimeout(NETWORK_COMM_TIMEOUT_MS);
	m_updateTimeoutTimer->start(NETWORK_COMM_TIMEOUT_MS, TRUE);
	processLockouts();
	mainEventLoop();
	return;
}

void FPGAProgramPart::connectionStatusChangedCallback() {
	processLockouts();
}

void FPGAProgramPart::programRunButtonClicked() {
	m_commHandlerState = ModeProgramming_StateReadFile;
	m_commHandlerMode = ModeProgramming;
	m_forcedUpdateTimer->start(0, TRUE);
	processLockouts();
}

void FPGAProgramPart::clearProgrammingLogBox() {
	m_base->programmingLogBox->setText("");
}

void FPGAProgramPart::saveProgrammingLogBox() {
	TQString saveFileName = KFileDialog::getSaveFileName(TQString::null, "*.txt|Text Files (*.txt)", 0, i18n("Save log file..."));
	if (saveFileName != "") {
		TQFile file(saveFileName);
		if (file.open(IO_WriteOnly)) {
			TQCString logFile = m_base->programmingLogBox->text().ascii();
			m_programmingFileData = file.writeBlock(logFile.data(), logFile.size());
			file.close();
		}
		else {
			KMessageBox::error(0, i18n("<qt>Unable to save log file<p>Please check permissions and try again</qt>"), i18n("Save Failed"));
		}
	}
}

#define UPDATEDISPLAY_TIMEOUT		m_connectionActiveAndValid = false;														\
					m_tickerState = 0;																\
					m_commHandlerState = ModeIdle_StateStatusRequest;												\
					m_commHandlerMode = ModeIdle;															\
					m_socket->clearIncomingData();															\
					setStatusMessage(i18n("Server ping timeout.  Please verify the status of your network connection."));						\
					m_updateTimeoutTimer->start(NETWORK_COMM_TIMEOUT_MS, TRUE);											\
					m_connectionMutex->unlock();															\
					return;

#define SET_WATCHDOG_TIMER		if (!m_updateTimeoutTimer->isActive()) m_updateTimeoutTimer->start(NETWORK_COMM_TIMEOUT_MS, TRUE);
#define PAT_WATCHDOG_TIMER		m_updateTimeoutTimer->stop(); m_updateTimeoutTimer->start(NETWORK_COMM_TIMEOUT_MS, TRUE);

#define SET_NEXT_STATE(x)		m_commHandlerState = x;	

#define EXEC_NEXT_STATE_IMMEDIATELY	m_forcedUpdateTimer->start(0, TRUE);

void FPGAProgramPart::setTickerMessage(TQString message) {
	m_connectionActiveAndValid = true;
	TQString tickerChar;
	switch (m_tickerState) {
		case 0:
			tickerChar = "-";
			break;
		case 1:
			tickerChar = "\\";
			break;
		case 2:
			tickerChar = "|";
			break;
		case 3:
			tickerChar = "/";
			break;
	}
	setStatusMessage(message + TQString("... %1").arg(tickerChar));
	m_tickerState++;
	if (m_tickerState > 3) {
		m_tickerState = 0;
	}
}

void FPGAProgramPart::mainEventLoop() {
	TQDataStream ds(m_socket);
	ds.setPrintableData(true);

	if (!m_connectionMutex->tryLock()) {
		EXEC_NEXT_STATE_IMMEDIATELY
		return;
	}

	if (m_socket) {
		if (m_commHandlerMode == ModeIdle) {
			// Normal operation
			switch (m_commHandlerState) {
				case ModeIdle_StateStatusRequest:
					// Get status of remote system
					// Clear buffers to synchronize frames in case of data corruption
					m_socket->clearIncomingData();
					ds << TQString("STATUS");
					m_socket->writeEndOfFrame();

					SET_NEXT_STATE(ModeIdle_StateProcessStatus)
					break;
				case ModeIdle_StateProcessStatus:
					// Get all data
					if (m_socket->canReadFrame()) {
						PAT_WATCHDOG_TIMER

						TQString status;
						ds >> status;
						m_socket->clearFrameTail();

						if (status == "") {
							// Transfer probably failed
							UPDATEDISPLAY_TIMEOUT
						}
						else if (status == "IDLE") {
							// Do nothing
						}
						else if (status == "LOGMESSAGES") {
							EXEC_NEXT_STATE_IMMEDIATELY
							// Retrieve messages on next event loop
							SET_NEXT_STATE(ModeIdle_StateGetLogMessages);
							m_commHandlerNextState = ModeIdle_StateDelay;
							m_commHandlerNextMode = ModeIdle;
						}

						setTickerMessage(i18n("Connected"));
						processLockouts();

						if (m_commHandlerState == ModeIdle_StateProcessStatus) {
							m_pingDelayTimer->start(250, TRUE);
							SET_NEXT_STATE(ModeIdle_StateDelay);
						}
					}
					else {
						if (!m_updateTimeoutTimer->isActive()) {
							UPDATEDISPLAY_TIMEOUT
						}
					}
					break;
				case ModeIdle_StateDelay:
					// Let the client and server rest for a bit to lower CPU/network overhead
					if (!m_pingDelayTimer->isActive()) {
						EXEC_NEXT_STATE_IMMEDIATELY
						// Execute query on next event loop
						SET_NEXT_STATE(ModeIdle_StateStatusRequest);
					}
					PAT_WATCHDOG_TIMER
					break;
				case ModeIdle_StateGetLogMessages:
					// Get new log messages
					if (m_socket->canReadFrame()) {
						PAT_WATCHDOG_TIMER

						TQString messages;
						ds >> messages;
						m_socket->clearFrameTail();

						if (messages == "") {
							// Transfer probably failed
							UPDATEDISPLAY_TIMEOUT
						}
						else {
							m_base->programmingLogBox->append(messages);

							m_pingDelayTimer->start(250, TRUE);
							SET_NEXT_STATE(m_commHandlerNextState);
							m_commHandlerMode = m_commHandlerNextMode;
						}
					}
					else {
						if (!m_updateTimeoutTimer->isActive()) {
							UPDATEDISPLAY_TIMEOUT
						}
					}
					break;
			}
		}
		else if (m_commHandlerMode == ModeProgramming) {
			// Program mode!
			if (m_commHandlerState == ModeProgramming_StateReadFile) {
				m_base->programmingStatusLabel->setText(i18n("Reading input file") + "...");
				TQFile file(m_base->programmingInputFile->url());
				if (file.open(IO_ReadOnly)) {
					m_programmingFileData = file.readAll();
					file.close();

					// Busy indicator
					m_base->programmingProgressBar->setTotalSteps(0);
					m_base->programmingProgressBar->setProgress(0);

					// Transmit file to remote server
					m_base->programmingStatusLabel->setText(i18n("Sending data to server") + "...");
					ds << TQString("FILE");
					m_socket->writeEndOfFrame();
					m_programmingFileTotalSize = m_programmingFileData.size();
					m_programmingFileTransferredBytes = 0;
					ds << m_programmingFileTotalSize;
					m_socket->writeEndOfFrame();
					m_base->programmingProgressBar->setTotalSteps(m_programmingFileTotalSize);

					SET_NEXT_STATE(ModeProgramming_StateInitProgramming);
				}
				else {
					KMessageBox::error(0, i18n("<qt>Unable to open selected programming file</qt>"), i18n("Program Failed"));
					m_commHandlerMode = ModeIdle;
					SET_NEXT_STATE(ModeIdle_StateStatusRequest);
					m_base->programmingProgressBar->reset();
					processLockouts();
				}
				PAT_WATCHDOG_TIMER
			}
			else if (m_commHandlerState == ModeProgramming_StateInitProgramming) {
				TQ_ULONG bytesLeft = (m_programmingFileTotalSize-m_programmingFileTransferredBytes);
				TQ_ULONG bytesToTransfer = bytesLeft;
				TQ_ULONG maxTransferChunk = maximumSocketDataChunkSize();
				if (bytesToTransfer > maxTransferChunk) {
					bytesToTransfer = maxTransferChunk;
				}
				m_programmingFileTransferredBytes = m_programmingFileTransferredBytes + m_socket->writeBlock(m_programmingFileData.data()+m_programmingFileTransferredBytes, bytesToTransfer);
				m_socket->flush();
				m_base->programmingProgressBar->setProgress(m_programmingFileTransferredBytes);
				if (m_programmingFileTransferredBytes >= m_programmingFileTotalSize) {
					// Initiate programming
					m_base->programmingStatusLabel->setText(i18n("Programming device") + "...");
					ds << TQString("PROGRAM");
					m_socket->writeEndOfFrame();

					// Request status
					ds << TQString("STATUS");
					m_socket->writeEndOfFrame();

					// Busy indicator
					m_base->programmingProgressBar->setTotalSteps(0);
					m_base->programmingProgressBar->setProgress(0);

					SET_NEXT_STATE(ModeProgramming_StateWaitForCompletion);
				}
				else {
					setTickerMessage(i18n("Transmitting data"));
					EXEC_NEXT_STATE_IMMEDIATELY
				}
				PAT_WATCHDOG_TIMER
			}
			else if (m_commHandlerState == ModeProgramming_StateWaitForCompletion) {
				// Get response
				if (m_socket->canReadFrame()) {
					PAT_WATCHDOG_TIMER

					TQString result;
					ds >> result;

					if (result == "PROGRAMMING") {
						m_socket->clearFrameTail();
						setTickerMessage(i18n("Programming device"));
						// Request status
						ds << TQString("STATUS");
						m_socket->writeEndOfFrame();
					}
					else if (result == "LOGMESSAGES") {
						m_socket->clearFrameTail();
						EXEC_NEXT_STATE_IMMEDIATELY
						// Retrieve messages on next event loop
						SET_NEXT_STATE(ModeIdle_StateGetLogMessages);
						m_commHandlerMode = ModeIdle;
						m_commHandlerNextState = ModeProgramming_StateRequestStatus;
						m_commHandlerNextMode = ModeProgramming;
					}
					else if (result == "DONE") {
						TQ_INT32 retCode;
						TQString log;

						ds >> retCode;
						m_socket->clearFrameTail();

						if (retCode != 0) {
							// Error!
							m_commHandlerMode = ModeIdle;
							SET_NEXT_STATE(ModeIdle_StateStatusRequest);
							EXEC_NEXT_STATE_IMMEDIATELY
							m_base->programmingProgressBar->reset();
							KMessageBox::error(0, i18n("<qt>Programming process failure<p>Please see log for details</qt>"), i18n("Program Failed"));
							processLockouts();
						}
						else {
							// Done!
							m_commHandlerMode = ModeIdle;
							SET_NEXT_STATE(ModeIdle_StateStatusRequest);
							EXEC_NEXT_STATE_IMMEDIATELY
							m_base->programmingProgressBar->reset();
							processLockouts();
						}
					}
					else if (result == "ERROR") {
						// Error!
						m_socket->clearFrameTail();
						m_commHandlerMode = ModeIdle;
						SET_NEXT_STATE(ModeIdle_StateStatusRequest);
						EXEC_NEXT_STATE_IMMEDIATELY
						m_base->programmingProgressBar->reset();
						KMessageBox::error(0, i18n("<qt>Unknown error</qt>"), i18n("Program Failed"));
						processLockouts();
					}
					else if (result == "IDLE") {
						m_socket->clearFrameTail();
						setTickerMessage(i18n("Initializing..."));
						// Request status
						ds << TQString("STATUS");
						m_socket->writeEndOfFrame();
					}
					else {
						// Unknown response
						// Request status
						m_socket->clearFrameTail();
						ds << TQString("STATUS");
						m_socket->writeEndOfFrame();
						m_base->programmingProgressBar->reset();
						KMessageBox::error(0, i18n("<qt>Timeout</qt>"), i18n("Program Failed"));
						processLockouts();
					}
				}
				else {
					if (!m_updateTimeoutTimer->isActive()) {
						m_commHandlerMode = ModeIdle;
						SET_NEXT_STATE(ModeIdle_StateStatusRequest);
						EXEC_NEXT_STATE_IMMEDIATELY
						m_base->programmingProgressBar->reset();
						processLockouts();
						UPDATEDISPLAY_TIMEOUT
					}
				}
			}
			else if (m_commHandlerState == ModeProgramming_StateRequestStatus) {
				// Request status
				ds << TQString("STATUS");
				m_socket->writeEndOfFrame();
				SET_NEXT_STATE(ModeProgramming_StateWaitForCompletion);
				PAT_WATCHDOG_TIMER
			}
		}

		SET_WATCHDOG_TIMER
	}
	else {
		SET_NEXT_STATE(ModeIdle_StateStatusRequest);
		m_commHandlerMode = ModeIdle;
	}

	m_connectionMutex->unlock();
}

TDEAboutData* FPGAProgramPart::createAboutData() {
	return new TDEAboutData( APP_NAME, I18N_NOOP( APP_PRETTYNAME ), APP_VERSION );
}

} //namespace RemoteLab

#include "part.moc"
