package dynamic

import (
	"fmt"
	"reflect"
	"time"

	"github.com/sirupsen/logrus"
)

type testEnvironment struct {
	startTime time.Time
}

func InjectField(target interface{}, fieldName string, obj interface{}, pointerOnly bool) error {
	rs := reflect.ValueOf(target)
	field := rs.FieldByName(fieldName)

	if !field.IsValid() {
		return nil
	}

	if !field.CanSet() {
		return fmt.Errorf("field %s of %s can not be set", fieldName, rs.Type())
	}

	rv := reflect.ValueOf(obj)
	if field.Kind() == reflect.Ptr {
		if field.Type() != rv.Type() {
			return fmt.Errorf("field type mismatches: %s != %s", field.Type(), rv.Type())
		}

		field.Set(rv)
	} else if field.Kind() == reflect.Interface {
		field.Set(rv)
	} else {
		// set as value
		if pointerOnly {
			return fmt.Errorf("field %s %s does not allow value assignment (pointer type only)", field.Type(), rv.Type())
		}

		field.Set(rv.Elem())
	}

	return nil
}

// ParseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type.
// if the given object is a reference of an object, the type of the target field MUST BE a pointer field.
// if the given object is a struct value, the type of the target field CAN BE a pointer field or a struct value field.
func ParseStructAndInject(f interface{}, objects ...interface{}) error {
	sv := reflect.ValueOf(f)
	st := reflect.TypeOf(f)

	if st.Kind() != reflect.Ptr {
		return fmt.Errorf("f needs to be a pointer of a struct, %s given", st)
	}

	// solve the reference
	st = st.Elem()
	sv = sv.Elem()

	if st.Kind() != reflect.Struct {
		return fmt.Errorf("f needs to be a struct, %s given", st)
	}

	for i := 0; i < sv.NumField(); i++ {
		fv := sv.Field(i)
		ft := fv.Type()

		// skip unexported fields
		if !st.Field(i).IsExported() {
			continue
		}

		fieldName := st.Field(i).Name

		switch k := fv.Kind(); k {

		case reflect.Ptr, reflect.Struct:
			for oi := 0; oi < len(objects); oi++ {
				obj := objects[oi]
				if obj == nil {
					continue
				}

				ot := reflect.TypeOf(obj)
				if ft.AssignableTo(ot) {
					if !fv.CanSet() {
						return fmt.Errorf("field %v of %s can not be set to %s, make sure it is an exported field", fv, sv.Type(), ot)
					}

					if k == reflect.Ptr && !fv.IsNil() {
						logrus.Debugf("[injection] field %s is not nil, not injecting", fieldName)
						continue
					}

					if k == reflect.Ptr && ot.Kind() == reflect.Struct {
						logrus.Debugf("[injection] found ptr + struct, injecting field %s to %T", fieldName, obj)
						fv.Set(reflect.ValueOf(obj).Addr())
					} else {
						logrus.Debugf("[injection] injecting field %s to %T", fieldName, obj)
						fv.Set(reflect.ValueOf(obj))
					}
				}
			}

		case reflect.Interface:
			for oi := 0; oi < len(objects); oi++ {
				obj := objects[oi]
				if obj == nil {
					continue
				}

				objT := reflect.TypeOf(obj)
				logrus.Debugln(
					ft.PkgPath(),
					ft.Name(),
					objT, "implements", ft, "=", objT.Implements(ft),
				)

				if objT.Implements(ft) {
					fv.Set(reflect.ValueOf(obj))
				}
			}
		}
	}

	return nil
}
