package tftypes

// these functions are based heavily on github.com/zclconf/go-cty
// used under the MIT License
//
// Copyright (c) 2017-2018 Martin Atkins
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import (
	"errors"
)

// Walk traverses a Value, calling the passed function for every element and
// attribute in the Value. The AttributePath passed to the callback function
// will identify which attribute or element is currently being surfaced by the
// Walk, and the passed Value will be the element or attribute at that
// AttributePath. Returning true from the callback function will indicate that
// any attributes or elements of the surfaced Value should be walked, too;
// returning false short-circuits the walk at that element or attribute, and
// does not visit any of its descendants. The return value of the callback does
// not matter when the Value that has been surfaced has no elements or
// attributes. Walk uses a depth-first traversal.
func Walk(val Value, cb func(*AttributePath, Value) (bool, error)) error {
	return walk(nil, val, cb)
}

func walk(path *AttributePath, val Value, cb func(*AttributePath, Value) (bool, error)) error {
	shouldContinue, err := cb(path, val)
	if err != nil {
		return path.NewError(err)
	}
	if !shouldContinue {
		return nil
	}

	if val.IsNull() || !val.IsKnown() {
		return nil
	}

	ty := val.Type()
	switch {
	case ty.Is(List{}), ty.Is(Set{}), ty.Is(Tuple{}):
		var v []Value
		err := val.As(&v)
		if err != nil {
			// should never happen
			return path.NewError(err)
		}
		for pos, el := range v {
			if ty.Is(Set{}) {
				path = path.WithElementKeyValue(el)
			} else {
				path = path.WithElementKeyInt(pos)
			}
			err = walk(path, el, cb)
			if err != nil {
				return path.NewError(err)
			}
			path = path.WithoutLastStep()
		}
	case ty.Is(Map{}), ty.Is(Object{}):
		v := map[string]Value{}
		err := val.As(&v)
		if err != nil {
			// should never happen
			return err
		}
		for k, el := range v {
			if ty.Is(Map{}) {
				path = path.WithElementKeyString(k)
			} else if ty.Is(Object{}) {
				path = path.WithAttributeName(k)
			}
			err = walk(path, el, cb)
			if err != nil {
				return path.NewError(err)
			}
			path = path.WithoutLastStep()
		}
	}

	return nil
}

// Transform uses a callback to mutate a Value. Each element or attribute will
// be visited in turn, with the AttributePath and Value surfaced to the
// callback, as in Walk. Unlike in Walk, the callback returns a Value instead
// of a boolean; this is the Value that will be stored at that AttributePath.
// The callback must return the passed Value unmodified if it wishes to not
// mutate a Value. Elements and attributes of a Value will be passed to the
// callback prior to the Value they belong to being passed to the callback,
// which means a callback can overwrite its own modifications. Values passed to
// the callback will always reflect the results of earlier callback calls.
func Transform(val Value, cb func(*AttributePath, Value) (Value, error)) (Value, error) {
	return transform(NewAttributePath(), val, cb)
}

func transform(path *AttributePath, val Value, cb func(*AttributePath, Value) (Value, error)) (Value, error) {
	var newVal Value
	ty := val.Type()

	if ty == nil {
		return val, path.NewError(errors.New("invalid transform: value missing type"))
	}

	switch {
	case val.IsNull() || !val.IsKnown():
		newVal = val
	case ty.Is(List{}), ty.Is(Set{}), ty.Is(Tuple{}):
		var v []Value
		err := val.As(&v)
		if err != nil {
			return val, err
		}
		if len(v) == 0 {
			newVal = val
		} else {
			elems := make([]Value, 0, len(v))
			for pos, el := range v {
				if ty.Is(Set{}) {
					path = path.WithElementKeyValue(el)
				} else {
					path = path.WithElementKeyInt(pos)
				}
				newEl, err := transform(path, el, cb)
				if err != nil {
					return val, path.NewError(err)
				}
				elems = append(elems, newEl)
				path = path.WithoutLastStep()
			}
			newVal, err = newValue(ty, elems)
			if err != nil {
				return val, path.NewError(err)
			}
		}
	case ty.Is(Map{}), ty.Is(Object{}):
		v := map[string]Value{}
		err := val.As(&v)
		if err != nil {
			return val, err
		}
		if len(v) == 0 {
			newVal = val
		} else {
			elems := map[string]Value{}
			for k, el := range v {
				if ty.Is(Map{}) {
					path = path.WithElementKeyString(k)
				} else {
					path = path.WithAttributeName(k)
				}
				newEl, err := transform(path, el, cb)
				if err != nil {
					return val, path.NewError(err)
				}
				elems[k] = newEl
				path = path.WithoutLastStep()
			}
			newVal, err = newValue(ty, elems)
			if err != nil {
				return val, path.NewError(err)
			}
		}
	default:
		newVal = val
	}
	res, err := cb(path, newVal)
	if err != nil {
		return res, path.NewError(err)
	}
	newTy := newVal.Type()
	if newTy == nil {
		return val, path.NewError(errors.New("invalid transform: new value missing type"))
	}
	if !newTy.UsableAs(ty) {
		return val, path.NewError(errors.New("invalid transform: value changed type"))
	}
	return res, err
}
