// ScriptRunner.cpp: implementation of the CScriptRunner class.
//


#include "pch.h"
#include "ScriptRunner.h"
#include "BK001001.h"
#include "Platform.h"

#include <iterator>

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#define new DEBUG_NEW
#endif

// замедлитель. чтобы передаваемые символы не терялись
constexpr auto TICK_DELAY = 70;
// задержка начала выполнения скрипта в миллисекундах
constexpr auto SCRIPT_START_DELAY = 1000;

// управляющие символы
constexpr TCHAR SCRIPT_PREFIX = _T('\\');
constexpr TCHAR SCRIPT_SET_DELAY = _T('d'); // \d
constexpr TCHAR SCRIPT_SET_FIXDELAY = _T('e'); // \e<nn>]
constexpr TCHAR SCRIPT_SET_PRSDELAY = _T('p'); // \p<nn>]
constexpr TCHAR SCRIPT_STOP_KEY  = _T('s'); // \s
constexpr TCHAR SCRIPT_AR2_KEY   = _T('n'); // \n
constexpr TCHAR SCRIPT_ARG_KEY   = _T('a'); // \a<n>
constexpr TCHAR SCRIPT_NUM_BREAK   = _T(']'); // символ завершитель числа аргумента
// \a0 .. \a9 - индексы аргументов из списка аргументов

/////////////////////////////////////////////////////////////////////////////
// Construction/Destruction

CScriptRunner::CScriptRunner()
	: m_nScriptLinePos(0)
	, m_nScriptLineLen(0)
	, m_nScriptCurrTick(0)
	, m_nScriptPDCurrTick(0)
	, m_nTickDelay(SCRIPT_START_DELAY)
	, m_nTickPDDelay(1)
	, m_bStopButton(false)
	, m_bRus(false)
	, m_bIsAr2Press(false)
	, m_bPressDelay(false)
	, m_bRegularKey(false)
	, m_bHasScript(false)
{
}

CScriptRunner::~CScriptRunner()
{
	if (m_fileScript.m_pStream)
	{
		m_fileScript.Close();
	}

	m_listArgs.clear();
}


void CScriptRunner::SetScript(const fs::path &strScriptPath, const fs::path &strScriptFileName, bool bXlat)
{
	if (m_fileScript.m_pStream)
	{
		m_fileScript.Close();
	}

	m_bRus = bXlat;
	m_bHasScript = !strScriptFileName.empty();
	m_strScriptFile = strScriptPath / strScriptFileName;
}

void CScriptRunner::StopScript()
{
	SetScript(_T(""), _T(""), false);
	m_listArgs.clear();
}

void CScriptRunner::SetArgumentList(ScriptArgList &ArgList)
{
	m_listArgs.merge(ArgList);
}


void CScriptRunner::SetArgument(CString &strArg)
{
	m_listArgs.push_back(strArg);
}

bool CScriptRunner::RunScript()
{
	const auto pBoard = g_Platform.GetBoardPtr();
	ASSERT(pBoard);

	if (m_bHasScript && pBoard)   // Если скрипт задан, и если есть эмулируемое устройство
	{
		if (!m_fileScript.m_pStream) // если файл ещё не открыт, попробуем открыть
		{
			// если файл не открыт, откроем его
			if (!m_fileScript.Open(m_strScriptFile.c_str(), CFile::modeRead))
			{
				// Если файл скрипта не открывается, то реинициализируем обработчик
				// и выйдем с ошибкой
				StopScript();
				return false;
			}

			// Если скрипт открылся, инициализируем переменные
			m_strScriptLine.Empty();
			m_nScriptLinePos = m_nScriptLineLen = 0;
			m_nScriptCurrTick = GetTickCount(); // Запускаем таймер
			m_nTickDelay = SCRIPT_START_DELAY;
		}

		if (m_bStopButton)
		{
			m_bStopButton = false;
			pBoard->UnStopInterrupt();
		}

		if (m_bRegularKey)
		{
			if (m_bPressDelay)
			{
				if ((GetTickCount() - m_nScriptPDCurrTick) < m_nTickPDDelay)
				{
					return true;
				}

				m_bPressDelay = false;
			}

			pBoard->m_reg177716in |= 0100;      // снимем флаг нажатия клавиши
			m_bRegularKey = false;
		}

		// Если скрипт открыт и всё в порядке
		// Ждём определённое время перед запуском скрипта
		if ((GetTickCount() - m_nScriptCurrTick) < m_nTickDelay)
		{
			return true;
		}

		m_nScriptCurrTick = GetTickCount();
		m_nTickDelay = TICK_DELAY;

		if (m_nScriptLinePos < m_nScriptLineLen) // Если есть необработанные символы в строке
		{
			if (!CheckEscChar(m_strScriptLine[m_nScriptLinePos]))
			{
				// Передадим новый символ только если регистр БК 177660 готов
				// т.е. ждём, пока БК прочтёт следующий код из 177662
				if (!(pBoard->m_reg177660 & 0200))
				{
					// TRACE1("script send char: %c\n", m_strScriptLine[m_nScriptLinePos]);
					ParseNextChar(m_strScriptLine[m_nScriptLinePos++]);
				}
			}
		}
		else
		{
			// Если дошли до конца текущей строки,
			m_nScriptLinePos = 0;

			// прочитаем следующую
			if (!m_fileScript.ReadString(m_strScriptLine))
			{
				// Если строки закончились,
				// закроем файл и переинициализируем переменные
				StopScript();
				return false;
			}

			m_strScriptLine.TrimRight(_T('\r'));
			m_strScriptLine += _T('\n'); // добавим перевод каретки в конец строки
			m_nScriptLineLen = m_strScriptLine.GetLength();
		}

		return true;
	}

	return false;
}

// Проверка управляющих символов
// выход: true - управляющий символ обработан, и нужно выйти из обработчика
// false - нужно продолжить
bool CScriptRunner::CheckEscChar(TCHAR ch)
{
	if (ch == SCRIPT_PREFIX)
	{
		m_nScriptLinePos++; // пропускаем управляющий символ
		ch = m_strScriptLine[m_nScriptLinePos++]; // смотрим следующий

		if (_T('0') <= ch && ch <= _T('7')) // если сразу число, то это ввод скан кода
		{
			m_nScriptLinePos--; // первый символ тоже надо обрабатывать
			int v = ParseArg8();
			if (v < 0)
			{
				return false;
			}
			// !!! Вот здесь мы отсылаем новый сканкод не дожидаясь чтения
			// это неправильно, но я как-то не вижу способа сделать правильно
			SendKoiChar(v);
			return true;
		}

		switch (ch)
		{
		case SCRIPT_PREFIX:
			// это значит, что надо передать знак управляющего символа в качестве обычного символа
			m_nScriptLinePos--;
			return false;

		case SCRIPT_SET_DELAY:
			// это снова надо установить задержку
			m_nScriptCurrTick = GetTickCount();
			m_nTickDelay = SCRIPT_START_DELAY;
			m_bIsAr2Press = false;
			m_bPressDelay = false;
			return true;

		case SCRIPT_STOP_KEY:
			// эмуляция кнопки СТОП
			m_bStopButton = true;
			g_Platform.GetBoardPtr()->StopInterrupt();
			m_bIsAr2Press = false;
			m_bPressDelay = false;
			return true;

		case SCRIPT_AR2_KEY:
			// нажатие кнопки АР2
			m_bIsAr2Press = true;
			m_bPressDelay = false;
			return true;

		case SCRIPT_ARG_KEY:
			// получение аргумента
			ch = m_strScriptLine[m_nScriptLinePos++]; // смотрим следующий
			// для начала будем обрабатывать только 10 аргументов, если будет мало,
			// то надо будет усложнять и парсить число.
			if (_T('0') <= ch && ch <= _T('9')) // обработка аргумента
			{
				m_bIsAr2Press = false;
				m_bPressDelay = false;
				int index = ch - _T('0');

				if ((unsigned)index < m_listArgs.size())
				{
					auto pos = m_listArgs.begin();
					std::advance(pos, index);
					CString &strArg = *pos;

					if (!strArg.IsEmpty())
					{
						m_strScriptLine.Insert(m_nScriptLinePos, strArg);
						m_nScriptLineLen = m_strScriptLine.GetLength();
					}
				}

				return true;
			}

			m_nScriptLinePos--; // откатываемся назад
			// и проигнорируем
			return false;

		case SCRIPT_SET_FIXDELAY:
			// задание фиксированной задержки
		{
			int v = ParseArg();
			if (v < 0)
			{
				return false;
			}
			m_nScriptCurrTick = GetTickCount();
			m_nTickDelay = v * 1000;
			m_bIsAr2Press = false;
			m_bPressDelay = false;
			return true;
		}

		case SCRIPT_SET_PRSDELAY:
			// задание задержки удержания клавиши
		{
			int v = ParseArg();
			if (v < 0)
			{
				return false;
			}
			m_nScriptPDCurrTick = GetTickCount();
			m_nTickPDDelay = v * 1000;
			m_bPressDelay = true;
			return true;
		}

		}
	}

	return false;
}

// получение значения аргумента кода.
int CScriptRunner::ParseArg()
{
	int nCurPos = m_nScriptLinePos; // с этой позиции начинаем
	int value = 0;					// тут будет формироваться результат

	while (nCurPos < m_nScriptLineLen)
	{
		TCHAR ch = m_strScriptLine[nCurPos++]; // смотрим следующий
		if (ch == SCRIPT_NUM_BREAK)
		{
			m_nScriptLinePos = nCurPos;
			return value; // выходим из цикла
		}
		if (_T('0') <= ch && ch <= _T('9')) // обработка аргумента
		{
			value = value * 10 + (ch - _T('0'));
		}
		else
		{
			// синтаксическая ошибка
			// возвратим -1 и будем считать что за кодом - просто текст
			return -1;
		}
	}

	// дошли до конца строки и не встретили символ-завершитель, значит
	// синтаксическая ошибка
	// возвратим -1 и будем считать что за кодом - просто текст
	return -1;
}

int CScriptRunner::ParseArg8()
{
	int nCurPos = m_nScriptLinePos; // с этой позиции начинаем
	int value = 0;					// тут будет формироваться результат

	while (nCurPos < m_nScriptLineLen)
	{
		TCHAR ch = m_strScriptLine[nCurPos++]; // смотрим следующий
		if (ch == SCRIPT_NUM_BREAK)
		{
			m_nScriptLinePos = nCurPos;
			return value; // выходим из цикла
		}
		if (_T('0') <= ch && ch <= _T('7')) // обработка аргумента
		{
			value = value * 8 + (ch - _T('0'));
		}
		else
		{
			// синтаксическая ошибка
			// возвратим -1 и будем считать что за кодом - просто текст
			return -1;
		}
	}

	// дошли до конца строки и не встретили символ-завершитель, значит
	// синтаксическая ошибка
	// возвратим -1 и будем считать что за кодом - просто текст
	return -1;
}

void CScriptRunner::ParseNextChar(TCHAR ch)
{
	uint8_t koi_ch = Global::WIDEtoBKChar(ch);

	if (!m_bRus && koi_ch >= 0300) // если буквы русские, а режим - не русский
	{
		m_nScriptLinePos--; // откатим позицию
		koi_ch = BKKEY_RUS; // пошлём код переключения языка
		m_bRus = true; // включаем русский режим
	}
	else if (m_bRus && (0100 <= koi_ch && koi_ch <= 0177)) // если буквы не русские, а режим - русский
	{
		m_nScriptLinePos--; // откатим позицию
		koi_ch = BKKEY_LAT; // пошлём код переключения языка
		m_bRus = false; // выключаем русский режим
	}

	SendKoiChar(koi_ch);
}

void CScriptRunner::SendKoiChar(uint8_t ch)
{
	const auto pBoard = g_Platform.GetBoardPtr();
	pBoard->m_reg177662in = ch & 0177; // Отправим код символа в 177662
	pBoard->m_reg177716in &= ~0100;    // Установим флаг нажатия клавиши в 177716
	pBoard->m_reg177660 |= 0200;       // Установим состояние готовности в 177660
	g_Platform.KeyboardInterrupt(m_bIsAr2Press ? INTERRUPT_274 : INTERRUPT_60);
	m_bIsAr2Press = false;
	m_bRegularKey = true;
}

