package merry

// The merry package augments standard golang errors with stacktraces
// and other context information.
//
// You can add any context information to an error with `e = merry.WithValue(e, "code", 12345)`
// You can retrieve that value with `v, _ := merry.Value(e, "code").(int)`
//
// Any error augmented like this will automatically get a stacktrace attached, if it doesn't have one
// already.  If you just want to add the stacktrace, use `Wrap(e)`
//
// It also providers a way to override an error's message:
//
//     var InvalidInputs = errors.New("Bad inputs")
//
// `Here()` captures a new stacktrace, and WithMessagef() sets a new error message:
//
//     return merry.Here(InvalidInputs).WithMessagef("Bad inputs: %v", inputs)
//
// Errors are immutable.  All functions and methods which add context return new errors.
// But errors can still be compared to the originals with `Is()`
//
//     if merry.Is(err, InvalidInputs) {
//
// Functions which add context to errors have equivalent methods on *Error, to allow
// convenient chaining:
//
//     return merry.New("Invalid body").WithHTTPCode(400)
//
// merry.Errors also implement fmt.Formatter, similar to github.com/pkg/errors.
//
//     fmt.Sprintf("%+v", e) == merry.Details(e)
//
// pkg/errors Cause() interface is not implemented (yet).
import (
	"errors"
	"fmt"
	"io"
	"runtime"
)

// MaxStackDepth is the maximum number of stackframes on any error.
var MaxStackDepth = 50

var captureStacks = true
var verbose = false

// StackCaptureEnabled returns whether stack capturing is enabled
func StackCaptureEnabled() bool {
	return captureStacks
}

// SetStackCaptureEnabled sets stack capturing globally.  Disabling stack capture can increase performance
func SetStackCaptureEnabled(enabled bool) {
	captureStacks = enabled
}

// VerboseDefault returns the global default for verbose mode.
// When true, e.Error() == Details(e)
// When false, e.Error() == Message(e)
func VerboseDefault() bool {
	return verbose
}

// SetVerboseDefault sets the global default for verbose mode.
// When true, e.Error() == Details(e)
// When false, e.Error() == Message(e)
func SetVerboseDefault(b bool) {
	verbose = b
}

// Error extends the standard golang `error` interface with functions
// for attachment additional data to the error
type Error interface {
	error
	Appendf(format string, args ...interface{}) Error
	Append(msg string) Error
	Prepend(msg string) Error
	Prependf(format string, args ...interface{}) Error
	WithMessage(msg string) Error
	WithMessagef(format string, args ...interface{}) Error
	WithUserMessage(msg string) Error
	WithUserMessagef(format string, args ...interface{}) Error
	WithValue(key, value interface{}) Error
	Here() Error
	WithStackSkipping(skip int) Error
	WithHTTPCode(code int) Error
	fmt.Formatter
}

// New creates a new error, with a stack attached.  The equivalent of golang's errors.New()
func New(msg string) Error {
	return WrapSkipping(errors.New(msg), 1)
}

// Errorf creates a new error with a formatted message and a stack.  The equivalent of golang's fmt.Errorf()
func Errorf(format string, a ...interface{}) Error {
	return WrapSkipping(fmt.Errorf(format, a...), 1)
}

// UserError creates a new error with a message intended for display to an
// end user.
func UserError(msg string) Error {
	return WrapSkipping(errors.New(""), 1).WithUserMessage(msg)
}

// UserErrorf is like UserError, but uses fmt.Sprintf()
func UserErrorf(format string, a ...interface{}) Error {
	return WrapSkipping(errors.New(""), 1).WithUserMessagef(format, a...)
}

// Wrap turns the argument into a merry.Error.  If the argument already is a
// merry.Error, this is a no-op.
// If e == nil, return nil
func Wrap(e error) Error {
	return WrapSkipping(e, 1)
}

// WrapSkipping turns the error arg into a merry.Error if the arg is not
// already a merry.Error.
// If e is nil, return nil.
// If a merry.Error is created by this call, the stack captured will skip
// `skip` frames (0 is the call site of `WrapSkipping()`)
func WrapSkipping(e error, skip int) Error {
	switch e1 := e.(type) {
	case nil:
		return nil
	case *merryErr:
		return e1
	default:
		return &merryErr{
			err:   e,
			key:   stack,
			value: captureStack(skip + 1),
		}
	}
}

// WithValue adds a context an error.  If the key was already set on e,
// the new value will take precedence.
// If e is nil, returns nil.
func WithValue(e error, key, value interface{}) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).WithValue(key, value)
}

// Value returns the value for key, or nil if not set.
// If e is nil, returns nil.
func Value(e error, key interface{}) interface{} {
	for {
		switch m := e.(type) {
		case nil:
			return nil
		case *merryErr:
			if m.key == key {
				return m.value
			}
			e = m.err
		default:
			return nil
		}
	}
}

// Values returns a map of all values attached to the error
// If a key has been attached multiple times, the map will
// contain the last value mapped
// If e is nil, returns nil.
func Values(e error) map[interface{}]interface{} {
	if e == nil {
		return nil
	}
	var values map[interface{}]interface{}
	for {
		w, ok := e.(*merryErr)
		if !ok {
			return values
		}
		if values == nil {
			values = make(map[interface{}]interface{}, 1)
		}
		if _, ok := values[w.key]; !ok {
			values[w.key] = w.value
		}
		e = w.err
	}
}

// Here returns an error with a new stacktrace, at the call site of Here().
// Useful when returning copies of exported package errors.
// If e is nil, returns nil.
func Here(e error) Error {
	switch m := e.(type) {
	case nil:
		return nil
	case *merryErr:
		// optimization: only capture the stack once, since its expensive
		return m.WithStackSkipping(1)
	default:
		return WrapSkipping(e, 1)
	}
}

// Stack returns the stack attached to an error, or nil if one is not attached
// If e is nil, returns nil.
func Stack(e error) []uintptr {
	stack, _ := Value(e, stack).([]uintptr)
	return stack
}

// WithHTTPCode returns an error with an http code attached.
// If e is nil, returns nil.
func WithHTTPCode(e error, code int) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).WithHTTPCode(code)
}

// HTTPCode converts an error to an http status code.  All errors
// map to 500, unless the error has an http code attached.
// If e is nil, returns 200.
func HTTPCode(e error) int {
	if e == nil {
		return 200
	}
	code, _ := Value(e, httpCode).(int)
	if code == 0 {
		return 500
	}
	return code
}

// UserMessage returns the end-user safe message.  Returns empty if not set.
// If e is nil, returns "".
func UserMessage(e error) string {
	if e == nil {
		return ""
	}
	msg, _ := Value(e, userMessage).(string)
	return msg
}

// Message returns just the error message.  It is equivalent to
// Error() when Verbose is false.
// The behavior of Error() is (pseudo-code):
//
//     if verbose
//       Details(e)
//     else
//       Message(e) || UserMessage(e)
//
// If e is nil, returns "".
func Message(e error) string {
	if e == nil {
		return ""
	}
	m, _ := Value(e, message).(string)
	if m == "" {
		return Unwrap(e).Error()
	}
	return m
}

// WithMessage returns an error with a new message.
// The resulting error's Error() method will return
// the new message.
// If e is nil, returns nil.
func WithMessage(e error, msg string) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).WithValue(message, msg)
}

// WithMessagef is the same as WithMessage(), using fmt.Sprintf().
func WithMessagef(e error, format string, a ...interface{}) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).WithMessagef(format, a...)
}

// WithUserMessage adds a message which is suitable for end users to see.
// If e is nil, returns nil.
func WithUserMessage(e error, msg string) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).WithUserMessage(msg)
}

// WithUserMessagef is the same as WithMessage(), using fmt.Sprintf()
func WithUserMessagef(e error, format string, args ...interface{}) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).WithUserMessagef(format, args...)
}

// Append a message after the current error message, in the format "original: new".
// If e == nil, return nil.
func Append(e error, msg string) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).Append(msg)
}

// Appendf is the same as Append, but uses fmt.Sprintf().
func Appendf(e error, format string, args ...interface{}) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).Appendf(format, args...)
}

// Prepend a message before the current error message, in the format "new: original".
// If e == nil, return nil.
func Prepend(e error, msg string) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).Prepend(msg)
}

// Prependf is the same as Prepend, but uses fmt.Sprintf()
func Prependf(e error, format string, args ...interface{}) Error {
	if e == nil {
		return nil
	}
	return WrapSkipping(e, 1).Prependf(format, args...)
}

// Is checks whether e is equal to or wraps the original, at any depth.
// If e == nil, return false.
// This is useful if your package uses the common golang pattern of
// exported error constants.  If your package exports an ErrEOF constant,
// which is initialized like this:
//
//     var ErrEOF = errors.New("End of file error")
//
// ...and your user wants to compare an error returned by your package
// with ErrEOF:
//
//     err := urpack.Read()
//     if err == urpack.ErrEOF {
//
// ...the comparison will fail if the error has been wrapped by merry
// at some point.  Replace the comparison with:
//
//     if merry.Is(err, urpack.ErrEOF) {
//
func Is(e error, originals ...error) bool {
	is := func(e, original error) bool {
		for {
			if e == original {
				return true
			}
			if e == nil || original == nil {
				return false
			}
			w, ok := e.(*merryErr)
			if !ok {
				return false
			}
			e = w.err
		}
	}
	for _, o := range originals {
		if is(e, o) {
			return true
		}
	}
	return false
}

// Unwrap returns the innermost underlying error.
// Only useful in advanced cases, like if you need to
// cast the underlying error to some type to get
// additional information from it.
// If e == nil, return nil.
func Unwrap(e error) error {
	if e == nil {
		return nil
	}
	for {
		w, ok := e.(*merryErr)
		if !ok {
			return e
		}
		e = w.err
	}
}

func captureStack(skip int) []uintptr {
	if !captureStacks {
		return nil
	}
	stack := make([]uintptr, MaxStackDepth)
	length := runtime.Callers(2+skip, stack[:])
	return stack[:length]
}

type errorProperty string

const (
	stack       errorProperty = "stack"
	message                   = "message"
	httpCode                  = "http status code"
	userMessage               = "user message"
)

type merryErr struct {
	err        error
	key, value interface{}
}

// Format implements fmt.Formatter
func (e *merryErr) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			io.WriteString(s, Details(e))
			return
		}
		fallthrough
	case 's':
		io.WriteString(s, e.Error())
	case 'q':
		fmt.Fprintf(s, "%q", e.Error())
	}
}

// make sure merryErr implements Error
var _ Error = (*merryErr)(nil)

// Error implements golang's error interface
// returns the message value if set, otherwise
// delegates to inner error
func (e *merryErr) Error() string {
	if verbose {
		return Details(e)
	}
	m := Message(e)
	if m == "" {
		return UserMessage(e)
	}
	return m
}

// return a new error with additional context
func (e *merryErr) WithValue(key, value interface{}) Error {
	if e == nil {
		return nil
	}
	return &merryErr{
		err:   e,
		key:   key,
		value: value,
	}
}

// Shorthand for capturing a new stack trace
func (e *merryErr) Here() Error {
	if e == nil {
		return nil
	}
	return e.WithStackSkipping(1)
}

// return a new error with a new stack capture
func (e *merryErr) WithStackSkipping(skip int) Error {
	if e == nil {
		return nil
	}
	return &merryErr{
		err:   e,
		key:   stack,
		value: captureStack(skip + 1),
	}
}

// return a new error with an http status code attached
func (e *merryErr) WithHTTPCode(code int) Error {
	if e == nil {
		return nil
	}
	return e.WithValue(httpCode, code)
}

// return a new error with a new message
func (e *merryErr) WithMessage(msg string) Error {
	if e == nil {
		return nil
	}
	return e.WithValue(message, msg)
}

// return a new error with a new formatted message
func (e *merryErr) WithMessagef(format string, a ...interface{}) Error {
	if e == nil {
		return nil
	}
	return e.WithMessage(fmt.Sprintf(format, a...))
}

// Add a message which is suitable for end users to see
func (e *merryErr) WithUserMessage(msg string) Error {
	if e == nil {
		return nil
	}
	return e.WithValue(userMessage, msg)
}

// Add a message which is suitable for end users to see
func (e *merryErr) WithUserMessagef(format string, args ...interface{}) Error {
	if e == nil {
		return nil
	}
	return e.WithUserMessage(fmt.Sprintf(format, args...))
}

// Append a message after the current error message, in the format "original: new"
func (e *merryErr) Append(msg string) Error {
	if e == nil {
		return nil
	}
	return e.WithMessagef("%s: %s", e.Error(), msg)
}

// Append a message after the current error message, in the format "original: new"
func (e *merryErr) Appendf(format string, args ...interface{}) Error {
	if e == nil {
		return nil
	}
	return e.Append(fmt.Sprintf(format, args...))
}

// Prepend a message before the current error message, in the format "new: original"
func (e *merryErr) Prepend(msg string) Error {
	if e == nil {
		return nil
	}
	return e.WithMessagef("%s: %s", msg, e.Error())
}

// Prepend a message before the current error message, in the format "new: original"
func (e *merryErr) Prependf(format string, args ...interface{}) Error {
	if e == nil {
		return nil
	}
	return e.Prepend(fmt.Sprintf(format, args...))
}
