const char *fp_version = "1.0"; // change this if you add/change commands
const char *offsetStr = "  Offset is %d (%X)";

//#include "xr_file_system_win32.h"
#define NOMINMAX
// we define WINVER here to force XP/Windows 2000 support (like GetFileSizeEx)
#define WINVER 0x0500
#include <windows.h>

//#include <sys/stat.h>
//#include <stdlib.h>
#include <stdio.h> //sscanf, rename
//#include "xr_types.h"
#include "db_tools.h"
//#include <string>
//#include "xr_utils.h"
//#include "xr_string_utils.h"

using namespace xray_re;

DWORD FileRead(const char* path, unsigned char **destBuffer_p, DWORD maxFileSize)
{
	HANDLE h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL,
			OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (h == INVALID_HANDLE_VALUE)
	{
		msg("Could not open %s for reading.", path);
		return 0;
	}
	LARGE_INTEGER size64;
	if (!GetFileSizeEx(h, &size64) || size64.HighPart != 0) {
		msg("%s:  Could not obtain size or size is too big.", path);
		CloseHandle(h);
		return 0;
	}
	DWORD length = size64.LowPart;
    if (length > maxFileSize)
    {
		msg("Can't process %s: Size is greater than specified limit of %d bytes.", path, maxFileSize);
		CloseHandle(h);
		return 0;
    }
	*destBuffer_p = (unsigned char *)malloc(length + 1);  // this approach avoids exceptions
	if (*destBuffer_p == NULL)
	{
		msg("Could not allocate memory of size %d.", length + 1);
		CloseHandle(h);
		return 0;
    }
	(*destBuffer_p)[length] = 0; // terminate the last line just in case it's a patch file
	DWORD read;
	if (ReadFile(h, *destBuffer_p, length, &read, NULL) && read == length)
	{
		CloseHandle(h);
		return length;
	}
	else
	{
		free(*destBuffer_p);
		*destBuffer_p = NULL;
		CloseHandle(h);
		msg("Could not read file: %s", path);
		return 0;
	}
}

bool FileWrite(const char* path, unsigned char *srcBuffer, DWORD fileSize)
{
	DWORD written;
	HANDLE h = CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ, NULL,
			CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (h == INVALID_HANDLE_VALUE) return false;
	BOOL ok = WriteFile(h, srcBuffer, fileSize, &written, NULL);
	CloseHandle(h);
	return ok && fileSize == written;
}

class CommandParser {
public:
					CommandParser(unsigned char* patchData, DWORD patchDataSize);
	virtual 		~CommandParser();
	unsigned char *	GetNextCommand();
//	bool			IsComment();
	unsigned char *	GetParamsBuffer();
	DWORD			numberOfParamChars();
	unsigned char *	GetFilteredParamsBuffer();
	DWORD			numberOfFilteredParamChars();
	unsigned char *	GetCurrentCommand();
	bool			CommandIs(const char* str);
	void			LowerCaseCommand();
	void			LowerCase(char *str);

protected:
	void			GetLine();

private:
	DWORD			m_linecnt;
	unsigned char*	m_command_data;
	unsigned char*	m_input_ptr;
	DWORD			m_command_data_size;
	unsigned char*	m_end_of_data;
	unsigned char*	m_command; // current command
	//unsigned char	wordarray[256];
	//unsigned char	linearray[1024];
	unsigned char*	m_param_chars;
	unsigned char	m_cooked_param_chars[1024];
	DWORD			m_param_chars_size;
	DWORD			m_cooked_param_chars_size;
};

CommandParser::~CommandParser() { }
inline unsigned char* CommandParser::GetCurrentCommand() { return m_command; }
inline unsigned char* CommandParser::GetParamsBuffer() { return m_param_chars; }
inline DWORD CommandParser::numberOfParamChars() { return m_param_chars_size; }
inline unsigned char* CommandParser::GetFilteredParamsBuffer() { return m_cooked_param_chars; }
inline DWORD CommandParser::numberOfFilteredParamChars() { return m_cooked_param_chars_size; }

CommandParser::CommandParser(unsigned char* patchData, DWORD patchDataSize) :
	m_command_data(patchData), m_command_data_size(patchDataSize)
{
	m_end_of_data = m_command_data + m_command_data_size;
	m_input_ptr = m_command_data; // we probably don't need both
	m_linecnt = 0;
}

unsigned char* CommandParser::GetNextCommand()
{
	bool found_command = false;
	m_command = NULL;
	m_param_chars = NULL;
	m_param_chars_size = 0;
	do {
		GetLine();
		if (m_command == NULL) break; // EOF or error encountered
		while (*m_command != 0) {
			unsigned char firstChar = *m_command;
			if (firstChar == ' ' ||
				firstChar ==  0x09 //||
				// firstChar ==  0x0D || // converted by GetLine()
				// firstChar ==  0x0A
				) {
					m_command++; // skip white space
			} else if ((firstChar < 'A' || firstChar > 'z') ||
				(firstChar > 'Z' && firstChar < 'a')) { // skip comment line
				break; // inner while only
			} else {
				found_command = true;
				break; // we have a command, theoretically
			}
		}
	} while (!found_command);
	if (m_command != NULL) { // find end of command/start of param string
		// find first space, drop zero
		unsigned char *ndx_ptr = m_command;
		unsigned char c;
		while (ndx_ptr < m_end_of_data) {
			c = *ndx_ptr;
			if (c == ' ' || c == 0x09 || c == 0) { // found end
				*ndx_ptr++ = 0; // terminate the command
				if (c != 0) while (ndx_ptr < m_end_of_data) {
					c = *ndx_ptr;
					if (c != ' ' && c != 0x09 && c != 0) { // found start of parameter clause
						m_param_chars = ndx_ptr++;
						while (*ndx_ptr != 0) ndx_ptr++; // rest of line is param string
						m_param_chars_size = ndx_ptr - m_param_chars;
						break;
					}
					ndx_ptr++;
				}
				break;
			}
			ndx_ptr++;
		}

	}
	return m_command;
}

void CommandParser::LowerCaseCommand()
{
	LowerCase((char *)m_command);
}

void CommandParser::LowerCase(char *str)
{
	for (int c; (c = *str) != 0;)
		*str++ = std::tolower(c);
}

bool CommandParser::CommandIs(const char* str)
{
	return strcmp((const char *)m_command, str)==0;
}

void CommandParser::GetLine(void)
{
unsigned char c;

	m_linecnt++;

// while not end of buffer and not line feed, move through data
	//while ((c=*m_input_ptr++)&&(c!=0xA)) {
	if (m_input_ptr >= m_end_of_data) {
		m_command = NULL;
		return;
	} else {
		m_command = m_input_ptr; // start of line
	}
	while (m_input_ptr < m_end_of_data) {
		c = *m_input_ptr;
		if (c == 0x0D) *m_input_ptr++ = (unsigned char)0;
		else if (c == 0X0A) {
			*m_input_ptr++ = (unsigned char)0;
			break;
		} else if (c==0) { // END OF BUFFER! or unexpected null
			if (m_input_ptr < m_end_of_data) {
				msg("\nUnexpected NULL character at sourceline %d, column %d.\n\n",m_linecnt, m_input_ptr-m_command);
			}
			m_command = NULL;
			break;
		} else
			m_input_ptr++;
	}
	// at this point m_command is either NULL or points to a zero-delimited C string
}

file_patcher::file_patcher() : m_patchdata(NULL), m_patchdata_size(0), m_filedata(NULL), m_filedata_size(0) { }
file_patcher::~file_patcher() { free(m_patchdata); free(m_filedata); }

int file_patcher::process(const cl_parser& cl)
{
	bool renameOriginal = false;
	bool verbose = cl.get_bool("-verbose");
	bool readonly = cl.get_bool("-ro");
	const char*	filenameStr = NULL;
	const char*	renameStr = NULL;
	int offset = 0;
	cl.get_string("-patchfile", m_patchfile);
	if (m_patchfile == "") {
		msg("\n  Patcher requires a patchfile.\n");
		return RESULT_FAILURE;
	}

	msg("\n SocPUP - Patch process started using %s\n", m_patchfile.c_str());

	m_patchdata_size = FileRead(m_patchfile.c_str(), &m_patchdata, 65535);
	if (m_patchdata_size == 0) return RESULT_FAILURE;

	CommandParser *cp = new CommandParser(m_patchdata, m_patchdata_size);
	unsigned char *cmd = cp->GetNextCommand();
	const char *paramsBuffer = NULL;
	bool matched = false;
	bool skipNextCommand = false;

	while (cmd != NULL)
	{
		if (verbose) msg("Command: %s", (const char *)cmd);
		cp->LowerCaseCommand();
		paramsBuffer = (const char *)cp->GetParamsBuffer();
		if (verbose && paramsBuffer != NULL) msg("  Parameter: %s", paramsBuffer);
		size_t paramChars = cp->numberOfParamChars();
		if (skipNextCommand) {
			if (verbose) msg("  -> Skipped this command");
			skipNextCommand = false;
		} else if (cp->CommandIs("exitnotfound")) {
			if (filenameStr) msg("\n  String to patch not found in %s\n", filenameStr);
			return RESULT_FAILURE;
		} else if (cp->CommandIs("exitalreadypatched")) {
			if (filenameStr) msg("  Already patched: %s", filenameStr);
			return RESULT_ACCEPTABLE;
		} else if (cp->CommandIs("ifmatch")) {
			matched = false;
			// compare paramsBuffer with characters at current file offset
			if (paramChars > 0)
				matched = strncmp((const char *)m_filedata + offset, paramsBuffer, paramChars) == 0;
			//puts((const char *)m_filedata + offset);
			//if (matched) puts("Matched"); else puts("Not matched");
			if (! matched) skipNextCommand = true;
		} else if (cp->CommandIs("ifnotmatch")) {
			matched = false;
			if (paramChars > 0)
				matched = strncmp((const char *)m_filedata + offset, paramsBuffer, paramChars) == 0;
			if (matched) skipNextCommand = true;
		} else if (cp->CommandIs("loadfile") && paramsBuffer != NULL) {
			filenameStr = paramsBuffer;
			m_filedata_size = FileRead(filenameStr, &m_filedata, 20971520); //20 MB
			msg("  Read %d bytes of %s", m_filedata_size, filenameStr);
			if (m_filedata_size == 0) {
				delete cp;
				return RESULT_FAILURE;
			}
		} else if (cp->CommandIs("renameoriginal")) {
			renameOriginal = true;
			renameStr = paramsBuffer;
		} else if (cp->CommandIs("replacewith")) {
			if (paramChars > 0)
				memcpy(m_filedata + offset, paramsBuffer, paramChars);
		} else if (cp->CommandIs("savefile") || cp->CommandIs("savefileas")) {
			if (renameOriginal) {
				std::string renameString;
				if (renameStr == NULL) {
					renameString = filenameStr;
					renameString = renameString + ".bak";
				} else {
					renameString = renameStr;
				}
				if (readonly) {
					msg("Pretending to rename file to %s", renameString.c_str());
				} else {
					rename(filenameStr, renameString.c_str()); // we don't care if it fails
				}
			}
			if (cp->CommandIs("savefileas")) {
				if (paramsBuffer == NULL) {
					msg("\n  SaveFileAs needs a filename!\n");
					return RESULT_FAILURE;
				}
				filenameStr = paramsBuffer;
			}
			if (readonly) {
				msg("Faking file save to %s", filenameStr);
			} else {
				if (! FileWrite(filenameStr, m_filedata, m_filedata_size)) {
					msg("\n  Problem writing updated file %s\n", filenameStr);
					return RESULT_FAILURE;
				}
			}
		} else if (cp->CommandIs("seek")) {
			sscanf(paramsBuffer, "%d", &offset);
			if (verbose) msg(offsetStr, offset, offset);
		} else if (cp->CommandIs("seekhex")) {
			cp->LowerCase((char *)paramsBuffer);
			sscanf(paramsBuffer, "%x", &offset);
			if (verbose) msg(offsetStr, offset, offset);
		} else if (cp->CommandIs("socpup_version")) {
			matched = strcmp(paramsBuffer, fp_version) <= 0;
			if (! matched) {
				msg("Patch script version is newer than patcher!");
				return RESULT_FAILURE;
			}
		} else msg("**** -> Unknown command ignored:  %s",(const char *)cmd);
		cmd = cp->GetNextCommand();
	}
	delete cp;

	msg("\n Patch process complete.");
	return RESULT_OK;
}

