package runtime

import (
	"internal/task"
	"runtime/interrupt"
	"tinygo"
	"unsafe"
)

// trap is a compiler hint that this function cannot be executed. It is
// translated into either a trap instruction or a call to abort().
//
//export llvm.trap
func trap()

// Inline assembly stub. It is essentially C longjmp but modified a bit for the
// purposes of TinyGo. It restores the stack pointer and jumps to the given pc.
//
//export tinygo_longjmp
func tinygo_longjmp(frame *deferFrame)

// Compiler intrinsic.
// Returns whether recover is supported on the current architecture.
func supportsRecover() bool

// Compile intrinsic.
// Returns which strategy is used. This is usually "print" but can be changed
// using the -panic= compiler flag.
func panicStrategy() uint8

// DeferFrame is a stack allocated object that stores information for the
// current "defer frame", which is used in functions that use the `defer`
// keyword.
// The compiler knows about the JumpPC struct offset and the DeferPtr field
// index, so they should not be moved without also updating compiler/defer.go.
type deferFrame struct {
	JumpSP     unsafe.Pointer                 // stack pointer to return to
	JumpPC     unsafe.Pointer                 // pc to return to
	ExtraRegs  [deferExtraRegs]unsafe.Pointer // extra registers (depending on the architecture)
	Previous   *deferFrame                    // previous recover buffer pointer
	Panicking  panicState                     // panic/Goexit state
	PanicValue interface{}                    // panic value, might be nil for panic(nil) for example
	DeferPtr   unsafe.Pointer                 // head of the stack-allocated defer list
}

type panicState uint8

const (
	panicTrue panicState = 1 << iota
	panicGoexit
)

// Builtin function panic(msg), used as a compiler intrinsic.
func _panic(message interface{}) {
	panicOrGoexit(message, panicTrue)
}

func panicOrGoexit(message interface{}, panicking panicState) {
	if panicking != panicGoexit && panicStrategy() == tinygo.PanicStrategyTrap {
		trap()
	}
	// Note: recover is not supported inside interrupts.
	// (This could be supported, like defer, but we currently don't).
	if supportsRecover() && !interrupt.In() {
		frame := (*deferFrame)(task.Current().DeferFrame)
		if frame != nil {
			frame.PanicValue = message
			if panicking&panicTrue != 0 {
				panicking |= frame.Panicking & panicGoexit
			}
			frame.Panicking = panicking
			tinygo_longjmp(frame)
			// unreachable
		}
	}
	if panicking == panicGoexit {
		// Call to Goexit() instead of a panic.
		// Exit the goroutine instead of printing a panic message.
		goexit()
	}
	printstring("panic: ")
	printitf(message)
	printnl()
	abort()
}

// Cause a runtime panic, which is (currently) always a string.
func runtimePanic(msg string) {
	// As long as this function is inined, llvm.returnaddress(0) will return
	// something sensible.
	runtimePanicAt(returnAddress(0), msg)
}

// runtimeFatal terminates for runtime failures that cannot safely be
// recovered, such as exhausting the heap.
func runtimeFatal(msg string) {
	if panicStrategy() == tinygo.PanicStrategyTrap {
		trap()
	}
	printstring("fatal error: ")
	printstring(msg)
	printnl()
	abort()
}

func runtimePanicAt(addr unsafe.Pointer, msg string) {
	if panicStrategy() == tinygo.PanicStrategyTrap {
		trap()
	}
	if supportsRecover() && !interrupt.In() {
		frame := (*deferFrame)(task.Current().DeferFrame)
		if frame != nil {
			// Use the normal panic mechanism so that this runtime error
			// can be recovered with recover().
			frame.PanicValue = plainError(msg)
			frame.Panicking = panicTrue | (frame.Panicking & panicGoexit)
			tinygo_longjmp(frame)
			// unreachable
		}
	}
	if hasReturnAddr {
		// Note: the string "panic: runtime error at " is also used in
		// runtime_cortexm_hardfault.go. It is kept the same so that the string
		// can be deduplicated by the compiler.
		printstring("panic: runtime error at ")
		printptr(uintptr(addr) - callInstSize)
		printstring(": ")
	} else {
		printstring("panic: runtime error: ")
	}
	printstring(msg)
	printnl()
	abort()
}

// Called at the start of a function that includes a deferred call.
// It gets passed in the stack-allocated defer frame and configures it.
// Note that the frame is not zeroed yet, so we need to initialize all values
// that will be used.
//
//go:inline
//go:nobounds
func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) {
	if interrupt.In() {
		// Defer is not currently allowed in interrupts.
		// We could add support for this, but since defer might also allocate
		// (especially in loops) it might not be a good idea anyway.
		runtimeFatal("defer in interrupt")
	}
	currentTask := task.Current()
	frame.Previous = (*deferFrame)(currentTask.DeferFrame)
	frame.JumpSP = jumpSP
	frame.Panicking = 0
	frame.DeferPtr = nil
	currentTask.DeferFrame = unsafe.Pointer(frame)
}

// Called right before the return instruction. It pops the defer frame from the
// linked list of defer frames. It also re-raises a panic if the goroutine is
// still panicking.
//
//go:inline
//go:nobounds
func destroyDeferFrame(frame *deferFrame) {
	task.Current().DeferFrame = unsafe.Pointer(frame.Previous)
	if frame.Panicking&panicTrue != 0 {
		// We're still panicking!
		// Re-raise the panic now.
		panicOrGoexit(frame.PanicValue, panicTrue)
	}
	if frame.Panicking&panicGoexit != 0 {
		// A deferred function panicked during Goexit, and that panic was
		// recovered. Continue the original Goexit instead of returning.
		panicOrGoexit(nil, panicGoexit)
	}
}

// _recover is the built-in recover() function. It tries to recover a currently
// panicking goroutine.
// useParentFrame is set when the caller of runtime._recover has a defer frame
// itself. In that case, recover() shouldn't check that frame but one frame up.
//
// TODO: Go only allows recover() to succeed when called directly from a
// deferred function, not from a sub-call (e.g. defer func() { sub() }() where
// sub() calls recover()). The Go compiler enforces this by walking the stack
// to count frames between gorecover and gopanic. TinyGo currently does not
// have a stack unwinder, so this restriction is not enforced at runtime.
// Functions calling recover() are marked noinline to prevent the most common
// case (inlined sub-call), but non-inlined sub-calls can still incorrectly
// recover. Fixing this properly requires either frame pointer support or a
// lightweight stack unwinder.
func _recover(useParentFrame bool) interface{} {
	if !supportsRecover() || interrupt.In() {
		// Either we're compiling without stack unwinding support, or we're
		// inside an interrupt where panic/recover is not supported. Either way,
		// make this a no-op since panic() won't do any long jumps to a deferred
		// function.
		return nil
	}
	frame := (*deferFrame)(task.Current().DeferFrame)
	if useParentFrame {
		// Don't recover panic from the current frame (which can't be panicking
		// already), but instead from the previous frame.
		frame = frame.Previous
	}
	if frame != nil && frame.Panicking != 0 {
		if frame.Panicking&panicTrue == 0 {
			// Special value that indicates we're exiting the goroutine using
			// Goexit(). Therefore, make this recover call a no-op.
			return nil
		}
		// Only the first call to recover returns the panic value. It also stops
		// the panicking sequence, hence setting panicking to false.
		frame.Panicking &^= panicTrue
		return frame.PanicValue
	}
	// Not panicking, so return a nil interface.
	return nil
}

// Panic when trying to dereference a nil pointer.
func nilPanic() {
	runtimePanicAt(returnAddress(0), "nil pointer dereference")
}

// Panic when trying to add an entry to a nil map
func nilMapPanic() {
	runtimePanicAt(returnAddress(0), "assignment to entry in nil map")
}

// Panic when trying to access an array or slice out of bounds.
func lookupPanic() {
	runtimePanicAt(returnAddress(0), "index out of range")
}

// Panic when trying to slice a slice out of bounds.
func slicePanic() {
	runtimePanicAt(returnAddress(0), "slice out of range")
}

// Panic when trying to convert a slice to an array pointer (Go 1.17+) and the
// slice is shorter than the array.
func sliceToArrayPointerPanic() {
	runtimePanicAt(returnAddress(0), "slice smaller than array")
}

// Panic when calling unsafe.Slice() (Go 1.17+) or unsafe.String() (Go 1.20+)
// with a len that's too large (which includes if the ptr is nil and len is
// nonzero).
func unsafeSlicePanic() {
	runtimePanicAt(returnAddress(0), "unsafe.Slice/String: len out of range")
}

// Panic when trying to create a new channel that is too big.
func chanMakePanic() {
	runtimePanicAt(returnAddress(0), "new channel is too big")
}

// Panic when a shift value is negative.
func negativeShiftPanic() {
	runtimePanicAt(returnAddress(0), "negative shift")
}

// Panic when there is a divide by zero.
func divideByZeroPanic() {
	runtimePanicAt(returnAddress(0), "divide by zero")
}

func blockingPanic() {
	runtimePanicAt(returnAddress(0), "trying to do blocking operation in exported function")
}

//go:linkname fips_fatal crypto/internal/fips140.fatal
func fips_fatal(msg string) {
	runtimeFatal(msg)
}
