#if defined(__SWITCH__)
#include "hw/sh4/sh4_if.h"
#include "hw/mem/addrspace.h"
#include "oslib/virtmem.h"

#include <switch.h>
#include <malloc.h>
#include <unordered_set>

namespace virtmem
{

#define siginfo_t switch_siginfo_t

// Allocated RAM
static void *ramBase;
static size_t ramSize;

// Reserved memory space
static void *reserved_base;
static size_t reserved_size;
static VirtmemReservation *virtmemReservation;

// Mapped regions
static Mapping *memMappings;
static u32 memMappingCount;

static void deleteMappings();

bool region_lock(void *start, size_t len)
{
	const size_t inpage = (uintptr_t)start & PAGE_MASK;
	len = (len + inpage + PAGE_SIZE - 1) & ~PAGE_MASK;

	Result rc;
	uintptr_t start_addr = (uintptr_t)start - inpage;
	for (uintptr_t addr = start_addr; addr < (start_addr + len); addr += PAGE_SIZE)
	{
		rc = svcSetMemoryPermission((void *)addr, PAGE_SIZE, Perm_R);
		if (R_FAILED(rc))
			ERROR_LOG(VMEM, "Failed to SetPerm Perm_R on %p len 0x%x rc 0x%x", (void*)addr, PAGE_SIZE, rc);
	}

	return true;
}

bool region_unlock(void *start, size_t len)
{
	const size_t inpage = (uintptr_t)start & PAGE_MASK;
	len = (len + inpage + PAGE_SIZE - 1) & ~PAGE_MASK;

	Result rc;
	uintptr_t start_addr = (uintptr_t)start - inpage;
	for (uintptr_t addr = start_addr; addr < (start_addr + len); addr += PAGE_SIZE)
	{
		rc = svcSetMemoryPermission((void *)addr, PAGE_SIZE, Perm_Rw);
		if (R_FAILED(rc))
			ERROR_LOG(VMEM, "Failed to SetPerm Perm_Rw on %p len 0x%x rc 0x%x", (void*)addr, PAGE_SIZE, rc);
	}

	return true;
}

// Implement vmem initialization for RAM, ARAM, VRAM and SH4 context, fpcb etc.

// vmem_base_addr points to an address space of 512MB that can be used for fast memory ops.
// In negative offsets of the pointer (up to FPCB size, usually 65/129MB) the context and jump table
// can be found. If the platform init returns error, the user is responsible for initializing the
// memory using a fallback (that is, regular mallocs and falling back to slow memory JIT).
bool init(void **vmem_base_addr, void **sh4rcb_addr, size_t ramSize)
{
	// Allocate RAM
	ramSize += sizeof(Sh4RCB);
	virtmem::ramSize = ramSize;
	ramBase = memalign(PAGE_SIZE, ramSize);
	if (ramBase == nullptr) {
		ERROR_LOG(VMEM, "memalign(%zx) failed", ramSize);
		return false;
	}

	// Reserve address space
	reserved_size = 512_MB + sizeof(Sh4RCB) + ARAM_SIZE_MAX;
	virtmemLock();
    // virtual mem
    reserved_base = virtmemFindCodeMemory(reserved_size, PAGE_SIZE);
	if (reserved_base != nullptr)
		virtmemReservation = virtmemAddReservation(reserved_base, reserved_size);
	virtmemUnlock();
	if (reserved_base == nullptr || virtmemReservation == nullptr)
	{
		ERROR_LOG(VMEM, "virtmemReserve(%zx) failed: base %p res %p", reserved_size, reserved_base, virtmemReservation);
		free(ramBase);
		return false;
	}

	constexpr size_t fpcb_size = sizeof(Sh4RCB::fpcb);
	void *sh4rcb_base_ptr  = (u8 *)reserved_base + fpcb_size;
	Handle process = envGetOwnProcessHandle();

	// Now map the memory for the SH4 context, do not include FPCB on purpose (paged on demand).
	size_t sz = sizeof(Sh4RCB) - fpcb_size;
	DEBUG_LOG(VMEM, "mapping %lx to %p size %zx", (u64)ramBase + fpcb_size, sh4rcb_base_ptr, sz);
	Result rc = svcMapProcessCodeMemory(process, (u64)sh4rcb_base_ptr, (u64)ramBase + fpcb_size, sz);
	if (R_FAILED(rc))
	{
		ERROR_LOG(VMEM, "Failed to Map Sh4RCB (%p, %p, %zx) -> %x", sh4rcb_base_ptr, (u8 *)ramBase + fpcb_size, sz, rc);
		destroy();
		return false;
	}
	rc = svcSetProcessMemoryPermission(process, (u64)sh4rcb_base_ptr, sz, Perm_Rw);
	if (R_FAILED(rc))
	{
		ERROR_LOG(VMEM, "Failed to set Sh4RCB perms (%p, %zx) -> %x", sh4rcb_base_ptr, sz, rc);
		destroy();
		return false;
	}
	*sh4rcb_addr = reserved_base;
	*vmem_base_addr = (u8 *)reserved_base + sizeof(Sh4RCB);
	NOTICE_LOG(VMEM, "virtmem::init successful");

	return true;
}

void destroy()
{
	deleteMappings();
	constexpr size_t fpcb_size = sizeof(Sh4RCB::fpcb);
	Result rc = svcUnmapProcessCodeMemory(envGetOwnProcessHandle(), (u64)reserved_base + fpcb_size,
			(u64)ramBase + fpcb_size, sizeof(Sh4RCB) - fpcb_size);
	if (R_FAILED(rc))
		ERROR_LOG(VMEM, "Failed to Unmap Sh4RCB -> %x", rc);
	if (virtmemReservation != nullptr)
	{
		virtmemLock();
		virtmemRemoveReservation(virtmemReservation);
		virtmemUnlock();
		virtmemReservation = nullptr;
	}
	free(ramBase);
	ramBase = nullptr;
	NOTICE_LOG(VMEM, "virtmem::destroy done");
}

// Flush (unmap) the FPCB array
void reset_mem(void *ptr, unsigned size)
{
	u64 offset = (u8 *)ptr - (u8 *)reserved_base;
	DEBUG_LOG(VMEM, "Unmapping %lx from %p size %x", (u64)ramBase + offset, ptr, size);
	Handle process = envGetOwnProcessHandle();
	for (; offset < size; offset += PAGE_SIZE)
	{
		svcUnmapProcessCodeMemory(process, (u64)ptr + offset, (u64)ramBase + offset, PAGE_SIZE);
		// fails if page is unmapped, which isn't an error
	}
}

// Allocates a memory page for the FPCB array
void ondemand_page(void *address, unsigned size)
{
	u64 offset = (u8 *)address - (u8 *)reserved_base;
	//DEBUG_LOG(VMEM, "Mapping %lx to %p size %x", (u64)ramBase + offset, address, size);
	Result rc = svcMapProcessCodeMemory(envGetOwnProcessHandle(), (u64)address, (u64)ramBase + offset, size);
	if (R_FAILED(rc)) {
		ERROR_LOG(VMEM, "svcMapProcessCodeMemory(%p, %lx, %x) failed: %x", address, (u64)ramBase + offset, size, rc);
		return;
	}
	rc = svcSetProcessMemoryPermission(envGetOwnProcessHandle(), (u64)address, size, Perm_Rw);
	if (R_FAILED(rc))
		ERROR_LOG(VMEM, "svcSetProcessMemoryPermission(%p, %x) failed: %x", address, size, rc);
}

static void deleteMappings()
{
	if (memMappings == nullptr)
		return;
	std::unordered_set<u64> backingAddrs;
	Handle process = envGetOwnProcessHandle();
	for (u32 i = 0; i < memMappingCount; i++)
	{
		// Ignore unmapped stuff
		if (memMappings[i].memsize == 0)
			continue;
		if (!memMappings[i].allow_writes) // don't (un)map ARAM read-only
			continue;

		u64 ramOffset = (u64)ramBase + sizeof(Sh4RCB) + memMappings[i].memoffset;
		if (backingAddrs.count(ramOffset) != 0)
			continue;
		backingAddrs.insert(ramOffset);
		u64 offset = memMappings[i].start_address + sizeof(Sh4RCB);
		Result rc = svcUnmapProcessCodeMemory(process, (u64)reserved_base + offset,
					ramOffset, memMappings[i].memsize);
		if (R_FAILED(rc))
			ERROR_LOG(VMEM, "deleteMappings: error unmapping offset %lx from %lx", memMappings[i].memoffset, offset);
	}
	delete [] memMappings;
	memMappings = nullptr;
	memMappingCount = 0;
}

// Creates mappings to the underlying ram (not including mirroring sections)
void create_mappings(const Mapping *vmem_maps, unsigned nummaps)
{
	deleteMappings();
	memMappingCount = nummaps;
	memMappings = new Mapping[nummaps];
	memcpy(memMappings, vmem_maps, nummaps * sizeof(Mapping));

	std::unordered_set<u64> backingAddrs;
	Handle process = envGetOwnProcessHandle();
	for (u32 i = 0; i < nummaps; i++)
	{
		// Ignore unmapped stuff, it is already reserved as PROT_NONE
		if (vmem_maps[i].memsize == 0)
			continue;
		if (!vmem_maps[i].allow_writes) // don't map ARAM read-only
			continue;

		u64 ramOffset = (u64)ramBase + sizeof(Sh4RCB) + vmem_maps[i].memoffset;
		if (backingAddrs.count(ramOffset) != 0)
			// already mapped once so ignore other mirrors
			continue;
		backingAddrs.insert(ramOffset);
		u64 offset = vmem_maps[i].start_address + sizeof(Sh4RCB);
		Result rc = svcMapProcessCodeMemory(process, (u64)reserved_base + offset, ramOffset, vmem_maps[i].memsize);
		if (R_FAILED(rc)) {
			ERROR_LOG(VMEM, "create_mappings: error mapping offset %lx to %lx", vmem_maps[i].memoffset, offset);
		}
		else
		{
			rc = svcSetProcessMemoryPermission(process, (u64)reserved_base + offset, vmem_maps[i].memsize, Perm_Rw);
			if (R_FAILED(rc))
				ERROR_LOG(VMEM, "svcSetProcessMemoryPermission() failed: %x", rc);
			else
				DEBUG_LOG(VMEM, "create_mappings: mapped offset %lx to %lx size %lx", vmem_maps[i].memoffset, offset, vmem_maps[i].memsize);
		}
	}
}

// Prepares the code region for JIT operations, thus marking it as RWX
bool prepare_jit_block(void *code_area, size_t size, void **code_area_rwx)
{
	die("Not supported in libnx");

	return false;
}

void release_jit_block(void *code_area, size_t size)
{
	die("Not supported in libnx");
}

// Use two addr spaces: need to remap something twice, therefore use allocate_shared_filemem()
bool prepare_jit_block(void *code_area, size_t size, void **code_area_rw, ptrdiff_t *rx_offset)
{
	const size_t size_aligned = ((size + PAGE_SIZE) & (~(PAGE_SIZE-1)));

	virtmemLock();
	void* ptr_rw = virtmemFindAslr(size_aligned, 0);
	bool failure = ptr_rw == nullptr
			|| R_FAILED(svcMapProcessMemory(ptr_rw, envGetOwnProcessHandle(), (u64)code_area, size_aligned));
	virtmemUnlock();
	if (failure)
	{
		ERROR_LOG(DYNAREC, "Failed to map jit rw block...");
		return false;
	}

	*code_area_rw = ptr_rw;
	*rx_offset = (char*)code_area - (char*)ptr_rw;
	INFO_LOG(DYNAREC, "Info: Using NO_RWX mode, rx ptr: %p, rw ptr: %p, offset: %ld\n", code_area, ptr_rw, (long)*rx_offset);

	return true;
}

void release_jit_block(void *code_area1, void *code_area2, size_t size)
{
	const size_t size_aligned = ((size + PAGE_SIZE) & (~(PAGE_SIZE-1)));
	virtmemLock();
	svcUnmapProcessMemory(code_area2, envGetOwnProcessHandle(), (u64)code_area1, size_aligned);
	virtmemUnlock();
}

} // namespace virtmem

#include <ucontext.h>
void fault_handler(int sn, siginfo_t * si, void *segfault_ctx);

extern "C"
{
alignas(16) u8 __nx_exception_stack[0x1000];
u64 __nx_exception_stack_size = sizeof(__nx_exception_stack);

void context_switch_aarch64(void* context);

void __libnx_exception_handler(ThreadExceptionDump *ctx)
{
	alignas(16) static ucontext_t u_ctx;
	u_ctx.uc_mcontext.pc = ctx->pc.x;
	for (int i = 0; i < 29; i++)
		u_ctx.uc_mcontext.regs[i] = ctx->cpu_gprs[i].x;

	siginfo_t sig_info;
	sig_info.si_addr = (void*)ctx->far.x;

	fault_handler(0, &sig_info, (void *)&u_ctx);

	alignas(16) static uint64_t savedRegisters[66];
	uint64_t *ptr = savedRegisters;
	// fpu registers (64 bits)
	for (int i = 0; i < 32; i++)
		ptr[i]  = *(u64 *)&ctx->fpu_gprs[i].d;
	// cpu gp registers
	for (int i = 0; i < 29; i++)
		ptr[i + 32]  = u_ctx.uc_mcontext.regs[i];
	// Special regs
	ptr[29 + 32] = ctx->fp.x;	// frame pointer
	ptr[30 + 32] = ctx->lr.x;	// link register
	ptr[31 + 32] = ctx->pstate;	// sprs
	ptr[32 + 32] = ctx->sp.x;	// stack pointer
	ptr[33 + 32] = u_ctx.uc_mcontext.pc; // PC

	context_switch_aarch64(ptr);
}
}

#ifndef LIBRETRO
[[noreturn]] void os_DebugBreak()
{
	diagAbortWithResult(MAKERESULT(350, 1));
}
#endif
#endif	// __SWITCH__
