// ITCPC.cpp: определяет точку входа для консольного приложения.
//

#include "pch.h"
#include "bk2ibm.h"
#include "BKASCFile.h"
#include "BKFile.h"
#include "getopt.h"
#include "ibm2bk.h"
#include "ibm2ibm.h"
#include "ibm2vxt.h"
#include "ITCPC.h"
#include "versionNo.h"
#include "WinFile.h"
#include <locale>
#include <regex>
#include <format>
#include "StringUtil.h"

namespace ITCPC
{
	static std::_tstring &ReplaceString(std::_tstring &subject, const std::_tstring &search,
	                                    const std::_tstring &replace)
	{
		size_t pos = 0;

		while ((pos = subject.find(search, pos)) != std::_tstring::npos)
		{
			subject.replace(pos, search.length(), replace);
			pos += replace.length();
		}

		return subject;
	}


	workMode WorkParam{};
}

int _tmain(int argc, TCHAR *argv[], TCHAR *envp[])
{
	setlocale(LC_ALL, "ru_RU");
	int nRetCode = 0;
	static struct option long_options[] =
	{
		{ _T("help"),        no_argument,       nullptr, _T('h') },
		{ _T("version"),     no_argument,       nullptr, _T('v') },
		{ _T("input"),       required_argument, nullptr, _T('i') },
		{ _T("output"),      required_argument, nullptr, _T('o') },
		{ _T("tab"),         required_argument, nullptr, _T('t') },
		{ _T("recursive"),   no_argument,       nullptr, _T('r') },
		{ _T("InputKOI"),    required_argument, nullptr, _T('I') },
		{ _T("OutputKOI"),   required_argument, nullptr, _T('O') },
		{ nullptr,           no_argument,       nullptr, 0 }
	};
	static TCHAR optstring[] = _T("hvi:o:t:rI:O:");
	ITCPC::WorkParam.chInM = _T('A');
	ITCPC::WorkParam.chOutM = _T('U');

	if (argc < 2)
	{
		// если запускаем без аргументов, то сразу вывести подсказку и выйти.
		ITCPC::Usage();
		return nRetCode;
	}

	std::_tstring strTmp;
	int option_index = 0;
	int c{};

	while ((c = getopt_long(argc, argv, optstring, long_options, &option_index)) != -1)
	{
		// Handle options
		switch (c)
		{
			case _T('i'):
				strTmp = (optarg) ? std::_tstring(optarg) : _T("A");

				if (!strTmp.empty())
				{
					ITCPC::WorkParam.chInM = std::toupper(strTmp.at(0), std::locale()); // оставим только первую букву
				}

				break;

			case _T('o'):
				strTmp = (optarg) ? std::_tstring(optarg) : _T("U");

				if (!strTmp.empty())
				{
					ITCPC::WorkParam.chOutM = std::toupper(strTmp.at(0), std::locale()); // оставим только первую букву
				}

				break;

			case _T('t'):
				if (optarg)
				{
					strTmp = std::_tstring(optarg);
					ITCPC::WorkParam.nTabWidth = std::stoi(strTmp);
				}

				break;
			case _T('I'):	// задать тип таблицы кои8 для входного файла
			{
				const auto strTmp = (optarg) ? std::_tstring(optarg) : _T("");
				if (!strTmp.empty())
				{
					switch (strTmp.at(0))
					{
						case _T('0'):
							ITCPC::WorkParam.nInpKOI = 0;
							break;
						case _T('1'):
							ITCPC::WorkParam.nInpKOI = 1;
							break;
						case _T('2'):
							ITCPC::WorkParam.nInpKOI = 2;
							break;
					}
					// неверный аргумент игнорируем
				}
			}
			break;
			case _T('O'):	// задать тип таблицы кои8 для выходного файла
			{
				const auto strTmp = (optarg) ? std::_tstring(optarg) : _T("");
				if (!strTmp.empty())
				{
					switch (strTmp.at(0))
					{
						case _T('0'):
							ITCPC::WorkParam.nOutKOI = 0;
							break;
						case _T('1'):
							ITCPC::WorkParam.nOutKOI = 1;
							break;
						case _T('2'):
							ITCPC::WorkParam.nOutKOI = 2;
							break;
					}
					// неверный аргумент игнорируем
				}
			}
			break;
			case _T('r'):
				ITCPC::WorkParam.bRecursive = true;
				break;

			case _T('h'):
				ITCPC::Usage();
				return nRetCode;

			case _T('v'):
				ITCPC::PrintVersion();
				return nRetCode;
		}
	}

	// проверяем параметры на корректность.
	if (!ITCPC::CheckInputParams())
	{
		ITCPC::Usage();
		return nRetCode;
	}

	argc -= optind;
	argv += optind;
	std::_tcout << _T("Преобразование ") << ITCPC::GetFormatName(ITCPC::WorkParam.chInM) << _T(" -> ") << ITCPC::GetFormatName(ITCPC::WorkParam.chOutM) << std::endl;

	if (ITCPC::WorkParam.chInM == ITCPC::WorkParam.chOutM)
	{
		std::_tcout << _T("Бессмысленное преобразование делать не будем.\n");
		return nRetCode;
	}

	//  if (ITCPC::WorkParam.chInM == _T('A') && (ITCPC::WorkParam.chOutM != _T('E') && ITCPC::WorkParam.chOutM != _T('V')))
	//  {
	//      std::_tcout << _T("Подозрительное преобразование на всякий случай делать не будем.\n");
	//      return nRetCode;
	//  }
	// из бейсик аск в вортекс преобразование невозможно
	// т.к. в аск потоке не реализован просмотр вперёд и произвольное перемещение по потоку.
	if (ITCPC::WorkParam.chInM == _T('B') && ITCPC::WorkParam.chOutM == _T('V'))
	{
		std::_tcout << _T("Такое преобразование невозможно по техническим причинам.\n");
		return nRetCode;
	}

	int nFilesCount = 0; // счётчик обработанных файлов

	for (; argc > 0; argc--, argv++) // теперь, пока аргументы не кончатся - обрабатывать
	{
		// достанем из аргумента путь, если он есть
		fs::path filePath = fs::current_path(); // предполагаем базовый путь
		fs::path srcArg = strUtil::makePath(std::_tstring(*argv)); // преобразуем в путь аргумент

		if (srcArg.is_absolute()) // абсолютный путь задан?
		{
			filePath = srcArg.parent_path(); // да - тогда берём его
		}
		else if (srcArg.is_relative() && srcArg.has_parent_path()) // относительный путь есть?
		{
			filePath /= srcArg.parent_path(); // да - тогда присоединяем его к базовому
		}

		// делаем из шаблона файла регулярное выражение
		std::_tstring mask = srcArg.filename()._tstring();
		mask = ITCPC::ReplaceString(mask, _T("."), _T("[.]"));
		mask = ITCPC::ReplaceString(mask, _T("*"), _T(".*"));
		mask = ITCPC::ReplaceString(mask, _T("?"), _T("."));
		mask = _T("^") + mask + _T("$"); // явно обозначим начало и конец
		std::_tregex fileMask(mask, std::regex_constants::icase);
		// теперь получим все файлы из директории.
		// эта штука не умеет выбирать из директории только нужные файлы, поэтому приходится
		// фильтровать вручную с помощью регулярных выражений
		std::error_code ec;

		if (ITCPC::WorkParam.bRecursive)
		{
			fs::recursive_directory_iterator dit(filePath, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec);

			if (ec)
			{
				std::cout << "Ошибка: '" << ec.message() << "'\n";
			}
			else
			{
				for (auto const &it : dit)
				{
					if (fs::is_regular_file(it.path()) && std::regex_match(it.path().filename()._tstring(), fileMask))
					{
						ITCPC::Process(it.path());
						nFilesCount++;
					}
				}
			}
		}
		else
		{
			fs::directory_iterator dit(filePath, fs::directory_options::skip_permission_denied | fs::directory_options::follow_directory_symlink, ec);

			if (ec)
			{
				std::cout << "Ошибка: '" << ec.message() << "'\n";
			}
			else
			{
				for (auto const &it : dit)
				{
					if (fs::is_regular_file(it.path()) && std::regex_match(it.path().filename()._tstring(), fileMask))
					{
						ITCPC::Process(it.path());
						nFilesCount++;
					}
				}
			}
		}
	}

	std::_tcout << _T("Готово, обработано ") << nFilesCount << _T(" файлов.\n");
	return nRetCode;
}

void ITCPC::Process(const fs::path &file)
{
	// делаем имя входного файла
	WorkParam.strInFileName = file; // полный путь к имени файла
	// делаем имя выходного файла.
	MakeOutName();
	std::_tcout << _T('\'') << WorkParam.strInFileName._tstring() << _T("' -> '") << WorkParam.strOutFileName.filename() << _T("'\n");
	std::unique_ptr <CBaseFile> pInFile;
	std::unique_ptr <CBaseFile> pOutFile;

	switch (WorkParam.chInM)
	{
		default:
		case _T('A'):
			pInFile = std::make_unique<CWinFile>(WorkParam.strInFileName);
			break;

		case _T('U'):
			pInFile = std::make_unique<CWinFile>(WorkParam.strInFileName, CWinFile::FILE_CHARSET::UTF16LE);
			break;

		case _T('W'):
			pInFile = std::make_unique<CWinFile>(WorkParam.strInFileName, CWinFile::FILE_CHARSET::CP1251);
			break;

		case _T('O'):
			pInFile = std::make_unique<CWinFile>(WorkParam.strInFileName, CWinFile::FILE_CHARSET::CP866);
			break;

		case _T('K'):
			pInFile = std::make_unique<CWinFile>(WorkParam.strInFileName, CWinFile::FILE_CHARSET::KOI8);
			break;

		case _T('E'):
			pInFile = std::make_unique <CBKFile>(WorkParam.strInFileName);
			break;

		case _T('V'):
			pInFile = std::make_unique <CBKFile>(WorkParam.strInFileName);
			break;

		case _T('B'):
			pInFile = std::make_unique <CBKASCFile>(WorkParam.strInFileName);
			break;
	}

	if (pInFile)
	{
		if (TABWIDTH_MIN <= WorkParam.nTabWidth && WorkParam.nTabWidth <= TABWIDTH_MAX)
		{
			pInFile->setTabWidth(WorkParam.nTabWidth);
		}
		pInFile->setKoiTbl(WorkParam.nInpKOI, WorkParam.nOutKOI);

		switch (WorkParam.chOutM)
		{
			default:
			case _T('U'):
				pOutFile = std::make_unique <CWinFile>(WorkParam.strOutFileName, CWinFile::FILE_CHARSET::UTF16LE);

				if (std::_tstring(_T("AWOK")).find(WorkParam.chInM) != std::_tstring::npos)
				{
					IBM2IBM(std::move(pInFile), std::move(pOutFile));
				}
				else
				{
					BK2IBM(std::move(pInFile), std::move(pOutFile));
				}

				break;

			case _T('8'):
				pOutFile = std::make_unique <CWinFile>(WorkParam.strOutFileName, CWinFile::FILE_CHARSET::UTF8);

				if (std::_tstring(_T("AUWOK")).find(WorkParam.chInM) != std::_tstring::npos)
				{
					IBM2IBM(std::move(pInFile), std::move(pOutFile));
				}
				else
				{
					BK2IBM(std::move(pInFile), std::move(pOutFile));
				}

				break;

			case _T('W'):
				pOutFile = std::make_unique <CWinFile>(WorkParam.strOutFileName, CWinFile::FILE_CHARSET::CP1251);

				if (std::_tstring(_T("AUOK")).find(WorkParam.chInM) != std::_tstring::npos)
				{
					IBM2IBM(std::move(pInFile), std::move(pOutFile));
				}
				else
				{
					BK2IBM(std::move(pInFile), std::move(pOutFile));
				}

				break;

			case _T('O'):
				pOutFile = std::make_unique <CWinFile>(WorkParam.strOutFileName, CWinFile::FILE_CHARSET::CP866);

				if (std::_tstring(_T("AUWK")).find(WorkParam.chInM) != std::_tstring::npos)
				{
					IBM2IBM(std::move(pInFile), std::move(pOutFile));
				}
				else
				{
					BK2IBM(std::move(pInFile), std::move(pOutFile));
				}

				break;

			case _T('K'):
				pOutFile = std::make_unique <CWinFile>(WorkParam.strOutFileName, CWinFile::FILE_CHARSET::KOI8);
				pOutFile->setKoiTbl(WorkParam.nInpKOI, WorkParam.nOutKOI);

				if (std::_tstring(_T("AUWO")).find(WorkParam.chInM) != std::_tstring::npos)
				{
					IBM2IBM(std::move(pInFile), std::move(pOutFile));
				}
				else
				{
					BK2IBM(std::move(pInFile), std::move(pOutFile));
				}

				break;

			case _T('E'):
				pOutFile = std::make_unique <CBKFile>(WorkParam.strOutFileName);
				pOutFile->setKoiTbl(WorkParam.nInpKOI, WorkParam.nOutKOI);
				IBM2BK(std::move(pInFile), std::move(pOutFile));
				break;

			case _T('V'):
				pOutFile = std::make_unique <CBKFile>(WorkParam.strOutFileName);
				pOutFile->setKoiTbl(WorkParam.nInpKOI, WorkParam.nOutKOI);
				IBM2VXT(std::move(pInFile), std::move(pOutFile));
				break;

			case _T('B'):
			{
				auto pOutFile1 = std::make_unique <CBKASCFile>(WorkParam.strOutFileName);
				pOutFile1->setKoiTbl(WorkParam.nInpKOI, WorkParam.nOutKOI);
				TXT2ASC(std::move(pInFile), std::move(pOutFile1));
			}
			break;
		}
	}
	else
	{
		std::_tcout << _T("Недостаточно памяти.\n");
	}
}

void ITCPC::Usage()
{
	std::_tcout << (_T(FILE_PRODUCT_STR"\n") \
	                _T("Использование:\n") \
	                _T("ITCPC -h (--help)\n") \
	                _T("  Вывод этой справки.\n\n") \
	                _T("ITCPC -v (--version)\n") \
	                _T("  Показать информацию о версии.\n\n") \
	                _T("ITCPC [-i<in_mode>][-o<out_mode>][-t<n>][-r] <InpFile1.ext> {...<InpFileN.ext>}\n") \
	                _T("  -i<in_mode> (--input <in_mode>) - задать формат входного файла.\n") \
	                _T("  Форматы: a - автоопределение (по умолчанию, если параметр отсутствует).\n") \
	                _T("               Не работает для БКшных файлов. Они опознаются как файл в KOI8R,\n") \
	                _T("               и будут обработаны неправильно.\n") \
	                _T("           b - Basic ASC файлы. InputFile1.ext должен быть заголовочным\n") \
	                _T("               .ASC файлом в .bin формате.\n") \
	                _T("           u - Unicode UTF-16LE или Unicode UTF-8, форматы корректно\n") \
	                _T("               определяются даже без BOM.\n") \
	                _T("           w - Windows CP-1251\n") \
	                _T("           o - OEM CP-866\n") \
	                _T("           k - KOI8R\n") \
	                _T("           e - БКшный файл в формате Edasp\n") \
	                _T("           v - БКшный файл в формате Vortex\n\n") \
	                _T("  -o<out_mode> (--output <out_mode>) - задать формат выходного файла.\n") \
	                _T("  Форматы: u - Unicode UTF-16LE (по умолчанию, если параметр отсутствует)\n") \
	                _T("           8 - Unicode UTF-8\n") \
	                _T("           w - Windows CP-1251\n") \
	                _T("           o - OEM CP-866\n") \
	                _T("           k - KOI8R\n") \
	                _T("           e - БКшный файл в формате Edasp\n") \
	                _T("           v - БКшный файл в формате Vortex\n") \
	                _T("           b - Basic ASC файлы (Имеет смысл только для листингов текстов\n") \
	                _T("               на Бейсике).\n\n") \
					_T("  -I<n> (--InputKOI <n>) - задать таблицу КОИ-8 для входного файла (ключ имеет\n") \
					_T("      смысл только когда входной файл в кодировке КОИ-8\n") \
					_T("  -O<n> (--OutputKOI <n>) - задать таблицу КОИ-8 для выходного файла (ключ имеет\n") \
					_T("      смысл только когда выходной файл в кодировке КОИ-8\n") \
					_T("      Возможные значения:\n") \
					_T("        0 - кодовая таблица КОИ8 БК0010 (по умолчанию)\n") \
					_T("        1 - кодовая таблица КОИ8 БК0011М\n") \
					_T("        2 - кодовая таблица КОИ8 RFC1489\n") \
					_T("      Этими ключами можно перекодировывать псевдографику из одного вида КОИ в другой.\n\n") \
	                _T("  -t<n> (--tab <n>) - задать ширину табулятора для входного файла.\n") \
	                _T("      Допускаются значения от 2 до 16, остальные игнорируются.\n") \
	                _T("      Принятые значения по умолчанию: для файлов Windows tab = 4,\n") \
	                _T("      для файлов БК tab = 8.\n\n") \
	                _T("  -r (--recursive) - рекурсивный обход всех подкаталогов.\n\n") \
	                _T("  { InpFile.ext } - список входных файлов. Допускается использование масок и\n") \
	                _T("      задание путей, как абсолютных, так и относительных.\n"));
}

void ITCPC::PrintVersion()
{
	std::_tcout << _T(FILE_PRODUCT_STR"\n");
	const auto strVers = std::format(_T("Версия: {}.{}.{}.{}\n"), _T(VERSION_MAJ_STR), _T(VERSION_MIN_STR), _T(VERSION_REV_STR), _T(VERSION_BLD_STR));
	std::_tcout << strVers;
	// ревизия и билд могут начинаться с нуля, но они тем не менее десятичные числа
	// поэтому слегка поизвращаемся.
	std::_tstring str = _T(VERSION_MIN_STR);
	const int vmin = std::stol(str, nullptr, 10);
	str = _T(VERSION_REV_STR);
	const int rev = std::stol(str, nullptr, 10);
	str = _T(VERSION_BLD_STR);
	const int bld = std::stol(str, nullptr, 10);
	const auto strDate = std::format(_T(" {:02d}-{:02d}-{:04d}"), (rev / 100), vmin, (2000 + VERSION_MAJ));
	const auto strTime = std::format(_T(" {:02d}:{:02d}:{:02d}"), (rev % 100), (bld / 100), (bld % 100));
	std::_tcout << _T("Дата сборки:") << strDate << strTime << std::endl;
}

bool ITCPC::CheckInputParams()
{
	static std::_tstring strInP{ _T("ABUWOKEV") };
	static std::_tstring strOutP{ _T("8BUWOKEV") };

	if (strInP.find(WorkParam.chInM) == std::_tstring::npos)
	{
		return false;
	}

	if (strOutP.find(WorkParam.chOutM) == std::_tstring::npos)
	{
		return false;
	}

	return true;
}

std::_tstring ITCPC::GetFormatName(const TCHAR ch)
{
	switch (ch)
	{
		case _T('A'):
			return { _T("Авто") };
		case _T('U'):
			return { _T("Unicode UTF-16LE") };
		case _T('W'):
			return { _T("Windows 1251") };
		case _T('O'):
			return { _T("OEM CP-866") };
		case _T('K'):
			return { _T("KOI8R") };
		case _T('E'):
			return { _T("Edasp") };
		case _T('V'):
			return { _T("Vortex") };
		case _T('8'):
			return { _T("Unicode UTF-8") };
		case _T('B'):
			return { _T("Basic ASC") };
	}

	return { _T("Неизвестно") };
}

// делаем имя выходного файла
void ITCPC::MakeOutName()
{
	// сперва скопируем имя файла
	WorkParam.strOutFileName = WorkParam.strInFileName;
	// если есть расширение, найдём его
	std::_tstring outExt;

	switch (WorkParam.chOutM)
	{
		case _T('E'):
			outExt = _T(".edp");
			break;

		case _T('V'):
			outExt = _T(".vxt");
			break;

		case _T('B'):
			outExt = _T(".asc");
			break;

		default:
			outExt = _T(".txt");
			break;
	}

	if (WorkParam.strOutFileName.has_extension()) // если расширение есть
	{
		const auto ext = strUtil::strToLower(WorkParam.strOutFileName.extension()._tstring());

		if (ext == outExt)
		{
			// если расширения равны
			WorkParam.strOutFileName += outExt; // то добавим ещё одно.
		}
		else
		{
			// если расширения не равны
			WorkParam.strOutFileName.replace_extension(outExt); // то заменим старое на новое
		}
	}
	else
	{
		// а если расширения нету
		WorkParam.strOutFileName.replace_extension(outExt); // то просто добавим новое.
	}
}
