// +build nintendoswitch

package runtime

import "unsafe"

type timeUnit int64

const asyncScheduler = false

const (
	// Handles
	infoTypeTotalMemorySize = 6          // Total amount of memory available for process.
	infoTypeUsedMemorySize  = 7          // Amount of memory currently used by process.
	currentProcessHandle    = 0xFFFF8001 // Pseudo handle for the current process.

	// Types of config Entry
	envEntryTypeEndOfList        = 0 // Entry list terminator.
	envEntryTypeMainThreadHandle = 1 // Provides the handle to the main thread.
	envEntryTypeOverrideHeap     = 3 // Provides heap override information.

	// Default heap size allocated by libnx
	defaultHeapSize = 0x2000000 * 16

	debugInit = false
)

//go:extern _saved_return_address
var savedReturnAddress uintptr

//export __stack_top
var stackTop uintptr

//go:extern _context
var context uintptr

//go:extern _main_thread
var mainThread uintptr

var (
	heapStart = uintptr(0)
	heapEnd   = uintptr(0)
	usedRam   = uint64(0)
	totalRam  = uint64(0)
	totalHeap = uint64(0)
)

func postinit() {}

func preinit() {
	// Unsafe to use heap here
	setupEnv()
	setupHeap()
}

// Entry point for Go. Initialize all packages and call main.main().
//export main
func main() {
	preinit()
	run()

	// Call exit to correctly finish the program
	// Without this, the application crashes at start, not sure why
	for {
		exit(0)
	}
}

// sleepTicks sleeps for the specified system ticks
func sleepTicks(d timeUnit) {
	svcSleepThread(uint64(ticksToNanoseconds(d)))
}

// armTicksToNs converts cpu ticks to nanoseconds
// Nintendo Switch CPU ticks has a fixed rate at 19200000
// It is basically 52 ns per tick
// The formula 625 / 12 is equivalent to 1e9 / 19200000
func ticksToNanoseconds(tick timeUnit) int64 {
	return int64(tick * 625 / 12)
}

func nanosecondsToTicks(ns int64) timeUnit {
	return timeUnit(12 * ns / 625)
}

func ticks() timeUnit {
	return timeUnit(ticksToNanoseconds(timeUnit(getArmSystemTick())))
}

var stdoutBuffer = make([]byte, 120)
var position = 0

func putchar(c byte) {
	if c == '\n' || position >= len(stdoutBuffer) {
		svcOutputDebugString(&stdoutBuffer[0], uint64(position))
		position = 0
		return
	}

	stdoutBuffer[position] = c
	position++
}

func abort() {
	for {
		exit(1)
	}
}

//export write
func write(fd int32, buf *byte, count int) int {
	// TODO: Proper handling write
	for i := 0; i < count; i++ {
		putchar(*buf)
		buf = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(buf)) + 1))
	}
	return count
}

// exit checks if a savedReturnAddress were provided by the launcher
// if so, calls the nxExit which restores the stack and returns to launcher
// otherwise just calls systemcall exit
func exit(code int) {
	if savedReturnAddress == 0 {
		svcExitProcess(code)
		return
	}

	nxExit(code, stackTop, savedReturnAddress)
}

type configEntry struct {
	Key   uint32
	Flags uint32
	Value [2]uint64
}

func setupEnv() {
	if debugInit {
		println("Saved Return Address:", savedReturnAddress)
		println("Context:", context)
		println("Main Thread Handle:", mainThread)
	}

	// See https://switchbrew.org/w/index.php?title=Homebrew_ABI
	// Here we parse only the required configs for initializing
	if context != 0 {
		ptr := context
		entry := (*configEntry)(unsafe.Pointer(ptr))
		for entry.Key != envEntryTypeEndOfList {
			switch entry.Key {
			case envEntryTypeOverrideHeap:
				if debugInit {
					println("Got heap override")
				}
				heapStart = uintptr(entry.Value[0])
				heapEnd = heapStart + uintptr(entry.Value[1])
			case envEntryTypeMainThreadHandle:
				mainThread = uintptr(entry.Value[0])
			default:
				if entry.Flags&1 > 0 {
					// Mandatory but not parsed
					runtimePanic("mandatory config entry not parsed")
				}
			}
			ptr += unsafe.Sizeof(configEntry{})
			entry = (*configEntry)(unsafe.Pointer(ptr))
		}
	}
	// Fetch used / total RAM for allocating HEAP
	svcGetInfo(&totalRam, infoTypeTotalMemorySize, currentProcessHandle, 0)
	svcGetInfo(&usedRam, infoTypeUsedMemorySize, currentProcessHandle, 0)
}

func setupHeap() {
	if heapStart != 0 {
		if debugInit {
			print("Heap already overrided by hblauncher")
		}
		// Already overrided
		return
	}

	if debugInit {
		print("No heap override. Using normal initialization")
	}

	size := uint32(defaultHeapSize)

	if totalRam > usedRam+0x200000 {
		// Get maximum possible heap
		size = uint32(totalRam-usedRam-0x200000) & ^uint32(0x1FFFFF)
	}

	if size < defaultHeapSize {
		size = defaultHeapSize
	}

	if debugInit {
		println("Trying to allocate", size, "bytes of heap")
	}

	svcSetHeapSize(&heapStart, uint64(size))

	if heapStart == 0 {
		runtimePanic("failed to allocate heap")
	}

	totalHeap = uint64(size)

	heapEnd = heapStart + uintptr(size)

	if debugInit {
		println("Heap Start", heapStart)
		println("Heap End  ", heapEnd)
		println("Total Heap", totalHeap)
	}
}

// getHeapBase returns the start address of the heap
// this is externally linked by gonx
func getHeapBase() uintptr {
	return heapStart
}

// getHeapEnd returns the end address of the heap
// this is externally linked by gonx
func getHeapEnd() uintptr {
	return heapEnd
}

// getContextPtr returns the hblauncher context
// this is externally linked by gonx
func getContextPtr() uintptr {
	return context
}

// getMainThreadHandle returns the main thread handler if any
// this is externally linked by gonx
func getMainThreadHandle() uintptr {
	return mainThread
}

//export armGetSystemTick
func getArmSystemTick() int64

// nxExit exits the program to homebrew launcher
//export __nx_exit
func nxExit(code int, stackTop uintptr, exitFunction uintptr)

// Horizon System Calls
// svcSetHeapSize Set the process heap to a given size. It can both extend and shrink the heap.
// svc 0x01
//export svcSetHeapSize
func svcSetHeapSize(addr *uintptr, length uint64) uint64

// svcExitProcess Exits the current process.
// svc 0x07
//export svcExitProcess
func svcExitProcess(code int)

// svcSleepThread Sleeps the current thread for the specified amount of time.
// svc 0x0B
//export svcSleepThread
func svcSleepThread(nanos uint64)

// svcOutputDebugString Outputs debug text, if used during debugging.
// svc 0x27
//export svcOutputDebugString
func svcOutputDebugString(str *uint8, size uint64) uint64

// svcGetInfo Retrieves information about the system, or a certain kernel object.
// svc 0x29
//export svcGetInfo
func svcGetInfo(output *uint64, id0 uint32, handle uint32, id1 uint64) uint64
