package interpolate

import (
	"fmt"
	"reflect"
	"strings"
	"sync"

	"github.com/mitchellh/mapstructure"
	"github.com/mitchellh/reflectwalk"
)

// RenderFilter is an option for filtering what gets rendered and
// doesn't within an interface.
type RenderFilter struct {
	Include []string
	Exclude []string

	once       sync.Once
	excludeSet map[string]struct{}
	includeSet map[string]struct{}
}

// RenderMap renders all the strings in the given interface. The
// interface must decode into a map[string]interface{}, but is left
// as an interface{} type to ease backwards compatibility with the way
// arguments are passed around in Packer.
func RenderMap(v interface{}, ctx *Context, f *RenderFilter) (map[string]interface{}, error) {
	// First decode it into the map
	var m map[string]interface{}
	if err := mapstructure.Decode(v, &m); err != nil {
		return nil, err
	}

	// Now go through each value and render it
	for k, raw := range m {
		// Always validate every field
		if err := ValidateInterface(raw, ctx); err != nil {
			return nil, fmt.Errorf("invalid '%s': %s", k, err)
		}

		if !f.include(k) {
			continue
		}

		raw, err := RenderInterface(raw, ctx)
		if err != nil {
			return nil, fmt.Errorf("render '%s': %s", k, err)
		}

		m[k] = raw
	}

	return m, nil
}

// RenderInterface renders any value and returns the resulting value.
func RenderInterface(v interface{}, ctx *Context) (interface{}, error) {
	f := func(v string) (string, error) {
		return RenderOnce(v, ctx)
	}

	walker := &renderWalker{
		F:       f,
		Replace: true,
	}
	err := reflectwalk.Walk(v, walker)
	if err != nil {
		return nil, err
	}

	if walker.Top != nil {
		v = walker.Top
	}
	return v, nil
}

// ValidateInterface renders any value and returns the resulting value.
func ValidateInterface(v interface{}, ctx *Context) error {
	f := func(v string) (string, error) {
		return v, Validate(v, ctx)
	}

	walker := &renderWalker{
		F:       f,
		Replace: false,
	}
	err := reflectwalk.Walk(v, walker)
	if err != nil {
		return err
	}

	return nil
}

// Include checks whether a key should be included.
func (f *RenderFilter) include(k string) bool {
	if f == nil {
		return true
	}

	k = strings.ToLower(k)

	f.once.Do(f.init)
	if len(f.includeSet) > 0 {
		_, ok := f.includeSet[k]
		return ok
	}
	if len(f.excludeSet) > 0 {
		_, ok := f.excludeSet[k]
		return !ok
	}

	return true
}

func (f *RenderFilter) init() {
	f.includeSet = make(map[string]struct{})
	for _, v := range f.Include {
		f.includeSet[strings.ToLower(v)] = struct{}{}
	}

	f.excludeSet = make(map[string]struct{})
	for _, v := range f.Exclude {
		f.excludeSet[strings.ToLower(v)] = struct{}{}
	}
}

// renderWalker implements interfaces for the reflectwalk package
// (github.com/mitchellh/reflectwalk) that can be used to automatically
// execute a callback for an interpolation.
type renderWalker struct {
	// F is the function to call for every interpolation. It can be nil.
	//
	// If Replace is true, then the return value of F will be used to
	// replace the interpolation.
	F       renderWalkerFunc
	Replace bool

	// ContextF is an advanced version of F that also receives the
	// location of where it is in the structure. This lets you do
	// context-aware validation.
	ContextF renderWalkerContextFunc

	// Top is the top value of the walk. This might get replaced if the
	// top value needs to be modified. It is valid to read after any walk.
	// If it is nil, it means the top wasn't replaced.
	Top interface{}

	key        []string
	lastValue  reflect.Value
	loc        reflectwalk.Location
	cs         []reflect.Value
	csKey      []reflect.Value
	csData     interface{}
	sliceIndex int
}

// renderWalkerFunc is the callback called by interpolationWalk.
// It is called with any interpolation found. It should return a value
// to replace the interpolation with, along with any errors.
//
// If Replace is set to false in renderWalker, then the replace
// value can be anything as it will have no effect.
type renderWalkerFunc func(string) (string, error)

// renderWalkerContextFunc is called by interpolationWalk if
// ContextF is set. This receives both the interpolation and the location
// where the interpolation is.
//
// This callback can be used to validate the location of the interpolation
// within the configuration.
type renderWalkerContextFunc func(reflectwalk.Location, string)

func (w *renderWalker) Enter(loc reflectwalk.Location) error {
	w.loc = loc
	return nil
}

func (w *renderWalker) Exit(loc reflectwalk.Location) error {
	w.loc = reflectwalk.None

	switch loc {
	case reflectwalk.Map:
		w.cs = w.cs[:len(w.cs)-1]
	case reflectwalk.MapValue:
		w.key = w.key[:len(w.key)-1]
		w.csKey = w.csKey[:len(w.csKey)-1]
	case reflectwalk.Slice:
		// Split any values that need to be split
		w.cs = w.cs[:len(w.cs)-1]
	case reflectwalk.SliceElem:
		w.csKey = w.csKey[:len(w.csKey)-1]
	}

	return nil
}

func (w *renderWalker) Map(m reflect.Value) error {
	w.cs = append(w.cs, m)
	return nil
}

func (w *renderWalker) MapElem(m, k, v reflect.Value) error {
	w.csData = k
	w.csKey = append(w.csKey, k)
	w.key = append(w.key, k.String())
	w.lastValue = v
	return nil
}

func (w *renderWalker) Slice(s reflect.Value) error {
	w.cs = append(w.cs, s)
	return nil
}

func (w *renderWalker) SliceElem(i int, elem reflect.Value) error {
	w.csKey = append(w.csKey, reflect.ValueOf(i))
	w.sliceIndex = i
	return nil
}

func (w *renderWalker) Primitive(v reflect.Value) error {
	setV := v

	// We only care about strings
	if v.Kind() == reflect.Interface {
		setV = v
		v = v.Elem()
	}
	if v.Kind() != reflect.String {
		return nil
	}

	strV := v.String()
	if w.ContextF != nil {
		w.ContextF(w.loc, strV)
	}

	if w.F == nil {
		return nil
	}

	replaceVal, err := w.F(strV)
	if err != nil {
		return fmt.Errorf(
			"%s in:\n\n%s",
			err, v.String())
	}

	if w.Replace {
		resultVal := reflect.ValueOf(replaceVal)
		switch w.loc {
		case reflectwalk.MapKey:
			m := w.cs[len(w.cs)-1]

			// Delete the old value
			var zero reflect.Value
			m.SetMapIndex(w.csData.(reflect.Value), zero)

			// Set the new key with the existing value
			m.SetMapIndex(resultVal, w.lastValue)

			// Set the key to be the new key
			w.csData = resultVal
		case reflectwalk.MapValue:
			// If we're in a map, then the only way to set a map value is
			// to set it directly.
			m := w.cs[len(w.cs)-1]
			mk := w.csData.(reflect.Value)
			m.SetMapIndex(mk, resultVal)
		case reflectwalk.WalkLoc:
			// At the root element, we can't write that, so we just save it
			w.Top = resultVal.Interface()
		default:
			// Otherwise, we should be addressable
			setV.Set(resultVal)
		}
	}

	return nil
}
