// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package util

import (
	"fmt"
	"runtime"
	"strings"
)

var packagePrefix string

func init() {
	_, filename, _, _ := runtime.Caller(0)
	packagePrefix = strings.TrimSuffix(filename, "util/panic.go")
	if packagePrefix == filename {
		// in case the source code file is moved, we can not trim the suffix, the code above should also be updated.
		panic("unable to detect correct package prefix, please update file: " + filename)
	}
}

func getStack() string {
	callersLength := 5
	var callers []uintptr
	var callersCount int
	for {
		callers = make([]uintptr, callersLength)
		callersCount = runtime.Callers(4, callers)
		if callersCount <= 0 {
			panic("runtime.Callers <= 0")
		}
		if callersCount >= callersLength {
			callersLength *= 2
		} else {
			break
		}
	}
	callers = callers[:callersCount]
	frames := runtime.CallersFrames(callers)
	stack := make([]string, 0, 10)
	for {
		frame, more := frames.Next()
		if strings.HasPrefix(frame.File, packagePrefix) {
			file := strings.TrimPrefix(frame.File, packagePrefix)
			if file == "util/panic.go" || file == "util/terminate.go" && more {
				continue
			}
			var function string
			dot := strings.LastIndex(frame.Function, ".")
			if dot >= 0 {
				function = frame.Function[dot+1:]
			}
			stack = append(stack, fmt.Sprintf("%s:%d:%s", file, frame.Line, function))
		}
		if !more {
			break
		}
	}
	return strings.Join(stack, "\n") + "\n"
}

type PanicError interface {
	error
	Stack() string
}

type panicError struct {
	message string
	stack   string
}

func (o panicError) Error() string { return o.message }
func (o panicError) Stack() string { return o.stack }

func PanicToError(fun func()) (err PanicError) {
	defer func() {
		if r := recover(); r != nil {
			recoveredErr, ok := r.(error)
			var message string
			if ok {
				message = recoveredErr.Error()
			}
			err = panicError{
				message: message,
				stack:   getStack(),
			}
			if !ok {
				panic(r)
			}
		}
	}()

	fun()

	return err
}
