package main

import (
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func main() {
	println("# simple recover")
	recoverSimple()

	println("\n# recover with result")
	result := recoverWithResult()
	println("result:", result)

	println("\n# nested defer frame")
	nestedDefer()

	println("\n# nested panic: panic inside recover")
	nestedPanic()

	println("\n# panic inside defer")
	panicInsideDefer()

	println("\n# panic replace")
	panicReplace()

	println("\n# defer panic")
	deferPanic()

	println("\n# indirect recover")
	indirectRecover()

	println("\n# runtime.Goexit")
	runtimeGoexit()

	println("\n# repanic")
	recoverRepanic()

	println("\n# recover runtime errors")
	recoverRuntimeError()

	println("\n# recover from nil map and closed channel")
	recoverNilMapAndChan()

	println("\n# recover from hardware signals")
	recoverSignals()
}

func recoverSimple() {
	defer func() {
		println("recovering...")
		printitf("recovered:", recover())
	}()
	println("running panic...")
	panic("panic")
}

func recoverWithResult() (result int) {
	defer func() {
		printitf("recovered:", recover())
	}()
	result = 3
	println("running panic...")
	panic("panic")
}

func nestedDefer() {
	defer func() {
		printitf("recovered:", recover())
	}()

	func() {
		// The defer here doesn't catch the panic using recover(), so the outer
		// panic should do that.
		defer func() {
			println("deferred nested function")
		}()
		panic("panic")
	}()
	println("unreachable")
}

func nestedPanic() {
	defer func() {
		printitf("recovered 1:", recover())

		defer func() {
			printitf("recovered 2:", recover())
		}()

		panic("foo")
	}()
	panic("panic")
}

func panicInsideDefer() {
	defer func() {
		printitf("recovered:", recover())
	}()
	defer func() {
		panic("panic")
	}()
}

func panicReplace() {
	defer func() {
		printitf("recovered:", recover())
	}()
	defer func() {
		println("panic 2")
		panic("panic 2")
	}()
	println("panic 1")
	panic("panic 1")
}

func deferPanic() {
	defer func() {
		printitf("recovered from deferred call:", recover())
	}()

	// This recover should not do anything.
	defer recover()

	defer panic("deferred panic")
	println("defer panic")
}

// TODO: Go only allows recover() to succeed when called directly from a
// deferred function. Update this test once runtime recover can distinguish it.
func indirectRecover() {
	defer func() {
		if r := indirectRecoverHelper(); r == nil {
			println("indirect recover returned nil")
		} else {
			printitf("indirect recover returned:", r)
		}
	}()
	panic("indirect panic")
}

//go:noinline
func indirectRecoverHelper() interface{} {
	return recover()
}

func runtimeGoexit() {
	wg.Add(1)
	go func() {
		defer func() {
			println("Goexit deferred function, recover is nil:", recover() == nil)
			wg.Done()
		}()

		runtime.Goexit()
	}()
	wg.Wait()

	wg.Add(1)
	go func() {
		defer wg.Done()
		runtimeGoexitRecoveredPanic()
		println("unreachable after Goexit helper")
	}()
	wg.Wait()

	wg.Add(1)
	go func() {
		defer wg.Done()
		runtimeGoexitNestedRecoveredPanic()
		println("unreachable after nested Goexit helper")
	}()
	wg.Wait()

	wg.Add(1)
	go func() {
		defer wg.Done()
		runtimeGoexitLoopDefers()
		println("unreachable after loop Goexit helper")
	}()
	wg.Wait()
}

func runtimeGoexitRecoveredPanic() {
	defer func() {
		if r := recover(); r != nil {
			printitf("Goexit recovered deferred panic:", r)
		}
	}()
	defer func() {
		panic("panic during Goexit")
	}()
	runtime.Goexit()
}

func runtimeGoexitNestedRecoveredPanic() {
	defer func() {
		defer func() {
			if r := recover(); r != nil {
				printitf("Goexit nested recovered deferred panic:", r)
			}
		}()
		panic("nested panic during Goexit")
	}()
	runtime.Goexit()
}

func runtimeGoexitLoopDefers() {
	for i := 0; i < 2; i++ {
		defer func() {
			println("Goexit loop defer")
		}()
	}
	defer func() {
		if r := recover(); r != nil {
			printitf("Goexit loop recovered deferred panic:", r)
		}
	}()
	defer func() {
		panic("loop panic during Goexit")
	}()
	runtime.Goexit()
}

// Test that a repanic inside a deferred function propagates correctly
// instead of re-running the same defer. This is a regression test for
// tinygo-org/tinygo issue 3449.
func recoverRepanic() {
	// Two defers: inner recovers and repanics, outer should catch it.
	defer func() {
		r := recover()
		if r != nil {
			printitf("outer recovered:", r)
		}
	}()
	defer func() {
		r := recover()
		if r != nil {
			println("inner, repanicking")
			panic(r)
		}
	}()
	panic("repanic value")
}

func printitf(msg string, itf interface{}) {
	switch itf := itf.(type) {
	case string:
		println(msg, itf)
	default:
		println(msg, itf)
	}
}

// Test recovering from runtime errors (bounds checks, type assertions, etc.)
func recoverRuntimeError() {
	recoverMustPanic("index", func() {
		s := make([]int, 5)
		_ = s[99]
	})
	recoverMustPanic("index from helper", func() {
		s := []byte{1}
		_ = readOutOfBounds(s)
	})
	recoverMustPanic("slice", func() {
		s := make([]int, 5)
		_ = s[3:99]
	})
	recoverMustPanic("type assert", func() {
		var x interface{} = 1
		_ = x.(string)
	})
	recoverEmptyInterfaceTypeAssert()
}

//go:noinline
func readOutOfBounds(s []byte) byte {
	return s[2]
}

func recoverEmptyInterfaceTypeAssert() {
	defer func() {
		r := recover()
		if r != nil {
			println("  failed empty interface type assert")
		} else {
			println("  recovered: empty interface type assert")
		}
	}()
	var intf interface{} = 3
	typed := intf.(interface{})
	useEmptyInterface(typed)
}

func useEmptyInterface(typed interface{}) {
	if typed.(int) != 3 {
		println("  failed empty interface value")
	}
}

func recoverMustPanic(name string, f func()) {
	defer func() {
		r := recover()
		if r != nil {
			println("  recovered:", name)
		} else {
			println("  failed to recover:", name)
		}
	}()
	f()
}

// Test recovering from nil map assignment and closed channel send.
func recoverNilMapAndChan() {
	recoverMustPanic("nil map", func() {
		var m map[string]int
		m["x"] = 1
	})
	recoverMustPanic("closed chan", func() {
		ch := make(chan int)
		close(ch)
		ch <- 1
	})
	recoverMustPanic("close nil chan", func() {
		var ch chan int
		close(ch)
	})
}

// Test recovering from hardware signals (SIGFPE, SIGSEGV).
func recoverSignals() {
	recoverMustPanic("divide by zero", func() {
		var x int
		println(1 / x)
	})
	recoverMustPanic("nil pointer dereference", func() {
		var p *int
		println(*p)
	})
}
