package wand

import (
	"code.squareroundforest.org/arpio/bind"
	"io"
	"reflect"
	"strings"
	"time"
)

var (
	structFieldsCache = make(map[reflect.Type][]bind.Field)
	bindableTypes     = make(map[reflect.Type]bool)
)

func filter[T any](list []T, predicate func(T) bool) []T {
	var filtered []T
	for _, item := range list {
		if predicate(item) {
			filtered = append(filtered, item)
		}
	}

	return filtered
}

func not[T any](p func(T) bool) func(T) bool {
	return func(v T) bool {
		return !p(v)
	}
}

func and[T any](p ...func(T) bool) func(T) bool {
	return func(v T) bool {
		for _, pi := range p {
			if !pi(v) {
				return false
			}
		}

		return true
	}
}

func or[T any](p ...func(T) bool) func(T) bool {
	return func(v T) bool {
		for _, pi := range p {
			if pi(v) {
				return true
			}
		}

		return false
	}
}

func unpackType(t reflect.Type) reflect.Type {
	if t == nil {
		return nil
	}

	switch t.Kind() {
	case reflect.Pointer, reflect.Slice:
		return unpackType(t.Elem())
	default:
		return t
	}
}

func unpackValueChecked(visited map[uintptr]bool, v reflect.Value) reflect.Value {
	if !v.IsValid() {
		return v
	}

	switch v.Kind() {
	case reflect.Pointer:
		p := v.Pointer()
		if visited[p] {
			return v
		}

		if visited == nil {
			visited = make(map[uintptr]bool)
		}

		visited[p] = true
		return unpackValueChecked(visited, v.Elem())
	case reflect.Interface:
		if v.IsNil() {
			return v
		}

		return unpackValueChecked(visited, v.Elem())
	default:
		return v
	}
}

func unpackValue(v reflect.Value) reflect.Value {
	return unpackValueChecked(nil, v)
}

func isFunc(v any) bool {
	r := reflect.ValueOf(v)
	r = unpackValue(r)
	return r.Kind() == reflect.Func
}

func isTime(t reflect.Type) bool {
	if t == nil {
		return false
	}

	t = unpackType(t)
	return t.ConvertibleTo(reflect.TypeFor[time.Time]())
}

func isStruct(t reflect.Type) bool {
	if t == nil {
		return false
	}

	t = unpackType(t)
	return !isTime(t) && t.Kind() == reflect.Struct
}

func isReader(t reflect.Type) bool {
	if t == nil || t.Kind() != reflect.Interface {
		return false
	}

	return t.NumMethod() == 1 && t.Implements(reflect.TypeFor[io.Reader]())
}

func isWriter(t reflect.Type) bool {
	if t == nil || t.Kind() != reflect.Interface {
		return false
	}

	return t.NumMethod() == 1 && t.Implements(reflect.TypeFor[io.Writer]())
}

func compatibleTypes(t ...bind.FieldType) bool {
	if len(t) == 0 {
		return false
	}

	if len(t) == 1 {
		return true
	}

	switch t[0] {
	case bind.String, bind.Any:
		return compatibleTypes(t[1:]...)
	}

	switch t[1] {
	case bind.String, bind.Any:
		return compatibleTypes(t[1:]...)
	}

	switch t[0] {
	case bind.Bool:
		switch t[1] {
		case bind.Bool:
			return compatibleTypes(t[1:]...)
		default:
			return false
		}
	}

	switch t[1] {
	case bind.Bool:
		return false
	default:
		return compatibleTypes(t[1:]...)
	}
}

func parameters(f any) []reflect.Type {
	r := reflect.ValueOf(f)
	r = unpackValue(r)
	if r.Kind() != reflect.Func {
		return nil
	}

	var p []reflect.Type
	t := r.Type()
	for i := 0; i < t.NumIn(); i++ {
		p = append(p, t.In(i))
	}

	return p
}

func structParameters(f any) []reflect.Type {
	return filter(parameters(f), isStruct)
}

func structFields(s reflect.Type) []bind.Field {
	if f, ok := structFieldsCache[s]; ok {
		return f
	}

	f := bind.FieldsOf(s)
	structFieldsCache[s] = f
	return f
}

func fields(f any) []bind.Field {
	var fields []bind.Field
	s := structParameters(f)
	for _, si := range s {
		fields = append(fields, structFields(si)...)
	}

	return fields
}

func mapFields(f any) map[string][]bind.Field {
	fields := fields(f)
	m := make(map[string][]bind.Field)
	for _, fi := range fields {
		m[fi.Name()] = append(m[fi.Name()], fi)
	}

	return m
}

func boolFields(f []bind.Field) []bind.Field {
	return filter(f, func(f bind.Field) bool { return f.Type() == bind.Bool })
}

func positional(f any) ([]reflect.Type, bool) {
	p := filter(
		parameters(f),
		not(or(isReader, isWriter, isStruct)),
	)

	r := reflect.ValueOf(f)
	r = unpackValue(r)
	t := r.Type()
	return p, t.IsVariadic()
}

func positionalIndices(f any) []int {
	r := reflect.ValueOf(f)
	r = unpackValue(r)
	if r.Kind() != reflect.Func {
		return nil
	}

	var indices []int
	t := r.Type()
	for i := 0; i < t.NumIn(); i++ {
		p := t.In(i)
		if isTime(p) || isStruct(p) || isReader(p) || isWriter(p) {
			continue
		}

		indices = append(indices, i)
	}

	return indices
}

func ioParameters(f any) ([]reflect.Type, []reflect.Type) {
	p := parameters(f)
	return filter(p, isReader), filter(p, isWriter)
}

func bindable(t reflect.Type) bool {
	if bindable, ok := bindableTypes[t]; ok {
		return bindable
	}

	bindable := bind.BindableType(t)
	bindableTypes[t] = bindable
	return bindable
}

func scalarTypeString(t bind.FieldType) string {
	switch t {
	case bind.Duration:
		return "duration"
	case bind.Time:
		return "time"
	default:
		return strings.ToLower(reflect.Kind(t).String())
	}
}

func canScan(t bind.FieldType, v any) bool {
	_, ok := bind.CreateAndBindScalarFieldType(t, v)
	return ok
}

func canScanType(t reflect.Type, v any) bool {
	_, ok := bind.CreateAndBindScalarFor(t, v)
	return ok
}

func allocate(t reflect.Type) reflect.Value {
	switch t.Kind() {
	case reflect.Pointer:
		et := t.Elem()
		v := allocate(et)
		p := reflect.New(et)
		p.Elem().Set(v)
		return p
	case reflect.Slice:
		v := allocate(t.Elem())
		s := reflect.MakeSlice(t, 1, 1)
		s.Index(0).Set(v)
		return s
	default:
		p := reflect.New(t)
		return p.Elem()
	}
}

func bindScalar(receiver reflect.Value, value any) {
	bind.BindScalar(receiver.Interface(), value)
}

func bindFields(receiver reflect.Value, values map[string][]any) []string {
	var f []bind.Field
	for name, vals := range values {
		for _, v := range vals {
			f = append(f, bind.NamedValue(name, v))
		}
	}

	unmapped := bind.Bind(receiver.Interface(), f...)

	var names []string
	for _, um := range unmapped {
		names = append(names, um.Name())
	}

	return names
}
