package arrutil

import (
	"errors"

	"github.com/gookit/goutil/comdef"
	"github.com/gookit/goutil/reflects"
)

// ErrElementNotFound is the error returned when the element is not found.
var ErrElementNotFound = errors.New("element not found")

// Comparer Function to compare two elements.
type Comparer[T any] func(a, b T) int

// type Comparer  func(a, b any) int

// Predicate Function to predicate a struct/value satisfies a condition.
type Predicate[T any] func(v T) bool

// StringEqualsComparer Comparer for string. It will compare the string by their value.
//
// returns: 0 if equal, -1 if a != b
func StringEqualsComparer(a, b string) int {
	if a == b {
		return 0
	}
	return -1
}

// ValueEqualsComparer Comparer for comdef.Compared type. It will compare by their value.
//
// returns: 0 if equal, -1 if a != b
func ValueEqualsComparer[T comdef.Compared](a, b T) int {
	if a == b {
		return 0
	}
	return -1
}

// ReflectEqualsComparer Comparer for struct ptr. It will compare by reflect.Value
//
// returns: 0 if equal, -1 if a != b
func ReflectEqualsComparer[T any](a, b T) int {
	if reflects.IsEqual(a, b) {
		return 0
	}
	return -1
}

// ElemTypeEqualsComparer Comparer for struct/value. It will compare the struct by their element type.
//
// returns: 0 if same type, -1 if not.
func ElemTypeEqualsComparer[T any](a, b T) int {
	at := reflects.TypeOf(a).SafeElem()
	bt := reflects.TypeOf(b).SafeElem()

	if at == bt {
		return 0
	}
	return -1
}

// TwowaySearch find specialized element in a slice forward and backward in the same time, should be more quickly.
//
//   - data: the slice to search in. MUST BE A SLICE.
//   - item: the element to search.
//   - fn: the comparer function.
//   - return: the index of the element, or -1 if not found.
func TwowaySearch[T any](data []T, item T, fn Comparer[T]) (int, error) {
	if data == nil {
		return -1, errors.New("collections.TwowaySearch: data is nil")
	}
	if fn == nil {
		return -1, errors.New("collections.TwowaySearch: fn is nil")
	}

	if len(data) == 0 {
		return -1, errors.New("collections.TwowaySearch: data is empty")
	}

	forward := 0
	backward := len(data) - 1

	for forward <= backward {
		if fn(data[forward], item) == 0 {
			return forward, nil
		}

		if fn(data[backward], item) == 0 {
			return backward, nil
		}

		forward++
		backward--
	}

	return -1, ErrElementNotFound
}

// CloneSlice Clone a slice.
//
//	data: the slice to clone.
//	returns: the cloned slice.
func CloneSlice[T any](data []T) []T {
	nt := make([]T, 0, len(data))
	nt = append(nt, data...)
	return nt
}

// Diff Produces the set difference of two slice according to a comparer function. alias of Differences
func Diff[T any](first, second []T, fn Comparer[T]) []T {
	return Differences(first, second, fn)
}

// Differences Produces the set difference of two slice according to a comparer function.
//
//   - first: the first slice. MUST BE A SLICE.
//   - second: the second slice. MUST BE A SLICE.
//   - fn: the comparer function.
//   - returns: the difference of the two slices.
//
// Example:
//
//	// Output: []string{"c"}
//	Differences([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.StringEqualsComparer
func Differences[T any](first, second []T, fn Comparer[T]) []T {
	firstLen := len(first)
	if firstLen == 0 {
		return CloneSlice(second)
	}

	secondLen := len(second)
	if secondLen == 0 {
		return CloneSlice(first)
	}

	max := firstLen
	if secondLen > firstLen {
		max = secondLen
	}

	result := make([]T, 0)
	for i := 0; i < max; i++ {
		if i < firstLen {
			s := first[i]
			if i, _ := TwowaySearch(second, s, fn); i < 0 {
				result = append(result, s)
			}
		}

		if i < secondLen {
			t := second[i]
			if i, _ := TwowaySearch(first, t, fn); i < 0 {
				result = append(result, t)
			}
		}
	}

	return result
}

// Excepts Produces the set difference of two slice according to a comparer function.
//
//   - first: the first slice. MUST BE A SLICE.
//   - second: the second slice. MUST BE A SLICE.
//   - fn: the comparer function.
//   - returns: the difference of the two slices.
//
// Example:
//
//	// Output: []string{"c"}
//	Excepts([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.StringEqualsComparer)
func Excepts[T any](first, second []T, fn Comparer[T]) []T {
	if len(first) == 0 {
		return make([]T, 0)
	}
	if len(second) == 0 {
		return CloneSlice(first)
	}

	result := make([]T, 0)
	for _, s := range first {
		if i, _ := TwowaySearch(second, s, fn); i < 0 {
			result = append(result, s)
		}
	}
	return result
}

// Intersects Produces to intersect of two slice according to a comparer function.
//
//   - first: the first slice. MUST BE A SLICE.
//   - second: the second slice. MUST BE A SLICE.
//   - fn: the comparer function.
//   - returns: to intersect of the two slices.
//
// Example:
//
//	// Output: []string{"a", "b"}
//	Intersects([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.ValueEqualsComparer)
func Intersects[T any](first, second []T, fn Comparer[T]) []T {
	if len(first) == 0 || len(second) == 0 {
		return make([]T, 0)
	}

	result := make([]T, 0)
	for _, s := range first {
		if i, _ := TwowaySearch(second, s, fn); i >= 0 {
			result = append(result, s)
		}
	}
	return result
}

// Union Produces the set union of two slice according to a comparer function
//
//   - first: the first slice. MUST BE A SLICE.
//   - second: the second slice. MUST BE A SLICE.
//   - fn: the comparer function.
//   - returns: the union of the two slices.
//
// Example:
//
//	// Output: []string{"a", "b", "c"}
//	sl := Union([]string{"a", "b", "c"}, []string{"a", "b"}, arrutil.ValueEqualsComparer)
func Union[T any](first, second []T, fn Comparer[T]) []T {
	if len(first) == 0 {
		return CloneSlice(second)
	}

	excepts := Excepts(second, first, fn)
	nt := make([]T, 0, len(first)+len(second))
	nt = append(nt, first...)
	return append(nt, excepts...)
}

// Find Produces the value of a slice according to a predicate function.
//
//   - source: the slice. MUST BE A SLICE.
//   - fn: the predicate function.
//   - returns: the struct/value of the slice.
//
// Example:
//
//	// Output: "c"
//	val := Find([]string{"a", "b", "c"}, func(s string) bool {
//		return s == "c"
//	})
func Find[T any](source []T, fn Predicate[T]) (v T, err error) {
	err = ErrElementNotFound
	if len(source) == 0 {
		return
	}

	for _, s := range source {
		if fn(s) {
			return s, nil
		}
	}
	return
}

// FindOrDefault Produce the value f a slice to a predicate function,
// Produce default value when predicate function not found.
//
//   - source: the slice. MUST BE A SLICE.
//   - fn: the predicate function.
//   - defaultValue: the default value.
//   - returns: the value of the slice.
//
// Example:
//
//	// Output: "d"
//	val := FindOrDefault([]string{"a", "b", "c"}, func(s string) bool {
//		return s == "d"
//	}, "d")
func FindOrDefault[T any](source []T, fn Predicate[T], defaultValue T) T {
	item, err := Find(source, fn)
	if err != nil {
		return defaultValue
	}
	return item
}

// TakeWhile Produce the set of a slice according to a predicate function,
// Produce empty slice when predicate function not matched.
//
//   - data: the slice. MUST BE A SLICE.
//   - fn: the predicate function.
//   - returns: the set of the slice.
//
// Example:
//
//	// Output: []string{"a", "b"}
//	sl := TakeWhile([]string{"a", "b", "c"}, func(s string) bool {
//		return s != "c"
//	})
func TakeWhile[T any](data []T, fn Predicate[T]) []T {
	result := make([]T, 0)
	if len(data) == 0 {
		return result
	}

	for _, v := range data {
		if fn(v) {
			result = append(result, v)
		}
	}
	return result
}

// ExceptWhile Produce the set of a slice except with a predicate function,
// Produce original slice when predicate function not match.
//
//   - data: the slice. MUST BE A SLICE.
//   - fn: the predicate function.
//   - returns: the set of the slice.
//
// Example:
//
//	// Output: []string{"a", "b"}
//	sl := ExceptWhile([]string{"a", "b", "c"}, func(s string) bool {
//		return s == "c"
//	})
func ExceptWhile[T any](data []T, fn Predicate[T]) []T {
	result := make([]T, 0)
	if len(data) == 0 {
		return result
	}

	for _, v := range data {
		if !fn(v) {
			result = append(result, v)
		}
	}
	return result
}
