package cty

// Walk visits all of the values in a possibly-complex structure, calling
// a given function for each value.
//
// For example, given a list of strings the callback would first be called
// with the whole list and then called once for each element of the list.
//
// The callback function may prevent recursive visits to child values by
// returning false. The callback function my halt the walk altogether by
// returning a non-nil error. If the returned error is about the element
// currently being visited, it is recommended to use the provided path
// value to produce a PathError describing that context.
//
// The path passed to the given function may not be used after that function
// returns, since its backing array is re-used for other calls.
func Walk(val Value, cb func(Path, Value) (bool, error)) error {
	var path Path
	return walk(path, val, cb)
}

func walk(path Path, val Value, cb func(Path, Value) (bool, error)) error {
	deeper, err := cb(path, val)
	if err != nil {
		return err
	}
	if !deeper {
		return nil
	}

	if val.IsNull() || !val.IsKnown() {
		// Can't recurse into null or unknown values, regardless of type
		return nil
	}

	ty := val.Type()
	switch {
	case ty.IsObjectType():
		for it := val.ElementIterator(); it.Next(); {
			nameVal, av := it.Element()
			path := append(path, GetAttrStep{
				Name: nameVal.AsString(),
			})
			err := walk(path, av, cb)
			if err != nil {
				return err
			}
		}
	case val.CanIterateElements():
		for it := val.ElementIterator(); it.Next(); {
			kv, ev := it.Element()
			path := append(path, IndexStep{
				Key: kv,
			})
			err := walk(path, ev, cb)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// Transformer is the interface used to optionally transform values in a
// possibly-complex structure. The Enter method is called before traversing
// through a given path, and the Exit method is called when traversal of a
// path is complete.
//
// Use Enter when you want to transform a complex value before traversal
// (preorder), and Exit when you want to transform a value after traversal
// (postorder).
//
// The path passed to the given function may not be used after that function
// returns, since its backing array is re-used for other calls.
type Transformer interface {
	Enter(Path, Value) (Value, error)
	Exit(Path, Value) (Value, error)
}

type postorderTransformer struct {
	callback func(Path, Value) (Value, error)
}

func (t *postorderTransformer) Enter(p Path, v Value) (Value, error) {
	return v, nil
}

func (t *postorderTransformer) Exit(p Path, v Value) (Value, error) {
	return t.callback(p, v)
}

// Transform visits all of the values in a possibly-complex structure,
// calling a given function for each value which has an opportunity to
// replace that value.
//
// Unlike Walk, Transform visits child nodes first, so for a list of strings
// it would first visit the strings and then the _new_ list constructed
// from the transformed values of the list items.
//
// This is useful for creating the effect of being able to make deep mutations
// to a value even though values are immutable. However, it's the responsibility
// of the given function to preserve expected invariants, such as homogenity of
// element types in collections; this function can panic if such invariants
// are violated, just as if new values were constructed directly using the
// value constructor functions. An easy way to preserve invariants is to
// ensure that the transform function never changes the value type.
//
// The callback function may halt the walk altogether by
// returning a non-nil error. If the returned error is about the element
// currently being visited, it is recommended to use the provided path
// value to produce a PathError describing that context.
//
// The path passed to the given function may not be used after that function
// returns, since its backing array is re-used for other calls.
func Transform(val Value, cb func(Path, Value) (Value, error)) (Value, error) {
	var path Path
	return transform(path, val, &postorderTransformer{cb})
}

// TransformWithTransformer allows the caller to more closely control the
// traversal used for transformation. See the documentation for Transformer for
// more details.
func TransformWithTransformer(val Value, t Transformer) (Value, error) {
	var path Path
	return transform(path, val, t)
}

func transform(path Path, val Value, t Transformer) (Value, error) {
	val, err := t.Enter(path, val)
	if err != nil {
		return DynamicVal, err
	}

	ty := val.Type()
	var newVal Value

	switch {

	case val.IsNull() || !val.IsKnown():
		// Can't recurse into null or unknown values, regardless of type
		newVal = val

	case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
		l := val.LengthInt()
		switch l {
		case 0:
			// No deep transform for an empty sequence
			newVal = val
		default:
			elems := make([]Value, 0, l)
			for it := val.ElementIterator(); it.Next(); {
				kv, ev := it.Element()
				path := append(path, IndexStep{
					Key: kv,
				})
				newEv, err := transform(path, ev, t)
				if err != nil {
					return DynamicVal, err
				}
				elems = append(elems, newEv)
			}
			switch {
			case ty.IsListType():
				newVal = ListVal(elems)
			case ty.IsSetType():
				newVal = SetVal(elems)
			case ty.IsTupleType():
				newVal = TupleVal(elems)
			default:
				panic("unknown sequence type") // should never happen because of the case we are in
			}
		}

	case ty.IsMapType():
		l := val.LengthInt()
		switch l {
		case 0:
			// No deep transform for an empty map
			newVal = val
		default:
			elems := make(map[string]Value)
			for it := val.ElementIterator(); it.Next(); {
				kv, ev := it.Element()
				path := append(path, IndexStep{
					Key: kv,
				})
				newEv, err := transform(path, ev, t)
				if err != nil {
					return DynamicVal, err
				}
				elems[kv.AsString()] = newEv
			}
			newVal = MapVal(elems)
		}

	case ty.IsObjectType():
		switch {
		case ty.Equals(EmptyObject):
			// No deep transform for an empty object
			newVal = val
		default:
			atys := ty.AttributeTypes()
			newAVs := make(map[string]Value)
			for name := range atys {
				av := val.GetAttr(name)
				path := append(path, GetAttrStep{
					Name: name,
				})
				newAV, err := transform(path, av, t)
				if err != nil {
					return DynamicVal, err
				}
				newAVs[name] = newAV
			}
			newVal = ObjectVal(newAVs)
		}

	default:
		newVal = val
	}

	newVal, err = t.Exit(path, newVal)
	if err != nil {
		return DynamicVal, err
	}
	return newVal, err
}
