package gval

import (
	"context"
	"fmt"
	"reflect"
)

type function func(ctx context.Context, arguments ...interface{}) (interface{}, error)

func toFunc(f interface{}) function {
	if f, ok := f.(func(arguments ...interface{}) (interface{}, error)); ok {
		return function(func(ctx context.Context, arguments ...interface{}) (interface{}, error) {
			var v interface{}
			errCh := make(chan error)
			go func() {
				defer func() {
					if recovered := recover(); recovered != nil {
						errCh <- fmt.Errorf("%v", recovered)
					}
				}()
				result, err := f(arguments...)
				v = result
				errCh <- err
			}()

			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case err := <-errCh:
				return v, err
			}
		})
	}
	if f, ok := f.(func(ctx context.Context, arguments ...interface{}) (interface{}, error)); ok {
		return function(f)
	}

	fun := reflect.ValueOf(f)
	t := fun.Type()
	return func(ctx context.Context, args ...interface{}) (interface{}, error) {
		var v interface{}
		errCh := make(chan error)
		go func() {
			defer func() {
				if recovered := recover(); recovered != nil {
					errCh <- fmt.Errorf("%v", recovered)
				}
			}()
			in, err := createCallArguments(ctx, t, args)
			if err != nil {
				errCh <- err
				return
			}
			out := fun.Call(in)

			r := make([]interface{}, len(out))
			for i, e := range out {
				r[i] = e.Interface()
			}

			err = nil
			errorInterface := reflect.TypeOf((*error)(nil)).Elem()
			if len(r) > 0 && t.Out(len(r)-1).Implements(errorInterface) {
				if r[len(r)-1] != nil {
					err = r[len(r)-1].(error)
				}
				r = r[0 : len(r)-1]
			}

			switch len(r) {
			case 0:
				v = nil
			case 1:
				v = r[0]
			default:
				v = r
			}
			errCh <- err
		}()

		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case err := <-errCh:
			return v, err
		}
	}
}

func createCallArguments(ctx context.Context, t reflect.Type, args []interface{}) ([]reflect.Value, error) {
	variadic := t.IsVariadic()
	numIn := t.NumIn()

	// if first argument is a context, use the given execution context
	if numIn > 0 {
		thisFun := reflect.ValueOf(createCallArguments)
		thisT := thisFun.Type()
		if t.In(0) == thisT.In(0) {
			args = append([]interface{}{ctx}, args...)
		}
	}

	if (!variadic && len(args) != numIn) || (variadic && len(args) < numIn-1) {
		return nil, fmt.Errorf("invalid number of parameters")
	}

	in := make([]reflect.Value, len(args))
	var inType reflect.Type
	for i, arg := range args {
		if !variadic || i < numIn-1 {
			inType = t.In(i)
		} else if i == numIn-1 {
			inType = t.In(numIn - 1).Elem()
		}
		argVal := reflect.ValueOf(arg)
		if arg == nil {
			argVal = reflect.ValueOf(reflect.Interface)
		} else if !argVal.Type().AssignableTo(inType) {
			return nil, fmt.Errorf("expected type %s for parameter %d but got %T",
				inType.String(), i, arg)
		}
		in[i] = argVal
	}
	return in, nil
}
