package recoverfn

import (
	"fmt"
	"io"
	"os"
	"runtime"
)

const stackSizeLimit = 256

type Raw struct {
	RecoverV  any
	RecoverPC uintptr
	PCs       []uintptr
}

// Run runs fn and return Raw{}, true on no-panic
// on panic it recovers and return a raw stacktrace and panic value to inspect
func Run(fn func()) (Raw, bool) {
	// TODO: once?
	var recoverPC [1]uintptr
	runtime.Callers(1, recoverPC[:])

	pc, v := func() (pcs []uintptr, v any) {
		defer func() {
			if recoverErr := recover(); recoverErr != nil {
				pcs = make([]uintptr, stackSizeLimit)
				pcs = pcs[0:runtime.Callers(0, pcs)]
				v = recoverErr
			}
		}()

		fn()

		return nil, nil
	}()

	if v == nil {
		return Raw{}, true
	}

	return Raw{
		RecoverV:  v,
		RecoverPC: recoverPC[0],
		PCs:       pc,
	}, false
}

func (r Raw) frames(startSkip int, bottomSkip int, bottomPC uintptr) []runtime.Frame {
	var bottomFrame runtime.Frame
	bottomIndex := -1
	if bottomPC != 0 {
		bottomPCs := [1]uintptr{bottomPC}
		bottomFrame, _ = runtime.CallersFrames(bottomPCs[:]).Next()
	}

	fs := make([]runtime.Frame, len(r.PCs))
	frames := runtime.CallersFrames(r.PCs)
	for i := 0; ; i++ {
		f, more := frames.Next()
		if !more {
			break
		}
		if bottomPC != 0 && f.Function == bottomFrame.Function {
			bottomIndex = i
		}
		fs[i] = f
	}

	endIndex := len(fs) - 1
	if bottomIndex != -1 {
		endIndex = bottomIndex - bottomSkip
	}

	return fs[startSkip:endIndex]
}

func (r Raw) Frames() []runtime.Frame {
	// 3 to skip runtime.Callers, Recover help function and runtime.gopanic
	// 1 to skip Recover defer recover() function
	return r.frames(3, 1, r.RecoverPC)
}

func (r Raw) RePanic() {
	var o io.Writer
	o = os.Stderr
	if p := os.Getenv("REPANIC_LOG"); p != "" {
		if f, err := os.Create(p); err == nil {
			o = f
			defer f.Close()
		}
	}

	fmt.Fprintf(o, "repanic: %v\n", r.RecoverV)
	for _, f := range r.frames(0, 0, 0) {
		fmt.Fprintf(o, "%s\n", f.Function)
		fmt.Fprintf(o, "\t%s:%d\n", f.File, f.Line)
	}
	panic(r.RecoverV)
}
