package function

import (
	"fmt"

	"github.com/zclconf/go-cty/cty"
)

// Function represents a function. This is the main type in this package.
type Function struct {
	spec *Spec
}

// Spec is the specification of a function, used to instantiate
// a new Function.
type Spec struct {
	// Params is a description of the positional parameters for the function.
	// The standard checking logic rejects any calls that do not provide
	// arguments conforming to this definition, freeing the function
	// implementer from dealing with such inconsistencies.
	Params []Parameter

	// VarParam is an optional specification of additional "varargs" the
	// function accepts. If this is non-nil then callers may provide an
	// arbitrary number of additional arguments (after those matching with
	// the fixed parameters in Params) that conform to the given specification,
	// which will appear as additional values in the slices of values
	// provided to the type and implementation functions.
	VarParam *Parameter

	// Type is the TypeFunc that decides the return type of the function
	// given its arguments, which may be Unknown. See the documentation
	// of TypeFunc for more information.
	//
	// Use StaticReturnType if the function's return type does not vary
	// depending on its arguments.
	Type TypeFunc

	// Impl is the ImplFunc that implements the function's behavior.
	//
	// Functions are expected to behave as pure functions, and not create
	// any visible side-effects.
	//
	// If a TypeFunc is also provided, the value returned from Impl *must*
	// conform to the type it returns, or a call to the function will panic.
	Impl ImplFunc
}

// New creates a new function with the given specification.
//
// After passing a Spec to this function, the caller must no longer read from
// or mutate it.
func New(spec *Spec) Function {
	f := Function{
		spec: spec,
	}
	return f
}

// TypeFunc is a callback type for determining the return type of a function
// given its arguments.
//
// Any of the values passed to this function may be unknown, even if the
// parameters are not configured to accept unknowns.
//
// If any of the given values are *not* unknown, the TypeFunc may use the
// values for pre-validation and for choosing the return type. For example,
// a hypothetical JSON-unmarshalling function could return
// cty.DynamicPseudoType if the given JSON string is unknown, but return
// a concrete type based on the JSON structure if the JSON string is already
// known.
type TypeFunc func(args []cty.Value) (cty.Type, error)

// ImplFunc is a callback type for the main implementation of a function.
//
// "args" are the values for the arguments, and this slice will always be at
// least as long as the argument definition slice for the function.
//
// "retType" is the type returned from the Type callback, included as a
// convenience to avoid the need to re-compute the return type for generic
// functions whose return type is a function of the arguments.
type ImplFunc func(args []cty.Value, retType cty.Type) (cty.Value, error)

// StaticReturnType returns a TypeFunc that always returns the given type.
//
// This is provided as a convenience for defining a function whose return
// type does not depend on the argument types.
func StaticReturnType(ty cty.Type) TypeFunc {
	return func([]cty.Value) (cty.Type, error) {
		return ty, nil
	}
}

// ReturnType returns the return type of a function given a set of candidate
// argument types, or returns an error if the given types are unacceptable.
//
// If the caller already knows values for at least some of the arguments
// it can be better to call ReturnTypeForValues, since certain functions may
// determine their return types from their values and return DynamicVal if
// the values are unknown.
func (f Function) ReturnType(argTypes []cty.Type) (cty.Type, error) {
	vals := make([]cty.Value, len(argTypes))
	for i, ty := range argTypes {
		vals[i] = cty.UnknownVal(ty)
	}
	return f.ReturnTypeForValues(vals)
}

// ReturnTypeForValues is similar to ReturnType but can be used if the caller
// already knows the values of some or all of the arguments, in which case
// the function may be able to determine a more definite result if its
// return type depends on the argument *values*.
//
// For any arguments whose values are not known, pass an Unknown value of
// the appropriate type.
func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error) {
	var posArgs []cty.Value
	var varArgs []cty.Value

	if f.spec.VarParam == nil {
		if len(args) != len(f.spec.Params) {
			return cty.Type{}, fmt.Errorf(
				"wrong number of arguments (%d required; %d given)",
				len(f.spec.Params), len(args),
			)
		}

		posArgs = args
		varArgs = nil
	} else {
		if len(args) < len(f.spec.Params) {
			return cty.Type{}, fmt.Errorf(
				"wrong number of arguments (at least %d required; %d given)",
				len(f.spec.Params), len(args),
			)
		}

		posArgs = args[0:len(f.spec.Params)]
		varArgs = args[len(f.spec.Params):]
	}

	for i, spec := range f.spec.Params {
		val := posArgs[i]

		if val.IsNull() && !spec.AllowNull {
			return cty.Type{}, NewArgErrorf(i, "argument must not be null")
		}

		// AllowUnknown is ignored for type-checking, since we expect to be
		// able to type check with unknown values. We *do* still need to deal
		// with DynamicPseudoType here though, since the Type function might
		// not be ready to deal with that.

		if val.Type() == cty.DynamicPseudoType {
			if !spec.AllowDynamicType {
				return cty.DynamicPseudoType, nil
			}
		} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
			// For now we'll just return the first error in the set, since
			// we don't have a good way to return the whole list here.
			// Would be good to do something better at some point...
			return cty.Type{}, NewArgError(i, errs[0])
		}
	}

	if varArgs != nil {
		spec := f.spec.VarParam
		for i, val := range varArgs {
			realI := i + len(posArgs)

			if val.IsNull() && !spec.AllowNull {
				return cty.Type{}, NewArgErrorf(realI, "argument must not be null")
			}

			if val.Type() == cty.DynamicPseudoType {
				if !spec.AllowDynamicType {
					return cty.DynamicPseudoType, nil
				}
			} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
				// For now we'll just return the first error in the set, since
				// we don't have a good way to return the whole list here.
				// Would be good to do something better at some point...
				return cty.Type{}, NewArgError(i, errs[0])
			}
		}
	}

	// Intercept any panics from the function and return them as normal errors,
	// so a calling language runtime doesn't need to deal with panics.
	defer func() {
		if r := recover(); r != nil {
			ty = cty.NilType
			err = errorForPanic(r)
		}
	}()

	return f.spec.Type(args)
}

// Call actually calls the function with the given arguments, which must
// conform to the function's parameter specification or an error will be
// returned.
func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
	expectedType, err := f.ReturnTypeForValues(args)
	if err != nil {
		return cty.NilVal, err
	}

	// Type checking already dealt with most situations relating to our
	// parameter specification, but we still need to deal with unknown
	// values.
	posArgs := args[:len(f.spec.Params)]
	varArgs := args[len(f.spec.Params):]

	for i, spec := range f.spec.Params {
		val := posArgs[i]

		if !val.IsKnown() && !spec.AllowUnknown {
			return cty.UnknownVal(expectedType), nil
		}
	}

	if f.spec.VarParam != nil {
		spec := f.spec.VarParam
		for _, val := range varArgs {
			if !val.IsKnown() && !spec.AllowUnknown {
				return cty.UnknownVal(expectedType), nil
			}
		}
	}

	var retVal cty.Value
	{
		// Intercept any panics from the function and return them as normal errors,
		// so a calling language runtime doesn't need to deal with panics.
		defer func() {
			if r := recover(); r != nil {
				val = cty.NilVal
				err = errorForPanic(r)
			}
		}()

		retVal, err = f.spec.Impl(args, expectedType)
		if err != nil {
			return cty.NilVal, err
		}
	}

	// Returned value must conform to what the Type function expected, to
	// protect callers from having to deal with inconsistencies.
	if errs := retVal.Type().TestConformance(expectedType); errs != nil {
		panic(fmt.Errorf(
			"returned value %#v does not conform to expected return type %#v: %s",
			retVal, expectedType, errs[0],
		))
	}

	return retVal, nil
}

// ProxyFunc the type returned by the method Function.Proxy.
type ProxyFunc func(args ...cty.Value) (cty.Value, error)

// Proxy returns a function that can be called with cty.Value arguments
// to run the function. This is provided as a convenience for when using
// a function directly within Go code.
func (f Function) Proxy() ProxyFunc {
	return func(args ...cty.Value) (cty.Value, error) {
		return f.Call(args)
	}
}

// Params returns information about the function's fixed positional parameters.
// This does not include information about any variadic arguments accepted;
// for that, call VarParam.
func (f Function) Params() []Parameter {
	new := make([]Parameter, len(f.spec.Params))
	copy(new, f.spec.Params)
	return new
}

// VarParam returns information about the variadic arguments the function
// expects, or nil if the function is not variadic.
func (f Function) VarParam() *Parameter {
	if f.spec.VarParam == nil {
		return nil
	}

	ret := *f.spec.VarParam
	return &ret
}
