// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package decls contains function and variable declaration structs and helper methods.
package decls

import (
	"fmt"
	"strings"

	chkdecls "github.com/google/cel-go/checker/decls"
	"github.com/google/cel-go/common"
	"github.com/google/cel-go/common/functions"
	"github.com/google/cel-go/common/operators"
	"github.com/google/cel-go/common/types"
	"github.com/google/cel-go/common/types/ref"

	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)

// NewFunction creates a new function declaration with a set of function options to configure overloads
// and function definitions (implementations).
//
// Functions are checked for name collisions and singleton redefinition.
func NewFunction(name string, opts ...FunctionOpt) (*FunctionDecl, error) {
	fn := &FunctionDecl{
		name:             name,
		overloads:        map[string]*OverloadDecl{},
		overloadOrdinals: []string{},
	}
	var err error
	for _, opt := range opts {
		fn, err = opt(fn)
		if err != nil {
			return nil, err
		}
	}
	if len(fn.overloads) == 0 {
		return nil, fmt.Errorf("function %s must have at least one overload", name)
	}
	return fn, nil
}

// FunctionDecl defines a function name, overload set, and optionally a singleton definition for all
// overload instances.
type FunctionDecl struct {
	name string
	doc  string

	// overloads associated with the function name.
	overloads map[string]*OverloadDecl

	// singleton implementation of the function for all overloads.
	//
	// If this option is set, an error will occur if any overloads specify a per-overload implementation
	// or if another function with the same name attempts to redefine the singleton.
	singleton *functions.Overload

	// disableTypeGuards is a performance optimization to disable detailed runtime type checks which could
	// add overhead on common operations. Setting this option true leaves error checks and argument checks
	// intact.
	disableTypeGuards bool

	// state indicates that the binding should be provided as a declaration, as a runtime binding, or both.
	state declarationState

	// overloadOrdinals indicates the order in which the overload was declared.
	overloadOrdinals []string
}

type declarationState int

const (
	declarationStateUnset declarationState = iota
	declarationDisabled
	declarationEnabled
)

// Documentation generates documentation about the Function and its overloads as a common.Doc object.
func (f *FunctionDecl) Documentation() *common.Doc {
	if f == nil {
		return nil
	}
	children := make([]*common.Doc, len(f.OverloadDecls()))
	for i, o := range f.OverloadDecls() {
		var examples []*common.Doc
		for _, ex := range o.Examples() {
			examples = append(examples, common.NewExampleDoc(ex))
		}
		od := common.NewOverloadDoc(o.ID(), formatSignature(f.Name(), o), examples...)
		children[i] = od
	}
	return common.NewFunctionDoc(
		f.Name(),
		f.Description(),
		children...)
}

// Name returns the function name in human-readable terms, e.g. 'contains' of 'math.least'
func (f *FunctionDecl) Name() string {
	if f == nil {
		return ""
	}
	return f.name
}

// Description provides an overview of the function's purpose.
//
// Usage examples should be included on specific overloads.
func (f *FunctionDecl) Description() string {
	if f == nil {
		return ""
	}
	return f.doc
}

// IsDeclarationDisabled indicates that the function implementation should be added to the dispatcher, but the
// declaration should not be exposed for use in expressions.
func (f *FunctionDecl) IsDeclarationDisabled() bool {
	if f == nil {
		return true
	}
	return f.state == declarationDisabled
}

// Merge combines an existing function declaration with another.
//
// If a function is extended, by say adding new overloads to an existing function, then it is merged with the
// prior definition of the function at which point its overloads must not collide with pre-existing overloads
// and its bindings (singleton, or per-overload) must not conflict with previous definitions either.
func (f *FunctionDecl) Merge(other *FunctionDecl) (*FunctionDecl, error) {
	if f == other {
		return f, nil
	}
	if f == nil || other == nil || f.Name() != other.Name() {
		return nil, fmt.Errorf("cannot merge unrelated functions. %q and %q", f.Name(), other.Name())
	}
	merged := &FunctionDecl{
		name:             f.Name(),
		overloads:        make(map[string]*OverloadDecl, len(f.overloads)),
		singleton:        f.singleton,
		overloadOrdinals: make([]string, len(f.overloads)),
		// if one function is expecting type-guards and the other is not, then they
		// must not be disabled.
		disableTypeGuards: f.disableTypeGuards && other.disableTypeGuards,
		// default to the current functions declaration state.
		state: f.state,
		doc:   f.doc,
	}
	// If the other state indicates that the declaration should be explicitly enabled or
	// disabled, then update the merged state with the most recent value.
	if other.state != declarationStateUnset {
		merged.state = other.state
	}
	// Allow for non-empty overrides of documentation
	if len(other.doc) != 0 && f.doc != other.doc {
		merged.doc = other.doc
	}
	// baseline copy of the overloads and their ordinals
	copy(merged.overloadOrdinals, f.overloadOrdinals)
	for oID, o := range f.overloads {
		merged.overloads[oID] = o
	}
	// overloads and their ordinals are added from the left
	for _, oID := range other.overloadOrdinals {
		o := other.overloads[oID]
		err := merged.AddOverload(o)
		if err != nil {
			return nil, fmt.Errorf("function declaration merge failed: %v", err)
		}
	}
	if other.singleton != nil {
		if merged.singleton != nil && merged.singleton != other.singleton {
			return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name())
		}
		merged.singleton = other.singleton
	}
	return merged, nil
}

// FunctionSubsetter subsets a function declaration or returns nil and false if the function
// subset was empty.
type FunctionSubsetter func(fn *FunctionDecl) (*FunctionDecl, bool)

// OverloadSelector selects an overload associated with a given function when it returns true.
//
// Used in combination with the Subset method.
type OverloadSelector func(overload *OverloadDecl) bool

// IncludeOverloads defines an OverloadSelector which allow-lists a set of overloads by their ids.
func IncludeOverloads(overloadIDs ...string) OverloadSelector {
	return func(overload *OverloadDecl) bool {
		for _, oID := range overloadIDs {
			if overload.id == oID {
				return true
			}
		}
		return false
	}
}

// ExcludeOverloads defines an OverloadSelector which deny-lists a set of overloads by their ids.
func ExcludeOverloads(overloadIDs ...string) OverloadSelector {
	return func(overload *OverloadDecl) bool {
		for _, oID := range overloadIDs {
			if overload.id == oID {
				return false
			}
		}
		return true
	}
}

// Subset returns a new function declaration which contains only the overloads with the specified IDs.
// If the subset function contains no overloads, then nil is returned to indicate the function is not
// functional.
func (f *FunctionDecl) Subset(selector OverloadSelector) *FunctionDecl {
	if f == nil {
		return nil
	}
	overloads := make(map[string]*OverloadDecl)
	overloadOrdinals := make([]string, 0, len(f.overloadOrdinals))
	for _, oID := range f.overloadOrdinals {
		overload := f.overloads[oID]
		if selector(overload) {
			overloads[oID] = overload
			overloadOrdinals = append(overloadOrdinals, oID)
		}
	}
	if len(overloads) == 0 {
		return nil
	}
	subset := &FunctionDecl{
		name:              f.Name(),
		doc:               f.doc,
		overloads:         overloads,
		singleton:         f.singleton,
		disableTypeGuards: f.disableTypeGuards,
		state:             f.state,
		overloadOrdinals:  overloadOrdinals,
	}
	return subset
}

// AddOverload ensures that the new overload does not collide with an existing overload signature;
// however, if the function signatures are identical, the implementation may be rewritten as its
// difficult to compare functions by object identity.
func (f *FunctionDecl) AddOverload(overload *OverloadDecl) error {
	if f == nil {
		return fmt.Errorf("nil function cannot add overload: %s", overload.ID())
	}
	if overload == nil {
		return fmt.Errorf("cannot add nil overload to funciton: %s", f.Name())
	}
	for oID, o := range f.overloads {
		if oID != overload.ID() && o.SignatureOverlaps(overload) {
			return fmt.Errorf("overload signature collision in function %s: %s collides with %s", f.Name(), oID, overload.ID())
		}
		if oID == overload.ID() {
			if o.SignatureEquals(overload) && o.IsNonStrict() == overload.IsNonStrict() {
				// Allow redefinition of an overload implementation so long as the signatures match.
				if overload.HasBinding() {
					f.overloads[oID] = overload
				}
				// Allow redefinition of the doc string.
				if len(overload.doc) != 0 && o.doc != overload.doc {
					o.doc = overload.doc
				}
				return nil
			}
			return fmt.Errorf("overload redefinition in function. %s: %s has multiple definitions", f.Name(), oID)
		}
		if overload.HasLateBinding() != o.HasLateBinding() {
			return fmt.Errorf("overload with late binding cannot be added to function %s: cannot mix late and non-late bindings", f.Name())
		}
	}
	f.overloadOrdinals = append(f.overloadOrdinals, overload.ID())
	f.overloads[overload.ID()] = overload
	return nil
}

// OverloadDecls returns the overload declarations in the order in which they were declared.
func (f *FunctionDecl) OverloadDecls() []*OverloadDecl {
	var emptySet []*OverloadDecl
	if f == nil {
		return emptySet
	}
	overloads := make([]*OverloadDecl, 0, len(f.overloads))
	for _, oID := range f.overloadOrdinals {
		overloads = append(overloads, f.overloads[oID])
	}
	return overloads
}

// HasSingletonBinding indicates whether the function has a singleton binding definition.
func (f *FunctionDecl) HasSingletonBinding() bool {
	if f == nil {
		return false
	}
	return f.singleton != nil
}

// HasLateBinding returns true if the function has late bindings. A function cannot mix late bindings with other bindings.
func (f *FunctionDecl) HasLateBinding() bool {
	if f == nil {
		return false
	}
	for _, oID := range f.overloadOrdinals {
		if f.overloads[oID].HasLateBinding() {
			return true
		}
	}
	return false
}

// Bindings produces a set of function bindings, if any are defined.
func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) {
	var emptySet []*functions.Overload
	if f == nil {
		return emptySet, nil
	}
	overloads := []*functions.Overload{}
	nonStrict := false
	hasLateBinding := false
	for _, oID := range f.overloadOrdinals {
		o := f.overloads[oID]
		hasLateBinding = hasLateBinding || o.HasLateBinding()
		if o.HasBinding() {
			overload := &functions.Overload{
				Operator:     o.ID(),
				Unary:        o.guardedUnaryOp(f.Name(), f.disableTypeGuards),
				Binary:       o.guardedBinaryOp(f.Name(), f.disableTypeGuards),
				Function:     o.guardedFunctionOp(f.Name(), f.disableTypeGuards),
				OperandTrait: o.OperandTrait(),
				NonStrict:    o.IsNonStrict(),
			}
			overloads = append(overloads, overload)
			nonStrict = nonStrict || o.IsNonStrict()
		}
	}
	if f.singleton != nil {
		if len(overloads) != 0 {
			return nil, fmt.Errorf("singleton function incompatible with specialized overloads: %s", f.Name())
		}
		if hasLateBinding {
			return nil, fmt.Errorf("singleton function incompatible with late bindings: %s", f.Name())
		}
		overloads = []*functions.Overload{
			{
				Operator:     f.Name(),
				Unary:        f.singleton.Unary,
				Binary:       f.singleton.Binary,
				Function:     f.singleton.Function,
				OperandTrait: f.singleton.OperandTrait,
			},
		}
		// fall-through to return single overload case.
	}
	if len(overloads) == 0 {
		return overloads, nil
	}
	// Single overload. Replicate an entry for it using the function name as well.
	if len(overloads) == 1 {
		if overloads[0].Operator == f.Name() {
			return overloads, nil
		}
		return append(overloads, &functions.Overload{
			Operator:     f.Name(),
			Unary:        overloads[0].Unary,
			Binary:       overloads[0].Binary,
			Function:     overloads[0].Function,
			NonStrict:    overloads[0].NonStrict,
			OperandTrait: overloads[0].OperandTrait,
		}), nil
	}
	// All of the defined overloads are wrapped into a top-level function which
	// performs dynamic dispatch to the proper overload based on the argument types.
	bindings := append([]*functions.Overload{}, overloads...)
	funcDispatch := func(args ...ref.Val) ref.Val {
		for _, oID := range f.overloadOrdinals {
			o := f.overloads[oID]
			// During dynamic dispatch over multiple functions, signature agreement checks
			// are preserved in order to assist with the function resolution step.
			switch len(args) {
			case 1:
				if o.unaryOp != nil && o.matchesRuntimeSignature(f.disableTypeGuards, args...) {
					return o.unaryOp(args[0])
				}
			case 2:
				if o.binaryOp != nil && o.matchesRuntimeSignature(f.disableTypeGuards, args...) {
					return o.binaryOp(args[0], args[1])
				}
			}
			if o.functionOp != nil && o.matchesRuntimeSignature(f.disableTypeGuards, args...) {
				return o.functionOp(args...)
			}
			// eventually this will fall through to the noSuchOverload below.
		}
		return MaybeNoSuchOverload(f.Name(), args...)
	}
	function := &functions.Overload{
		Operator:  f.Name(),
		Function:  funcDispatch,
		NonStrict: nonStrict,
	}
	return append(bindings, function), nil
}

// MaybeNoSuchOverload determines whether to propagate an error if one is provided as an argument, or
// to return an unknown set, or to produce a new error for a missing function signature.
func MaybeNoSuchOverload(funcName string, args ...ref.Val) ref.Val {
	argTypes := make([]string, len(args))
	var unk *types.Unknown = nil
	for i, arg := range args {
		if types.IsError(arg) {
			return arg
		}
		if types.IsUnknown(arg) {
			unk = types.MergeUnknowns(arg.(*types.Unknown), unk)
		}
		argTypes[i] = arg.Type().TypeName()
	}
	if unk != nil {
		return unk
	}
	signature := strings.Join(argTypes, ", ")
	return types.NewErr("no such overload: %s(%s)", funcName, signature)
}

// FunctionOpt defines a functional option for mutating a function declaration.
type FunctionOpt func(*FunctionDecl) (*FunctionDecl, error)

// FunctionDocs configures documentation from a list of strings separated by newlines.
func FunctionDocs(docs ...string) FunctionOpt {
	return func(fn *FunctionDecl) (*FunctionDecl, error) {
		fn.doc = common.MultilineDescription(docs...)
		return fn, nil
	}
}

// DisableTypeGuards disables automatically generated function invocation guards on direct overload calls.
// Type guards remain on during dynamic dispatch for parsed-only expressions.
func DisableTypeGuards(value bool) FunctionOpt {
	return func(fn *FunctionDecl) (*FunctionDecl, error) {
		fn.disableTypeGuards = value
		return fn, nil
	}
}

// DisableDeclaration indicates that the function declaration should be disabled, but the runtime function
// binding should be provided. Marking a function as runtime-only is a safe way to manage deprecations
// of function declarations while still preserving the runtime behavior for previously compiled expressions.
func DisableDeclaration(value bool) FunctionOpt {
	return func(fn *FunctionDecl) (*FunctionDecl, error) {
		if value {
			fn.state = declarationDisabled
		} else {
			fn.state = declarationEnabled
		}
		return fn, nil
	}
}

// SingletonUnaryBinding creates a singleton function definition to be used for all function overloads.
//
// Note, this approach works well if operand is expected to have a specific trait which it implements,
// e.g. traits.ContainerType. Otherwise, prefer per-overload function bindings.
func SingletonUnaryBinding(fn functions.UnaryOp, traits ...int) FunctionOpt {
	trait := 0
	for _, t := range traits {
		trait = trait | t
	}
	return func(f *FunctionDecl) (*FunctionDecl, error) {
		if f.singleton != nil {
			return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name())
		}
		f.singleton = &functions.Overload{
			Operator:     f.Name(),
			Unary:        fn,
			OperandTrait: trait,
		}
		return f, nil
	}
}

// SingletonBinaryBinding creates a singleton function definition to be used with all function overloads.
//
// Note, this approach works well if operand is expected to have a specific trait which it implements,
// e.g. traits.ContainerType. Otherwise, prefer per-overload function bindings.
func SingletonBinaryBinding(fn functions.BinaryOp, traits ...int) FunctionOpt {
	trait := 0
	for _, t := range traits {
		trait = trait | t
	}
	return func(f *FunctionDecl) (*FunctionDecl, error) {
		if f.singleton != nil {
			return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name())
		}
		f.singleton = &functions.Overload{
			Operator:     f.Name(),
			Binary:       fn,
			OperandTrait: trait,
		}
		return f, nil
	}
}

// SingletonFunctionBinding creates a singleton function definition to be used with all function overloads.
//
// Note, this approach works well if operand is expected to have a specific trait which it implements,
// e.g. traits.ContainerType. Otherwise, prefer per-overload function bindings.
func SingletonFunctionBinding(fn functions.FunctionOp, traits ...int) FunctionOpt {
	trait := 0
	for _, t := range traits {
		trait = trait | t
	}
	return func(f *FunctionDecl) (*FunctionDecl, error) {
		if f.singleton != nil {
			return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name())
		}
		f.singleton = &functions.Overload{
			Operator:     f.Name(),
			Function:     fn,
			OperandTrait: trait,
		}
		return f, nil
	}
}

// Overload defines a new global overload with an overload id, argument types, and result type. Through the
// use of OverloadOpt options, the overload may also be configured with a binding, an operand trait, and to
// be non-strict.
//
// Note: function bindings should be commonly configured with Overload instances whereas operand traits and
// strict-ness should be rare occurrences.
func Overload(overloadID string,
	args []*types.Type, resultType *types.Type,
	opts ...OverloadOpt) FunctionOpt {
	return newOverload(overloadID, false, args, resultType, opts...)
}

// MemberOverload defines a new receiver-style overload (or member function) with an overload id, argument types,
// and result type. Through the use of OverloadOpt options, the overload may also be configured with a binding,
// an operand trait, and to be non-strict.
//
// Note: function bindings should be commonly configured with Overload instances whereas operand traits and
// strict-ness should be rare occurrences.
func MemberOverload(overloadID string,
	args []*types.Type, resultType *types.Type,
	opts ...OverloadOpt) FunctionOpt {
	return newOverload(overloadID, true, args, resultType, opts...)
}

func newOverload(overloadID string,
	memberFunction bool, args []*types.Type, resultType *types.Type,
	opts ...OverloadOpt) FunctionOpt {
	return func(f *FunctionDecl) (*FunctionDecl, error) {
		overload, err := newOverloadInternal(overloadID, memberFunction, args, resultType, opts...)
		if err != nil {
			return nil, err
		}
		err = f.AddOverload(overload)
		if err != nil {
			return nil, err
		}
		return f, nil
	}
}

func newOverloadInternal(overloadID string,
	memberFunction bool, args []*types.Type, resultType *types.Type,
	opts ...OverloadOpt) (*OverloadDecl, error) {
	overload := &OverloadDecl{
		id:               overloadID,
		argTypes:         args,
		resultType:       resultType,
		isMemberFunction: memberFunction,
	}
	var err error
	for _, opt := range opts {
		overload, err = opt(overload)
		if err != nil {
			return nil, err
		}
	}
	return overload, nil
}

// OverloadDecl contains the definition of a single overload id with a specific signature, and an optional
// implementation.
type OverloadDecl struct {
	id               string
	doc              string
	argTypes         []*types.Type
	resultType       *types.Type
	isMemberFunction bool
	// hasLateBinding indicates that the function has a binding which is not known at compile time.
	// This is useful for functions which have side-effects or are not deterministically computable.
	hasLateBinding bool
	// nonStrict indicates that the function will accept error and unknown arguments as inputs.
	nonStrict bool
	// operandTrait indicates whether the member argument should have a specific type-trait.
	//
	// This is useful for creating overloads which operate on a type-interface rather than a concrete type.
	operandTrait int

	// Function implementation options. Optional, but encouraged.
	// unaryOp is a function binding that takes a single argument.
	unaryOp functions.UnaryOp
	// binaryOp is a function binding that takes two arguments.
	binaryOp functions.BinaryOp
	// functionOp is a catch-all for zero-arity and three-plus arity functions.
	functionOp functions.FunctionOp
}

// Examples returns a list of string examples for the overload.
func (o *OverloadDecl) Examples() []string {
	var emptySet []string
	if o == nil || len(o.doc) == 0 {
		return emptySet
	}
	return common.ParseDescriptions(o.doc)
}

// ID mirrors the overload signature and provides a unique id which may be referenced within the type-checker
// and interpreter to optimize performance.
//
// The ID format is usually one of two styles:
// global: <functionName>_<argType>_<argTypeN>
// member: <memberType>_<functionName>_<argType>_<argTypeN>
func (o *OverloadDecl) ID() string {
	if o == nil {
		return ""
	}
	return o.id
}

// ArgTypes contains the set of argument types expected by the overload.
//
// For member functions ArgTypes[0] represents the member operand type.
func (o *OverloadDecl) ArgTypes() []*types.Type {
	if o == nil {
		return emptyArgs
	}
	return o.argTypes
}

// IsMemberFunction indicates whether the overload is a member function
func (o *OverloadDecl) IsMemberFunction() bool {
	if o == nil {
		return false
	}
	return o.isMemberFunction
}

// IsNonStrict returns whether the overload accepts errors and unknown values as arguments.
func (o *OverloadDecl) IsNonStrict() bool {
	if o == nil {
		return false
	}
	return o.nonStrict
}

// HasLateBinding returns whether the overload has a binding which is not known at compile time.
func (o *OverloadDecl) HasLateBinding() bool {
	if o == nil {
		return false
	}
	return o.hasLateBinding
}

// OperandTrait returns the trait mask of the first operand to the overload call, e.g.
// `traits.Indexer`
func (o *OverloadDecl) OperandTrait() int {
	if o == nil {
		return 0
	}
	return o.operandTrait
}

// ResultType indicates the output type from calling the function.
func (o *OverloadDecl) ResultType() *types.Type {
	if o == nil {
		// *types.Type is nil-safe
		return nil
	}
	return o.resultType
}

// TypeParams returns the type parameter names associated with the overload.
func (o *OverloadDecl) TypeParams() []string {
	typeParams := map[string]struct{}{}
	collectParamNames(typeParams, o.ResultType())
	for _, arg := range o.ArgTypes() {
		collectParamNames(typeParams, arg)
	}
	params := make([]string, 0, len(typeParams))
	for param := range typeParams {
		params = append(params, param)
	}
	return params
}

// SignatureEquals determines whether the incoming overload declaration signature is equal to the current signature.
//
// Result type, operand trait, and strict-ness are not considered as part of signature equality.
func (o *OverloadDecl) SignatureEquals(other *OverloadDecl) bool {
	if o == other {
		return true
	}
	if o.ID() != other.ID() || o.IsMemberFunction() != other.IsMemberFunction() || len(o.ArgTypes()) != len(other.ArgTypes()) {
		return false
	}
	for i, at := range o.ArgTypes() {
		oat := other.ArgTypes()[i]
		if !at.IsEquivalentType(oat) {
			return false
		}
	}
	return o.ResultType().IsEquivalentType(other.ResultType())
}

// SignatureOverlaps indicates whether two functions have non-equal, but overloapping function signatures.
//
// For example, list(dyn) collides with list(string) since the 'dyn' type can contain a 'string' type.
func (o *OverloadDecl) SignatureOverlaps(other *OverloadDecl) bool {
	if o.IsMemberFunction() != other.IsMemberFunction() || len(o.ArgTypes()) != len(other.ArgTypes()) {
		return false
	}
	argsOverlap := true
	for i, argType := range o.ArgTypes() {
		otherArgType := other.ArgTypes()[i]
		argsOverlap = argsOverlap &&
			(argType.IsAssignableType(otherArgType) ||
				otherArgType.IsAssignableType(argType))
	}
	return argsOverlap
}

// HasBinding indicates whether the overload already has a definition.
func (o *OverloadDecl) HasBinding() bool {
	return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil)
}

// guardedUnaryOp creates an invocation guard around the provided unary operator, if one is defined.
func (o *OverloadDecl) guardedUnaryOp(funcName string, disableTypeGuards bool) functions.UnaryOp {
	if o.unaryOp == nil {
		return nil
	}
	return func(arg ref.Val) ref.Val {
		if !o.matchesRuntimeUnarySignature(disableTypeGuards, arg) {
			return MaybeNoSuchOverload(funcName, arg)
		}
		return o.unaryOp(arg)
	}
}

// guardedBinaryOp creates an invocation guard around the provided binary operator, if one is defined.
func (o *OverloadDecl) guardedBinaryOp(funcName string, disableTypeGuards bool) functions.BinaryOp {
	if o.binaryOp == nil {
		return nil
	}
	return func(arg1, arg2 ref.Val) ref.Val {
		if !o.matchesRuntimeBinarySignature(disableTypeGuards, arg1, arg2) {
			return MaybeNoSuchOverload(funcName, arg1, arg2)
		}
		return o.binaryOp(arg1, arg2)
	}
}

// guardedFunctionOp creates an invocation guard around the provided variadic function binding, if one is provided.
func (o *OverloadDecl) guardedFunctionOp(funcName string, disableTypeGuards bool) functions.FunctionOp {
	if o.functionOp == nil {
		return nil
	}
	return func(args ...ref.Val) ref.Val {
		if !o.matchesRuntimeSignature(disableTypeGuards, args...) {
			return MaybeNoSuchOverload(funcName, args...)
		}
		return o.functionOp(args...)
	}
}

// matchesRuntimeUnarySignature indicates whether the argument type is runtime assiganble to the overload's expected argument.
func (o *OverloadDecl) matchesRuntimeUnarySignature(disableTypeGuards bool, arg ref.Val) bool {
	return matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[0], arg) &&
		matchOperandTrait(o.OperandTrait(), arg)
}

// matchesRuntimeBinarySignature indicates whether the argument types are runtime assiganble to the overload's expected arguments.
func (o *OverloadDecl) matchesRuntimeBinarySignature(disableTypeGuards bool, arg1, arg2 ref.Val) bool {
	return matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[0], arg1) &&
		matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[1], arg2) &&
		matchOperandTrait(o.OperandTrait(), arg1)
}

// matchesRuntimeSignature indicates whether the argument types are runtime assiganble to the overload's expected arguments.
func (o *OverloadDecl) matchesRuntimeSignature(disableTypeGuards bool, args ...ref.Val) bool {
	if len(args) != len(o.ArgTypes()) {
		return false
	}
	if len(args) == 0 {
		return true
	}
	for i, arg := range args {
		if !matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[i], arg) {
			return false
		}
	}
	return matchOperandTrait(o.OperandTrait(), args[0])
}

func matchRuntimeArgType(nonStrict, disableTypeGuards bool, argType *types.Type, arg ref.Val) bool {
	if nonStrict && (disableTypeGuards || types.IsUnknownOrError(arg)) {
		return true
	}
	if types.IsUnknownOrError(arg) {
		return false
	}
	return disableTypeGuards || argType.IsAssignableRuntimeType(arg)
}

func matchOperandTrait(trait int, arg ref.Val) bool {
	return trait == 0 || arg.Type().HasTrait(trait) || types.IsUnknownOrError(arg)
}

// OverloadOpt is a functional option for configuring a function overload.
type OverloadOpt func(*OverloadDecl) (*OverloadDecl, error)

// OverloadExamples configures example expressions for the overload.
func OverloadExamples(examples ...string) OverloadOpt {
	return func(o *OverloadDecl) (*OverloadDecl, error) {
		o.doc = common.MultilineDescription(examples...)
		return o, nil
	}
}

// UnaryBinding provides the implementation of a unary overload. The provided function is protected by a runtime
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
func UnaryBinding(binding functions.UnaryOp) OverloadOpt {
	return func(o *OverloadDecl) (*OverloadDecl, error) {
		if o.HasBinding() {
			return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
		}
		if len(o.ArgTypes()) != 1 {
			return nil, fmt.Errorf("unary function bound to non-unary overload: %s", o.ID())
		}
		if o.hasLateBinding {
			return nil, fmt.Errorf("overload already has a late binding: %s", o.ID())
		}
		o.unaryOp = binding
		return o, nil
	}
}

// BinaryBinding provides the implementation of a binary overload. The provided function is protected by a runtime
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
func BinaryBinding(binding functions.BinaryOp) OverloadOpt {
	return func(o *OverloadDecl) (*OverloadDecl, error) {
		if o.HasBinding() {
			return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
		}
		if len(o.ArgTypes()) != 2 {
			return nil, fmt.Errorf("binary function bound to non-binary overload: %s", o.ID())
		}
		if o.hasLateBinding {
			return nil, fmt.Errorf("overload already has a late binding: %s", o.ID())
		}
		o.binaryOp = binding
		return o, nil
	}
}

// FunctionBinding provides the implementation of a variadic overload. The provided function is protected by a runtime
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
func FunctionBinding(binding functions.FunctionOp) OverloadOpt {
	return func(o *OverloadDecl) (*OverloadDecl, error) {
		if o.HasBinding() {
			return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
		}
		if o.hasLateBinding {
			return nil, fmt.Errorf("overload already has a late binding: %s", o.ID())
		}
		o.functionOp = binding
		return o, nil
	}
}

// LateFunctionBinding indicates that the function has a binding which is not known at compile time.
// This is useful for functions which have side-effects or are not deterministically computable.
func LateFunctionBinding() OverloadOpt {
	return func(o *OverloadDecl) (*OverloadDecl, error) {
		if o.HasBinding() {
			return nil, fmt.Errorf("overload already has a binding: %s", o.ID())
		}
		o.hasLateBinding = true
		return o, nil
	}
}

// OverloadIsNonStrict enables the function to be called with error and unknown argument values.
//
// Note: do not use this option unless absoluately necessary as it should be an uncommon feature.
func OverloadIsNonStrict() OverloadOpt {
	return func(o *OverloadDecl) (*OverloadDecl, error) {
		o.nonStrict = true
		return o, nil
	}
}

// OverloadOperandTrait configures a set of traits which the first argument to the overload must implement in order to be
// successfully invoked.
func OverloadOperandTrait(trait int) OverloadOpt {
	return func(o *OverloadDecl) (*OverloadDecl, error) {
		o.operandTrait = trait
		return o, nil
	}
}

// NewConstant creates a new constant declaration.
func NewConstant(name string, t *types.Type, v ref.Val) *VariableDecl {
	return &VariableDecl{name: name, varType: t, value: v}
}

// NewVariable creates a new variable declaration.
func NewVariable(name string, t *types.Type) *VariableDecl {
	return &VariableDecl{name: name, varType: t}
}

// NewVariableWithDoc creates a new variable declaration with usage documentation.
func NewVariableWithDoc(name string, t *types.Type, doc string) *VariableDecl {
	return &VariableDecl{name: name, varType: t, doc: doc}
}

// VariableDecl defines a variable declaration which may optionally have a constant value.
type VariableDecl struct {
	name    string
	doc     string
	varType *types.Type
	value   ref.Val
}

// Documentation returns name, type, and description for the variable.
func (v *VariableDecl) Documentation() *common.Doc {
	if v == nil {
		return nil
	}
	return common.NewVariableDoc(v.Name(), describeCELType(v.Type()), v.Description())
}

// Name returns the fully-qualified variable name
func (v *VariableDecl) Name() string {
	if v == nil {
		return ""
	}
	return v.name
}

// Description returns the usage documentation for the variable, if set.
//
// Good usage instructions provide information about the valid formats, ranges, sizes for the variable type.
func (v *VariableDecl) Description() string {
	if v == nil {
		return ""
	}
	return v.doc
}

// Type returns the types.Type value associated with the variable.
func (v *VariableDecl) Type() *types.Type {
	if v == nil {
		// types.Type is nil-safe
		return nil
	}
	return v.varType
}

// Value returns the constant value associated with the declaration.
func (v *VariableDecl) Value() ref.Val {
	if v == nil {
		return nil
	}
	return v.value
}

// DeclarationIsEquivalent returns true if one variable declaration has the same name and same type as the input.
func (v *VariableDecl) DeclarationIsEquivalent(other *VariableDecl) bool {
	if v == other {
		return true
	}
	return v.Name() == other.Name() && v.Type().IsEquivalentType(other.Type())
}

// TypeVariable creates a new type identifier for use within a types.Provider
func TypeVariable(t *types.Type) *VariableDecl {
	return NewVariable(t.TypeName(), types.NewTypeTypeWithParam(t))
}

// VariableDeclToExprDecl converts a go-native variable declaration into a protobuf-type variable declaration.
func VariableDeclToExprDecl(v *VariableDecl) (*exprpb.Decl, error) {
	return variableDeclToExprDecl(v)
}

// variableDeclToExprDecl converts a go-native variable declaration into a protobuf-type variable declaration.
func variableDeclToExprDecl(v *VariableDecl) (*exprpb.Decl, error) {
	varType, err := types.TypeToExprType(v.Type())
	if err != nil {
		return nil, err
	}
	return chkdecls.NewVarWithDoc(v.Name(), varType, v.doc), nil
}

// FunctionDeclToExprDecl converts a go-native function declaration into a protobuf-typed function declaration.
func FunctionDeclToExprDecl(f *FunctionDecl) (*exprpb.Decl, error) {
	return functionDeclToExprDecl(f)
}

// functionDeclToExprDecl converts a go-native function declaration into a protobuf-typed function declaration.
func functionDeclToExprDecl(f *FunctionDecl) (*exprpb.Decl, error) {
	overloads := make([]*exprpb.Decl_FunctionDecl_Overload, len(f.overloads))
	for i, oID := range f.overloadOrdinals {
		o := f.overloads[oID]
		paramNames := map[string]struct{}{}
		argTypes := make([]*exprpb.Type, len(o.ArgTypes()))
		for j, a := range o.ArgTypes() {
			collectParamNames(paramNames, a)
			at, err := types.TypeToExprType(a)
			if err != nil {
				return nil, err
			}
			argTypes[j] = at
		}
		collectParamNames(paramNames, o.ResultType())
		resultType, err := types.TypeToExprType(o.ResultType())
		if err != nil {
			return nil, err
		}
		if len(paramNames) == 0 {
			if o.IsMemberFunction() {
				overloads[i] = chkdecls.NewInstanceOverload(oID, argTypes, resultType)
			} else {
				overloads[i] = chkdecls.NewOverload(oID, argTypes, resultType)
			}
		} else {
			params := []string{}
			for pn := range paramNames {
				params = append(params, pn)
			}
			if o.IsMemberFunction() {
				overloads[i] = chkdecls.NewParameterizedInstanceOverload(oID, argTypes, resultType, params)
			} else {
				overloads[i] = chkdecls.NewParameterizedOverload(oID, argTypes, resultType, params)
			}
		}
		doc := common.MultilineDescription(o.Examples()...)
		overloads[i].Doc = doc
	}
	return chkdecls.NewFunctionWithDoc(f.Name(), f.Description(), overloads...), nil
}

func collectParamNames(paramNames map[string]struct{}, arg *types.Type) {
	if arg.Kind() == types.TypeParamKind {
		paramNames[arg.TypeName()] = struct{}{}
	}
	for _, param := range arg.Parameters() {
		collectParamNames(paramNames, param)
	}
}

func formatSignature(fnName string, o *OverloadDecl) string {
	if opName, isOperator := operators.FindReverse(fnName); isOperator {
		if opName == "" {
			opName = fnName
		}
		return formatOperator(opName, o)
	}
	return formatCall(fnName, o)
}

func formatOperator(opName string, o *OverloadDecl) string {
	args := o.ArgTypes()
	argTypes := make([]string, len(o.ArgTypes()))
	for j, a := range args {
		argTypes[j] = describeCELType(a)
	}
	ret := describeCELType(o.ResultType())
	switch len(args) {
	case 1:
		return fmt.Sprintf("%s%s -> %s", opName, argTypes[0], ret)
	case 2:
		if opName == operators.Index {
			return fmt.Sprintf("%s[%s] -> %s", argTypes[0], argTypes[1], ret)
		}
		return fmt.Sprintf("%s %s %s -> %s", argTypes[0], opName, argTypes[1], ret)
	default:
		if opName == operators.Conditional {
			return fmt.Sprint("bool ? <T> : <T> -> <T>")
		}
		return formatCall(opName, o)
	}
}

func formatCall(funcName string, o *OverloadDecl) string {
	args := make([]string, len(o.ArgTypes()))
	ret := describeCELType(o.ResultType())
	for j, a := range o.ArgTypes() {
		args[j] = describeCELType(a)
	}
	if o.IsMemberFunction() {
		target := args[0]
		args = args[1:]
		return fmt.Sprintf("%s.%s(%s) -> %s", target, funcName, strings.Join(args, ", "), ret)
	}
	return fmt.Sprintf("%s(%s) -> %s", funcName, strings.Join(args, ", "), ret)
}

func describeCELType(t *types.Type) string {
	if t.Kind() == types.TypeKind {
		return "type"
	}
	return t.String()
}

var (
	emptyArgs []*types.Type
)
