package jsxtransforms

import (
	"maps"
	"slices"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/printer"
	"github.com/microsoft/typescript-go/internal/scanner"
	"github.com/microsoft/typescript-go/internal/stringutil"
	"github.com/microsoft/typescript-go/internal/transformers"
)

type JSXTransformer struct {
	transformers.Transformer
	compilerOptions *core.CompilerOptions
	emitResolver    printer.EmitResolver

	importSpecifier                string
	filenameDeclaration            *ast.Node
	utilizedImplicitRuntimeImports collections.OrderedMap[string, map[string]*ast.Node]
	inJsxChild                     bool

	currentSourceFile *ast.SourceFile
}

func NewJSXTransformer(opts *transformers.TransformOptions) *transformers.Transformer {
	compilerOptions := opts.CompilerOptions
	emitContext := opts.Context
	tx := &JSXTransformer{
		compilerOptions: compilerOptions,
		emitResolver:    opts.EmitResolver,
	}
	return tx.NewTransformer(tx.visit, emitContext)
}

func (tx *JSXTransformer) getCurrentFileNameExpression() *ast.Node {
	if tx.filenameDeclaration != nil {
		return tx.filenameDeclaration.AsVariableDeclaration().Name()
	}
	d := tx.Factory().NewVariableDeclaration(
		tx.Factory().NewUniqueNameEx("_jsxFileName", printer.AutoGenerateOptions{
			Flags: printer.GeneratedIdentifierFlagsOptimistic | printer.GeneratedIdentifierFlagsFileLevel,
		}),
		nil,
		nil,
		tx.Factory().NewStringLiteral(tx.currentSourceFile.FileName(), ast.TokenFlagsNone),
	)
	tx.filenameDeclaration = d
	return d.AsVariableDeclaration().Name()
}

func (tx *JSXTransformer) getJsxFactoryCalleePrimitive(isStaticChildren bool) string {
	if tx.compilerOptions.Jsx == core.JsxEmitReactJSXDev {
		return "jsxDEV"
	}
	if isStaticChildren {
		return "jsxs"
	}
	return "jsx"
}

func (tx *JSXTransformer) getJsxFactoryCallee(isStaticChildren bool) *ast.Node {
	t := tx.getJsxFactoryCalleePrimitive(isStaticChildren)
	return tx.getImplicitImportForName(t)
}

func (tx *JSXTransformer) getImplicitJsxFragmentReference() *ast.Node {
	return tx.getImplicitImportForName("Fragment")
}

func (tx *JSXTransformer) getImplicitImportForName(name string) *ast.Node {
	importSource := tx.importSpecifier
	if name != "createElement" {
		importSource = ast.GetJSXRuntimeImport(importSource, tx.compilerOptions)
	}
	existing, ok := tx.utilizedImplicitRuntimeImports.Get(importSource)
	if ok {
		elem, ok := existing[name]
		if ok {
			return elem.AsImportSpecifier().Name()
		}
	} else {
		existing = make(map[string]*ast.Node)
		tx.utilizedImplicitRuntimeImports.Set(importSource, existing)
	}

	generatedName := tx.Factory().NewUniqueNameEx("_"+name, printer.AutoGenerateOptions{
		Flags: printer.GeneratedIdentifierFlagsOptimistic | printer.GeneratedIdentifierFlagsFileLevel | printer.GeneratedIdentifierFlagsAllowNameSubstitution,
	})
	specifier := tx.Factory().NewImportSpecifier(false, tx.Factory().NewIdentifier(name), generatedName)
	tx.emitResolver.SetReferencedImportDeclaration(generatedName, specifier)
	existing[name] = specifier
	return specifier.Name()
}

func (tx *JSXTransformer) setInChild(v bool) {
	tx.inJsxChild = v
}

func (tx *JSXTransformer) visit(node *ast.Node) *ast.Node {
	if node == nil {
		return nil
	}
	if node.SubtreeFacts()&ast.SubtreeContainsJsx == 0 {
		return node
	}
	switch node.Kind {
	case ast.KindSourceFile:
		tx.setInChild(false)
		return tx.visitSourceFile(node.AsSourceFile())
	case ast.KindJsxElement:
		return tx.visitJsxElement(node.AsJsxElement())
	case ast.KindJsxSelfClosingElement:
		return tx.visitJsxSelfClosingElement(node.AsJsxSelfClosingElement())
	case ast.KindJsxFragment:
		return tx.visitJsxFragment(node.AsJsxFragment())
	case ast.KindJsxOpeningElement:
		panic("JsxOpeningElement should not be visited, handled in visitJsxElement")
	case ast.KindJsxOpeningFragment:
		panic("JsxOpeningFragment should not be visited, handled in visitJsxFragment")
	case ast.KindJsxText:
		tx.setInChild(false)
		return tx.visitJsxText(node.AsJsxText())
	case ast.KindJsxExpression:
		tx.setInChild(false)
		return tx.visitJsxExpression(node.AsJsxExpression())
	}
	tx.setInChild(false)
	return tx.Visitor().VisitEachChild(node) // by default, do nothing
}

/**
 * The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread
 */
func hasKeyAfterPropsSpread(node *ast.Node) bool {
	spread := false
	opener := node
	if node.Kind == ast.KindJsxElement {
		opener = node.AsJsxElement().OpeningElement
	} // otherwise self-closing
	for _, elem := range opener.Attributes().Properties() {
		if ast.IsJsxSpreadAttribute(elem) && (!ast.IsObjectLiteralExpression(elem.Expression()) || core.Some(elem.Expression().Properties(), ast.IsSpreadAssignment)) {
			spread = true
		} else if spread && ast.IsJsxAttribute(elem) && ast.IsIdentifier(elem.Name()) && elem.Name().Text() == "key" {
			return true
		}
	}
	return false
}

func (tx *JSXTransformer) shouldUseCreateElement(node *ast.Node) bool {
	return len(tx.importSpecifier) == 0 || hasKeyAfterPropsSpread(node)
}

func insertStatementAfterPrologue[T any](to []*ast.Node, statement *ast.Node, isPrologueDirective func(callee T, node *ast.Node) bool, callee T) []*ast.Node {
	if statement == nil {
		return to
	}
	statementIdx := 0
	// skip all prologue directives to insert at the correct position
	for ; statementIdx < len(to); statementIdx++ {
		if !isPrologueDirective(callee, to[statementIdx]) {
			break
		}
	}
	return slices.Insert(to, statementIdx, statement)
}

func (tx *JSXTransformer) isAnyPrologueDirective(node *ast.Node) bool {
	return ast.IsPrologueDirective(node) || (tx.EmitContext().EmitFlags(node)&printer.EFCustomPrologue != 0)
}

func (tx *JSXTransformer) insertStatementAfterCustomPrologue(to []*ast.Node, statement *ast.Node) []*ast.Node {
	return insertStatementAfterPrologue(to, statement, (*JSXTransformer).isAnyPrologueDirective, tx)
}

func sortImportSpecifiers(a *ast.Node, b *ast.Node) int {
	res := stringutil.CompareStringsCaseSensitive(a.PropertyName().Text(), b.PropertyName().Text())
	if res != 0 {
		return res
	}
	return stringutil.CompareStringsCaseSensitive(a.AsImportSpecifier().Name().Text(), b.AsImportSpecifier().Name().Text())
}

func getSortedSpecifiers(m map[string]*ast.Node) []*ast.Node {
	res := slices.Collect(maps.Values(m))
	slices.SortFunc(res, sortImportSpecifiers)
	return res
}

func (tx *JSXTransformer) visitSourceFile(file *ast.SourceFile) *ast.Node {
	if file.IsDeclarationFile {
		return file.AsNode()
	}

	tx.currentSourceFile = file
	tx.importSpecifier = ast.GetJSXImplicitImportBase(tx.compilerOptions, file)
	tx.filenameDeclaration = nil
	tx.utilizedImplicitRuntimeImports.Clear()

	visited := tx.Visitor().VisitEachChild(file.AsNode())
	tx.EmitContext().AddEmitHelper(visited.AsNode(), tx.EmitContext().ReadEmitHelpers()...)
	statements := visited.Statements()
	statementsUpdated := false
	if tx.filenameDeclaration != nil {
		statements = tx.insertStatementAfterCustomPrologue(statements, tx.Factory().NewVariableStatement(nil, tx.Factory().NewVariableDeclarationList(
			tx.Factory().NewNodeList([]*ast.Node{tx.filenameDeclaration}),
			ast.NodeFlagsConst,
		)))
		statementsUpdated = true
	}

	if tx.utilizedImplicitRuntimeImports.Size() > 0 {
		if ast.IsExternalModule(file) {
			statementsUpdated = true
			newStatements := make([]*ast.Node, 0, tx.utilizedImplicitRuntimeImports.Size())
			for importSource, importSpecifiersMap := range tx.utilizedImplicitRuntimeImports.Entries() {
				s := tx.Factory().NewImportDeclaration(
					nil,
					tx.Factory().NewImportClause(ast.KindUnknown, nil, tx.Factory().NewNamedImports(tx.Factory().NewNodeList(getSortedSpecifiers(importSpecifiersMap)))),
					tx.Factory().NewStringLiteral(importSource, ast.TokenFlagsNone),
					nil,
				)
				ast.SetParentInChildren(s)
				newStatements = append(newStatements, s)

			}
			for _, e := range newStatements {
				statements = tx.insertStatementAfterCustomPrologue(statements, e)
			}
		} else if ast.IsExternalOrCommonJSModule(file) {
			statementsUpdated = true
			newStatements := make([]*ast.Node, 0, tx.utilizedImplicitRuntimeImports.Size())
			for importSource, importSpecifiersMap := range tx.utilizedImplicitRuntimeImports.Entries() {
				sorted := getSortedSpecifiers(importSpecifiersMap)
				asBindingElems := make([]*ast.Node, 0, len(sorted))
				for _, elem := range sorted {
					asBindingElems = append(asBindingElems, tx.Factory().NewBindingElement(nil, elem.PropertyName(), elem.AsImportSpecifier().Name(), nil))
				}
				s := tx.Factory().NewVariableStatement(nil, tx.Factory().NewVariableDeclarationList(tx.Factory().NewNodeList([]*ast.Node{tx.Factory().NewVariableDeclaration(
					tx.Factory().NewBindingPattern(ast.KindObjectBindingPattern, tx.Factory().NewNodeList(asBindingElems)),
					nil,
					nil,
					tx.Factory().NewCallExpression(
						tx.Factory().NewIdentifier("require"),
						nil,
						nil,
						tx.Factory().NewNodeList([]*ast.Node{tx.Factory().NewStringLiteral(importSource, ast.TokenFlagsNone)}), ast.NodeFlagsNone,
					),
				)}), ast.NodeFlagsConst))
				ast.SetParentInChildren(s)
				newStatements = append(newStatements, s)
			}
			for _, e := range newStatements {
				statements = tx.insertStatementAfterCustomPrologue(statements, e)
			}
		} else {
			// Do nothing (script file) - consider an error in the checker?
		}
	}

	if statementsUpdated {
		visited = tx.Factory().UpdateSourceFile(file, tx.Factory().NewNodeList(statements), file.EndOfFileToken)
	}

	tx.currentSourceFile = nil
	tx.importSpecifier = ""
	tx.filenameDeclaration = nil
	tx.utilizedImplicitRuntimeImports.Clear()

	return visited
}

func (tx *JSXTransformer) visitJsxElement(element *ast.JsxElement) *ast.Node {
	tagTransform := (*JSXTransformer).visitJsxOpeningLikeElementJSX
	if tx.shouldUseCreateElement(element.AsNode()) {
		tagTransform = (*JSXTransformer).visitJsxOpeningLikeElementCreateElement
	}
	location := core.NewTextRange(scanner.SkipTrivia(tx.currentSourceFile.Text(), element.Pos()), element.End())
	return tagTransform(tx, element.OpeningElement, element.Children, location)
}

func (tx *JSXTransformer) visitJsxSelfClosingElement(element *ast.JsxSelfClosingElement) *ast.Node {
	tagTransform := (*JSXTransformer).visitJsxOpeningLikeElementJSX
	if tx.shouldUseCreateElement(element.AsNode()) {
		tagTransform = (*JSXTransformer).visitJsxOpeningLikeElementCreateElement
	}
	location := core.NewTextRange(scanner.SkipTrivia(tx.currentSourceFile.Text(), element.Pos()), element.End())
	return tagTransform(tx, element.AsNode(), nil, location)
}

func (tx *JSXTransformer) visitJsxFragment(fragment *ast.JsxFragment) *ast.Node {
	tagTransform := (*JSXTransformer).visitJsxOpeningFragmentJSX
	if len(tx.importSpecifier) == 0 {
		tagTransform = (*JSXTransformer).visitJsxOpeningFragmentCreateElement
	}
	location := core.NewTextRange(scanner.SkipTrivia(tx.currentSourceFile.Text(), fragment.Pos()), fragment.End())
	return tagTransform(tx, fragment.OpeningFragment.AsJsxOpeningFragment(), fragment.Children, location)
}

func (tx *JSXTransformer) convertJsxChildrenToChildrenPropObject(children []*ast.JsxChild) *ast.Node {
	prop := tx.convertJsxChildrenToChildrenPropAssignment(children)
	if prop == nil {
		return nil
	}
	return tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{prop}), false)
}

func (tx *JSXTransformer) transformJsxChildToExpression(node *ast.Node) *ast.Node {
	prev := tx.inJsxChild
	tx.setInChild(true)
	defer tx.setInChild(prev)
	return tx.Visitor().Visit(node)
}

func (tx *JSXTransformer) convertJsxChildrenToChildrenPropAssignment(children []*ast.JsxChild) *ast.Node {
	nonWhitespceChildren := ast.GetSemanticJsxChildren(children)
	if len(nonWhitespceChildren) == 1 && (nonWhitespceChildren[0].Kind != ast.KindJsxExpression || nonWhitespceChildren[0].AsJsxExpression().DotDotDotToken == nil) {
		result := tx.transformJsxChildToExpression(nonWhitespceChildren[0])
		if result == nil {
			return nil
		}
		return tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("children"), nil, nil, result)
	}
	// For multiple children in the children property array, don't set StartOnNewLine
	// on child elements — the array literal is single-line.
	results := make([]*ast.Node, 0, len(nonWhitespceChildren))
	for _, child := range nonWhitespceChildren {
		res := tx.transformJsxChildToExpression(child)
		if res == nil {
			continue
		}
		tx.EmitContext().SetEmitFlags(res, tx.EmitContext().EmitFlags(res) & ^printer.EFStartOnNewLine)
		results = append(results, res)
	}
	if len(results) == 0 {
		return nil
	}
	return tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("children"), nil, nil, tx.Factory().NewArrayLiteralExpression(tx.Factory().NewNodeList(results), false))
}

func (tx *JSXTransformer) getTagName(node *ast.Node) *ast.Node {
	if node.Kind == ast.KindJsxElement {
		return tx.getTagName(node.AsJsxElement().OpeningElement)
	} else if ast.IsJsxOpeningLikeElement(node) {
		tagName := node.TagName()
		if ast.IsIdentifier(tagName) && scanner.IsIntrinsicJsxName(tagName.Text()) {
			return tx.Factory().NewStringLiteral(tagName.Text(), ast.TokenFlagsNone)
		} else if ast.IsJsxNamespacedName(tagName) {
			return tx.Factory().NewStringLiteral(
				tagName.AsJsxNamespacedName().Namespace.Text()+":"+tagName.AsJsxNamespacedName().Name().Text(), ast.TokenFlagsNone,
			)
		} else {
			return tx.Factory().CreateExpressionFromEntityName(tagName)
		}
	} else {
		panic("unhandled node kind passed to getTagName: " + node.Kind.String())
	}
}

func (tx *JSXTransformer) visitJsxOpeningLikeElementJSX(element *ast.Node, children *ast.NodeList, location core.TextRange) *ast.Node {
	tagName := tx.getTagName(element)
	var childrenProp *ast.Node
	if children != nil && len(children.Nodes) > 0 {
		childrenProp = tx.convertJsxChildrenToChildrenPropAssignment(children.Nodes)
	}
	var keyAttr *ast.Node
	attrs := element.Attributes().Properties()
	for i, p := range attrs {
		if p.Kind == ast.KindJsxAttribute && p.AsJsxAttribute().Name() != nil && ast.IsIdentifier(p.AsJsxAttribute().Name()) && p.AsJsxAttribute().Name().Text() == "key" {
			keyAttr = p
			attrs = slices.Clone(attrs)
			attrs = slices.Delete(attrs, i, i+1)
			break
		}
	}
	var object *ast.Node
	if len(attrs) > 0 {
		object = tx.transformJsxAttributesToObjectProps(attrs, childrenProp)
	} else {
		objectChildren := []*ast.Node{}
		if childrenProp != nil {
			objectChildren = append(objectChildren, childrenProp)
		}
		object = tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(objectChildren), false) // When there are no attributes, React wants {}
	}
	return tx.visitJsxOpeningLikeElementOrFragmentJSX(
		tagName,
		object,
		keyAttr,
		children,
		location,
	)
}

func (tx *JSXTransformer) transformJsxAttributesToObjectProps(attrs []*ast.Node, childrenProp *ast.Node) *ast.Node {
	target := tx.compilerOptions.GetEmitScriptTarget()
	if target >= core.ScriptTargetES2018 {
		// target has object spreads, can keep as-is
		return tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(tx.transformJsxAttributesToProps(attrs, childrenProp)), false)
	}
	return tx.transformJsxAttributesToExpression(attrs, childrenProp)
}

func (tx *JSXTransformer) transformJsxAttributesToExpression(attrs []*ast.Node, childrenProp *ast.Node) *ast.Node {
	expressions := make([]*ast.Expression, 0, 2)
	properties := make([]*ast.ObjectLiteralElement, 0, len(attrs))

	for _, attr := range attrs {
		if ast.IsJsxSpreadAttribute(attr) {
			// as an optimization we try to flatten the first level of spread inline object
			// as if its props would be passed as JSX attributes
			if ast.IsObjectLiteralExpression(attr.Expression()) && !hasProto(attr.Expression().AsObjectLiteralExpression()) {
				for _, prop := range attr.Expression().Properties() {
					if ast.IsSpreadAssignment(prop) {
						expressions, properties = tx.combinePropertiesIntoNewExpression(expressions, properties)
						expressions = append(expressions, tx.Visitor().Visit(prop.Expression()))
						continue
					}
					properties = append(properties, tx.Visitor().Visit(prop))
				}
				continue
			}
			expressions, properties = tx.combinePropertiesIntoNewExpression(expressions, properties)
			expressions = append(expressions, tx.Visitor().Visit(attr.Expression()))
			continue
		}
		properties = append(properties, tx.transformJsxAttributeToObjectLiteralElement(attr.AsJsxAttribute()))
	}

	if childrenProp != nil {
		properties = append(properties, childrenProp)
	}

	expressions, _ = tx.combinePropertiesIntoNewExpression(expressions, properties)

	if len(expressions) > 0 && !ast.IsObjectLiteralExpression(expressions[0]) {
		// We must always emit at least one object literal before a spread attribute
		// as the JSX always factory expects a fresh object, so we need to make a copy here
		// we also avoid mutating an external reference by doing this (first expression is used as assign's target)
		expressions = append([]*ast.Expression{tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{}), false)}, expressions...)
	}

	if len(expressions) == 1 {
		return expressions[0]
	}
	return tx.Factory().NewAssignHelper(expressions, tx.compilerOptions.GetEmitScriptTarget())
}

func (tx *JSXTransformer) combinePropertiesIntoNewExpression(expressions []*ast.Expression, props []*ast.ObjectLiteralElement) ([]*ast.Expression, []*ast.ObjectLiteralElement) {
	if len(props) == 0 {
		return expressions, props
	}
	newObj := tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList(props), false)
	expressions = append(expressions, newObj)
	return expressions, nil
}

func (tx *JSXTransformer) transformJsxAttributesToProps(attrs []*ast.Node, childrenProp *ast.Node) []*ast.Node {
	props := make([]*ast.Node, 0, len(attrs))
	for _, attr := range attrs {
		if attr.Kind == ast.KindJsxSpreadAttribute {
			res := tx.transformJsxSpreadAttributesToProps(attr.AsJsxSpreadAttribute())
			props = append(props, res...)
		} else {
			props = append(props, tx.transformJsxAttributeToObjectLiteralElement(attr.AsJsxAttribute()))
		}
	}
	if childrenProp != nil {
		props = append(props, childrenProp)
	}
	return props
}

func hasProto(obj *ast.ObjectLiteralExpression) bool {
	for _, p := range obj.Properties.Nodes {
		if ast.IsPropertyAssignment(p) && (ast.IsStringLiteral(p.Name()) || ast.IsIdentifier(p.Name())) && p.Name().Text() == "__proto__" {
			return true
		}
	}
	return false
}

func (tx *JSXTransformer) transformJsxSpreadAttributesToProps(node *ast.JsxSpreadAttribute) []*ast.Node {
	if ast.IsObjectLiteralExpression(node.Expression) && !hasProto(node.Expression.AsObjectLiteralExpression()) {
		res, _ := tx.Visitor().VisitSlice(node.Expression.Properties())
		return res
	}
	return []*ast.Node{tx.Factory().NewSpreadAssignment(tx.Visitor().Visit(node.Expression))}
}

func (tx *JSXTransformer) transformJsxAttributeToObjectLiteralElement(node *ast.JsxAttribute) *ast.Node {
	name := tx.getAttributeName(node)
	expression := tx.transformJsxAttributeInitializer(node.Initializer)
	return tx.Factory().NewPropertyAssignment(nil, name, nil, nil, expression)
}

/**
* Emit an attribute name, which is quoted if it needs to be quoted. Because
* these emit into an object literal property name, we don't need to be worried
* about keywords, just non-identifier characters
 */
func (tx *JSXTransformer) getAttributeName(node *ast.JsxAttribute) *ast.Node {
	name := node.Name()
	if ast.IsIdentifier(name) {
		text := name.Text()
		if scanner.IsIdentifierText(text, core.LanguageVariantStandard) {
			return name
		}
		return tx.Factory().NewStringLiteral(text, ast.TokenFlagsNone)
	}
	// must be jsx namespace
	return tx.Factory().NewStringLiteral(
		name.AsJsxNamespacedName().Namespace.Text()+":"+name.AsJsxNamespacedName().Name().Text(), ast.TokenFlagsNone,
	)
}

func (tx *JSXTransformer) transformJsxAttributeInitializer(node *ast.Node) *ast.Node {
	if node == nil {
		return tx.Factory().NewTrueExpression()
	}
	if node.Kind == ast.KindStringLiteral {
		// Always recreate the literal to escape any escape sequences or newlines which may be in the original jsx string and which
		// Need to be escaped to be handled correctly in a normal string
		res := tx.Factory().NewStringLiteral(decodeEntities(node.Text()), node.AsStringLiteral().TokenFlags)
		res.Loc = node.Loc
		// Preserve the original quote style (single vs double quotes)
		res.AsStringLiteral().TokenFlags = node.AsStringLiteral().TokenFlags
		return res
	}
	if node.Kind == ast.KindJsxExpression {
		if node.Expression() == nil {
			return tx.Factory().NewTrueExpression()
		}
		return tx.Visitor().Visit(node.Expression())
	}
	if ast.IsJsxElement(node) || ast.IsJsxSelfClosingElement(node) || ast.IsJsxFragment(node) {
		tx.setInChild(false)
		return tx.Visitor().Visit(node)
	}
	panic("Unhandled node kind found in jsx initializer: " + node.Kind.String())
}

func (tx *JSXTransformer) visitJsxOpeningLikeElementOrFragmentJSX(
	tagName *ast.Expression,
	object *ast.Expression,
	keyAttr *ast.Node,
	children *ast.NodeList,
	location core.TextRange,
) *ast.Node {
	var nonWhitespaceChildren []*ast.Node
	if children != nil {
		nonWhitespaceChildren = ast.GetSemanticJsxChildren(children.Nodes)
	}
	isStaticChildren := len(nonWhitespaceChildren) > 1 || (len(nonWhitespaceChildren) == 1 && ast.IsJsxExpression(nonWhitespaceChildren[0]) && nonWhitespaceChildren[0].AsJsxExpression().DotDotDotToken != nil)
	args := make([]*ast.Node, 0, 3)
	args = append(args, tagName, object)
	// function jsx(type, config, maybeKey) {}
	// "maybeKey" is optional. It is acceptable to use "_jsx" without a third argument
	if keyAttr != nil {
		args = append(args, tx.transformJsxAttributeInitializer(keyAttr.Initializer()))
	}

	if tx.compilerOptions.Jsx == core.JsxEmitReactJSXDev {
		originalFile := tx.EmitContext().MostOriginal(tx.currentSourceFile.AsNode())
		if originalFile != nil && ast.IsSourceFile(originalFile) {
			// "maybeKey" has to be replaced with "void 0" to not break the jsxDEV signature
			if keyAttr == nil {
				args = append(args, tx.Factory().NewVoidZeroExpression())
			}
			// isStaticChildren development flag
			if isStaticChildren {
				args = append(args, tx.Factory().NewTrueExpression())
			} else {
				args = append(args, tx.Factory().NewFalseExpression())
			}
			// __source development flag
			line, col := scanner.GetECMALineAndUTF16CharacterOfPosition(originalFile.AsSourceFile(), location.Pos())
			args = append(args, tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{
				tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("fileName"), nil, nil, tx.getCurrentFileNameExpression()),
				tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("lineNumber"), nil, nil, tx.Factory().NewNumericLiteral(strconv.FormatInt(int64(line+1), 10), ast.TokenFlagsNone)),
				tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("columnNumber"), nil, nil, tx.Factory().NewNumericLiteral(strconv.FormatInt(int64(col)+1, 10), ast.TokenFlagsNone)),
			}), false))
			// __self development flag
			args = append(args, tx.Factory().NewThisExpression())
		}
	}

	element := tx.Factory().NewCallExpression(tx.getJsxFactoryCallee(isStaticChildren), nil, nil, tx.Factory().NewNodeList(args), ast.NodeFlagsNone)
	element.Loc = location

	if tx.inJsxChild {
		tx.EmitContext().AddEmitFlags(element, printer.EFStartOnNewLine)
	}

	return element
}

func (tx *JSXTransformer) visitJsxOpeningFragmentJSX(fragment *ast.JsxOpeningFragment, children *ast.NodeList, location core.TextRange) *ast.Node {
	var childrenProps *ast.Expression
	if children != nil && len(children.Nodes) > 0 {
		result := tx.convertJsxChildrenToChildrenPropObject(children.Nodes)
		if result != nil {
			childrenProps = result
		}
	}
	if childrenProps == nil {
		childrenProps = tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{}), false)
	}
	return tx.visitJsxOpeningLikeElementOrFragmentJSX(
		tx.getImplicitJsxFragmentReference(),
		childrenProps,
		nil,
		children,
		location,
	)
}

func (tx *JSXTransformer) createReactNamespace(reactNamespace string, parent *ast.Node) *ast.Node {
	// To ensure the emit resolver can properly resolve the namespace, we need to
	// treat this identifier as if it were a source tree node by clearing the `Synthesized`
	// flag and setting a parent node. TODO: Is this still true? The emit resolver is supposed to be
	// hardened aginast this, so long as the node retains original node pointers back to a parsed node
	if len(reactNamespace) == 0 {
		reactNamespace = "React"
	}
	react := tx.Factory().NewIdentifier(reactNamespace)
	react.Flags &^= ast.NodeFlagsSynthesized

	// Set the parent that is in parse tree
	// this makes sure that parent chain is intact for checker to traverse complete scope tree
	react.Parent = tx.EmitContext().ParseNode(parent) //nolint:customlint // Parent is intentionally wired to a parse-tree node for resolver traversal.

	// If the identifier refers to an exported member of a namespace, substitute with
	// a qualified namespace property access (e.g., `React` -> `M.React`).
	// See also: RuntimeSyntaxTransformer.visitExpressionIdentifier in runtimesyntax.go
	if container := tx.emitResolver.GetReferencedExportContainer(react, false /*prefixLocals*/); container != nil && ast.IsModuleDeclaration(container) {
		containerName := tx.Factory().NewGeneratedNameForNode(container)
		return tx.Factory().NewPropertyAccessExpression(containerName, nil, react, ast.NodeFlagsNone)
	}

	return react
}

func (tx *JSXTransformer) createJsxFactoryExpressionFromEntityName(e *ast.Node, parent *ast.Node) *ast.Node {
	if ast.IsQualifiedName(e) {
		left := tx.createJsxFactoryExpressionFromEntityName(e.AsQualifiedName().Left, parent)
		right := tx.Factory().NewIdentifier(e.AsQualifiedName().Right.Text())
		return tx.Factory().NewPropertyAccessExpression(left, nil, right, ast.NodeFlagsNone)
	}
	return tx.createReactNamespace(e.Text(), parent)
}

func (tx *JSXTransformer) createJsxPseudoFactoryExpression(parent *ast.Node, e *ast.Node, target string) *ast.Node {
	if e != nil {
		return tx.createJsxFactoryExpressionFromEntityName(e, parent)
	}
	return tx.Factory().NewPropertyAccessExpression(
		tx.createReactNamespace(tx.compilerOptions.ReactNamespace, parent),
		nil,
		tx.Factory().NewIdentifier(target),
		ast.NodeFlagsNone,
	)
}

func (tx *JSXTransformer) createJsxFactoryExpression(parent *ast.Node) *ast.Node {
	e := tx.emitResolver.GetJsxFactoryEntity(tx.currentSourceFile.AsNode())
	return tx.createJsxPseudoFactoryExpression(parent, e, "createElement")
}

func (tx *JSXTransformer) createJsxFragmentFactoryExpression(parent *ast.Node) *ast.Node {
	e := tx.emitResolver.GetJsxFragmentFactoryEntity(tx.currentSourceFile.AsNode())
	return tx.createJsxPseudoFactoryExpression(parent, e, "Fragment")
}

func (tx *JSXTransformer) visitJsxOpeningLikeElementCreateElement(element *ast.Node, children *ast.NodeList, location core.TextRange) *ast.Node {
	tagName := tx.getTagName(element)
	attrs := element.Attributes().Properties()
	var objectProperties *ast.Expression
	if len(attrs) > 0 {
		objectProperties = tx.transformJsxAttributesToObjectProps(attrs, nil)
	} else {
		objectProperties = tx.Factory().NewKeywordExpression(ast.KindNullKeyword) // When there are no attributes, React wants "null"
	}

	var callee *ast.Expression
	if len(tx.importSpecifier) == 0 {
		callee = tx.createJsxFactoryExpression(element)
	} else {
		callee = tx.getImplicitImportForName("createElement")
	}

	var newChildren []*ast.Node
	if children != nil && len(children.Nodes) > 0 {
		for _, c := range children.Nodes {
			res := tx.transformJsxChildToExpression(c)
			if res != nil {
				newChildren = append(newChildren, res)
			}
		}
	}

	// Add StartOnNewLine flag only if there are multiple actual children (after filtering)
	if len(newChildren) > 1 {
		for _, child := range newChildren {
			tx.EmitContext().AddEmitFlags(child, printer.EFStartOnNewLine)
		}
	}

	args := make([]*ast.Expression, 0, len(newChildren)+2)
	args = append(args, tagName)
	args = append(args, objectProperties)
	args = append(args, newChildren...)

	result := tx.Factory().NewCallExpression(
		callee,
		nil,
		nil,
		tx.Factory().NewNodeList(args),
		ast.NodeFlagsNone,
	)
	result.Loc = location

	if tx.inJsxChild {
		tx.EmitContext().AddEmitFlags(result, printer.EFStartOnNewLine)
	}
	return result
}

func (tx *JSXTransformer) visitJsxOpeningFragmentCreateElement(fragment *ast.JsxOpeningFragment, children *ast.NodeList, location core.TextRange) *ast.Node {
	tagName := tx.createJsxFragmentFactoryExpression(fragment.AsNode())
	callee := tx.createJsxFactoryExpression(fragment.AsNode())

	var newChildren []*ast.Node
	if children != nil && len(children.Nodes) > 0 {
		for _, c := range children.Nodes {
			res := tx.transformJsxChildToExpression(c)
			if res != nil {
				newChildren = append(newChildren, res)
			}
		}
	}

	// Add StartOnNewLine flag only if there are multiple actual children (after filtering)
	if len(newChildren) > 1 {
		for _, child := range newChildren {
			tx.EmitContext().AddEmitFlags(child, printer.EFStartOnNewLine)
		}
	}

	args := make([]*ast.Expression, 0, len(newChildren)+2)
	args = append(args, tagName)
	args = append(args, tx.Factory().NewKeywordExpression(ast.KindNullKeyword))
	args = append(args, newChildren...)

	result := tx.Factory().NewCallExpression(
		callee,
		nil,
		nil,
		tx.Factory().NewNodeList(args),
		ast.NodeFlagsNone,
	)
	result.Loc = location

	if tx.inJsxChild {
		tx.EmitContext().AddEmitFlags(result, printer.EFStartOnNewLine)
	}
	return result
}

func (tx *JSXTransformer) visitJsxText(text *ast.JsxText) *ast.Node {
	fixed := fixupWhitespaceAndDecodeEntities(text.Text)
	if len(fixed) == 0 {
		return nil
	}
	return tx.Factory().NewStringLiteral(fixed, ast.TokenFlagsNone)
}

func addLineOfJsxText(b *strings.Builder, trimmedLine string, isInitial bool) {
	// We do not escape the string here as that is handled by the printer
	// when it emits the literal. We do, however, need to decode JSX entities.
	decoded := decodeEntities(trimmedLine)
	if !isInitial {
		b.WriteString(" ")
	}
	b.WriteString(decoded)
}

/**
* JSX trims whitespace at the end and beginning of lines, except that the
* start/end of a tag is considered a start/end of a line only if that line is
* on the same line as the closing tag. See examples in
* tests/cases/conformance/jsx/tsxReactEmitWhitespace.tsx
* See also https://www.w3.org/TR/html4/struct/text.html#h-9.1 and https://www.w3.org/TR/CSS2/text.html#white-space-model
*
* An equivalent algorithm would be:
* - If there is only one line, return it.
* - If there is only whitespace (but multiple lines), return `undefined`.
* - Split the text into lines.
* - 'trimRight' the first line, 'trimLeft' the last line, 'trim' middle lines.
* - Decode entities on each line (individually).
* - Remove empty lines and join the rest with " ".
 */
func fixupWhitespaceAndDecodeEntities(text string) string {
	acc := &strings.Builder{}
	initial := true
	// First non-whitespace character on this line.
	firstNonWhitespace := 0
	// End byte position of the last non-whitespace character on this line.
	lastNonWhitespaceEnd := -1
	// These initial values are special because the first line is:
	// firstNonWhitespace = 0 to indicate that we want leading whitespace,
	// but lastNonWhitespaceEnd = -1 as a special flag to indicate that we *don't* include the line if it's all whitespace.
	for i := 0; i < len(text); i++ {
		c, size := utf8.DecodeRuneInString(text[i:])
		if stringutil.IsLineBreak(c) {
			// If we've seen any non-whitespace characters on this line, add the 'trim' of the line.
			// (lastNonWhitespaceEnd === -1 is a special flag to detect whether the first line is all whitespace.)
			if firstNonWhitespace != -1 && lastNonWhitespaceEnd != -1 {
				addLineOfJsxText(acc, text[firstNonWhitespace:lastNonWhitespaceEnd+1], initial)
				initial = false
			}

			// Reset firstNonWhitespace for the next line.
			// Don't bother to reset lastNonWhitespaceEnd because we ignore it if firstNonWhitespace = -1.
			firstNonWhitespace = -1
		} else if !stringutil.IsWhiteSpaceSingleLine(c) {
			lastNonWhitespaceEnd = i + size - 1 // Store the end byte position of the character
			if firstNonWhitespace == -1 {
				firstNonWhitespace = i
			}
		}

		if size > 1 {
			i += (size - 1)
		}
	}

	if firstNonWhitespace != -1 {
		// Last line had a non-whitespace character. Emit the 'trimLeft', meaning keep trailing whitespace.
		addLineOfJsxText(acc, text[firstNonWhitespace:], initial)
	}
	return acc.String()
}

func (tx *JSXTransformer) visitJsxExpression(expression *ast.JsxExpression) *ast.Node {
	e := tx.Visitor().Visit(expression.Expression)
	if expression.DotDotDotToken != nil {
		return tx.Factory().NewSpreadElement(e)
	}
	return e
}

/**
* Replace entities like "&nbsp;", "&#123;", and "&#xDEADBEEF;" with the characters they encode.
* See https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
 */
func decodeEntities(text string) string {
	i := strings.IndexByte(text, '&')
	if i < 0 {
		return text
	}

	var result strings.Builder
	result.Grow(len(text))
	for {
		result.WriteString(text[:i])
		text = text[i:]

		semi := strings.IndexByte(text, ';')
		if semi < 0 {
			break
		}

		// Skip past any intervening '&' characters between the current '&'
		// and the ';'. Each such '&' is not part of a valid entity, so emit
		// it (and any text before the next '&') as literals.
		for {
			nextAmp := strings.IndexByte(text[1:semi], '&')
			if nextAmp < 0 {
				break
			}
			result.WriteString(text[:nextAmp+1])
			text = text[nextAmp+1:]
			semi -= nextAmp + 1
		}

		entity := text[1:semi]
		decoded, ok := decodeEntity(entity)
		if ok {
			// Use the JS-string encoder so lone surrogates (e.g. "&#xD800;")
			// are preserved rather than being lost to U+FFFD by WriteRune.
			result.WriteString(stringutil.EncodeJSStringRune(decoded))
		} else {
			result.WriteString(text[:semi+1])
		}
		text = text[semi+1:]

		i = strings.IndexByte(text, '&')
		if i < 0 {
			break
		}
	}
	result.WriteString(text)
	return result.String()
}

func decodeEntity(entity string) (rune, bool) {
	if len(entity) == 0 {
		return 0, false
	}

	if entity[0] == '#' {
		entity = entity[1:]
		if len(entity) == 0 {
			return 0, false
		}

		base := 10
		if entity[0] == 'x' {
			base = 16
			entity = entity[1:]
		}

		if len(entity) == 0 {
			return 0, false
		}

		for _, c := range entity {
			if base == 16 && !stringutil.IsHexDigit(c) {
				return 0, false
			}
			if base == 10 && !stringutil.IsDigit(c) {
				return 0, false
			}
		}

		parsed, err := strconv.ParseInt(entity, base, 32)
		if err != nil {
			return 0, false
		}
		return rune(parsed), true
	}

	r, ok := entities[entity]
	return r, ok
}

var entities = map[string]rune{
	"quot":     0x0022,
	"amp":      0x0026,
	"apos":     0x0027,
	"lt":       0x003C,
	"gt":       0x003E,
	"nbsp":     0x00A0,
	"iexcl":    0x00A1,
	"cent":     0x00A2,
	"pound":    0x00A3,
	"curren":   0x00A4,
	"yen":      0x00A5,
	"brvbar":   0x00A6,
	"sect":     0x00A7,
	"uml":      0x00A8,
	"copy":     0x00A9,
	"ordf":     0x00AA,
	"laquo":    0x00AB,
	"not":      0x00AC,
	"shy":      0x00AD,
	"reg":      0x00AE,
	"macr":     0x00AF,
	"deg":      0x00B0,
	"plusmn":   0x00B1,
	"sup2":     0x00B2,
	"sup3":     0x00B3,
	"acute":    0x00B4,
	"micro":    0x00B5,
	"para":     0x00B6,
	"middot":   0x00B7,
	"cedil":    0x00B8,
	"sup1":     0x00B9,
	"ordm":     0x00BA,
	"raquo":    0x00BB,
	"frac14":   0x00BC,
	"frac12":   0x00BD,
	"frac34":   0x00BE,
	"iquest":   0x00BF,
	"Agrave":   0x00C0,
	"Aacute":   0x00C1,
	"Acirc":    0x00C2,
	"Atilde":   0x00C3,
	"Auml":     0x00C4,
	"Aring":    0x00C5,
	"AElig":    0x00C6,
	"Ccedil":   0x00C7,
	"Egrave":   0x00C8,
	"Eacute":   0x00C9,
	"Ecirc":    0x00CA,
	"Euml":     0x00CB,
	"Igrave":   0x00CC,
	"Iacute":   0x00CD,
	"Icirc":    0x00CE,
	"Iuml":     0x00CF,
	"ETH":      0x00D0,
	"Ntilde":   0x00D1,
	"Ograve":   0x00D2,
	"Oacute":   0x00D3,
	"Ocirc":    0x00D4,
	"Otilde":   0x00D5,
	"Ouml":     0x00D6,
	"times":    0x00D7,
	"Oslash":   0x00D8,
	"Ugrave":   0x00D9,
	"Uacute":   0x00DA,
	"Ucirc":    0x00DB,
	"Uuml":     0x00DC,
	"Yacute":   0x00DD,
	"THORN":    0x00DE,
	"szlig":    0x00DF,
	"agrave":   0x00E0,
	"aacute":   0x00E1,
	"acirc":    0x00E2,
	"atilde":   0x00E3,
	"auml":     0x00E4,
	"aring":    0x00E5,
	"aelig":    0x00E6,
	"ccedil":   0x00E7,
	"egrave":   0x00E8,
	"eacute":   0x00E9,
	"ecirc":    0x00EA,
	"euml":     0x00EB,
	"igrave":   0x00EC,
	"iacute":   0x00ED,
	"icirc":    0x00EE,
	"iuml":     0x00EF,
	"eth":      0x00F0,
	"ntilde":   0x00F1,
	"ograve":   0x00F2,
	"oacute":   0x00F3,
	"ocirc":    0x00F4,
	"otilde":   0x00F5,
	"ouml":     0x00F6,
	"divide":   0x00F7,
	"oslash":   0x00F8,
	"ugrave":   0x00F9,
	"uacute":   0x00FA,
	"ucirc":    0x00FB,
	"uuml":     0x00FC,
	"yacute":   0x00FD,
	"thorn":    0x00FE,
	"yuml":     0x00FF,
	"OElig":    0x0152,
	"oelig":    0x0153,
	"Scaron":   0x0160,
	"scaron":   0x0161,
	"Yuml":     0x0178,
	"fnof":     0x0192,
	"circ":     0x02C6,
	"tilde":    0x02DC,
	"Alpha":    0x0391,
	"Beta":     0x0392,
	"Gamma":    0x0393,
	"Delta":    0x0394,
	"Epsilon":  0x0395,
	"Zeta":     0x0396,
	"Eta":      0x0397,
	"Theta":    0x0398,
	"Iota":     0x0399,
	"Kappa":    0x039A,
	"Lambda":   0x039B,
	"Mu":       0x039C,
	"Nu":       0x039D,
	"Xi":       0x039E,
	"Omicron":  0x039F,
	"Pi":       0x03A0,
	"Rho":      0x03A1,
	"Sigma":    0x03A3,
	"Tau":      0x03A4,
	"Upsilon":  0x03A5,
	"Phi":      0x03A6,
	"Chi":      0x03A7,
	"Psi":      0x03A8,
	"Omega":    0x03A9,
	"alpha":    0x03B1,
	"beta":     0x03B2,
	"gamma":    0x03B3,
	"delta":    0x03B4,
	"epsilon":  0x03B5,
	"zeta":     0x03B6,
	"eta":      0x03B7,
	"theta":    0x03B8,
	"iota":     0x03B9,
	"kappa":    0x03BA,
	"lambda":   0x03BB,
	"mu":       0x03BC,
	"nu":       0x03BD,
	"xi":       0x03BE,
	"omicron":  0x03BF,
	"pi":       0x03C0,
	"rho":      0x03C1,
	"sigmaf":   0x03C2,
	"sigma":    0x03C3,
	"tau":      0x03C4,
	"upsilon":  0x03C5,
	"phi":      0x03C6,
	"chi":      0x03C7,
	"psi":      0x03C8,
	"omega":    0x03C9,
	"thetasym": 0x03D1,
	"upsih":    0x03D2,
	"piv":      0x03D6,
	"ensp":     0x2002,
	"emsp":     0x2003,
	"thinsp":   0x2009,
	"zwnj":     0x200C,
	"zwj":      0x200D,
	"lrm":      0x200E,
	"rlm":      0x200F,
	"ndash":    0x2013,
	"mdash":    0x2014,
	"lsquo":    0x2018,
	"rsquo":    0x2019,
	"sbquo":    0x201A,
	"ldquo":    0x201C,
	"rdquo":    0x201D,
	"bdquo":    0x201E,
	"dagger":   0x2020,
	"Dagger":   0x2021,
	"bull":     0x2022,
	"hellip":   0x2026,
	"permil":   0x2030,
	"prime":    0x2032,
	"Prime":    0x2033,
	"lsaquo":   0x2039,
	"rsaquo":   0x203A,
	"oline":    0x203E,
	"frasl":    0x2044,
	"euro":     0x20AC,
	"image":    0x2111,
	"weierp":   0x2118,
	"real":     0x211C,
	"trade":    0x2122,
	"alefsym":  0x2135,
	"larr":     0x2190,
	"uarr":     0x2191,
	"rarr":     0x2192,
	"darr":     0x2193,
	"harr":     0x2194,
	"crarr":    0x21B5,
	"lArr":     0x21D0,
	"uArr":     0x21D1,
	"rArr":     0x21D2,
	"dArr":     0x21D3,
	"hArr":     0x21D4,
	"forall":   0x2200,
	"part":     0x2202,
	"exist":    0x2203,
	"empty":    0x2205,
	"nabla":    0x2207,
	"isin":     0x2208,
	"notin":    0x2209,
	"ni":       0x220B,
	"prod":     0x220F,
	"sum":      0x2211,
	"minus":    0x2212,
	"lowast":   0x2217,
	"radic":    0x221A,
	"prop":     0x221D,
	"infin":    0x221E,
	"ang":      0x2220,
	"and":      0x2227,
	"or":       0x2228,
	"cap":      0x2229,
	"cup":      0x222A,
	"int":      0x222B,
	"there4":   0x2234,
	"sim":      0x223C,
	"cong":     0x2245,
	"asymp":    0x2248,
	"ne":       0x2260,
	"equiv":    0x2261,
	"le":       0x2264,
	"ge":       0x2265,
	"sub":      0x2282,
	"sup":      0x2283,
	"nsub":     0x2284,
	"sube":     0x2286,
	"supe":     0x2287,
	"oplus":    0x2295,
	"otimes":   0x2297,
	"perp":     0x22A5,
	"sdot":     0x22C5,
	"lceil":    0x2308,
	"rceil":    0x2309,
	"lfloor":   0x230A,
	"rfloor":   0x230B,
	"lang":     0x2329,
	"rang":     0x232A,
	"loz":      0x25CA,
	"spades":   0x2660,
	"clubs":    0x2663,
	"hearts":   0x2665,
	"diams":    0x2666,
}
