package convert

import (
	"github.com/hashicorp/go-cty/cty"
)

// The current unify implementation is somewhat inefficient, but we accept this
// under the assumption that it will generally be used with small numbers of
// types and with types of reasonable complexity. However, it does have a
// "happy path" where all of the given types are equal.
//
// This function is likely to have poor performance in cases where any given
// types are very complex (lots of deeply-nested structures) or if the list
// of types itself is very large. In particular, it will walk the nested type
// structure under the given types several times, especially when given a
// list of types for which unification is not possible, since each permutation
// will be tried to determine that result.
func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
	if len(types) == 0 {
		// Degenerate case
		return cty.NilType, nil
	}

	// If all of the given types are of the same structural kind, we may be
	// able to construct a new type that they can all be unified to, even if
	// that is not one of the given types. We must try this before the general
	// behavior below because in unsafe mode we can convert an object type to
	// a subset of that type, which would be a much less useful conversion for
	// unification purposes.
	{
		mapCt := 0
		objectCt := 0
		tupleCt := 0
		dynamicCt := 0
		for _, ty := range types {
			switch {
			case ty.IsMapType():
				mapCt++
			case ty.IsObjectType():
				objectCt++
			case ty.IsTupleType():
				tupleCt++
			case ty == cty.DynamicPseudoType:
				dynamicCt++
			default:
				break
			}
		}
		switch {
		case mapCt > 0 && (mapCt+dynamicCt) == len(types):
			return unifyMapTypes(types, unsafe, dynamicCt > 0)
		case objectCt > 0 && (objectCt+dynamicCt) == len(types):
			return unifyObjectTypes(types, unsafe, dynamicCt > 0)
		case tupleCt > 0 && (tupleCt+dynamicCt) == len(types):
			return unifyTupleTypes(types, unsafe, dynamicCt > 0)
		case objectCt > 0 && tupleCt > 0:
			// Can never unify object and tuple types since they have incompatible kinds
			return cty.NilType, nil
		}
	}

	prefOrder := sortTypes(types)

	// sortTypes gives us an order where earlier items are preferable as
	// our result type. We'll now walk through these and choose the first
	// one we encounter for which conversions exist for all source types.
	conversions := make([]Conversion, len(types))
Preferences:
	for _, wantTypeIdx := range prefOrder {
		wantType := types[wantTypeIdx]
		for i, tryType := range types {
			if i == wantTypeIdx {
				// Don't need to convert our wanted type to itself
				conversions[i] = nil
				continue
			}

			if tryType.Equals(wantType) {
				conversions[i] = nil
				continue
			}

			if unsafe {
				conversions[i] = GetConversionUnsafe(tryType, wantType)
			} else {
				conversions[i] = GetConversion(tryType, wantType)
			}

			if conversions[i] == nil {
				// wantType is not a suitable unification type, so we'll
				// try the next one in our preference order.
				continue Preferences
			}
		}

		return wantType, conversions
	}

	// If we fall out here, no unification is possible
	return cty.NilType, nil
}

func unifyMapTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
	// If we had any dynamic types in the input here then we can't predict
	// what path we'll take through here once these become known types, so
	// we'll conservatively produce DynamicVal for these.
	if hasDynamic {
		return unifyAllAsDynamic(types)
	}

	elemTypes := make([]cty.Type, 0, len(types))
	for _, ty := range types {
		elemTypes = append(elemTypes, ty.ElementType())
	}
	retElemType, _ := unify(elemTypes, unsafe)
	if retElemType == cty.NilType {
		return cty.NilType, nil
	}

	retTy := cty.Map(retElemType)

	conversions := make([]Conversion, len(types))
	for i, ty := range types {
		if ty.Equals(retTy) {
			continue
		}
		if unsafe {
			conversions[i] = GetConversionUnsafe(ty, retTy)
		} else {
			conversions[i] = GetConversion(ty, retTy)
		}
		if conversions[i] == nil {
			// Shouldn't be reachable, since we were able to unify
			return cty.NilType, nil
		}
	}

	return retTy, conversions
}

func unifyObjectTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
	// If we had any dynamic types in the input here then we can't predict
	// what path we'll take through here once these become known types, so
	// we'll conservatively produce DynamicVal for these.
	if hasDynamic {
		return unifyAllAsDynamic(types)
	}

	// There are two different ways we can succeed here:
	// - If all of the given object types have the same set of attribute names
	//   and the corresponding types are all unifyable, then we construct that
	//   type.
	// - If the given object types have different attribute names or their
	//   corresponding types are not unifyable, we'll instead try to unify
	//   all of the attribute types together to produce a map type.
	//
	// Our unification behavior is intentionally stricter than our conversion
	// behavior for subset object types because user intent is different with
	// unification use-cases: it makes sense to allow {"foo":true} to convert
	// to emptyobjectval, but unifying an object with an attribute with the
	// empty object type should be an error because unifying to the empty
	// object type would be suprising and useless.

	firstAttrs := types[0].AttributeTypes()
	for _, ty := range types[1:] {
		thisAttrs := ty.AttributeTypes()
		if len(thisAttrs) != len(firstAttrs) {
			// If number of attributes is different then there can be no
			// object type in common.
			return unifyObjectTypesToMap(types, unsafe)
		}
		for name := range thisAttrs {
			if _, ok := firstAttrs[name]; !ok {
				// If attribute names don't exactly match then there can be
				// no object type in common.
				return unifyObjectTypesToMap(types, unsafe)
			}
		}
	}

	// If we get here then we've proven that all of the given object types
	// have exactly the same set of attribute names, though the types may
	// differ.
	retAtys := make(map[string]cty.Type)
	atysAcross := make([]cty.Type, len(types))
	for name := range firstAttrs {
		for i, ty := range types {
			atysAcross[i] = ty.AttributeType(name)
		}
		retAtys[name], _ = unify(atysAcross, unsafe)
		if retAtys[name] == cty.NilType {
			// Cannot unify this attribute alone, which means that unification
			// of everything down to a map type can't be possible either.
			return cty.NilType, nil
		}
	}
	retTy := cty.Object(retAtys)

	conversions := make([]Conversion, len(types))
	for i, ty := range types {
		if ty.Equals(retTy) {
			continue
		}
		if unsafe {
			conversions[i] = GetConversionUnsafe(ty, retTy)
		} else {
			conversions[i] = GetConversion(ty, retTy)
		}
		if conversions[i] == nil {
			// Shouldn't be reachable, since we were able to unify
			return unifyObjectTypesToMap(types, unsafe)
		}
	}

	return retTy, conversions
}

func unifyObjectTypesToMap(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
	// This is our fallback case for unifyObjectTypes, where we see if we can
	// construct a map type that can accept all of the attribute types.

	var atys []cty.Type
	for _, ty := range types {
		for _, aty := range ty.AttributeTypes() {
			atys = append(atys, aty)
		}
	}

	ety, _ := unify(atys, unsafe)
	if ety == cty.NilType {
		return cty.NilType, nil
	}

	retTy := cty.Map(ety)
	conversions := make([]Conversion, len(types))
	for i, ty := range types {
		if ty.Equals(retTy) {
			continue
		}
		if unsafe {
			conversions[i] = GetConversionUnsafe(ty, retTy)
		} else {
			conversions[i] = GetConversion(ty, retTy)
		}
		if conversions[i] == nil {
			return cty.NilType, nil
		}
	}
	return retTy, conversions
}

func unifyTupleTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
	// If we had any dynamic types in the input here then we can't predict
	// what path we'll take through here once these become known types, so
	// we'll conservatively produce DynamicVal for these.
	if hasDynamic {
		return unifyAllAsDynamic(types)
	}

	// There are two different ways we can succeed here:
	// - If all of the given tuple types have the same sequence of element types
	//   and the corresponding types are all unifyable, then we construct that
	//   type.
	// - If the given tuple types have different element types or their
	//   corresponding types are not unifyable, we'll instead try to unify
	//   all of the elements types together to produce a list type.

	firstEtys := types[0].TupleElementTypes()
	for _, ty := range types[1:] {
		thisEtys := ty.TupleElementTypes()
		if len(thisEtys) != len(firstEtys) {
			// If number of elements is different then there can be no
			// tuple type in common.
			return unifyTupleTypesToList(types, unsafe)
		}
	}

	// If we get here then we've proven that all of the given tuple types
	// have the same number of elements, though the types may differ.
	retEtys := make([]cty.Type, len(firstEtys))
	atysAcross := make([]cty.Type, len(types))
	for idx := range firstEtys {
		for tyI, ty := range types {
			atysAcross[tyI] = ty.TupleElementTypes()[idx]
		}
		retEtys[idx], _ = unify(atysAcross, unsafe)
		if retEtys[idx] == cty.NilType {
			// Cannot unify this element alone, which means that unification
			// of everything down to a map type can't be possible either.
			return cty.NilType, nil
		}
	}
	retTy := cty.Tuple(retEtys)

	conversions := make([]Conversion, len(types))
	for i, ty := range types {
		if ty.Equals(retTy) {
			continue
		}
		if unsafe {
			conversions[i] = GetConversionUnsafe(ty, retTy)
		} else {
			conversions[i] = GetConversion(ty, retTy)
		}
		if conversions[i] == nil {
			// Shouldn't be reachable, since we were able to unify
			return unifyTupleTypesToList(types, unsafe)
		}
	}

	return retTy, conversions
}

func unifyTupleTypesToList(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
	// This is our fallback case for unifyTupleTypes, where we see if we can
	// construct a list type that can accept all of the element types.

	var etys []cty.Type
	for _, ty := range types {
		for _, ety := range ty.TupleElementTypes() {
			etys = append(etys, ety)
		}
	}

	ety, _ := unify(etys, unsafe)
	if ety == cty.NilType {
		return cty.NilType, nil
	}

	retTy := cty.List(ety)
	conversions := make([]Conversion, len(types))
	for i, ty := range types {
		if ty.Equals(retTy) {
			continue
		}
		if unsafe {
			conversions[i] = GetConversionUnsafe(ty, retTy)
		} else {
			conversions[i] = GetConversion(ty, retTy)
		}
		if conversions[i] == nil {
			// Shouldn't be reachable, since we were able to unify
			return unifyObjectTypesToMap(types, unsafe)
		}
	}
	return retTy, conversions
}

func unifyAllAsDynamic(types []cty.Type) (cty.Type, []Conversion) {
	conversions := make([]Conversion, len(types))
	for i := range conversions {
		conversions[i] = func(cty.Value) (cty.Value, error) {
			return cty.DynamicVal, nil
		}
	}
	return cty.DynamicPseudoType, conversions
}
