package context

import (
	"errors"
	"reflect"
	"sync"
)

// ErrInvalidArgs fires when the `Context.CallFunc`
// is called with invalid number of arguments.
var ErrInvalidArgs = errors.New("invalid arguments")

// Func represents a function registered by the Context.
// See its `buildMeta` and `call` internal methods.
type Func struct {
	RegisterName    string        // the name of which this function is registered, for information only.
	Raw             interface{}   // the Raw function, can be used for custom casting.
	PersistenceArgs []interface{} // the persistence input arguments given on registration.

	once sync.Once // guards build once, on first call.
	// Available after the first call.
	Meta *FuncMeta
}

func newFunc(name string, fn interface{}, persistenceArgs ...interface{}) *Func {
	return &Func{
		RegisterName:    name,
		Raw:             fn,
		PersistenceArgs: persistenceArgs,
	}
}

// FuncMeta holds the necessary information about a registered
// context function. Built once by the Func.
type FuncMeta struct {
	Handler            Handler              // when it's just a handler.
	HandlerWithErr     func(*Context) error // when it's just a handler which returns an error.
	RawFunc            func()               // when it's just a func.
	RawFuncWithErr     func() error         // when it's just a func which returns an error.
	RawFuncArgs        func(...interface{})
	RawFuncArgsWithErr func(...interface{}) error

	Value                   reflect.Value
	Type                    reflect.Type
	ExpectedArgumentsLength int
	PersistenceInputs       []reflect.Value
	AcceptsContext          bool // the Context, if exists should be always first argument.
	ReturnsError            bool // when the function's last output argument is error.
}

func (f *Func) buildMeta() {
	switch fn := f.Raw.(type) {
	case Handler:
		f.Meta = &FuncMeta{Handler: fn}
		return
	case func(*Context):
		f.Meta = &FuncMeta{Handler: fn}
		return
	case func(*Context) error:
		f.Meta = &FuncMeta{HandlerWithErr: fn}
		return
	case func():
		f.Meta = &FuncMeta{RawFunc: fn}
		return
	case func() error:
		f.Meta = &FuncMeta{RawFuncWithErr: fn}
		return
	case func(...interface{}):
		f.Meta = &FuncMeta{RawFuncArgs: fn}
		return
	case func(...interface{}) error:
		f.Meta = &FuncMeta{RawFuncArgsWithErr: fn}
		return
	}

	fn := f.Raw

	meta := FuncMeta{}
	if val, ok := fn.(reflect.Value); ok {
		meta.Value = val
	} else {
		meta.Value = reflect.ValueOf(fn)
	}

	meta.Type = meta.Value.Type()

	if meta.Type.Kind() != reflect.Func {
		return
	}

	meta.ExpectedArgumentsLength = meta.Type.NumIn()

	skipInputs := len(meta.PersistenceInputs)
	if meta.ExpectedArgumentsLength > skipInputs {
		meta.AcceptsContext = isContext(meta.Type.In(skipInputs))
	}

	if numOut := meta.Type.NumOut(); numOut > 0 {
		// error should be the last output.
		if isError(meta.Type.Out(numOut - 1)) {
			meta.ReturnsError = true
		}
	}

	persistenceArgs := f.PersistenceArgs
	if len(persistenceArgs) > 0 {
		inputs := make([]reflect.Value, 0, len(persistenceArgs))
		for _, arg := range persistenceArgs {
			if in, ok := arg.(reflect.Value); ok {
				inputs = append(inputs, in)
			} else {
				inputs = append(inputs, reflect.ValueOf(in))
			}
		}

		meta.PersistenceInputs = inputs
	}

	f.Meta = &meta
}

func (f *Func) call(ctx *Context, args ...interface{}) ([]reflect.Value, error) {
	f.once.Do(f.buildMeta)
	meta := f.Meta

	if meta.Handler != nil {
		meta.Handler(ctx)
		return nil, nil
	}

	if meta.HandlerWithErr != nil {
		return nil, meta.HandlerWithErr(ctx)
	}

	if meta.RawFunc != nil {
		meta.RawFunc()
		return nil, nil
	}

	if meta.RawFuncWithErr != nil {
		return nil, meta.RawFuncWithErr()
	}

	if meta.RawFuncArgs != nil {
		meta.RawFuncArgs(args...)
		return nil, nil
	}

	if meta.RawFuncArgsWithErr != nil {
		return nil, meta.RawFuncArgsWithErr(args...)
	}

	inputs := make([]reflect.Value, 0, f.Meta.ExpectedArgumentsLength)
	inputs = append(inputs, f.Meta.PersistenceInputs...)
	if f.Meta.AcceptsContext {
		inputs = append(inputs, reflect.ValueOf(ctx))
	}

	for _, arg := range args {
		if in, ok := arg.(reflect.Value); ok {
			inputs = append(inputs, in)
		} else {
			inputs = append(inputs, reflect.ValueOf(arg))
		}
	}

	// keep it here, the inptus may contain the context.
	if f.Meta.ExpectedArgumentsLength != len(inputs) {
		return nil, ErrInvalidArgs
	}

	outputs := f.Meta.Value.Call(inputs)
	return outputs, getError(outputs)
}

var contextType = reflect.TypeOf((*Context)(nil))

// isContext returns true if the "typ" is a type of Context.
func isContext(typ reflect.Type) bool {
	return typ == contextType
}

var errTyp = reflect.TypeOf((*error)(nil)).Elem()

// isError returns true if "typ" is type of `error`.
func isError(typ reflect.Type) bool {
	return typ.Implements(errTyp)
}

func getError(outputs []reflect.Value) error {
	if n := len(outputs); n > 0 {
		lastOut := outputs[n-1]
		if isError(lastOut.Type()) {
			if lastOut.IsNil() {
				return nil
			}

			return lastOut.Interface().(error)
		}
	}

	return nil
}
