package errors

import (
	"fmt"
	"io"
	"runtime"
	"strings"

	"github.com/pkg/errors"
)

func matchesFunc(f errors.Frame, prefixes ...string) bool {
	fn := funcName(f)
	for _, prefix := range prefixes {
		if strings.HasPrefix(fn, prefix) {
			return true
		}
	}
	return false
}

// funcName returns the name of this function, if known.
func funcName(f errors.Frame) string {
	// this looks a bit like magic, but follows example here:
	// https://github.com/pkg/errors/blob/v0.8.1/stack.go#L43-L50
	pc := uintptr(f) - 1
	fn := runtime.FuncForPC(pc)
	if fn == nil {
		return "unknown"
	}
	return fn.Name()
}

func fileLine(f errors.Frame) (string, int) {
	// this looks a bit like magic, but follows example here:
	// https://github.com/pkg/errors/blob/v0.8.1/stack.go#L14-L27
	// as this is where we get the Frames
	pc := uintptr(f) - 1
	fn := runtime.FuncForPC(pc)
	if fn == nil {
		return "unknown", 0
	}
	return fn.FileLine(pc)
}

func trimInternal(st errors.StackTrace) errors.StackTrace {
	// trim our internal parts here
	// manual error creation, or runtime for caught panics
	for matchesFunc(st[0],
		// where we create errors
		"github.com/cosmos/cosmos-sdk/types/errors.Wrap",
		"github.com/cosmos/cosmos-sdk/types/errors.Wrapf",
		"github.com/cosmos/cosmos-sdk/types/errors.WithType",
		// runtime are added on panics
		"runtime.",
		// _test is defined in coverage tests, causing failure
		// "/_test/"
	) {
		st = st[1:]
	}
	// trim out outer wrappers (runtime.goexit and test library if present)
	for l := len(st) - 1; l > 0 && matchesFunc(st[l], "runtime.", "testing."); l-- {
		st = st[:l]
	}
	return st
}

func writeSimpleFrame(s io.Writer, f errors.Frame) {
	file, line := fileLine(f)
	// cut file at "github.com/"
	// TODO: generalize better for other hosts?
	chunks := strings.SplitN(file, "github.com/", 2)
	if len(chunks) == 2 {
		file = chunks[1]
	}
	fmt.Fprintf(s, " [%s:%d]", file, line)
}

// Format works like pkg/errors, with additions.
// %s is just the error message
// %+v is the full stack trace
// %v appends a compressed [filename:line] where the error
//    was created
//
// Inspired by https://github.com/pkg/errors/blob/v0.8.1/errors.go#L162-L176
func (e *wrappedError) Format(s fmt.State, verb rune) {
	// normal output here....
	if verb != 'v' {
		fmt.Fprint(s, e.Error())
		return
	}
	// work with the stack trace... whole or part
	stack := trimInternal(stackTrace(e))
	if s.Flag('+') {
		fmt.Fprintf(s, "%+v\n", stack)
		fmt.Fprint(s, e.Error())
	} else {
		fmt.Fprint(s, e.Error())
		writeSimpleFrame(s, stack[0])
	}
}

// stackTrace returns the first found stack trace frame carried by given error
// or any wrapped error. It returns nil if no stack trace is found.
func stackTrace(err error) errors.StackTrace {
	type stackTracer interface {
		StackTrace() errors.StackTrace
	}

	for {
		if st, ok := err.(stackTracer); ok {
			return st.StackTrace()
		}

		if c, ok := err.(causer); ok {
			err = c.Cause()
		} else {
			return nil
		}
	}
}
