package adverr

import (
	"errors"
	"reflect"
	"strings"
)

// Error is a wrapper for error with stack trace
type Error struct {
	msg       string
	callTrace *CallTrace
	tmpl      *ErrTmpl
	cause     error
	prev      []error
}

// New returns a new Error with the given message
func New(msg string) *Error {
	return &Error{
		msg:       msg,
		callTrace: Trace(2),
	}
}

// Wrap returns a new Error with the given message which is caused by cause
func Wrap(msg string, cause error) *Error {
	return &Error{
		msg:       msg,
		cause:     cause,
		callTrace: Trace(2),
	}
}

// Chain returns a new Error with the given message and a slice of errors
// which were caused in the same function in succession
func Chain(msg string, errors []error) *Error {
	return &Error{
		msg:       msg,
		callTrace: Trace(2),
		prev:      errors,
	}
}

func errtype(err error) string {
	if e, ok := err.(*Error); ok && e.tmpl != nil {
		return errtype(e.tmpl)
	} else if tmpl, ok := err.(*ErrTmpl); ok {
		return tmpl.name
	}

	t := reflect.TypeOf(err)
	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	return t.PkgPath() + "." + t.Name()
}

func (e *Error) Unwrap() error {
	return e.cause
}

func (e *Error) Error() string {
	b := new(strings.Builder)
	printErr(e, b)
	return b.String()
}

func (e *Error) Message() string {
	return e.msg
}

// Is implements the error equality function used by errors.Is()
// It returns true if the error is the same instance or is created using the same ErrTmpl
func (e *Error) Is(target error) bool {
	// same error instance
	if target == e {
		return true
	}

	// no template used, therefore no equality possible
	if e.tmpl == nil {
		return false
	}

	// same template, therefore errors are equal to another
	if tErr, ok := target.(*Error); ok {
		return tErr.tmpl == e.tmpl
	}

	// target is the template itself, therefore they are considered equal to another
	if tTmpl, ok := target.(*ErrTmpl); ok {
		return tTmpl == e.tmpl
	}

	return false
}

// Get Returns the first error in the chain for which errors.Is(target) returns true
func (e *Error) Get(target error) error {
	if e.prev == nil {
		return nil
	}

	for _, prevErr := range e.prev {
		if errors.Is(prevErr, target) {
			return prevErr
		}
	}
	return nil
}

// GetByIndex returns the i'th error in the chain
func (e *Error) GetByIndex(i int) error {
	if e.prev == nil {
		return nil
	}
	return e.prev[i]
}

// Contains is a shorthand for Get(target) != nil.
// Can be considered as an errors.Is function but for chains instead of causes
func (e *Error) Contains(target error) bool {
	return e.Get(target) != nil
}

// Chain returns a slice of all chained errors
func (e *Error) Chain() []error {
	return e.prev[:]
}

func printErr(err error, b *strings.Builder) {
	e, ok := err.(*Error)

	if ok {
		b.WriteString(errtype(e))
		b.WriteString(": ")
		b.WriteString(e.msg)
		b.WriteString("\n")
		b.WriteString(e.callTrace.String())
	} else {
		b.WriteString(errtype(err))
		b.WriteString(": ")
		b.WriteString(err.Error())
		b.WriteString("\n")
		b.WriteString("\t(Unknown source)\n")
	}

	cause := errors.Unwrap(err)
	if cause != nil {
		b.WriteString("Caused by ")
		printErr(cause, b)
	}

	if ok {
		for _, prevErr := range e.prev {
			b.WriteString("Previously thrown ")
			printErr(prevErr, b)
		}
	}
}