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

#include "pch.h"
#include "BKCrun.h"
#include "BKPack.h"
#include "BKTools.h"
#include "getopt.h"
#include "versionNo.h"
#include <clocale>
#include <cstring>
#include <cwctype>
#include <format>

#pragma warning(disable:4996)

// глобальные переменные
fs::path g_strInFileName;   // имя входного файла
fs::path g_strOutFileName;  // имя выходного файла
bool g_bRaw = false;        // флаг, создавать выходной файл в RAW форме
bool g_bUnpack = false;     // флаг, задействовать распаковщик
int g_nWorkArea = -1;       // адрес конца рабочей области по умолчанию. (-1 - не задано, выбирается автоматически)

FILE *g_File = nullptr;     // файл
int g_nFileLen = 0, g_nFileAddress = 01000; // параметры файла по умолчанию
int g_nFileLoadAddress = 0; // адрес загрузки файла в массив

BKTools g_tool = UNDEFINE;  // инструмент по умолчанию

MemoryModel MMemory;


static void Usage()
{
	std::_tcout << (_T(FILE_PRODUCT_STR"\n") \
	                _T("(с) 2017-2026 gid\n\n") \
	                _T("Использование:\n") \
	                _T("BKTools -h (--help)\n") \
	                _T("  Вывод этой справки.\n\n") \
	                _T("BKTools -v (--version)\n") \
	                _T("  Показать информацию о версии.\n\n") \
	                _T("BKTools -t<name> [-d][-l][-r][-u][-w<0addr>] <input_file_name> [output_file_name]\n") \
	                _T("  -t<name> (--tool name) - выбор необходимого инструмента.\n") \
	                _T("      name: BKPACK - упаковщик/распаковщик BKpack.\n") \
	                _T("            BKCRUNCH - упаковщик/распаковщик Cruncher.\n\n") \
	                _T("  -d (--data) - флаг упаковки данных, а не программы.\n") \
	                _T("      По умолчанию упаковка программы.\n\n") \
	                _T("  -l (--long) - использовать длинный автораспаковщик.\n") \
	                _T("      По умолчанию - обычный короткий. Не знаю, пользовался ли кто либо\n") \
	                _T("      когда-нибудь таким, но функционал сохранён.\n\n") \
	                _T("  -r (--raw) - создавать просто файл, не использовать формат '.bin'.\n\n") \
	                _T("  -u (--unpack) - распаковка запакованного файла.\n") \
	                _T("      Если этот ключ не указан, производится упаковка в заданный архив.\n\n") \
	                _T("  -w<0addr> (--work 0addr) - задать адрес рабочей области. Только для упаковки\n") \
	                _T("      BKPack. Для простоты, задаётся адрес конца рабочей области, например 040000,\n") \
	                _T("      если нужно, чтобы рабочая область располагалась перед областью экрана.\n\n") \
	                _T("  input_file_name - входной файл.\n") \
	                _T("  output_file_name - необязательное имя выходного файла, если нужно задать\n") \
	                _T("      своё имя файла создаваемому новому файлу.\n") \
	                _T("  По умолчанию, у выходного имени файла заменяется расширение на '.bkp'\n") \
	                _T("  (или на '.bkfile' при распаковке) или добавляется, если его не было.\n"));
}
static void 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;
}
// открываем и анализируем входной файл
// если всё хорошо, загружаем его в MMemory с индекса 0.
static bool OpenInFile()
{
	bool bRet = false;
	uint16_t mBinHeader[2] {};

	if (fs::exists(g_strInFileName))
	{
		auto size = fs::file_size(g_strInFileName);

		if (size < 0160000)
		{
			if ((g_File = _tfopen(g_strInFileName._tstring().c_str(), _T("rb"))) == nullptr)
			{
				std::_tcout << _T("Ошибка:: не удалось открыть файл '") << g_strInFileName << _T("'.\n");
			}
			else
			{
				// файл открыт, теперь надо узнать, .bin формат или нет,
				// и какой адрес запуска у файла
				int nOffset = 0;
				g_nFileLen = static_cast<int>(size);

				if (sizeof(mBinHeader) == fread(mBinHeader, 1, sizeof(mBinHeader), g_File))
				{
					if (mBinHeader[1] == g_nFileLen - 4)
					{
						nOffset = 4;
					}
					else if (mBinHeader[1] == g_nFileLen - 22)
					{
						nOffset = 20;
					}

					if (nOffset) // если действительно бин
					{
						g_nFileAddress = mBinHeader[0];  // берём данные из заголовка
						g_nFileLen = mBinHeader[1];
					}

					if (0 != fseek(g_File, nOffset, SEEK_SET))
					{
						std::_tcout << _T("Ошибка:: не удалось прочитать файл '") << g_strInFileName << _T("'.\n");
					}
					else
					{
						// читаем данные в массив
						// g_nFileLoadAddress = g_nFileAddress; // загружаем по адресу загрузки
						if (g_nFileLen == fread(&MMemory.b[g_nFileLoadAddress], 1, g_nFileLen, g_File))
						{
							fclose(g_File);

							if (g_bUnpack)
							{
								std::_tcout << _T("Распаковываем '") << g_strInFileName << _T("' :: ");
							}
							else
							{
								std::_tcout << _T("Пакуем '") << g_strInFileName << _T("' :: ");
							}

							bRet = true;
						}
						else
						{
							std::_tcout << _T("Ошибка:: не удалось прочитать файл '") << g_strInFileName << _T("'.\n");
						}
					}
				}
				else
				{
					std::_tcout << _T("Ошибка:: не удалось прочитать файл '") << g_strInFileName << _T("'.\n");
				}
			}
		}
		else
		{
			std::_tcout << _T("Ошибка:: файл слишком большой.\n");
		}
	}
	else
	{
		std::_tcout << _T("Ошибка:: файл не найден.\n");
	}

	return bRet;
}

// сохраняем результат работы BKPacka из
// массива MMemory
static bool SaveOutFileBKPack()
{
	bool bRet = false;
	uint16_t mBinHeader[2] {};

	if ((g_File = _tfopen(g_strOutFileName._tstring().c_str(), _T("wb"))) == nullptr)
	{
		std::_tcout << _T("Ошибка:: не удалось открыть файл для записи '") << g_strOutFileName << _T("'.\n");
	}
	else
	{
		// добавим заголовок .bin
		mBinHeader[0] = g_nFileAddress;
		mBinHeader[1] = g_bUnpack ? BKPkParam.nFileLen + BKPkParam.LENAZ : BKPkParam.EPK;

		if (!g_bRaw)
		{
			fwrite(mBinHeader, 1, sizeof(mBinHeader), g_File);
		}

		fwrite(&MMemory.b[g_nFileLoadAddress], 1, mBinHeader[1], g_File);
		fclose(g_File);
		bRet = true;
	}

	return bRet;
}
// сохраняем результат работы BKCrunch из
// массива CrunMemory
static bool SaveOutFileCrunch()
{
	bool bRet = false;
	uint16_t mBinHeader[2] {};

	if ((g_File = _tfopen(g_strOutFileName._tstring().c_str(), _T("wb"))) == nullptr)
	{
		std::_tcout << _T("Ошибка:: не удалось открыть файл для записи '") << g_strOutFileName << _T("'.\n");
	}
	else
	{
		// добавим заголовок .bin
		mBinHeader[0] = g_nFileAddress;
		mBinHeader[1] = g_nFileLen;

		if (!g_bRaw)
		{
			fwrite(mBinHeader, 1, sizeof(mBinHeader), g_File);
		}

		fwrite(&CrunMemory.b[0], 1, mBinHeader[1], g_File);
		fclose(g_File);
		bRet = true;
	}

	return bRet;
}
// формируем имя выходного файла
static void getOutName(int argc, TCHAR **argv, const fs::path &ext)
{
	if (argc >= 1)
	{
		g_strOutFileName = (*argv) ? fs::path(*argv) : _T("out");
	}
	else
	{
		g_strOutFileName = g_strInFileName;
	}

	// добавим или заменим расширение
	if (g_strOutFileName.has_extension())
	{
		g_strOutFileName.replace_extension(ext);
	}
	else
	{
		g_strOutFileName += ext;
	}
}

int _tmain(int argc, TCHAR *argv[], TCHAR *envp[])
{
	setlocale(LC_ALL, "");
	//std::locale::global(std::locale("ru_RU.UTF-8"));
	static struct option long_options[] =
	{
		{ _T("help"),    no_argument,       nullptr, _T('h') },
		{ _T("version"), no_argument,       nullptr, _T('v') },
		{ _T("tool"),    required_argument, nullptr, _T('t') },
		{ _T("raw"),     no_argument,       nullptr, _T('r') },
		{ _T("unpack"),  no_argument,       nullptr, _T('u') },
		{ _T("data"),    no_argument,       nullptr, _T('d') },
		{ _T("long"),    no_argument,       nullptr, _T('l') },
		{ _T("work"),    required_argument, nullptr, _T('w') },
		{ nullptr,       no_argument,       nullptr, 0 }
	};
	static TCHAR optstring[] = _T("hvt:rudlw:");
	int option_index = 0;
	int c = 0;
	memset(&BKPkParam, 0, sizeof(BKPkParam));

	while ((c = getopt_long(argc, argv, optstring, long_options, &option_index)) != -1)
	{
		// Handle options
		switch (c)
		{
			case _T('h'):
				Usage();
				return 0;

			case _T('v'):
				PrintVersion();
				return 0;

			case _T('t'):
				if (optarg)
				{
					auto tool = std::_tstring(optarg);

					for (auto &i : tool)
					{
						i = std::toupper(i, std::locale());
					}

					if (tool == _T("BKPACK"))
					{
						g_tool = BKPACK;
					}
					else if (tool == _T("BKCRUNCH"))
					{
						g_tool = BKCRUNCH;
					}
					else
					{
						Usage();
						return 0;
					}
				}
				else
				{
					Usage();
					return 0;
				}

				break;

			case _T('d'):
				BKPkParam.bData = true;
				break;

			case _T('l'):
				BKPkParam.bLongUnpacker = true;
				break;

			case _T('r'):
				g_bRaw = true;
				break;

			case _T('u'):
				g_bUnpack = true;
				break;

			case _T('w'):
				if (optarg)
				{
					auto strTmp = std::_tstring(optarg);
					const int nTmp = std::stoi(strTmp, nullptr, 8) & 0xffff;

					if (0 <= nTmp && nTmp <= 0140000)
					{
						g_nWorkArea = nTmp;
					}
				}

				break;
		}
	}

	argc -= optind;
	argv += optind;

	if (argc < 1)
	{
		std::_tcout << _T("Ошибка в командной строке: не задано имя входного файла.\n\n");
		Usage();
	}
	else
	{
		g_strInFileName = fs::path(*argv);
		argc--;
		argv++;

		switch (g_tool)
		{
			case BKPACK:
			{
				getOutName(argc, argv, g_bUnpack ? fs::path(_T(".bkfile")) : fs::path(_T(".bkp")));

				if (OpenInFile())
				{
					if (g_bUnpack)
					{
						if (BKUnPacking(g_nFileLoadAddress, g_nFileLen, g_nFileAddress))
						{
							SaveOutFileBKPack();
						}
					}
					else
					{
						if (BKPacking(0, g_nFileLen, g_nFileAddress))
						{
							SaveOutFileBKPack();
						}
					}
				}
			}
			break;

			case BKCRUNCH:
			{
				getOutName(argc, argv, g_bUnpack ? fs::path(_T(".bkfile")) : fs::path(_T(".crn")));

				if (OpenInFile())
				{
					if (g_bUnpack)
					{
						CrunUnPack(0, g_nFileLen, g_nFileAddress);
						SaveOutFileCrunch();
					}
					else
					{
						CrunPack(0, g_nFileLen, g_nFileAddress);
						SaveOutFileCrunch();
					}
				}
			}
			break;

			default:
				std::_tcout << _T("Забыли задать правильный рабочий инструмент.\n");
				Usage();
				break;
		}
	}

	return 0;
}

