package merry

import (
	"errors"
	"fmt"
	"reflect"
)

type errKey int

const (
	errKeyNone errKey = iota
	errKeyStack
	errKeyMessage
	errKeyHTTPCode
	errKeyUserMessage
	errKeyForceCapture
	errKeyHooked
)

func (e errKey) String() string {
	switch e {
	case errKeyNone:
		return "none"
	case errKeyStack:
		return "stack"
	case errKeyMessage:
		return "message"
	case errKeyHTTPCode:
		return "http status code"
	case errKeyUserMessage:
		return "user message"
	case errKeyForceCapture:
		return "force stack capture"
	default:
		return ""
	}
}

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

// Format implements fmt.Formatter
func (e *errWithValue) Format(s fmt.State, verb rune) {
	Format(s, verb, e)
}

// Error implements golang's error interface
// returns the message value if set, otherwise
// delegates to inner error
func (e *errWithValue) Error() string {
	if e.key == errKeyMessage {
		if s, ok := e.value.(string); ok {
			return s
		}
	}
	return e.err.Error()
}

// String implements fmt.Stringer
func (e *errWithValue) String() string {
	return e.Error()
}

// Unwrap returns the next wrapped error.
func (e *errWithValue) Unwrap() error {
	return e.err
}

// isMerryError is a marker method for identifying error types implemented by this package.
func (e *errWithValue) isMerryError() {}

type errWithCause struct {
	err   error
	cause error
}

func (e *errWithCause) Unwrap() error {
	// skip through any directly nested errWithCauses.
	// our implementation of Is/As already recursed through them,
	// so we want to dig down to the first non-errWithCause.

	nextErr := e.err
	for {
		if e, ok := nextErr.(*errWithCause); ok {
			nextErr = e.err
		} else {
			break
		}
	}

	// errWithCause.Is/As() also already checked nextErr, so we want to
	// unwrap it and get to the next error down.
	nextErr = errors.Unwrap(nextErr)

	// we've reached the end of this wrapper chain.  Return the cause.
	if nextErr == nil {
		return e.cause
	}

	// return a new errWithCause wrapper, wrapping next error, but bundling
	// it will our cause, ignoring the causes of the errWithCauses we skip
	// over above.  This is how we carry the latest cause along as we unwrap
	// the chain.  When we get to the end of the chain, we'll return this latest
	// cause.
	return &errWithCause{err: nextErr, cause: e.cause}
}

func (e *errWithCause) String() string {
	return e.Error()
}

func (e *errWithCause) Error() string {
	return e.err.Error()
}

func (e *errWithCause) Format(f fmt.State, verb rune) {
	Format(f, verb, e)
}

// errWithCause needs to provide custome implementations of Is and As.
// errors.Is() doesn't work on errWithCause because error.Is() uses errors.Unwrap() to traverse the error
// chain.  But errWithCause.Unwrap() doesn't return the next error in the chain.  Instead,
// it wraps the next error in a shim.  The standard Is/As tests would compare the shim to the target.
// We need to override Is/As to compare the target to the error inside the shim.

func (e *errWithCause) Is(target error) bool {
	// This does most of what errors.Is() does, by delegating
	// to the nested error.  But it does not use Unwrap to recurse
	// any further.  This just compares target with next error in the stack.
	isComparable := reflect.TypeOf(target).Comparable()
	if isComparable && e.err == target {
		return true
	}

	// since errWithCause implements Is(), this will effectively recurse through
	// any directly nested errWithCauses.
	if x, ok := e.err.(interface{ Is(error) bool }); ok && x.Is(target) {
		return true
	}
	return false
}

func (e *errWithCause) As(target interface{}) bool {
	// This does most of what errors.As() does, by delegating
	// to the nested error.  But it does not use Unwrap to recurse
	// any further. This just compares target with next error in the stack.
	val := reflect.ValueOf(target)
	typ := val.Type()
	targetType := typ.Elem()
	if reflect.TypeOf(e.err).AssignableTo(targetType) {
		val.Elem().Set(reflect.ValueOf(e.err))
		return true
	}

	// since errWithCause implements As(), this will effectively recurse through
	// any directly nested errWithCauses.
	if x, ok := e.err.(interface{ As(interface{}) bool }); ok && x.As(target) {
		return true
	}
	return false
}

// isMerryError is a marker method for identifying error types implemented by this package.
func (e *errWithCause) isMerryError() {}
