// 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 ast

import "github.com/google/cel-go/common/types/ref"

// ExprFactory interfaces defines a set of methods necessary for building native expression values.
type ExprFactory interface {
	// CopyExpr creates a deep copy of the input Expr value.
	CopyExpr(Expr) Expr

	// CopyEntryExpr creates a deep copy of the input EntryExpr value.
	CopyEntryExpr(EntryExpr) EntryExpr

	// NewCall creates an Expr value representing a global function call.
	NewCall(id int64, function string, args ...Expr) Expr

	// NewComprehension creates an Expr value representing a one-variable comprehension over a value range.
	NewComprehension(id int64, iterRange Expr, iterVar, accuVar string, accuInit, loopCondition, loopStep, result Expr) Expr

	// NewComprehensionTwoVar creates an Expr value representing a two-variable comprehension over a value range.
	NewComprehensionTwoVar(id int64, iterRange Expr, iterVar, iterVar2, accuVar string, accuInit, loopCondition, loopStep, result Expr) Expr

	// NewMemberCall creates an Expr value representing a member function call.
	NewMemberCall(id int64, function string, receiver Expr, args ...Expr) Expr

	// NewIdent creates an Expr value representing an identifier.
	NewIdent(id int64, name string) Expr

	// NewAccuIdent creates an Expr value representing an accumulator identifier within a
	// comprehension.
	NewAccuIdent(id int64) Expr

	// AccuIdentName reports the name of the accumulator variable to be used within a comprehension.
	AccuIdentName() string

	// NewLiteral creates an Expr value representing a literal value, such as a string or integer.
	NewLiteral(id int64, value ref.Val) Expr

	// NewList creates an Expr value representing a list literal expression with optional indices.
	//
	// Optional indices will typically be empty unless the CEL optional types are enabled.
	NewList(id int64, elems []Expr, optIndices []int32) Expr

	// NewMap creates an Expr value representing a map literal expression
	NewMap(id int64, entries []EntryExpr) Expr

	// NewMapEntry creates a MapEntry with a given key, value, and a flag indicating whether
	// the key is optionally set.
	NewMapEntry(id int64, key, value Expr, isOptional bool) EntryExpr

	// NewPresenceTest creates an Expr representing a field presence test on an operand expression.
	NewPresenceTest(id int64, operand Expr, field string) Expr

	// NewSelect creates an Expr representing a field selection on an operand expression.
	NewSelect(id int64, operand Expr, field string) Expr

	// NewStruct creates an Expr value representing a struct literal with a given type name and a
	// set of field initializers.
	NewStruct(id int64, typeName string, fields []EntryExpr) Expr

	// NewStructField creates a StructField with a given field name, value, and a flag indicating
	// whether the field is optionally set.
	NewStructField(id int64, field string, value Expr, isOptional bool) EntryExpr

	// NewUnspecifiedExpr creates an empty expression node.
	NewUnspecifiedExpr(id int64) Expr

	isExprFactory()
}

type baseExprFactory struct {
	accumulatorName string
}

// NewExprFactory creates an ExprFactory instance.
func NewExprFactory() ExprFactory {
	return &baseExprFactory{
		"@result",
	}
}

// NewExprFactoryWithAccumulator creates an ExprFactory instance with a custom
// accumulator identifier name.
func NewExprFactoryWithAccumulator(id string) ExprFactory {
	return &baseExprFactory{
		id,
	}
}

func (fac *baseExprFactory) NewCall(id int64, function string, args ...Expr) Expr {
	if len(args) == 0 {
		args = []Expr{}
	}
	return fac.newExpr(
		id,
		&baseCallExpr{
			function: function,
			target:   nilExpr,
			args:     args,
			isMember: false,
		})
}

func (fac *baseExprFactory) NewMemberCall(id int64, function string, target Expr, args ...Expr) Expr {
	if len(args) == 0 {
		args = []Expr{}
	}
	return fac.newExpr(
		id,
		&baseCallExpr{
			function: function,
			target:   target,
			args:     args,
			isMember: true,
		})
}

func (fac *baseExprFactory) NewComprehension(id int64, iterRange Expr, iterVar, accuVar string, accuInit, loopCond, loopStep, result Expr) Expr {
	// Set the iter_var2 to empty string to indicate the second variable is omitted
	return fac.NewComprehensionTwoVar(id, iterRange, iterVar, "", accuVar, accuInit, loopCond, loopStep, result)
}

func (fac *baseExprFactory) NewComprehensionTwoVar(id int64, iterRange Expr, iterVar, iterVar2, accuVar string, accuInit, loopCond, loopStep, result Expr) Expr {
	return fac.newExpr(
		id,
		&baseComprehensionExpr{
			iterRange: iterRange,
			iterVar:   iterVar,
			iterVar2:  iterVar2,
			accuVar:   accuVar,
			accuInit:  accuInit,
			loopCond:  loopCond,
			loopStep:  loopStep,
			result:    result,
		})
}

func (fac *baseExprFactory) NewIdent(id int64, name string) Expr {
	return fac.newExpr(id, baseIdentExpr(name))
}

func (fac *baseExprFactory) NewAccuIdent(id int64) Expr {
	return fac.NewIdent(id, fac.AccuIdentName())
}

func (fac *baseExprFactory) AccuIdentName() string {
	return fac.accumulatorName
}

func (fac *baseExprFactory) NewLiteral(id int64, value ref.Val) Expr {
	return fac.newExpr(id, &baseLiteral{Val: value})
}

func (fac *baseExprFactory) NewList(id int64, elems []Expr, optIndices []int32) Expr {
	optIndexMap := make(map[int32]struct{}, len(optIndices))
	for _, idx := range optIndices {
		optIndexMap[idx] = struct{}{}
	}
	return fac.newExpr(id,
		&baseListExpr{
			elements:    elems,
			optIndices:  optIndices,
			optIndexMap: optIndexMap,
		})
}

func (fac *baseExprFactory) NewMap(id int64, entries []EntryExpr) Expr {
	return fac.newExpr(id, &baseMapExpr{entries: entries})
}

func (fac *baseExprFactory) NewMapEntry(id int64, key, value Expr, isOptional bool) EntryExpr {
	return fac.newEntryExpr(
		id,
		&baseMapEntry{
			key:        key,
			value:      value,
			isOptional: isOptional,
		})
}

func (fac *baseExprFactory) NewPresenceTest(id int64, operand Expr, field string) Expr {
	return fac.newExpr(
		id,
		&baseSelectExpr{
			operand:  operand,
			field:    field,
			testOnly: true,
		})
}

func (fac *baseExprFactory) NewSelect(id int64, operand Expr, field string) Expr {
	return fac.newExpr(
		id,
		&baseSelectExpr{
			operand: operand,
			field:   field,
		})
}

func (fac *baseExprFactory) NewStruct(id int64, typeName string, fields []EntryExpr) Expr {
	return fac.newExpr(
		id,
		&baseStructExpr{
			typeName: typeName,
			fields:   fields,
		})
}

func (fac *baseExprFactory) NewStructField(id int64, field string, value Expr, isOptional bool) EntryExpr {
	return fac.newEntryExpr(
		id,
		&baseStructField{
			field:      field,
			value:      value,
			isOptional: isOptional,
		})
}

func (fac *baseExprFactory) NewUnspecifiedExpr(id int64) Expr {
	return fac.newExpr(id, nil)
}

func (fac *baseExprFactory) CopyExpr(e Expr) Expr {
	// unwrap navigable expressions to avoid unnecessary allocations during copying.
	if nav, ok := e.(*navigableExprImpl); ok {
		e = nav.Expr
	}
	switch e.Kind() {
	case CallKind:
		c := e.AsCall()
		argsCopy := make([]Expr, len(c.Args()))
		for i, arg := range c.Args() {
			argsCopy[i] = fac.CopyExpr(arg)
		}
		if !c.IsMemberFunction() {
			return fac.NewCall(e.ID(), c.FunctionName(), argsCopy...)
		}
		return fac.NewMemberCall(e.ID(), c.FunctionName(), fac.CopyExpr(c.Target()), argsCopy...)
	case ComprehensionKind:
		compre := e.AsComprehension()
		return fac.NewComprehensionTwoVar(e.ID(),
			fac.CopyExpr(compre.IterRange()),
			compre.IterVar(),
			compre.IterVar2(),
			compre.AccuVar(),
			fac.CopyExpr(compre.AccuInit()),
			fac.CopyExpr(compre.LoopCondition()),
			fac.CopyExpr(compre.LoopStep()),
			fac.CopyExpr(compre.Result()))
	case IdentKind:
		return fac.NewIdent(e.ID(), e.AsIdent())
	case ListKind:
		l := e.AsList()
		elemsCopy := make([]Expr, l.Size())
		for i, elem := range l.Elements() {
			elemsCopy[i] = fac.CopyExpr(elem)
		}
		return fac.NewList(e.ID(), elemsCopy, l.OptionalIndices())
	case LiteralKind:
		return fac.NewLiteral(e.ID(), e.AsLiteral())
	case MapKind:
		m := e.AsMap()
		entriesCopy := make([]EntryExpr, m.Size())
		for i, entry := range m.Entries() {
			entriesCopy[i] = fac.CopyEntryExpr(entry)
		}
		return fac.NewMap(e.ID(), entriesCopy)
	case SelectKind:
		s := e.AsSelect()
		if s.IsTestOnly() {
			return fac.NewPresenceTest(e.ID(), fac.CopyExpr(s.Operand()), s.FieldName())
		}
		return fac.NewSelect(e.ID(), fac.CopyExpr(s.Operand()), s.FieldName())
	case StructKind:
		s := e.AsStruct()
		fieldsCopy := make([]EntryExpr, len(s.Fields()))
		for i, field := range s.Fields() {
			fieldsCopy[i] = fac.CopyEntryExpr(field)
		}
		return fac.NewStruct(e.ID(), s.TypeName(), fieldsCopy)
	default:
		return fac.NewUnspecifiedExpr(e.ID())
	}
}

func (fac *baseExprFactory) CopyEntryExpr(e EntryExpr) EntryExpr {
	switch e.Kind() {
	case MapEntryKind:
		entry := e.AsMapEntry()
		return fac.NewMapEntry(e.ID(),
			fac.CopyExpr(entry.Key()), fac.CopyExpr(entry.Value()), entry.IsOptional())
	case StructFieldKind:
		field := e.AsStructField()
		return fac.NewStructField(e.ID(),
			field.Name(), fac.CopyExpr(field.Value()), field.IsOptional())
	default:
		return fac.newEntryExpr(e.ID(), nil)
	}
}

func (*baseExprFactory) isExprFactory() {}

func (fac *baseExprFactory) newExpr(id int64, e exprKindCase) Expr {
	return &expr{
		id:           id,
		exprKindCase: e,
	}
}

func (fac *baseExprFactory) newEntryExpr(id int64, e entryExprKindCase) EntryExpr {
	return &entryExpr{
		id:                id,
		entryExprKindCase: e,
	}
}

var (
	defaultFactory = &baseExprFactory{}
)
