package canister

import (
	"errors"
	"gardening/src/app/container"
	"reflect"
)

func (this *Canister) ConstructCanister() {
	container := reflect.ValueOf(this).Elem()

	for i := 0; i < container.NumField(); i++ {
		service := container.Field(i)
		// IsNil allows to pre-populate the container via custom services
		if service.Kind() == reflect.Ptr && service.IsNil() {
			if service.CanSet() {
				service.Set(reflect.New(service.Type().Elem()))
			}
		}
	}
}

func (this *Canister) InjectCanister(container *container.Container) {
	canisterReflected := reflect.ValueOf(this).Elem()
	containerReflected := reflect.ValueOf(container).Elem()

	for i := 0; i < canisterReflected.NumField(); i++ {
		service := canisterReflected.Field(i)
		this.performInjectionIntoCanisterService(service, canisterReflected, containerReflected)
	}
}

func (this *Canister) performInjectionIntoCanisterService(service reflect.Value, canisterReflected reflect.Value, containerReflected reflect.Value) {
	indService := reflect.Indirect(service)
	serviceName := service.Elem().Type().Name()

	kind := service.Kind()
	// Interfaces are built via custom
	if kind == reflect.Interface {
		return
	}
	if kind != reflect.Pointer {
		panic(errors.New("Should be a pointer"))
	}
	for j := 0; j < indService.NumField(); j++ {
		injected := indService.Field(j)
		fieldName := service.Elem().Type().Field(j).Name
		this.injectInjected(fieldName, injected, serviceName, canisterReflected, containerReflected)
	}
}

func (this *Canister) injectInjected(name string, injected reflect.Value, serviceName string, canisterReflected reflect.Value, containerReflected reflect.Value) {
	injectedKind := injected.Kind()

	// Struct are allowed to contain shared state values
	if injectedKind == reflect.Struct {
		return
	}

	isInterface := injectedKind == reflect.Interface
	isPointer := injectedKind == reflect.Pointer
	pointerOrInterface := isInterface || isPointer
	if !pointerOrInterface {
		panic(errors.New(name + " of " + serviceName + " should be a pointer or interface"))
	}

	value := this.findValueToInject(name, serviceName, canisterReflected, containerReflected)
	injected.Set(value)
}

func (this *Canister) findValueToInject(name string, serviceName string, canister reflect.Value, container reflect.Value) reflect.Value {
	containerValue := container.FieldByName(name)
	if containerValue.IsValid() {
		return containerValue
	}
	canisterValue := canister.FieldByName(name)
	if canisterValue.IsValid() {
		return canisterValue
	}
	panic(errors.New(name + " service of " + serviceName + " has not been found neither in the container nor the canister"))
}
