package config

import (
	"fmt"
	"reflect"
	"runtime"

	"github.com/libp2p/go-libp2p-core/connmgr"
	"github.com/libp2p/go-libp2p-core/host"

	tptu "github.com/libp2p/go-libp2p-transport-upgrader"
)

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

// checks if a function returns either the specified type or the specified type
// and an error.
func checkReturnType(fnType, tptType reflect.Type) error {
	switch fnType.NumOut() {
	case 2:
		if fnType.Out(1) != errorType {
			return fmt.Errorf("expected (optional) second return value from transport constructor to be an error")
		}

		fallthrough
	case 1:
		if !fnType.Out(0).Implements(tptType) {
			return fmt.Errorf("transport constructor returns %s which doesn't implement %s", fnType.Out(0), tptType)
		}
	default:
		return fmt.Errorf("expected transport constructor to return a transport and, optionally, an error")
	}
	return nil
}

// Handles return values with optional errors. That is, return values of the
// form `(something, error)` or just `something`.
//
// Panics if the return value isn't of the correct form.
func handleReturnValue(out []reflect.Value) (interface{}, error) {
	switch len(out) {
	case 2:
		err := out[1]
		if err != (reflect.Value{}) && !err.IsNil() {
			return nil, err.Interface().(error)
		}
		fallthrough
	case 1:
		tpt := out[0]

		// Check for nil value and nil error.
		if tpt == (reflect.Value{}) {
			return nil, fmt.Errorf("unspecified error")
		}
		switch tpt.Kind() {
		case reflect.Ptr, reflect.Interface, reflect.Func:
			if tpt.IsNil() {
				return nil, fmt.Errorf("unspecified error")
			}
		}

		return tpt.Interface(), nil
	default:
		panic("expected 1 or 2 return values from transport constructor")
	}
}

// calls the transport constructor and annotates the error with the name of the constructor.
func callConstructor(c reflect.Value, args []reflect.Value) (interface{}, error) {
	val, err := handleReturnValue(c.Call(args))
	if err != nil {
		name := runtime.FuncForPC(c.Pointer()).Name()
		if name != "" {
			// makes debugging easier
			return nil, fmt.Errorf("transport constructor %s failed: %s", name, err)
		}
	}
	return val, err
}

type constructor func(h host.Host, u *tptu.Upgrader, cg connmgr.ConnectionGater) interface{}

func makeArgumentConstructors(fnType reflect.Type, argTypes map[reflect.Type]constructor) ([]constructor, error) {
	out := make([]constructor, fnType.NumIn())
	for i := range out {
		argType := fnType.In(i)
		c, ok := argTypes[argType]
		if !ok {
			return nil, fmt.Errorf("argument %d has an unexpected type %s", i, argType.Name())
		}
		out[i] = c
	}
	return out, nil
}

// makes a transport constructor.
func makeConstructor(
	tpt interface{},
	tptType reflect.Type,
	argTypes map[reflect.Type]constructor,
) (func(host.Host, *tptu.Upgrader, connmgr.ConnectionGater) (interface{}, error), error) {
	v := reflect.ValueOf(tpt)
	// avoid panicing on nil/zero value.
	if v == (reflect.Value{}) {
		return nil, fmt.Errorf("expected a transport or transport constructor, got a %T", tpt)
	}
	t := v.Type()
	if t.Kind() != reflect.Func {
		return nil, fmt.Errorf("expected a transport or transport constructor, got a %T", tpt)
	}

	if err := checkReturnType(t, tptType); err != nil {
		return nil, err
	}

	argConstructors, err := makeArgumentConstructors(t, argTypes)
	if err != nil {
		return nil, err
	}

	return func(h host.Host, u *tptu.Upgrader, cg connmgr.ConnectionGater) (interface{}, error) {
		arguments := make([]reflect.Value, len(argConstructors))
		for i, makeArg := range argConstructors {
			if arg := makeArg(h, u, cg); arg != nil {
				arguments[i] = reflect.ValueOf(arg)
			} else {
				// ValueOf an un-typed nil yields a zero reflect
				// value. However, we _want_ the zero value of
				// the _type_.
				arguments[i] = reflect.Zero(t.In(i))
			}
		}
		return callConstructor(v, arguments)
	}, nil
}
