// SPDX-License-Identifier: BSD-3-Clause
#ifndef SUBSTRATE_MMAP
#define SUBSTRATE_MMAP

#include <cstdint>
#ifdef _WIN32
#	ifndef NOMINMAX
#		define NOMINMAX
#	endif
#	ifndef WIN32_LEAN_AND_MEAN
#		define WIN32_LEAN_AND_MEAN
#	endif
#	include <io.h>
#	include <windows.h>
#else
#	include <unistd.h>
#	include <sys/mman.h>
#endif
#include <stdexcept>
#include <cstring>
#include <cassert>
#include <string>
#if __cplusplus >= 201703L
#include <string_view>
#endif
#include <substrate/utility>

namespace substrate
{
	namespace constants
	{
#ifdef _WIN32
		SUBSTRATE_NOWARN_UNUSED(static constexpr DWORD PROT_READ{PAGE_READONLY});
		SUBSTRATE_NOWARN_UNUSED(static constexpr DWORD PROT_WRITE{PAGE_READWRITE});
		SUBSTRATE_NOWARN_UNUSED(static constexpr int32_t MAP_PRIVATE{0});
		SUBSTRATE_NOWARN_UNUSED(static constexpr int32_t MAP_FAILED{-1});
		SUBSTRATE_NOWARN_UNUSED(static constexpr int32_t MAP_SHARED{0});

		SUBSTRATE_NOWARN_UNUSED(static constexpr auto MADV_SEQUENTIAL{0});
		SUBSTRATE_NOWARN_UNUSED(static constexpr auto MADV_WILLNEED{0});
		SUBSTRATE_NOWARN_UNUSED(static constexpr auto MADV_DONTDUMP{0});
#endif
	} // namespace constants

	using namespace constants;

	namespace internal
	{
#ifdef _WIN32
		SUBSTRATE_CXX14_CONSTEXPR inline DWORD cleanProt(const DWORD prot) noexcept
		{
			return (prot & PROT_WRITE) ? prot & ~PROT_READ : prot;
		}

		SUBSTRATE_CXX14_CONSTEXPR inline DWORD protToAccess(const DWORD prot) noexcept
		{
			if ((prot & PAGE_READWRITE) || (prot & PAGE_WRITECOPY))
				return FILE_MAP_WRITE;
			else if (prot & PAGE_READONLY)
				return FILE_MAP_READ;
			return {};
		}
#endif

		SUBSTRATE_NO_DISCARD(inline bool lock(void *address, const std::size_t length) noexcept)
#ifdef _WIN32
			{ return VirtualLock(address, length); }
#else
			{ return ::mlock(address, length) == 0; }
#endif

		SUBSTRATE_NO_DISCARD(inline bool unlock(void *address, const std::size_t length) noexcept)
#ifdef _WIN32
			{ return VirtualUnlock(address, length); }
#else
			{ return ::munlock(address, length) == 0; }
#endif

		inline bool unmap(void *address, const std::size_t length) noexcept
		{
#ifdef _WIN32
			(void)length;
			return UnmapViewOfFile(address);
#else
			return ::munmap(address, length) == 0;
#endif
		}
	} // namespace internal

	struct mmap_t final
	{
	private:
		using off_t = substrate::off_t;
		using realptr_t = substrate::conditional_t<sizeof(uint32_t *) == sizeof(void *), uint32_t *, uint64_t *>;

		std::size_t _len{0};
#ifdef _WIN32
		// NOLINTNEXTLINE(performance-no-int-to-ptr,cppcoreguidelines-pro-type-cstyle-cast)
		HANDLE _mapping{INVALID_HANDLE_VALUE};
#endif
		void *_addr{nullptr};
		int32_t _fd{-1};

#ifdef _WIN32
		// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
		SUBSTRATE_NO_DISCARD(static inline HANDLE
			createMapping(const int32_t _fd, const std::size_t length, const int32_t prot) noexcept)
		{
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast,performance-no-int-to-ptr)
			auto *const file{reinterpret_cast<HANDLE>(_get_osfhandle(_fd))};
			return CreateFileMappingA(file, nullptr, internal::cleanProt(static_cast<DWORD>(prot)),
				DWORD(length >> 32U), DWORD(length), nullptr);
		}

		SUBSTRATE_NO_DISCARD(static inline LPVOID
			mapAddressWithOffset(HANDLE _mapping, const int32_t prot, const off_t offset, const size_t length) noexcept)
		{
			if (!_mapping)
				return nullptr;
			if (length > std::numeric_limits<uint32_t>::max())
				return nullptr;
			return MapViewOfFile(_mapping, internal::protToAccess(static_cast<DWORD>(prot)), DWORD(offset >> 32U),
				DWORD(offset), SIZE_T(length));
		}

		SUBSTRATE_NO_DISCARD(static inline LPVOID mapAddress(HANDLE _mapping, const int32_t prot) noexcept)
		{
			if (!_mapping)
				return nullptr;
			return MapViewOfFile(_mapping, internal::protToAccess(static_cast<DWORD>(prot)), 0, 0, 0);
		}
#else
		SUBSTRATE_NO_DISCARD(static inline void *
			mapAddress(void *addr, const std::size_t length, const int32_t prot, const int32_t flags, int32_t _fd) noexcept)
		{
			const auto ptr = ::mmap(addr, length, prot, flags, _fd, 0);
			return ptr == MAP_FAILED ? nullptr : ptr;
		}
#endif

		// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
		mmap_t(const mmap_t &map, const std::size_t len, const int32_t prot,
			SUBSTRATE_NOWARN_UNUSED(const int32_t flags = MAP_SHARED),
			SUBSTRATE_NOWARN_UNUSED(void *addr = nullptr)) noexcept : _len{len},
#ifdef _WIN32
			_mapping{createMapping(map._fd, len, prot)}, _addr{mapAddress(_mapping, prot)}
#else
			_addr{mapAddress(addr, len, prot, flags, map._fd)}
#endif
			{ }

		template<typename T> SUBSTRATE_NO_DISCARD(substrate::enable_if_t<
			substrate::is_pod<T>::value && !has_nullable_ctor<T>::value && !std::is_same<T, void *>::value, T *>
				index(const std::size_t idx) const)
		{
			if (idx < _len)
			{
				// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
				auto *const addr{reinterpret_cast<uint8_t *>(_addr)};
				// XXX: Do we actually want to be multiplying by sizeof(T) here?
				// Seems unlikely with the current index check
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-owning-memory)
				return new (addr + (idx * sizeof(T))) T{};
			}
			throw std::out_of_range("mmap_t index out of range");
		}

		template<typename T> SUBSTRATE_NO_DISCARD(
			substrate::enable_if_t<has_nullable_ctor<T>::value && !std::is_same<T, void *>::value, T *>
				index(const std::size_t idx) const)
		{
			if (idx < _len)
			{
				// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
				auto *const addr{reinterpret_cast<uint8_t *>(_addr)};
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic,cppcoreguidelines-owning-memory)
				return new (addr + (idx * sizeof(T))) T{nullptr};
			}
			throw std::out_of_range("mmap_t index out of range");
		}

		template<typename T> SUBSTRATE_NO_DISCARD(substrate::enable_if_t<std::is_same<T, void *>::value, void *>
			index(const std::size_t idx) const)
		{
			if (idx < _len)
			{
				// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
				auto *const addr {reinterpret_cast<realptr_t>(_addr)};
				// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
				return (addr + idx);
			}
			throw std::out_of_range("mmap_t index out of range");
		}

	public:
		constexpr mmap_t() noexcept = default;

		// NOLINTNEXTLINE(bugprone-easily-swappable-parameters,readability-identifier-length)
		mmap_t(const int32_t fd, const std::size_t len, const int32_t prot,
			SUBSTRATE_NOWARN_UNUSED(const int32_t flags = MAP_SHARED),
			SUBSTRATE_NOWARN_UNUSED(void *addr = nullptr)) noexcept :
			_len{len},
#ifdef _WIN32
			_mapping{createMapping(fd, len, prot)}, _addr{mapAddress(_mapping, prot)},
#else
			_addr{mapAddress(addr, len, prot, flags, fd)},
#endif
			_fd{fd}
		{ }

#ifdef _WIN32
		// NOLINTNEXTLINE(bugprone-easily-swappable-parameters,readability-identifier-length)
		mmap_t(const int32_t fd, const off_t offset, const std::size_t length, const int32_t prot,
			const int32_t = 0, void * = nullptr) noexcept :
			_len{length},
			_mapping{createMapping(fd, 0, prot)}, _addr{mapAddressWithOffset(_mapping, prot, offset, length)},
			_fd{fd}
		{ }
#endif

		~mmap_t() noexcept
		{
			if (_addr)
				internal::unmap(_addr, _len);
#ifdef _WIN32
			if (_mapping)
				CloseHandle(_mapping);
			if (_fd != -1)
				_close(_fd);
#else
			if (_fd != -1)
				::close(_fd);
#endif
		}

		SUBSTRATE_NO_DISCARD(SUBSTRATE_CXX14_CONSTEXPR bool valid() const noexcept)
#ifdef _WIN32
			{ return _mapping && _addr; }
#else
			{ return _addr; }
#endif

		mmap_t(const mmap_t &) = delete;
		mmap_t &operator=(const mmap_t &) = delete;
		mmap_t(mmap_t &&other) noexcept : mmap_t{} { *this = std::move(other); }

		mmap_t &operator=(mmap_t &&map) noexcept
		{
			std::swap(_fd, map._fd);
			std::swap(_addr, map._addr);
#ifdef _WIN32
			std::swap(_mapping, map._mapping);
#endif
			std::swap(_len, map._len);
			return *this;
		}

		SUBSTRATE_NO_DISCARD(mmap_t
			dup(const int32_t prot, const std::size_t len, const int32_t flags, void *addr) const noexcept)
		{
			if (!valid())
				return {};
			return {*this, len, prot, flags, addr};
		}

		template<typename T> SUBSTRATE_NO_DISCARD(bool chperm(const T prot) noexcept)
#ifdef _WIN32
			{ return VirtualProtect(_addr, _len, prot, nullptr) == 0; }
#else
			{ return mprotect(_addr, _len, prot) == 0; }
#endif

		template<typename T> T *address() noexcept { return static_cast<T *>(_addr); }
		template<typename T> const T *address() const noexcept { return static_cast<const T *>(_addr); }

		SUBSTRATE_NO_DISCARD(std::size_t length() const noexcept) { return _len; }

		template<typename T> T *operator [](const std::size_t idx) { return index<T>(idx); }
		template<typename T> const T *operator [](const off_t idx) const { return index<const T>(idx); }
		template<typename T> T *at(const std::size_t idx) { return index<T>(idx); }
		template<typename T> const T *at(const std::size_t idx) const { return index<const T>(idx); }

		// NOLINTNEXTLINE(bugprone-exception-escape)
		SUBSTRATE_NO_DISCARD(void *address(const std::size_t offset) noexcept) { return index<void *>(offset); }
		// NOLINTNEXTLINE(bugprone-exception-escape)
		SUBSTRATE_NO_DISCARD(const void *address(const std::size_t offset) const noexcept)
			{ return index<const void *>(offset); }

		SUBSTRATE_NO_DISCARD(std::uintptr_t numeric_address() const noexcept)
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
			{ return reinterpret_cast<std::uintptr_t>(_addr); } // lgtm[cpp/reinterpret-cast]

		SUBSTRATE_NO_DISCARD(bool lock() const noexcept) { return lock(_len); }
		SUBSTRATE_NO_DISCARD(bool lock(const std::size_t length) const noexcept)
			{ return internal::lock(_addr, length); }

		SUBSTRATE_NO_DISCARD(bool lock_at(const std::size_t idx, const size_t length) const noexcept)
		{
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
			auto *const addr{reinterpret_cast<uint8_t *>(_addr)};
			// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
			return internal::lock(addr + idx, length);
		}

		SUBSTRATE_NO_DISCARD(bool unlock() const noexcept) { return unlock(_len); }
		SUBSTRATE_NO_DISCARD(bool unlock(const std::size_t length) const noexcept)
			{ return internal::unlock(_addr, length); }

		SUBSTRATE_NO_DISCARD(bool unlock_at(const std::size_t idx, const std::size_t length) const noexcept)
		{
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
			auto *const addr{reinterpret_cast<realptr_t>(_addr)};
			// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
			return internal::unlock(addr + idx, length);
		}

// TODO: implement remap as unmap + map in these platforms
#if !defined(__APPLE__) && !defined(_WIN32)
		SUBSTRATE_NO_DISCARD(bool remap(const int32_t flags, const std::size_t new_length) noexcept)
		{
			const auto old_len = _len;
			_len = new_length;

			return (_addr = ::mremap(_addr, old_len, _len, flags)) != MAP_FAILED;
		}

		SUBSTRATE_NO_DISCARD(bool remap(const int32_t flags, const std::size_t new_length,
			const std::uintptr_t new_addr) noexcept)
		{
			const auto old_len = _len;
			_len = new_length;

			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
			const void *wanted_addr = reinterpret_cast<void *>(new_addr); // lgtm[cpp/reinterpret-cast]

			return (_addr = ::mremap(_addr, old_len, _len, flags, wanted_addr)) != MAP_FAILED;
		}
#endif

#ifndef _WIN32
		SUBSTRATE_NO_DISCARD(bool sync(const int flags = MS_SYNC | MS_INVALIDATE) const noexcept)
			{ return sync(_len, flags); }

		SUBSTRATE_NO_DISCARD(bool sync(const std::size_t length,
				const int flags = MS_SYNC | MS_INVALIDATE) const noexcept)
			{ return msync(_addr, length, flags) == 0; }

		SUBSTRATE_NO_DISCARD(bool advise(const int32_t advice) const noexcept)
			{ return advise(advice, _len); }
		SUBSTRATE_NO_DISCARD(bool advise(const int32_t advice, const std::size_t length) const noexcept)
			{ return ::madvise(_addr, length, advice) == 0; }

		SUBSTRATE_NO_DISCARD(bool advise_at(const int32_t advice, const std::size_t length,
			const std::size_t idx) const noexcept)
		{
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
			const auto addr = reinterpret_cast<std::uintptr_t>(_addr); // lgtm[cpp/reinterpret-cast]
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
			return ::madvise(reinterpret_cast<void *>(addr + idx), length, advice) == 0; // lgtm[cpp/reinterpret-cast]
		}
#else
		SUBSTRATE_NO_DISCARD(bool sync() const noexcept) { return sync(_len); }
		SUBSTRATE_NO_DISCARD(bool sync(const std::size_t length) const noexcept)
			{ return FlushViewOfFile(_addr, length); }

		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		SUBSTRATE_NO_DISCARD(bool advise(const int32_t) const noexcept) { return true; }
		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		SUBSTRATE_NO_DISCARD(bool advise(const int32_t, const std::size_t) const noexcept) { return true; }
		// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
		SUBSTRATE_NO_DISCARD(bool advise_at(const int32_t, const std::size_t, const std::size_t) const noexcept)
			{ return true; }
#endif

		template<typename T> void copyTo(const std::size_t idx, T &value) const
		{
			const auto *const src{index<const void *>(idx)};
			assert(sizeof(T) <= _len - idx);
			std::memcpy(&value, src, sizeof(T));
		}

		template<typename T, std::size_t N> void copyTo(const std::size_t idx,
			std::array<T, N> &value) const
		{
			constexpr auto length{sizeof(T) * N};
			const auto *const src{index<void *>(idx)};
			assert(length <= _len - idx);
			std::memcpy(value.data(), src, length);
		}

		void copyTo(const std::size_t idx, std::string &value) const
		{
			const auto *const src{index<void *>(idx)};
			assert(value.size() <= _len - idx);
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
			std::memcpy(const_cast<char *>(value.data()), src, value.size());
		}

#if __cplusplus >= 201703L
		void copyTo(const std::size_t idx, std::string_view &value) const
		{
			const auto *const src{index<void *>(idx)};
			assert(value.size() <= _len - idx);
			// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
			std::memcpy(const_cast<char *>(value.data()), src, value.size());
		}
#endif

		template<typename T> void copyFrom(const std::size_t idx, const T &value) const
		{
			auto *const dest{index<void *>(idx)};
			assert(sizeof(T) <= _len - idx);
			std::memcpy(dest, &value, sizeof(T));
		}

		template<typename T, std::size_t N> void copyFrom(const std::size_t idx,
			const std::array<T, N> &value) const
		{
			constexpr auto length{sizeof(T) * N};
			auto *const dest{index<void *>(idx)};
			assert(length <= _len - idx);
			std::memcpy(dest, value.data(), length);
		}

		void copyFrom(const std::size_t idx, const std::string &value) const
		{
			auto *const dest{index<void *>(idx)};
			assert(value.size() <= _len - idx);
			std::memcpy(dest, value.data(), value.size());
		}

#if __cplusplus >= 201703L
		void copyFrom(const std::size_t idx, const std::string_view &value) const
		{
			auto *const dest{index<void *>(idx)};
			assert(value.size() <= _len - idx);
			std::memcpy(dest, value.data(), value.size());
		}
#endif

		bool operator ==(const mmap_t &rhs) const noexcept
			{ return _fd == rhs._fd && _addr == rhs._addr && _len == rhs._len; }
		bool operator !=(const mmap_t &rhs) const noexcept
			{ return !(*this == rhs); }
	};
} // namespace substrate

#endif /* SUBSTRATE_MMAP */
/* vim: set ft=cpp ts=4 sw=4 noexpandtab: */
