// Copyright 2021-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

// This is the main header for process/OS stuff; it's included in most plugins
// because values can rarely be retrieved without accessing the process' memory.
//
// Various functions are provided for common tasks when reading from memory.
// The most important are getModuleAddr() and peekProc(); the first returns the
// base address of a module (e.g. shared library), the latter reads memory at the
// specified address and stores it in a variable.

#ifndef MUMBLE_POSITIONAL_AUDIO_MAIN_H_
#define MUMBLE_POSITIONAL_AUDIO_MAIN_H_

#if !defined(OS_WINDOWS) && !defined(OS_LINUX)
#	error "Please define either OS_WINDOWS or OS_LINUX"
#endif

#include "mumble_positional_audio_win32_internals.h"

#include <chrono>
#include <cstring>
#include <vector>

#ifdef OS_WINDOWS
static const bool isWin32 = true;
#else
static bool isWin32;
#endif

static bool is64Bit;
static procid_t pPid;
static procptr_t pModule;

#define GET_POINTER_SIZE (is64Bit ? 8 : 4)

static inline bool peekProc(const procptr_t &addr, void *dest, const size_t &len);
static inline procptr_t getModuleAddr(const procid_t &pid, const wchar_t *modname);

static inline procptr_t getModuleAddr(const wchar_t *modname) {
	return getModuleAddr(pPid, modname);
}

template< class T > static inline bool peekProc(const procptr_t &addr, T &dest) {
	return peekProc(addr, &dest, sizeof(T));
}

template< class T > static inline T peekProc(const procptr_t &addr) {
	T ret;

	if (!peekProc(addr, &ret, sizeof(T))) {
		memset(&ret, 0, sizeof(ret));
	}

	return ret;
}

template< typename T > static inline std::vector< T > peekProcVector(const procptr_t addr, const size_t elements) {
	try {
		std::vector< T > var(elements);
		peekProc(addr, &var[0], sizeof(T) * elements);
		return var;
	} catch (std::bad_alloc &) {
		return std::vector< T >();
	}
}

/// This function is very useful when there is an array of structures and we only need to store a part of each.
/// The Source Engine plugin needs it because there is a structure that happens to be larger in the Linux version of
/// Left 4 Dead 2. Since the extra members are unknown, we simply discard the extra bytes when reading the array of
/// structures.
template< typename T >
static inline std::vector< T > peekProcVector(const procptr_t addr, const size_t elements,
											  const size_t realStructSize) {
	if (realStructSize == sizeof(T)) {
		// If the structure's size is correct, there is no need for this special function.
		return peekProcVector< T >(addr, elements);
	} else if (realStructSize < sizeof(T)) {
		return std::vector< T >();
	}

	// Read the full structures into a vector.
	const std::vector< uint8_t > fullStructs = peekProcVector< uint8_t >(addr, realStructSize * elements);

	// Allocate vector for the known structures.
	std::vector< T > ret(elements);

	// Copy elements from the vector containing the full structures, discarding extra bytes.
	const uint8_t *seek = &fullStructs[0];

	for (size_t i = 0; i < elements; ++i, seek += realStructSize) {
		memcpy(&ret[i], seek, sizeof(T));
	}

	return ret;
}

/// Reads the specified amount of data at the specified address and returns it as std::string.
/// An empty std::string is returned in case of error.
///
/// If \p length is 0, the function reads one byte at a time and stops when either '\0' is found or 3 seconds have
/// passed. The successfully read data is returned, also in case of error.
static inline std::string peekProcString(const procptr_t addr, const size_t length = 0) {
	std::string string;

	if (length > 0) {
		string.resize(length);

		if (!peekProc(addr, &string[0], length)) {
			return std::string();
		}
	} else {
		auto now       = std::chrono::steady_clock::now();
		const auto end = now + std::chrono::seconds(3);

		for (procptr_t i = 0; now < end; ++i) {
			char ch = 0;
			if (!peekProc(addr + i, &ch, sizeof(ch)) || ch == '\0') {
				break;
			}

			string += ch;

			// Update current time.
			now = std::chrono::steady_clock::now();
		}
	}

	return string;
}

static inline procptr_t peekProcPtr(const procptr_t &addr) {
	procptr_t v = 0;

	if (!peekProc(addr, &v, GET_POINTER_SIZE)) {
		return 0;
	}

	return v;
}

static inline procptr_t getVirtualFunction(const procptr_t classObject, const size_t index) {
	const auto vTable = peekProcPtr(classObject);
	if (!vTable) {
		return 0;
	}

	return peekProcPtr(vTable + (index * GET_POINTER_SIZE));
}

#ifdef OS_WINDOWS
static inline procptr_t getExportedSymbol(const std::string &symbol, const procptr_t module) {
#else
/// We use a different name because the function is called by the Linux version
/// of getExportedSymbol() in case the process is running through Wine.
static inline procptr_t getWin32ExportedSymbol(const std::string &symbol, const procptr_t module) {
#endif
	const auto dos = peekProc< ImageDosHeader >(module);
	if (!(dos.magic[0] == 'M' && dos.magic[1] == 'Z')) {
		// Invalid DOS signature
		return -1;
	}

	procptr_t dataAddress;

	if (is64Bit) {
		const auto nt = peekProc< ImageNtHeaders64 >(module + dos.addressOfNtHeader);
		dataAddress   = nt.optionalHeader.dataDirectory[0].virtualAddress;
	} else {
		const auto nt = peekProc< ImageNtHeaders32 >(module + dos.addressOfNtHeader);
		dataAddress   = nt.optionalHeader.dataDirectory[0].virtualAddress;
	}

	if (!dataAddress) {
		return 0;
	}

	const auto exportDir = peekProc< ImageExportDirectory >(module + dataAddress);

	const auto funcs = peekProcVector< uint32_t >(module + exportDir.addressOfFunctions, exportDir.numberOfFunctions);
	const auto names = peekProcVector< uint32_t >(module + exportDir.addressOfNames, exportDir.numberOfNames);
	const auto ords  = peekProcVector< uint16_t >(module + exportDir.addressOfNameOrdinals, exportDir.numberOfNames);

	for (uint32_t i = 0; i < exportDir.numberOfNames; ++i) {
		if (names[i]) {
			const auto name = peekProcString(module + names[i], symbol.size());

			if (name == symbol) {
				return module + funcs[ords[i]];
			}
		}
	}

	return 0;
}

// This function returns:
// -1 in case of failure.
// 0 if the process is 32-bit.
// 1 if the process is 64-bit.
#ifdef OS_WINDOWS
static inline int8_t isProcess64Bit(const procptr_t &baseAddress) {
#else
// We use a different name because the function is called by the Linux version
// of isProcess64Bit() in case the process is running through Wine.
static inline int8_t isWin32Process64Bit(const procptr_t &baseAddress) {
#endif
	const auto dos = peekProc< ImageDosHeader >(baseAddress);
	if (!(dos.magic[0] == 'M' && dos.magic[1] == 'Z')) {
		// Invalid DOS signature
		return -1;
	}

	const auto nt = peekProc< ImageNtHeadersNoOptional >(baseAddress + dos.addressOfNtHeader);
	if (!(nt.signature[0] == 'P' && nt.signature[1] == 'E' && nt.signature[2] == '\0' && nt.signature[3] == '\0')) {
		// Invalid NT signature
		return -1;
	}

	switch (nt.fileHeader.machine) {
		case 0x14c: // IMAGE_FILE_MACHINE_I386
			return 0;
		default:
			return 1;
	}
}

#ifdef WIN32
#	include "mumble_positional_audio_win32.h"
#else
#	include "mumble_positional_audio_linux.h"
#endif

#endif
