package errors

import (
	"errors"
	"fmt"
	"io"
	"strings"
	"sync/atomic"
)

// Export a number of functions or variables from package errors.
var (
	As     = errors.As
	Is     = errors.Is
	Unwrap = errors.Unwrap
	//Join   = errors.Join
)

// includeBacktrace is a static variable that decides if when creating an error we store the backtrace with it.
var includeBacktrace atomic.Bool

func SetIncludeBacktrace(v bool) {
	includeBacktrace.Store(v)
}

// Err is our custom error type that can store backtrace, file and line number
type Err struct {
	m string
	c error
	t stack
}

func (e Err) Format(s fmt.State, verb rune) {
	switch verb {
	case 's':
		io.WriteString(s, e.m)
		switch {
		case s.Flag('+'):
			if e.c != nil {
				io.WriteString(s, ": ")
				io.WriteString(s, fmt.Sprintf("%+s", e.c))
			}
		}
	case 'v':
		e.Format(s, 's')
		switch {
		case s.Flag('+'):
			if e.t != nil {
				io.WriteString(s, "\n\t")
				e.t.Format(s, 'v')
			}
		}
	}
}

// Error implements the error interface
func (e Err) Error() string {
	if includeBacktrace.Load() {
		return e.m
	}
	s := strings.Builder{}
	s.WriteString(e.m)
	if ch := errors.Unwrap(e); ch != nil {
		s.WriteString(": ")
		s.WriteString(ch.Error())
	}
	return s.String()
}

// Unwrap implements the errors.Wrapper interface
func (e Err) Unwrap() error {
	return e.c
}

// StackTrace returns the stack trace as returned by the debug.Stack function
func (e Err) StackTrace() StackTrace {
	return e.t.StackTrace()
}

// Annotatef wraps an error with new message
func Annotatef(e error, s string, args ...interface{}) *Err {
	err := wrap(e, s, args...)
	return &err
}

// Newf creaates a new error
func Newf(s string, args ...interface{}) *Err {
	err := wrap(nil, s, args...)
	return &err
}

// Errorf is an alias for Newf
func Errorf(s string, args ...interface{}) error {
	err := wrap(nil, s, args...)
	return &err
}

func (e Err) Is(err interface{}) bool {
	switch err.(type) {
	case **Err:
		return true
	case *Err:
		return true
	case Err:
		return true
	default:
		return false
	}
}

// As implements support for errors.As
func (e *Err) As(err interface{}) bool {
	switch x := err.(type) {
	case **Err:
		*x = e
	case *Err:
		*x = *e
	case Err:
		x = *e
	default:
		return false
	}
	return true
}

type StackTracer interface {
	StackTrace() StackTrace
}

// ancestorOfCause returns true if the caller looks to be an ancestor of the given stack
// trace. We check this by seeing whether our stack prefix-matches the cause stack, which
// should imply the error was generated directly from our goroutine.
func ancestorOfCause(ourStack stack, causeStack StackTrace) bool {
	// Stack traces are ordered such that the deepest frame is first. We'll want to check
	// for prefix matching in reverse.
	//
	// As an example, imagine we have a prefix-matching stack for ourselves:
	// [
	//   "github.com/go-ap/processing/processing.Validate",
	//   "testing.tRunner",
	//   "runtime.goexit"
	// ]
	//
	// We'll want to compare this against an error cause that will have happened further
	// down the stack. An example stack trace from such an error might be:
	// [
	//   "github.com/go-ap/errors/errors.New",
	//   "testing.tRunner",
	//   "runtime.goexit"
	// ]
	//
	// Their prefix matches, but we'll have to handle the match carefully as we need to match
	// from back to forward.

	// We can't possibly prefix match if our stack is larger than the cause stack.
	if len(ourStack) > len(causeStack) {
		return false
	}

	// We know the sizes are compatible, so compare program counters from back to front.
	for idx := 0; idx < len(ourStack); idx++ {
		if ourStack[len(ourStack)-1] != (uintptr)(causeStack[len(causeStack)-1]) {
			return false
		}
	}

	return true
}

func wrap(e error, s string, args ...interface{}) Err {
	err := Err{
		c: e,
		m: fmt.Sprintf(s, args...),
	}
	if includeBacktrace.Load() {
		causeStackTracer := new(StackTracer)
		// If our cause has set a stack trace, and that trace is a child of our own function
		// as inferred by prefix matching our current program counter stack, then we only want
		// to decorate the error message rather than add a redundant stack trace.
		stack := callers(2)
		if !(As(e, causeStackTracer) && ancestorOfCause(*stack, (*causeStackTracer).StackTrace())) {
			err.t = *stack
		}
	}
	return err
}
