package printer

import (
	"fmt"
	"strconv"
	"strings"

	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
)

type NodeFactory struct {
	ast.NodeFactory
	emitContext *EmitContext
}

func NewNodeFactory(context *EmitContext) *NodeFactory {
	return &NodeFactory{
		NodeFactory: *ast.NewNodeFactory(ast.NodeFactoryHooks{
			OnCreate: context.onCreate,
			OnUpdate: context.onUpdate,
			OnClone:  context.onClone,
		}),
		emitContext: context,
	}
}

func (f *NodeFactory) newGeneratedIdentifier(kind GeneratedIdentifierFlags, text string, node *ast.Node, options AutoGenerateOptions) *ast.IdentifierNode {
	id := AutoGenerateId(nextAutoGenerateId.Add(1))

	if len(text) == 0 {
		switch {
		case node == nil:
			text = fmt.Sprintf("(auto@%d)", id)
		case ast.IsMemberName(node):
			text = node.Text()
		default:
			text = fmt.Sprintf("(generated@%v)", ast.GetNodeId(f.emitContext.getNodeForGeneratedNameWorker(node, id)))
		}
		text = FormatGeneratedName(false /*privateName*/, options.Prefix, text, options.Suffix)
	}

	name := f.NewIdentifier(text)
	autoGenerate := &AutoGenerateInfo{
		Id:     id,
		Flags:  kind | (options.Flags & ^GeneratedIdentifierFlagsKindMask),
		Prefix: options.Prefix,
		Suffix: options.Suffix,
		Node:   node,
	}
	if f.emitContext.autoGenerate == nil {
		f.emitContext.autoGenerate = make(map[*ast.MemberName]*AutoGenerateInfo)
	}
	f.emitContext.autoGenerate[name] = autoGenerate
	return name
}

// Allocates a new temp variable name, but does not record it in the environment. It is recommended to pass this to either
// `AddVariableDeclaration` or `AddLexicalDeclaration` to ensure it is properly tracked, if you are not otherwise handling
// it yourself.
func (f *NodeFactory) NewTempVariable() *ast.IdentifierNode {
	return f.NewTempVariableEx(AutoGenerateOptions{})
}

// Allocates a new temp variable name, but does not record it in the environment. It is recommended to pass this to either
// `AddVariableDeclaration` or `AddLexicalDeclaration` to ensure it is properly tracked, if you are not otherwise handling
// it yourself.
func (f *NodeFactory) NewTempVariableEx(options AutoGenerateOptions) *ast.IdentifierNode {
	return f.newGeneratedIdentifier(GeneratedIdentifierFlagsAuto, "", nil /*node*/, options)
}

// Allocates a new loop variable name.
func (f *NodeFactory) NewLoopVariable() *ast.IdentifierNode {
	return f.NewLoopVariableEx(AutoGenerateOptions{})
}

// Allocates a new loop variable name.
func (f *NodeFactory) NewLoopVariableEx(options AutoGenerateOptions) *ast.IdentifierNode {
	return f.newGeneratedIdentifier(GeneratedIdentifierFlagsLoop, "", nil /*node*/, options)
}

// Allocates a new unique name based on the provided text.
func (f *NodeFactory) NewUniqueName(text string) *ast.IdentifierNode {
	return f.NewUniqueNameEx(text, AutoGenerateOptions{})
}

// Allocates a new unique name based on the provided text.
func (f *NodeFactory) NewUniqueNameEx(text string, options AutoGenerateOptions) *ast.IdentifierNode {
	return f.newGeneratedIdentifier(GeneratedIdentifierFlagsUnique, text, nil /*node*/, options)
}

// Allocates a new unique name based on the provided node.
func (f *NodeFactory) NewGeneratedNameForNode(node *ast.Node) *ast.IdentifierNode {
	return f.NewGeneratedNameForNodeEx(node, AutoGenerateOptions{})
}

// Allocates a new unique name based on the provided node.
func (f *NodeFactory) NewGeneratedNameForNodeEx(node *ast.Node, options AutoGenerateOptions) *ast.IdentifierNode {
	if len(options.Prefix) > 0 || len(options.Suffix) > 0 {
		options.Flags |= GeneratedIdentifierFlagsOptimistic
	}

	return f.newGeneratedIdentifier(GeneratedIdentifierFlagsNode, "", node, options)
}

func (f *NodeFactory) newGeneratedPrivateIdentifier(kind GeneratedIdentifierFlags, text string, node *ast.Node, options AutoGenerateOptions) *ast.PrivateIdentifierNode {
	id := AutoGenerateId(nextAutoGenerateId.Add(1))

	if len(text) == 0 {
		switch {
		case node == nil:
			text = fmt.Sprintf("(auto@%d)", id)
		case ast.IsMemberName(node):
			text = node.Text()
		default:
			text = fmt.Sprintf("(generated@%v)", ast.GetNodeId(f.emitContext.getNodeForGeneratedNameWorker(node, id)))
		}
		text = FormatGeneratedName(true /*privateName*/, options.Prefix, text, options.Suffix)
	} else if !strings.HasPrefix(text, "#") {
		panic("First character of private identifier must be #: " + text)
	}

	name := f.NewPrivateIdentifier(text)
	autoGenerate := &AutoGenerateInfo{
		Id:     id,
		Flags:  kind | (options.Flags &^ GeneratedIdentifierFlagsKindMask),
		Prefix: options.Prefix,
		Suffix: options.Suffix,
		Node:   node,
	}
	if f.emitContext.autoGenerate == nil {
		f.emitContext.autoGenerate = make(map[*ast.MemberName]*AutoGenerateInfo)
	}
	f.emitContext.autoGenerate[name] = autoGenerate
	return name
}

// Allocates a new unique private name based on the provided text.
func (f *NodeFactory) NewUniquePrivateName(text string) *ast.PrivateIdentifierNode {
	return f.NewUniquePrivateNameEx(text, AutoGenerateOptions{})
}

// Allocates a new unique private name based on the provided text.
func (f *NodeFactory) NewUniquePrivateNameEx(text string, options AutoGenerateOptions) *ast.PrivateIdentifierNode {
	return f.newGeneratedPrivateIdentifier(GeneratedIdentifierFlagsUnique, text, nil /*node*/, options)
}

// Allocates a new unique private name based on the provided node.
func (f *NodeFactory) NewGeneratedPrivateNameForNode(node *ast.Node) *ast.PrivateIdentifierNode {
	return f.NewGeneratedPrivateNameForNodeEx(node, AutoGenerateOptions{})
}

// Allocates a new unique private name based on the provided node.
func (f *NodeFactory) NewGeneratedPrivateNameForNodeEx(node *ast.Node, options AutoGenerateOptions) *ast.PrivateIdentifierNode {
	if len(options.Prefix) > 0 || len(options.Suffix) > 0 {
		options.Flags |= GeneratedIdentifierFlagsOptimistic
	}

	return f.newGeneratedPrivateIdentifier(GeneratedIdentifierFlagsNode, "", node, options)
}

// Allocates a new StringLiteral whose source text is derived from the provided node. This is often used to create a
// string representation of an Identifier or NumericLiteral.
func (f *NodeFactory) NewStringLiteralFromNode(textSourceNode *ast.Node) *ast.Node {
	var text string
	switch textSourceNode.Kind {
	case ast.KindIdentifier,
		ast.KindPrivateIdentifier,
		ast.KindJsxNamespacedName,
		ast.KindNumericLiteral,
		ast.KindBigIntLiteral,
		ast.KindNoSubstitutionTemplateLiteral,
		ast.KindTemplateHead,
		ast.KindTemplateMiddle,
		ast.KindTemplateTail,
		ast.KindRegularExpressionLiteral:
		text = textSourceNode.Text()
	}
	node := f.NewStringLiteral(text)
	if f.emitContext.textSource == nil {
		f.emitContext.textSource = make(map[*ast.StringLiteralNode]*ast.Node)
	}
	f.emitContext.textSource[node] = textSourceNode
	return node
}

//
// Common Tokens
//

func (f *NodeFactory) NewThisExpression() *ast.Expression {
	return f.NewKeywordExpression(ast.KindThisKeyword)
}

func (f *NodeFactory) NewTrueExpression() *ast.Expression {
	return f.NewKeywordExpression(ast.KindTrueKeyword)
}

func (f *NodeFactory) NewFalseExpression() *ast.Expression {
	return f.NewKeywordExpression(ast.KindFalseKeyword)
}

//
// Common Operators
//

func (f *NodeFactory) NewCommaExpression(left *ast.Expression, right *ast.Expression) *ast.Expression {
	return f.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, f.NewToken(ast.KindCommaToken), right)
}

func (f *NodeFactory) NewAssignmentExpression(left *ast.Expression, right *ast.Expression) *ast.Expression {
	return f.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, f.NewToken(ast.KindEqualsToken), right)
}

func (f *NodeFactory) NewLogicalORExpression(left *ast.Expression, right *ast.Expression) *ast.Expression {
	return f.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, f.NewToken(ast.KindBarBarToken), right)
}

// func (f *NodeFactory) NewLogicalANDExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
// func (f *NodeFactory) NewBitwiseORExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
// func (f *NodeFactory) NewBitwiseXORExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
// func (f *NodeFactory) NewBitwiseANDExpression(left *ast.Expression, right *ast.Expression) *ast.Expression
func (f *NodeFactory) NewStrictEqualityExpression(left *ast.Expression, right *ast.Expression) *ast.Expression {
	return f.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, f.NewToken(ast.KindEqualsEqualsEqualsToken), right)
}

func (f *NodeFactory) NewStrictInequalityExpression(left *ast.Expression, right *ast.Expression) *ast.Expression {
	return f.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, f.NewToken(ast.KindExclamationEqualsEqualsToken), right)
}

//
// Compound Nodes
//

func (f *NodeFactory) NewVoidZeroExpression() *ast.Expression {
	return f.NewVoidExpression(f.NewNumericLiteral("0"))
}

func flattenCommaElement(node *ast.Expression, expressions []*ast.Expression) []*ast.Expression {
	if ast.IsBinaryExpression(node) && ast.NodeIsSynthesized(node) && node.AsBinaryExpression().OperatorToken.Kind == ast.KindCommaToken {
		expressions = flattenCommaElement(node.AsBinaryExpression().Left, expressions)
		expressions = flattenCommaElement(node.AsBinaryExpression().Right, expressions)
	} else {
		expressions = append(expressions, node)
	}
	return expressions
}

func flattenCommaElements(expressions []*ast.Expression) []*ast.Expression {
	var result []*ast.Expression
	for _, expression := range expressions {
		result = flattenCommaElement(expression, result)
	}
	return result
}

// Converts a slice of expressions into a single comma-delimited expression. Returns nil if expressions is nil or empty.
// NOTE: Unlike Strada, the Corsa implementation does not currently use `ast.KindCommaListExpression`.
func (f *NodeFactory) InlineExpressions(expressions []*ast.Expression) *ast.Expression {
	if len(expressions) == 0 {
		return nil
	}
	if len(expressions) == 1 {
		return expressions[0]
	}
	expressions = flattenCommaElements(expressions)
	expression := expressions[0]
	for _, next := range expressions[1:] {
		expression = f.NewCommaExpression(expression, next)
	}
	return expression
}

//
// Utilities
//

func (f *NodeFactory) NewTypeCheck(value *ast.Node, tag string) *ast.Node {
	if tag == "null" {
		return f.NewStrictEqualityExpression(value, f.NewKeywordExpression(ast.KindNullKeyword))
	} else if tag == "undefined" {
		return f.NewStrictEqualityExpression(value, f.NewVoidZeroExpression())
	} else {
		return f.NewStrictEqualityExpression(f.NewTypeOfExpression(value), f.NewStringLiteral(tag))
	}
}

func (f *NodeFactory) NewMethodCall(object *ast.Node, methodName *ast.Node, argumentsList []*ast.Node) *ast.Node {
	// Preserve the optionality of `object`.
	if ast.IsCallExpression(object) && (object.Flags&ast.NodeFlagsOptionalChain != 0) {
		return f.NewCallExpression(
			f.NewPropertyAccessExpression(object, nil, methodName, ast.NodeFlagsNone),
			nil,
			nil,
			f.NewNodeList(argumentsList),
			ast.NodeFlagsOptionalChain,
		)
	}
	return f.NewCallExpression(
		f.NewPropertyAccessExpression(object, nil, methodName, ast.NodeFlagsNone),
		nil,
		nil,
		f.NewNodeList(argumentsList),
		ast.NodeFlagsNone,
	)
}

func (f *NodeFactory) NewGlobalMethodCall(globalObjectName string, methodName string, argumentsList []*ast.Node) *ast.Node {
	return f.NewMethodCall(f.NewIdentifier(globalObjectName), f.NewIdentifier(methodName), argumentsList)
}

func (f *NodeFactory) NewFunctionCallCall(target *ast.Expression, thisArg *ast.Expression, argumentsList []*ast.Node) *ast.Node {
	if thisArg == nil {
		panic("Attempted to construct function call call without this argument expression")
	}
	args := append([]*ast.Expression{thisArg}, argumentsList...)
	return f.NewMethodCall(target, f.NewIdentifier("call"), args)
}

func (f *NodeFactory) NewArraySliceCall(array *ast.Expression, start int) *ast.Node {
	var args []*ast.Node
	if start != 0 {
		args = append(args, f.NewNumericLiteral(strconv.Itoa(start)))
	}
	return f.NewMethodCall(array, f.NewIdentifier("slice"), args)
}

// Determines whether a node is a parenthesized expression that can be ignored when recreating outer expressions.
//
// A parenthesized expression can be ignored when all of the following are true:
//
// - It's `pos` and `end` are not -1
// - It does not have a custom source map range
// - It does not have a custom comment range
// - It does not have synthetic leading or trailing comments
//
// If an outermost parenthesized expression is ignored, but the containing expression requires a parentheses around
// the expression to maintain precedence, a new parenthesized expression should be created automatically when
// the containing expression is created/updated.
func (f *NodeFactory) isIgnorableParen(node *ast.Expression) bool {
	return ast.IsParenthesizedExpression(node) &&
		ast.NodeIsSynthesized(node) &&
		ast.RangeIsSynthesized(f.emitContext.SourceMapRange(node)) &&
		ast.RangeIsSynthesized(f.emitContext.CommentRange(node)) // &&
	// len(emitContext.SyntheticLeadingComments(node)) == 0 &&
	// len(emitContext.SyntheticTrailingComments(node)) == 0
}

func (f *NodeFactory) updateOuterExpression(outerExpression *ast.Expression /*OuterExpression*/, expression *ast.Expression) *ast.Expression {
	switch outerExpression.Kind {
	case ast.KindParenthesizedExpression:
		return f.UpdateParenthesizedExpression(outerExpression.AsParenthesizedExpression(), expression)
	case ast.KindTypeAssertionExpression:
		return f.UpdateTypeAssertion(outerExpression.AsTypeAssertion(), outerExpression.Type(), expression)
	case ast.KindAsExpression:
		return f.UpdateAsExpression(outerExpression.AsAsExpression(), expression, outerExpression.Type())
	case ast.KindSatisfiesExpression:
		return f.UpdateSatisfiesExpression(outerExpression.AsSatisfiesExpression(), expression, outerExpression.Type())
	case ast.KindNonNullExpression:
		return f.UpdateNonNullExpression(outerExpression.AsNonNullExpression(), expression)
	case ast.KindExpressionWithTypeArguments:
		return f.UpdateExpressionWithTypeArguments(outerExpression.AsExpressionWithTypeArguments(), expression, outerExpression.TypeArgumentList())
	case ast.KindPartiallyEmittedExpression:
		return f.UpdatePartiallyEmittedExpression(outerExpression.AsPartiallyEmittedExpression(), expression)
	default:
		panic(fmt.Sprintf("Unexpected outer expression kind: %s", outerExpression.Kind))
	}
}

func (f *NodeFactory) RestoreOuterExpressions(outerExpression *ast.Expression, innerExpression *ast.Expression, kinds ast.OuterExpressionKinds) *ast.Expression {
	if outerExpression != nil && ast.IsOuterExpression(outerExpression, kinds) && !f.isIgnorableParen(outerExpression) {
		return f.updateOuterExpression(
			outerExpression,
			f.RestoreOuterExpressions(outerExpression.Expression(), innerExpression, ast.OEKAll),
		)
	}
	return innerExpression
}

// Ensures `"use strict"` is the first statement of a slice of statements.
func (f *NodeFactory) EnsureUseStrict(statements []*ast.Statement) []*ast.Statement {
	for _, statement := range statements {
		if ast.IsPrologueDirective(statement) && statement.AsExpressionStatement().Expression.Text() == "use strict" {
			return statements
		} else {
			break
		}
	}
	useStrictPrologue := f.NewExpressionStatement(f.NewStringLiteral("use strict"))
	statements = append([]*ast.Statement{useStrictPrologue}, statements...)
	return statements
}

// Splits a slice of statements into two parts: standard prologue statements and the rest of the statements
func (f *NodeFactory) SplitStandardPrologue(source []*ast.Statement) (prologue []*ast.Statement, rest []*ast.Statement) {
	for i, statement := range source {
		if !ast.IsPrologueDirective(statement) {
			return source[:i], source[i:]
		}
	}
	return nil, source
}

// Splits a slice of statements into two parts: custom prologue statements (e.g., with `EFCustomPrologue` set) and the rest of the statements
func (f *NodeFactory) SplitCustomPrologue(source []*ast.Statement) (prologue []*ast.Statement, rest []*ast.Statement) {
	for i, statement := range source {
		if ast.IsPrologueDirective(statement) || f.emitContext.EmitFlags(statement)&EFCustomPrologue == 0 {
			return source[:i], source[i:]
		}
	}
	return nil, source
}

//
// Declaration Names
//

type NameOptions struct {
	AllowComments   bool // indicates whether comments may be emitted for the name.
	AllowSourceMaps bool // indicates whether source maps may be emitted for the name.
}

type AssignedNameOptions struct {
	AllowComments      bool // indicates whether comments may be emitted for the name.
	AllowSourceMaps    bool // indicates whether source maps may be emitted for the name.
	IgnoreAssignedName bool // indicates whether the assigned name of a declaration shouldn't be considered.
}

func (f *NodeFactory) getName(node *ast.Declaration, emitFlags EmitFlags, opts AssignedNameOptions) *ast.IdentifierNode {
	var nodeName *ast.IdentifierNode
	if node != nil {
		if opts.IgnoreAssignedName {
			nodeName = ast.GetNonAssignedNameOfDeclaration(node)
		} else {
			nodeName = ast.GetNameOfDeclaration(node)
		}
	}

	if nodeName != nil {
		name := nodeName.Clone(f)
		if !opts.AllowComments {
			emitFlags |= EFNoComments
		}
		if !opts.AllowSourceMaps {
			emitFlags |= EFNoSourceMap
		}
		f.emitContext.AddEmitFlags(name, emitFlags)
		return name
	}

	return f.NewGeneratedNameForNode(node)
}

// Gets the local name of a declaration. This is primarily used for declarations that can be referred to by name in the
// declaration's immediate scope (classes, enums, namespaces). A local name will *never* be prefixed with a module or
// namespace export modifier like "exports." when emitted as an expression.
func (f *NodeFactory) GetLocalName(node *ast.Declaration) *ast.IdentifierNode {
	return f.GetLocalNameEx(node, AssignedNameOptions{})
}

// Gets the local name of a declaration. This is primarily used for declarations that can be referred to by name in the
// declaration's immediate scope (classes, enums, namespaces). A local name will *never* be prefixed with a module or
// namespace export modifier like "exports." when emitted as an expression.
func (f *NodeFactory) GetLocalNameEx(node *ast.Declaration, opts AssignedNameOptions) *ast.IdentifierNode {
	return f.getName(node, EFLocalName, opts)
}

// Gets the export name of a declaration. This is primarily used for declarations that can be
// referred to by name in the declaration's immediate scope (classes, enums, namespaces). An
// export name will *always* be prefixed with an module or namespace export modifier like
// `"exports."` when emitted as an expression if the name points to an exported symbol.
func (f *NodeFactory) GetExportName(node *ast.Declaration) *ast.IdentifierNode {
	return f.GetExportNameEx(node, AssignedNameOptions{})
}

// Gets the export name of a declaration. This is primarily used for declarations that can be
// referred to by name in the declaration's immediate scope (classes, enums, namespaces). An
// export name will *always* be prefixed with an module or namespace export modifier like
// `"exports."` when emitted as an expression if the name points to an exported symbol.
func (f *NodeFactory) GetExportNameEx(node *ast.Declaration, opts AssignedNameOptions) *ast.IdentifierNode {
	return f.getName(node, EFExportName, opts)
}

// Gets the name of a declaration to use during emit.
func (f *NodeFactory) GetDeclarationName(node *ast.Declaration) *ast.IdentifierNode {
	return f.GetDeclarationNameEx(node, NameOptions{})
}

// Gets the name of a declaration to use during emit.
func (f *NodeFactory) GetDeclarationNameEx(node *ast.Declaration, opts NameOptions) *ast.IdentifierNode {
	return f.getName(node, EFNone, AssignedNameOptions{AllowComments: opts.AllowComments, AllowSourceMaps: opts.AllowSourceMaps})
}

func (f *NodeFactory) GetNamespaceMemberName(ns *ast.IdentifierNode, name *ast.IdentifierNode, opts NameOptions) *ast.IdentifierNode {
	if !f.emitContext.HasAutoGenerateInfo(name) {
		name = name.Clone(f)
	}
	qualifiedName := f.NewPropertyAccessExpression(ns, nil /*questionDotToken*/, name, ast.NodeFlagsNone)
	f.emitContext.AssignCommentAndSourceMapRanges(qualifiedName, name)
	if !opts.AllowComments {
		f.emitContext.AddEmitFlags(qualifiedName, EFNoComments)
	}
	if !opts.AllowSourceMaps {
		f.emitContext.AddEmitFlags(qualifiedName, EFNoSourceMap)
	}
	return qualifiedName
}

//
// Emit Helpers
//

// Allocates a new Identifier representing a reference to a helper function.
func (f *NodeFactory) NewUnscopedHelperName(name string) *ast.IdentifierNode {
	node := f.NewIdentifier(name)
	f.emitContext.SetEmitFlags(node, EFHelperName)
	return node
}

// !!! TypeScript Helpers

// ESNext Helpers

func (f *NodeFactory) NewAddDisposableResourceHelper(envBinding *ast.Expression, value *ast.Expression, async bool) *ast.Expression {
	f.emitContext.RequestEmitHelper(addDisposableResourceHelper)
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__addDisposableResource"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList([]*ast.Expression{envBinding, value, f.NewKeywordExpression(core.IfElse(async, ast.KindTrueKeyword, ast.KindFalseKeyword))}),
		ast.NodeFlagsNone,
	)
}

func (f *NodeFactory) NewDisposeResourcesHelper(envBinding *ast.Expression) *ast.Expression {
	f.emitContext.RequestEmitHelper(disposeResourcesHelper)
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__disposeResources"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList([]*ast.Expression{envBinding}),
		ast.NodeFlagsNone,
	)
}

// !!! Class Fields Helpers
// !!! ES2018 Helpers
// Chains a sequence of expressions using the __assign helper or Object.assign if available in the target
func (f *NodeFactory) NewAssignHelper(attributesSegments []*ast.Expression, scriptTarget core.ScriptTarget) *ast.Expression {
	if scriptTarget >= core.ScriptTargetES2015 {
		return f.NewCallExpression(f.NewPropertyAccessExpression(f.NewIdentifier("Object"), nil, f.NewIdentifier("assign"), ast.NodeFlagsNone), nil, nil, f.NewNodeList(attributesSegments), ast.NodeFlagsNone)
	}
	f.emitContext.RequestEmitHelper(assignHelper)
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__assign"),
		nil,
		nil,
		f.NewNodeList(attributesSegments),
		ast.NodeFlagsNone,
	)
}

// ES2018 Destructuring Helpers

func (f *NodeFactory) NewRestHelper(value *ast.Expression, elements []*ast.Node, computedTempVariables []*ast.Node, location core.TextRange) *ast.Expression {
	f.emitContext.RequestEmitHelper(restHelper)
	var propertyNames []*ast.Node
	computedTempVariableOffset := 0
	for i, element := range elements {
		if i == len(elements)-1 {
			break
		}
		propertyName := ast.TryGetPropertyNameOfBindingOrAssignmentElement(element)
		if propertyName != nil {
			if ast.IsComputedPropertyName(propertyName) {
				debug.AssertIsDefined(computedTempVariables, "Encountered computed property name but 'computedTempVariables' argument was not provided.")
				temp := computedTempVariables[computedTempVariableOffset]
				computedTempVariableOffset++
				// typeof _tmp === "symbol" ? _tmp : _tmp + ""
				propertyNames = append(propertyNames, f.NewConditionalExpression(
					f.NewTypeCheck(temp, "symbol"),
					f.NewToken(ast.KindQuestionToken),
					temp,
					f.NewToken(ast.KindColonToken),
					f.NewBinaryExpression(nil, temp, nil, f.NewToken(ast.KindPlusToken), f.NewStringLiteral("")),
				))
			} else {
				propertyNames = append(propertyNames, f.NewStringLiteralFromNode(propertyName))
			}
		}
	}
	propNames := f.NewArrayLiteralExpression(f.NewNodeList(propertyNames), false)
	propNames.Loc = location
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__rest"),
		nil,
		nil,
		f.NewNodeList([]*ast.Node{
			value,
			propNames,
		}),
		ast.NodeFlagsNone,
	)
}

// !!! ES2017 Helpers

// ES2015 Helpers

func (f *NodeFactory) NewPropKeyHelper(expr *ast.Expression) *ast.Expression {
	f.emitContext.RequestEmitHelper(propKeyHelper)
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__propKey"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList([]*ast.Expression{expr}),
		ast.NodeFlagsNone,
	)
}

func (f *NodeFactory) NewSetFunctionNameHelper(fn *ast.Expression, name *ast.Expression, prefix string) *ast.Expression {
	f.emitContext.RequestEmitHelper(setFunctionNameHelper)
	var arguments []*ast.Expression
	if len(prefix) > 0 {
		arguments = []*ast.Expression{fn, name, f.NewStringLiteral(prefix)}
	} else {
		arguments = []*ast.Expression{fn, name}
	}
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__setFunctionName"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList(arguments),
		ast.NodeFlagsNone,
	)
}

// ES Module Helpers

// Allocates a new Call expression to the `__importDefault` helper.
func (f *NodeFactory) NewImportDefaultHelper(expression *ast.Expression) *ast.Expression {
	f.emitContext.RequestEmitHelper(importDefaultHelper)
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__importDefault"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList([]*ast.Expression{expression}),
		ast.NodeFlagsNone,
	)
}

// Allocates a new Call expression to the `__importStar` helper.
func (f *NodeFactory) NewImportStarHelper(expression *ast.Expression) *ast.Expression {
	f.emitContext.RequestEmitHelper(importStarHelper)
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__importStar"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList([]*ast.Expression{expression}),
		ast.NodeFlagsNone,
	)
}

// Allocates a new Call expression to the `__exportStar` helper.
func (f *NodeFactory) NewExportStarHelper(moduleExpression *ast.Expression, exportsExpression *ast.Expression) *ast.Expression {
	f.emitContext.RequestEmitHelper(exportStarHelper)
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__exportStar"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList([]*ast.Expression{moduleExpression, exportsExpression}),
		ast.NodeFlagsNone,
	)
}

// Allocates a new Call expression to the `__rewriteRelativeImportExtension` helper.
func (f *NodeFactory) NewRewriteRelativeImportExtensionsHelper(firstArgument *ast.Node, preserveJsx bool) *ast.Expression {
	f.emitContext.RequestEmitHelper(rewriteRelativeImportExtensionsHelper)
	var arguments []*ast.Expression
	if preserveJsx {
		arguments = []*ast.Expression{firstArgument, f.NewToken(ast.KindTrueKeyword)}
	} else {
		arguments = []*ast.Expression{firstArgument}
	}
	return f.NewCallExpression(
		f.NewUnscopedHelperName("__rewriteRelativeImportExtension"),
		nil, /*questionDotToken*/
		nil, /*typeArguments*/
		f.NewNodeList(arguments),
		ast.NodeFlagsNone,
	)
}
