//go:build scheduler.asyncify

package task

import (
	"unsafe"
)

// Stack canary, to detect a stack overflow. The number is a random number
// generated by random.org. The bit fiddling dance is necessary because
// otherwise Go wouldn't allow the cast to a smaller integer size.
const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0)))

//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(str string)

// state is a structure which holds a reference to the state of the task.
// When the task is suspended, the stack pointers are saved here.
type state struct {
	// entry is the entry function of the task.
	// This is needed every time the function is invoked so that asyncify knows what to rewind.
	entry uintptr

	// args are a pointer to a struct holding the arguments of the function.
	args unsafe.Pointer

	// stackState is the state of the stack while unwound.
	stackState

	launched bool
}

// stackState is the saved state of a stack while unwound.
// The stack is arranged with asyncify at the bottom, C stack at the top, and a gap of available stack space between the two.
type stackState struct {
	// asyncify is the stack pointer of the asyncify stack.
	// This starts from the bottom and grows upwards.
	asyncifysp unsafe.Pointer

	// asyncify is stack pointer of the C stack.
	// This starts from the top and grows downwards.
	csp unsafe.Pointer

	// Pointer to the first (lowest address) of the stack. It must never be
	// overwritten. It can be checked from time to time to see whether a stack
	// overflow happened in the past.
	canaryPtr *uintptr
}

// start creates and starts a new goroutine with the given function and arguments.
// The new goroutine is immediately started.
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
	t := &Task{}
	t.state.initialize(fn, args, stackSize)
	runqueuePushBack(t)
}

//export tinygo_launch
func (*state) launch()

//go:linkname align runtime.align
func align(p uintptr) uintptr

// initialize the state and prepare to call the specified function with the specified argument bundle.
func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
	// Save the entry call.
	s.entry = fn
	s.args = args

	// Create a stack.
	stack := runtime_alloc(stackSize, nil)

	// Set up the stack canary, a random number that should be checked when
	// switching from the task back to the scheduler. The stack canary pointer
	// points to the first word of the stack. If it has changed between now and
	// the next stack switch, there was a stack overflow.
	s.canaryPtr = (*uintptr)(stack)
	*s.canaryPtr = stackCanary

	// Calculate stack base addresses.
	s.asyncifysp = unsafe.Add(stack, unsafe.Sizeof(uintptr(0)))
	s.csp = unsafe.Add(stack, stackSize)
}

//go:linkname runqueuePushBack runtime.runqueuePushBack
func runqueuePushBack(*Task)

// currentTask is the current running task, or nil if currently in the scheduler.
var currentTask *Task

// Current returns the current active task.
func Current() *Task {
	return currentTask
}

// Pause suspends the current task and returns to the scheduler.
// This function may only be called when running on a goroutine stack, not when running on the system stack.
func Pause() {
	if *currentTask.state.canaryPtr != stackCanary {
		runtimePanic("stack overflow")
	}

	currentTask.state.unwind()
}

//export tinygo_unwind
func (*stackState) unwind()

// Resume the task until it pauses or completes.
// This may only be called from the scheduler.
func (t *Task) Resume() {
	// The current task must be saved and restored because this can nest on WASM with JS.
	prevTask := currentTask
	t.gcData.swap()
	currentTask = t
	if !t.state.launched {
		t.state.launch()
		t.state.launched = true
	} else {
		t.state.rewind()
	}
	currentTask = prevTask
	t.gcData.swap()
	if uintptr(t.state.asyncifysp) > uintptr(t.state.csp) {
		runtimePanic("stack overflow")
	}
}

//export tinygo_rewind
func (*state) rewind()

// OnSystemStack returns whether the caller is running on the system stack.
func OnSystemStack() bool {
	// If there is not an active goroutine, then this must be running on the system stack.
	return Current() == nil
}
