package errors

import (
	"errors"
	"reflect"
	_ "unsafe"

	"codeberg.org/gruf/go-bitutil"
)

// errtype is a ptr to the error interface type.
var errtype = reflect.TypeOf((*error)(nil)).Elem()

// Comparable is functionally equivalent to calling errors.Is() on multiple errors (up to a max of 64).
func Comparable(err error, targets ...error) bool {
	var flags bitutil.Flags64

	// Flags only has 64 bit-slots
	if len(targets) > 64 {
		panic("too many targets")
	}

	for i := 0; i < len(targets); {
		if targets[i] == nil {
			if err == nil {
				return true
			}

			// Drop nil targets from slice.
			copy(targets[i:], targets[i+1:])
			targets = targets[:len(targets)-1]
			continue
		}

		// Check if this error is directly comparable
		if reflect.TypeOf(targets[i]).Comparable() {
			flags = flags.Set(uint8(i))
		}

		i++
	}

	for err != nil {
		// Check if this layer supports .Is interface
		is, ok := err.(interface{ Is(error) bool })

		if !ok {
			// Error does not support interface
			//
			// Only try perform direct compare
			for i := 0; i < len(targets); i++ {
				// Try directly compare errors
				if flags.Get(uint8(i)) &&
					err == targets[i] {
					return true
				}
			}
		} else {
			// Error supports the .Is interface
			//
			// Perform direct compare AND .Is()
			for i := 0; i < len(targets); i++ {
				if (flags.Get(uint8(i)) &&
					err == targets[i]) ||
					is.Is(targets[i]) {
					return true
				}
			}
		}

		// Unwrap to next layer
		err = errors.Unwrap(err)
	}

	return false
}

// Assignable is functionally equivalent to calling errors.As() on multiple errors,
// except that it only checks assignability as opposed to setting the target.
func Assignable(err error, targets ...error) bool {
	if err == nil {
		// Fastest case.
		return false
	}

	for i := 0; i < len(targets); {
		if targets[i] == nil {
			// Drop nil targets from slice.
			copy(targets[i:], targets[i+1:])
			targets = targets[:len(targets)-1]
			continue
		}
		i++
	}

	for err != nil {
		// Check if this layer supports .As interface
		as, ok := err.(interface{ As(any) bool })

		// Get reflected err type.
		te := reflect.TypeOf(err)

		if !ok {
			// Error does not support interface.
			//
			// Check assignability using reflection.
			for i := 0; i < len(targets); i++ {
				tt := reflect.TypeOf(targets[i])
				if te.AssignableTo(tt) {
					return true
				}
			}
		} else {
			// Error supports the .As interface.
			//
			// Check using .As() and reflection.
			for i := 0; i < len(targets); i++ {
				if as.As(targets[i]) {
					return true
				} else if tt := reflect.TypeOf(targets[i]); // nocollapse
				te.AssignableTo(tt) {
					return true
				}
			}
		}

		// Unwrap to next layer.
		err = errors.Unwrap(err)
	}

	return false
}

// As finds the first error in err's tree that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling Unwrap. When err wraps multiple errors, As examines err followed by a
// depth-first traversal of its children.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// An error type might provide an As method so it can be treated as if it were a
// different error type.
//
// As panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.
//
//go:linkname As errors.As
func As(err error, target any) bool

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error. Otherwise, Unwrap returns nil.
//
//go:linkname Unwrap errors.Unwrap
func Unwrap(err error) error