//..............................................................................
//
// Replay Log layer
//

import "doc_Layer.jnc"
import "io_base.jncx"
import "io_MappedFile.jnc"
import "log_RecordFile.jnc"
import "sys_Timer.jnc"
import "ui_History.jnc"
import "ui_Dialog.jnc"

using extension ui.ComboBoxHistory;

//..............................................................................

class ReplayLogLayer: doc.Layer
{
protected:
	enum
	{
		UpdateProgressPeriod = 500 // update progress twice a second
	}

	enum Defaults
	{
		ReplayTx         = true,
		ReplayRx         = false,
		ReplayCustomCode = false,
	}

	enum ActionId
	{
		Browse,
		Play,
		_Count,
	}

	io.MappedFile m_file;
	log.RecordFileHdr const* m_hdr;
	log.Record m_nextRecord;
	uint64_t m_offset;
	uint64_t m_prevRecordOffset;
	uint64_t m_firstRecordTimestamp;
	uint64_t m_baseTimestamp;
	uint64_t m_customRecordCode;
	sys.Timer m_progressTimer;
	sys.Timer m_transmitTimer;
	uint_t m_syncId;

	ui.Action* m_actionTable[ActionId._Count];
	ui.ComboBox* m_fileNameCombo;
	ui.FileProperty* m_fileNameProp;
	ui.BoolProperty* m_replayTxProp;
	ui.BoolProperty* m_replayRxProp;
	ui.BoolProperty* m_replayCustomCodeProp;
	ui.StringProperty* m_customCodeProp;
	ui.FileDlg m_fileDlg;
	ui.ProgressDlg m_progressDlg;
	reactor m_uiReactor;

public:
	construct(doc.PluginHost* pluginHost);

	override void load(doc.Storage* storage);
	override void save(doc.Storage* storage);
	override void updateProperties();
	override bool errorcode applyProperties();
	override void restoreDefaultProperties();

protected:
	void createUi();
	void stop();
	void close();
	void handleError();
	bool errorcode readLoop();
	bool errorcode sendNextRecord();

	void onBrowse();
	void onFileDlgCompleted(ui.StdDlgButton button);
	void onPlay();
	void onTransmitTimer(uint_t syncId);
	void onProgressTimer(uint_t syncId);

	void onProgressDlgButtonClicked(ui.StdDlgButton button)
	{
		close(); // any button essentialy ends the replay
	}
}

// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

ReplayLogLayer.construct(doc.PluginHost* pluginHost)
{
	basetype1.construct(pluginHost);

	m_fileDlg.m_onCompleted += onFileDlgCompleted;
	m_progressDlg.m_onButtonClicked += onProgressDlgButtonClicked;

	createUi();
}

void ReplayLogLayer.load(doc.Storage* storage)
{
	m_fileNameCombo.loadHistory(storage, "fileNameHistory");
	m_fileNameCombo.m_editText = storage.readString("fileName");
	m_replayTxProp.m_value = storage.readBool("replayTx", Defaults.ReplayTx);
	m_replayRxProp.m_value = storage.readBool("replayRx", Defaults.ReplayRx);
	m_replayCustomCodeProp.m_value = storage.readBool("replayCustomCode", Defaults.ReplayCustomCode);
	m_customCodeProp.m_value = storage.readString("customCode", m_customCodeProp.m_value);
	m_customRecordCode = strtoul(m_customCodeProp.m_value, null, 0);
}

void ReplayLogLayer.save(doc.Storage* storage)
{
	m_fileNameCombo.saveHistory(storage, "fileNameHistory");
	storage.writeString("fileName", m_fileNameCombo.m_editText);
	storage.writeBool("replayTx", m_replayTxProp.m_value);
	storage.writeBool("replayRx", m_replayRxProp.m_value);
	storage.writeBool("replayCustomCode", m_replayCustomCodeProp.m_value);
	storage.writeString("customCode", m_customCodeProp.m_value);
}

void ReplayLogLayer.updateProperties()
{
	m_fileNameProp.m_value = m_fileNameCombo.m_editText;
}

bool errorcode ReplayLogLayer.applyProperties()
{
	m_fileNameCombo.m_editText = m_fileNameProp.m_value;
	m_customRecordCode = strtoul(m_customCodeProp.m_value, null, 0);
	return true;
}

void ReplayLogLayer.restoreDefaultProperties()
{
	m_replayTxProp.m_value = Defaults.ReplayTx;
	m_replayRxProp.m_value = Defaults.ReplayRx;
	m_replayCustomCodeProp.m_value = Defaults.ReplayCustomCode;
}

void ReplayLogLayer.createUi()
{
	m_pluginHost.m_toolBar.addSeparator();

	m_pluginHost.m_toolBar.addLabel("File:");
	m_fileNameCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_fileNameCombo.m_isEditable = true;
	m_fileNameCombo.m_onEnter += onPlay;
	m_fileNameCombo.m_placeholderText = "Enter a log file name...";

	m_actionTable[ActionId.Browse] = createAction("Browse", "images/browse.png");
	m_actionTable[ActionId.Browse].m_onTriggered += onBrowse;
	m_actionTable[ActionId.Play] = createAction("Replay log", "images/play.png");
	m_actionTable[ActionId.Play].m_onTriggered += onPlay;

	m_fileNameProp = m_pluginHost.m_propertyGrid.createFileProperty("File name", "Path to a *.njlog file");
	m_replayTxProp = m_pluginHost.m_propertyGrid.createBoolProperty("Replay TX", "Replay TX records");
	m_replayRxProp = m_pluginHost.m_propertyGrid.createBoolProperty("Replay RX", "Replay RX records");

	m_replayCustomCodeProp = m_pluginHost.m_propertyGrid.createBoolProperty(
		"Replay custom code",
		"Replay log records with the specificed record code value"
		);

	m_customCodeProp = m_pluginHost.m_propertyGrid.createStringProperty(
		m_replayCustomCodeProp,,
		"Custom code",
		"Enter a log record code...",
		"Specify a custom log record code to replay"
		);

	m_customCodeProp.m_value = "0x0000000000000000";

	m_uiReactor.start();
}

void ReplayLogLayer.stop()
{
	m_file.close();
	m_progressTimer.stop();
	m_transmitTimer.stop();
	m_syncId++;
}

void ReplayLogLayer.close()
{
	stop();

	m_progressDlg.hide();

	m_hdr = null;
	m_offset = 0;
	m_prevRecordOffset = 0;
	m_firstRecordTimestamp = 0;
	m_baseTimestamp = 0;
}

void ReplayLogLayer.handleError()
{
	stop();

	m_progressDlg.m_status = $"<font color=#800>$(std.getLastErrorDescription ())</font>";
	m_progressDlg.m_buttons = ui.StdDlgButtons.Close;
}

bool errorcode ReplayLogLayer.readLoop()
{
	while (m_offset < m_hdr.m_totalRecordSize)
	{
		log.Record const* record = (log.Record const*) m_file.view(
			m_hdr.m_recordOffset + m_offset,
			sizeof(log.Record)
			);

		if (record.m_signature != log.RecordSignature)
		{
			std.setError("Invalid log file format (bad file signature)");
			return false;
		}

		bool isReplayed;

		switch (record.m_code)
		{
		case log.StdRecordCode.Tx:
			isReplayed = m_replayTxProp.m_value;
			break;

		case log.StdRecordCode.Rx:
			isReplayed = m_replayRxProp.m_value;
			break;
		}

		if (m_replayCustomCodeProp.m_value && record.m_code == m_customRecordCode)
			isReplayed = true;

		if (!isReplayed)
		{
			m_offset += sizeof(log.Record) + record.m_dataSize;
			continue;
		}

		m_nextRecord = *record;

		if (m_baseTimestamp)
		{
			m_transmitTimer.startDueTimer(
				onTransmitTimer~(m_syncId) @ m_pluginHost.m_mainThreadScheduler,
				m_baseTimestamp + record.m_timestamp - m_firstRecordTimestamp
				);

			return true;
		}

		m_baseTimestamp = sys.getTimestamp();
		m_firstRecordTimestamp = record.m_timestamp;
		sendNextRecord();

		m_progressTimer.startPeriodicTimer(
			onProgressTimer~(m_syncId) @ m_pluginHost.m_mainThreadScheduler,
			UpdateProgressPeriod
			);
	}

	stop();

	m_progressDlg.m_status = "Replay completed";
	m_progressDlg.m_progress = 100;
	m_progressDlg.m_buttons = ui.StdDlgButtons.Ok;
	return true;
}

bool errorcode ReplayLogLayer.sendNextRecord()
{
	void const* p = m_file.view(
		m_hdr.m_recordOffset + m_offset + sizeof(log.Record),
		m_nextRecord.m_dataSize
		);

	transmit(p, m_nextRecord.m_dataSize);
	m_prevRecordOffset = m_offset;
	m_offset += sizeof(log.Record) + m_nextRecord.m_dataSize;
	return true;
}

void ReplayLogLayer.onBrowse()
{
	m_fileDlg.m_kind = ui.FileDlgKind.Open;
	m_fileDlg.m_title = "Replay Log File...";
	m_fileDlg.m_filter = "Log files (*.njlog);;All files (*.*)";
	m_fileDlg.show();
}

void ReplayLogLayer.onFileDlgCompleted(ui.StdDlgButton button)
{
	if (button == ui.StdDlgButton.Ok)
		m_fileNameCombo.m_editText = m_fileDlg.m_filePath;
}

void ReplayLogLayer.onPlay()
{
	close();

	m_fileNameCombo.addEditTextToHistory();
	char const* fileName = m_fileNameCombo.m_editText;

	m_progressDlg.m_title = fileName;
	m_progressDlg.m_status = "Initializing...";
	m_progressDlg.m_progress = 0;
	m_progressDlg.m_buttons = ui.StdDlgButtons.Abort;
	m_progressDlg.show();

	m_file.open(fileName, io.FileOpenFlags.OpenExisting | io.FileOpenFlags.ReadOnly);

	m_hdr = (log.RecordFileHdr const*) m_file.view(0, sizeof(log.RecordFileHdr), true);
	if (m_hdr.m_signature != log.RecordFileSignature)
	{
		std.setError("Invalid log file format (bad file signature)");
		throw;
	}

	if (!m_hdr.m_totalRecordSize)
	{
		std.setError("Log file is empty");
		throw;
	}

	m_progressDlg.m_status = "Replay in progress...";
	readLoop();

catch:
	handleError();
}

void ReplayLogLayer.onTransmitTimer(uint_t syncId)
{
	if (syncId != m_syncId)
		return;

	sendNextRecord();
	readLoop();

catch:
	handleError();
}

void ReplayLogLayer.onProgressTimer(uint_t syncId)
{
	if (syncId != m_syncId || m_nextRecord.m_timestamp == m_firstRecordTimestamp)
		return;

	uint64_t timestamp = sys.getTimestamp();

	double timeFraction = (double)(timestamp - m_baseTimestamp) / (m_nextRecord.m_timestamp - m_firstRecordTimestamp);
	double offset = m_prevRecordOffset + (m_offset - m_prevRecordOffset) * timeFraction;

	m_progressDlg.m_progress = (int)(offset / m_hdr.m_totalRecordSize * 100);
}

reactor ReplayLogLayer.m_uiReactor
{
	m_actionTable[ActionId.Play].m_isEnabled = m_isTransmitEnabled && strlen(m_fileNameCombo.m_editText);
	m_customCodeProp.m_isEnabled = m_replayCustomCodeProp.m_value;
}

//..............................................................................
