package blackmagic

import (
	"reflect"

	"github.com/pkg/errors"
)

// AssignIfCompatible is a convenience function to safely
// assign arbitrary values. dst must be a pointer to an
// empty interface, or it must be a pointer to a compatible
// variable type that can hold src.
func AssignIfCompatible(dst, src interface{}) error {
	orv := reflect.ValueOf(src) // save this value for error reporting
	result := orv

	// t can be a pointer or a slice, and the code will slightly change
	// depending on this
	var isSlice bool
	switch result.Kind() {
	case reflect.Ptr:
		// no op
	case reflect.Slice:
		isSlice = true
	default:
		return errors.Errorf("argument t to AssignIfCompatible must be a pointer or a slice: %T", src)
	}

	rv := reflect.ValueOf(dst)
	if rv.Kind() != reflect.Ptr {
		return errors.Errorf(`argument to AssignIfCompatible() must be a pointer: %T`, dst)
	}

	actualDst := rv.Elem()
	switch actualDst.Kind() {
	case reflect.Interface:
		// If it's an interface, we can just assign the pointer to the interface{}
	default:
		// If it's a pointer to the struct we're looking for, we need to set
		// the de-referenced struct
		if !isSlice {
			result = result.Elem()
		}
	}
	if !result.Type().AssignableTo(actualDst.Type()) {
		return errors.Errorf(`argument to AssignIfCompatible() must be compatible with %T (was %T)`, orv.Interface(), dst)
	}

	if !actualDst.CanSet() {
		return errors.Errorf(`argument to AssignIfCompatible() must be settable`)
	}
	actualDst.Set(result)

	return nil
}
