#pragma once

#include "gtest/gtest.h"

#include <functional>
#include <map>
#include <string>

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

namespace Retro {

template<typename T = uint8_t>
class MemoryView {
public:
	MemoryView() {}
	MemoryView(const MemoryView<T>&) = delete;
	~MemoryView();

	bool open(const std::string& file, size_t bytes = 0);
	void open(void* buffer, size_t bytes);
	void open(size_t bytes);
	void open(std::initializer_list<T>);
	void close();

	bool ok() const;
	void clone(const void* buffer, size_t bytes);
	void clone(const MemoryView<T>&);
	void clone();

	T& operator[](size_t);
	const T& operator[](size_t) const;
	MemoryView<T>& operator=(MemoryView<T>&&);

	void* offset(size_t);
	const void* offset(size_t) const;

	size_t size() const;

private:
	T* m_buffer = nullptr;
	int m_backingFd = -1;
	bool m_managed = false;
	size_t m_size = 0;
};

template<typename T>
MemoryView<T>::~MemoryView() {
	close();
}

template<typename T>
bool MemoryView<T>::open(const std::string& file, size_t bytes) {
	if (ok()) {
		close();
	}
	int flags = O_RDWR;
	if (bytes) {
		flags |= O_CREAT;
	}
	m_backingFd = ::open(file.c_str(), flags, 0600);
	if (m_backingFd < 0) {
		return false;
	}
	if (bytes) {
		ftruncate(m_backingFd, bytes);
		m_size = bytes;
	} else {
		m_size = lseek(m_backingFd, 0, SEEK_END);
	}
	m_managed = true;
	m_buffer = reinterpret_cast<T*>(static_cast<uint8_t*>(mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_backingFd, 0)));
	if (m_buffer == reinterpret_cast<T*>(-1)) {
		m_buffer = nullptr;
		m_managed = false;
		::close(m_backingFd);
		return false;
	}
	return true;
}

template<typename T>
void MemoryView<T>::open(void* buffer, size_t bytes) {
	if (ok()) {
		close();
	}
	m_backingFd = -1;
	m_size = bytes;
	m_managed = false;
	m_buffer = static_cast<T*>(buffer);
}

template<typename T>
void MemoryView<T>::open(size_t bytes) {
	if (ok()) {
		close();
	}
	m_backingFd = -1;
	m_size = bytes;
	m_managed = true;
	m_buffer = static_cast<T*>(mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0));
}

template<typename T>
void MemoryView<T>::open(std::initializer_list<T> list) {
	open(list.size());
	std::copy(list.begin(), list.end(), m_buffer);
}

template<typename T>
void MemoryView<T>::close() {
	if (!ok()) {
		return;
	}
	if (m_managed) {
		if (m_buffer) {
			munmap(m_buffer, m_size);
		}
		if (m_backingFd >= 0) {
			::close(m_backingFd);
			m_backingFd = -1;
		}
	}
	m_buffer = nullptr;
	m_size = 0;
	m_managed = false;
}

template<typename T>
bool MemoryView<T>::ok() const {
	return m_buffer && m_size;
}

template<typename T>
void MemoryView<T>::clone() {
	if (!ok() || m_managed) {
		return;
	}

	clone(static_cast<void*>(m_buffer), m_size);
}

template<typename T>
void MemoryView<T>::clone(const void* buffer, size_t bytes) {
	if (m_managed && bytes == m_size) {
		memmove(m_buffer, buffer, bytes);
		return;
	}
	if (static_cast<void*>(m_buffer) != buffer || !m_managed) {
		close();
	}
	T* newBuffer = static_cast<T*>(mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0));
	memcpy(newBuffer, buffer, bytes);
	m_buffer = newBuffer;
	m_size = bytes;
	m_managed = true;
}

template<typename T>
void MemoryView<T>::clone(const MemoryView<T>& other) {
	clone(static_cast<const void*>(other.m_buffer), other.m_size);
}

template<typename T>
T& MemoryView<T>::operator[](size_t index) {
	return m_buffer[index];
}

template<typename T>
const T& MemoryView<T>::operator[](size_t index) const {
	return m_buffer[index];
}

template<typename T>
MemoryView<T>& MemoryView<T>::operator=(MemoryView<T>&& other) {
	close();
	m_buffer = other.m_buffer;
	m_backingFd = other.m_backingFd;
	m_managed = other.m_managed;
	m_size = other.m_size;
	other.m_managed = false;
	return *this;
}

template<typename T>
void* MemoryView<T>::offset(size_t index) {
	return reinterpret_cast<void*>(&m_buffer[index]);
}

template<typename T>
const void* MemoryView<T>::offset(size_t index) const {
	return reinterpret_cast<const void*>(&m_buffer[index]);
}

template<typename T>
size_t MemoryView<T>::size() const {
	return m_size;
}

enum class Endian : char {
	BIG = 0b01,
	LITTLE = 0b10,
	NATIVE = 0b11,
	MIXED_BL = 0b1001,
	MIXED_LB = 0b0110,
	MIXED_BN = 0b1101,
	MIXED_LN = 0b1110,
#if defined(__LITTLE_ENDIAN__) || __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
	REAL_NATIVE = LITTLE,
	REAL_MIXED_BN = MIXED_BL,
	REAL_MIXED_LN = LITTLE,
#else
	REAL_NATIVE = BIG,
	REAL_MIXED_BN = BIG,
	REAL_MIXED_LN = MIXED_LN,
#endif
	UNDEF = 0
};

Endian reduce(Endian);
bool reduceCompare(Endian, Endian);

enum class Repr : char {
	SIGNED = 'i',
	UNSIGNED = 'u',
	BCD = 'd',
	LN_BCD = 'n'
};

class Datum;
class MemoryOverlay;
class DataType {
public:
	DataType(const char*);
	DataType(const std::string&);
	DataType(const DataType&) = default;

	Datum operator()(void*) const;
	Datum operator()(void*, size_t offset, const MemoryOverlay&) const;
	bool operator==(const DataType&) const;
	bool operator!=(const DataType&) const;

	void encode(void* buffer, int64_t value) const;
	int64_t decode(const void* buffer) const;

	const size_t width;
	const Endian endian;
	const Repr repr;

	const char type[5];

private:
	FRIEND_TEST(DataTypeShift, 1);
	FRIEND_TEST(DataTypeShift, 2);
	FRIEND_TEST(DataTypeShift, 3);
	FRIEND_TEST(DataTypeShift, 4);
	FRIEND_TEST(DataTypeShift, 5);
	FRIEND_TEST(DataTypeShift, 6);
	FRIEND_TEST(DataTypeShift, 7);
	FRIEND_TEST(DataTypeShift, 8);

	const uint8_t maskLo;
	const uint8_t maskHi;
	const unsigned cvt;
	int64_t shift[8]{};
};

struct Variable {
	Variable(const DataType&, size_t address, uint64_t mask = UINT64_MAX);
	Variable(const Variable&) = default;
	bool operator==(const Variable&) const;

	const DataType type;
	const size_t address;
	const uint64_t mask = UINT64_MAX;
};

class MemoryOverlay {
public:
	MemoryOverlay(Endian backing = Endian::NATIVE, Endian real = Endian::NATIVE, size_t width = 1);
	MemoryOverlay(char backing, char real, size_t width = 1);

	void* parse(const void* in, size_t offset, void* out, size_t size) const;
	void unparse(void* out, size_t offset, const void* in, size_t size) const;

	const size_t width;

private:
	DataType m_backing;
	DataType m_real;
};

class Datum {
public:
	Datum() {}
	Datum(void*, const DataType&);
	Datum(void* base, const Variable&, const MemoryOverlay& overlay = {});
	Datum(void* base, size_t offset, const DataType&, const MemoryOverlay& overlay = {});

	Datum& operator=(int64_t);
	operator int64_t() const;
	bool operator==(int64_t);

private:
	void* const m_base = nullptr;
	const size_t m_offset = 0;
	const DataType m_type{ "|u1" };
	const uint64_t m_mask = UINT64_MAX;
	const MemoryOverlay m_overlay{};
};

class DynamicMemoryView {
public:
	DynamicMemoryView(void* buffer, size_t bytes, const DataType&, const MemoryOverlay& = {});

	Datum operator[](size_t);
	int64_t operator[](size_t) const;

	const DataType dtype;
	const MemoryOverlay overlay;

private:
	MemoryView<> m_mem;
};

class AddressSpace {
public:
	void addBlock(size_t offset, size_t size, void* data = nullptr);
	void addBlock(size_t offset, size_t size, const void* data);
	void addBlock(size_t offset, const MemoryView<>& base);
	void updateBlock(size_t offset, void* data);
	void updateBlock(size_t offset, const void* data);
	void updateBlock(size_t offset, const MemoryView<>& base);

	bool hasBlock(size_t offset) const;
	const MemoryView<>& block(size_t offset) const;
	MemoryView<>& block(size_t offset);

	const std::map<size_t, MemoryView<>>& blocks() const { return m_blocks; }
	std::map<size_t, MemoryView<>>& blocks() { return m_blocks; }

	bool ok() const;
	void reset();
	void clone(const AddressSpace&);
	void clone();

	void setOverlay(const MemoryOverlay& overlay);
	const MemoryOverlay& overlay() const { return *m_overlay; };

	Datum operator[](size_t);
	Datum operator[](const Variable&);
	uint8_t operator[](size_t) const;
	int64_t operator[](const Variable&) const;

	AddressSpace& operator=(AddressSpace&&);

private:
	static const DataType s_type;;
	std::map<size_t, MemoryView<>> m_blocks;
	std::unique_ptr<MemoryOverlay> m_overlay = std::make_unique<MemoryOverlay>();
};

int64_t toBcd(int64_t);
int64_t toLNBcd(int64_t);
bool isBcd(uint64_t);
}

namespace std {
template<>
struct hash<Retro::DataType> {
	size_t operator()(const Retro::DataType&) const;
};
}