package ls

import (
	"context"
	"slices"
	"strings"
	"unicode"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/astnav"
	"github.com/microsoft/typescript-go/internal/checker"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
	"github.com/microsoft/typescript-go/internal/evaluator"
	"github.com/microsoft/typescript-go/internal/ls/lsconv"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/nodebuilder"
	"github.com/microsoft/typescript-go/internal/printer"
	"github.com/microsoft/typescript-go/internal/scanner"
	"github.com/microsoft/typescript-go/internal/stringutil"
)

func (l *LanguageService) ProvideInlayHint(
	ctx context.Context,
	params *lsproto.InlayHintParams,
) (lsproto.InlayHintResponse, error) {
	userPreferences := l.UserPreferences()
	inlayHintPreferences := &userPreferences.InlayHints
	if !isAnyInlayHintEnabled(inlayHintPreferences) {
		return lsproto.InlayHintsOrNull{InlayHints: nil}, nil
	}

	program, file := l.getProgramAndFile(params.TextDocument.Uri)
	quotePreference := getQuotePreference(file, userPreferences)

	checker, done := program.GetTypeCheckerForFile(ctx, file)
	defer done()
	inlayHintState := &inlayHintState{
		ctx:             ctx,
		span:            l.converters.FromLSPRange(file, params.Range),
		preferences:     inlayHintPreferences,
		quotePreference: quotePreference,
		file:            file,
		checker:         checker,
		converters:      l.converters,
	}
	inlayHintState.visit(file.AsNode())
	return lsproto.InlayHintsOrNull{InlayHints: &inlayHintState.result}, nil
}

type inlayHintState struct {
	ctx             context.Context
	span            core.TextRange
	preferences     *lsutil.InlayHintsPreferences
	quotePreference quotePreference
	file            *ast.SourceFile
	checker         *checker.Checker
	converters      *lsconv.Converters
	result          []*lsproto.InlayHint
}

func (s *inlayHintState) visit(node *ast.Node) bool {
	if node == nil || node.End()-node.Pos() == 0 {
		return false
	}

	switch node.Kind {
	case ast.KindModuleDeclaration, ast.KindClassDeclaration, ast.KindInterfaceDeclaration,
		ast.KindFunctionDeclaration, ast.KindClassExpression, ast.KindFunctionExpression,
		ast.KindMethodDeclaration, ast.KindArrowFunction:
		if s.ctx.Err() != nil {
			return true
		}
	}

	if !s.span.Intersects(node.Loc) {
		return false
	}

	if ast.IsTypeNode(node) && !ast.IsExpressionWithTypeArguments(node) {
		return false
	}

	if s.preferences.IncludeInlayVariableTypeHints && ast.IsVariableDeclaration(node) {
		s.visitVariableLikeDeclaration(node)
	} else if s.preferences.IncludeInlayPropertyDeclarationTypeHints && ast.IsPropertyDeclaration(node) {
		s.visitVariableLikeDeclaration(node)
	} else if s.preferences.IncludeInlayEnumMemberValueHints && ast.IsEnumMember(node) {
		s.visitEnumMember(node)
	} else if shouldShowParameterNameHints(s.preferences) && (ast.IsCallExpression(node) || ast.IsNewExpression(node)) {
		s.visitCallOrNewExpression(node)
	} else {
		if s.preferences.IncludeInlayFunctionParameterTypeHints &&
			ast.IsFunctionLikeDeclaration(node) &&
			ast.HasContextSensitiveParameters(node) {
			s.visitFunctionLikeForParameterType(node)
		}
		if s.preferences.IncludeInlayFunctionLikeReturnTypeHints &&
			isSignatureSupportingReturnAnnotation(node) {
			s.visitFunctionDeclarationLikeForReturnType(node)
		}
	}
	return node.ForEachChild(s.visit)
}

// FunctionDeclaration | MethodDeclaration | GetAccessorDeclaration | FunctionExpression | ArrowFunction
func (s *inlayHintState) visitFunctionDeclarationLikeForReturnType(decl *ast.FunctionLikeDeclaration) {
	if ast.IsArrowFunction(decl) {
		if astnav.FindChildOfKind(decl, ast.KindOpenParenToken, s.file) == nil {
			return
		}
	}

	typeAnnotation := decl.Type()
	if typeAnnotation != nil || decl.Body() == nil {
		return
	}

	signature := s.checker.GetSignatureFromDeclaration(decl)
	if signature == nil {
		return
	}

	typePredicate := s.checker.GetTypePredicateOfSignature(signature)

	if typePredicate != nil && typePredicate.Type() != nil {
		hintParts := s.typePredicateToInlayHintParts(typePredicate)
		s.addTypeHints(hintParts, s.getTypeAnnotationPosition(decl))
		return
	}

	returnType := s.checker.GetReturnTypeOfSignature(signature)
	if isModuleReferenceType(returnType) {
		return
	}

	hintParts := s.typeToInlayHintParts(returnType)
	s.addTypeHints(hintParts, s.getTypeAnnotationPosition(decl))
}

func (s *inlayHintState) visitCallOrNewExpression(expr *ast.CallOrNewExpression) {
	args := expr.Arguments()
	if len(args) == 0 {
		return
	}

	signature := s.checker.GetResolvedSignature(expr)
	if signature == nil {
		return
	}

	signatureParamPos := 0
	for _, originalArg := range args {
		arg := ast.SkipParentheses(originalArg)
		if shouldShowLiteralParameterNameHintsOnly(s.preferences) && !isHintableLiteral(arg) {
			signatureParamPos++
			continue
		}

		spreadArgs := 0
		if ast.IsSpreadElement(arg) {
			spreadType := s.checker.GetTypeAtLocation(arg.Expression())
			if spreadType.IsTupleType() {
				elementFlags := spreadType.Target().AsTupleType().ElementFlags()
				fixedLength := spreadType.Target().AsTupleType().FixedLength()
				if fixedLength == 0 {
					continue
				}
				firstOptionalIndex := slices.IndexFunc(elementFlags, func(f checker.ElementFlags) bool {
					return f&checker.ElementFlagsRequired == 0
				})
				requiredArgs := core.IfElse(firstOptionalIndex < 0, fixedLength, firstOptionalIndex)
				if requiredArgs > 0 {
					spreadArgs = requiredArgs
				}
			}
		}

		identifierInfo := s.getParameterIdentifierInfoAtPosition(signature, signatureParamPos)
		signatureParamPos = signatureParamPos + core.IfElse(spreadArgs > 0, spreadArgs, 1)
		if identifierInfo == nil {
			return
		}

		parameter := identifierInfo.parameter
		parameterName := identifierInfo.name
		isFirstVariadicArgument := identifierInfo.isRestParameter
		parameterNameNotSameAsArgument := s.preferences.IncludeInlayParameterNameHintsWhenArgumentMatchesName ||
			!identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName)
		if !parameterNameNotSameAsArgument && !isFirstVariadicArgument {
			continue
		}

		if s.leadingCommentsContainsParameterName(arg, parameterName) {
			continue
		}

		s.addParameterHints(
			parameterName,
			parameter,
			astnav.GetStartOfNode(originalArg, s.file, false /*includeJSDoc*/),
			isFirstVariadicArgument,
		)
	}
}

func (s *inlayHintState) visitEnumMember(member *ast.EnumMemberNode) {
	if member.Initializer() != nil {
		return
	}

	enumValue := s.checker.GetConstantValue(member)
	if enumValue != nil {
		s.addEnumMemberValueHints(evaluator.AnyToString(enumValue), member.End())
	}
}

func (s *inlayHintState) visitVariableLikeDeclaration(decl *ast.VariableOrPropertyDeclaration) {
	if decl.Initializer() == nil &&
		!(ast.IsPropertyDeclaration(decl) && s.checker.GetTypeAtLocation(decl).Flags()&checker.TypeFlagsAny == 0) ||
		ast.IsBindingPattern(decl.Name()) || (ast.IsVariableDeclaration(decl) && !isHintableDeclaration(decl)) {
		return
	}

	typeAnnotation := decl.Type()
	if typeAnnotation != nil {
		return
	}

	declarationType := s.checker.GetTypeAtLocation(decl)
	if isModuleReferenceType(declarationType) {
		return
	}

	hintParts := s.typeToInlayHintParts(declarationType)
	var hintText string
	if hintParts.String != nil {
		hintText = *hintParts.String
	} else if hintParts.InlayHintLabelParts != nil {
		var b strings.Builder
		for _, part := range *hintParts.InlayHintLabelParts {
			b.WriteString(part.Value)
		}
		hintText = b.String()
	}
	if !s.preferences.IncludeInlayVariableTypeHintsWhenTypeMatchesName &&
		stringutil.EquateStringCaseInsensitive(decl.Name().Text(), hintText) {
		return
	}
	s.addTypeHints(hintParts, decl.Name().End())
}

func (s *inlayHintState) visitFunctionLikeForParameterType(node *ast.FunctionLikeDeclaration) {
	signature := s.checker.GetSignatureFromDeclaration(node)
	if signature == nil {
		return
	}

	pos := 0
	for _, param := range node.Parameters() {
		if isHintableDeclaration(param) {
			var symbol *ast.Symbol
			if ast.IsThisParameter(param) {
				symbol = signature.ThisParameter()
			} else {
				symbol = signature.Parameters()[pos]
			}
			s.addParameterTypeHint(param, symbol)
		}
		if ast.IsThisParameter(param) {
			continue
		}
		pos++
	}
}

func (s *inlayHintState) addParameterTypeHint(node *ast.ParameterDeclarationNode, symbol *ast.Symbol) {
	typeAnnotation := node.Type()
	if typeAnnotation != nil || symbol == nil {
		return
	}
	typeHints := s.getParameterDeclarationTypeHints(symbol)
	if typeHints == nil {
		return
	}
	var pos int
	if node.QuestionToken() != nil {
		pos = node.QuestionToken().End()
	} else {
		pos = node.Name().End()
	}
	s.addTypeHints(*typeHints, pos)
}

func (s *inlayHintState) getParameterDeclarationTypeHints(symbol *ast.Symbol) *lsproto.StringOrInlayHintLabelParts {
	valueDeclaration := symbol.ValueDeclaration
	if valueDeclaration == nil || !ast.IsParameter(valueDeclaration) {
		return nil
	}

	signatureParamType := s.checker.GetTypeOfSymbolAtLocation(symbol, valueDeclaration)
	if isModuleReferenceType(signatureParamType) {
		return nil
	}

	return ptrTo(s.typeToInlayHintParts(signatureParamType))
}

func (s *inlayHintState) typeToInlayHintParts(t *checker.Type) lsproto.StringOrInlayHintLabelParts {
	flags := nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsAllowUniqueESSymbolType |
		nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope
	typeNode := s.checker.TypeToTypeNode(t, nil /*enclosingDeclaration*/, flags)
	debug.AssertIsDefined(typeNode, "should always get typenode")
	return lsproto.StringOrInlayHintLabelParts{
		InlayHintLabelParts: ptrTo(s.getInlayHintLabelParts(typeNode)),
	}
}

func (s *inlayHintState) typePredicateToInlayHintParts(typePredicate *checker.TypePredicate) lsproto.StringOrInlayHintLabelParts {
	flags := nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsAllowUniqueESSymbolType |
		nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope
	typeNode := s.checker.TypePredicateToTypePredicateNode(typePredicate, nil /*enclosingDeclaration*/, flags)
	debug.AssertIsDefined(typeNode, "should always get typePredicateNode")
	return lsproto.StringOrInlayHintLabelParts{
		InlayHintLabelParts: ptrTo(s.getInlayHintLabelParts(typeNode)),
	}
}

func (s *inlayHintState) addTypeHints(hint lsproto.StringOrInlayHintLabelParts, position int) {
	if hint.String != nil {
		hint.String = ptrTo(": " + *hint.String)
	} else {
		hint.InlayHintLabelParts = ptrTo(append([]*lsproto.InlayHintLabelPart{{Value: ": "}}, *hint.InlayHintLabelParts...))
	}
	s.result = append(s.result, &lsproto.InlayHint{
		Label:       hint,
		Position:    s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
		Kind:        ptrTo(lsproto.InlayHintKindType),
		PaddingLeft: ptrTo(true),
	})
}

func (s *inlayHintState) addEnumMemberValueHints(text string, position int) {
	s.result = append(s.result, &lsproto.InlayHint{
		Label: lsproto.StringOrInlayHintLabelParts{
			String: ptrTo("= " + text),
		},
		Position:    s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
		PaddingLeft: ptrTo(true),
	})
}

func (s *inlayHintState) addParameterHints(text string, parameter *ast.IdentifierNode, position int, isFirstVariadicArgument bool) {
	hintText := core.IfElse(isFirstVariadicArgument, "...", "") + text
	displayParts := []*lsproto.InlayHintLabelPart{
		s.getNodeDisplayPart(hintText, parameter),
		{
			Value: ":",
		},
	}
	labelParts := lsproto.StringOrInlayHintLabelParts{InlayHintLabelParts: &displayParts}

	s.result = append(s.result, &lsproto.InlayHint{
		Label:        labelParts,
		Position:     s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
		Kind:         ptrTo(lsproto.InlayHintKindParameter),
		PaddingRight: ptrTo(true),
	})
}

func shouldShowParameterNameHints(preferences *lsutil.InlayHintsPreferences) bool {
	return (preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsLiterals ||
		preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsAll)
}

func shouldShowLiteralParameterNameHintsOnly(preferences *lsutil.InlayHintsPreferences) bool {
	return preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsLiterals
}

// node is FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessorDeclaration
func isSignatureSupportingReturnAnnotation(node *ast.Node) bool {
	return ast.IsArrowFunction(node) || ast.IsFunctionExpression(node) || ast.IsFunctionDeclaration(node) ||
		ast.IsMethodDeclaration(node) || ast.IsGetAccessorDeclaration(node)
}

func isHintableDeclaration(node *ast.VariableOrParameterDeclaration) bool {
	if (ast.IsPartOfParameterDeclaration(node) || ast.IsVariableDeclaration(node) && ast.IsVarConst(node)) &&
		node.Initializer() != nil {
		initializer := ast.SkipParentheses(node.Initializer())
		return !(isHintableLiteral(initializer) || ast.IsNewExpression(initializer) ||
			ast.IsObjectLiteralExpression(initializer) || ast.IsAssertionExpression(initializer))
	}
	return true
}

func isHintableLiteral(node *ast.Node) bool {
	switch node.Kind {
	case ast.KindPrefixUnaryExpression:
		operand := node.AsPrefixUnaryExpression().Operand
		return ast.IsLiteralExpression(operand) || ast.IsIdentifier(operand) && ast.IsInfinityOrNaNString(operand.Text())
	case ast.KindTrueKeyword, ast.KindFalseKeyword, ast.KindNullKeyword,
		ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateExpression:
		return true
	case ast.KindIdentifier:
		name := node.Text()
		return name == "undefined" || ast.IsInfinityOrNaNString(name)
	}
	return ast.IsLiteralExpression(node)
}

func isModuleReferenceType(t *checker.Type) bool {
	symbol := t.Symbol()
	return symbol != nil && symbol.Flags&ast.SymbolFlagsModule != 0
}

func (s *inlayHintState) getInlayHintLabelParts(node *ast.Node) []*lsproto.InlayHintLabelPart {
	var parts []*lsproto.InlayHintLabelPart

	var visitForDisplayParts func(node *ast.Node)
	var visitDisplayPartList func(nodes []*ast.Node, separator string)
	var visitParametersAndTypeParameters func(node *ast.SignatureDeclaration)

	visitForDisplayParts = func(node *ast.Node) {
		if node == nil {
			return
		}

		tokenString := scanner.TokenToString(node.Kind)
		if tokenString != "" {
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: tokenString})
			return
		}

		if ast.IsLiteralExpression(node) {
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
			return
		}

		switch node.Kind {
		case ast.KindIdentifier:
			identifierText := node.Text()
			var name *ast.Node
			// !!! This won't work in Corsa since we don't store symbols on identifiers. We need another strategy for it.
			if node.Symbol() != nil && len(node.Symbol().Declarations) != 0 {
				name = ast.GetNameOfDeclaration(node.Symbol().Declarations[0])
			}
			if name != nil {
				parts = append(parts, s.getNodeDisplayPart(identifierText, name))
			} else {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: identifierText})
			}
		case ast.KindQualifiedName:
			visitForDisplayParts(node.AsQualifiedName().Left)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
			visitForDisplayParts(node.AsQualifiedName().Right)
		case ast.KindTypePredicate:
			if node.AsTypePredicateNode().AssertsModifier != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "asserts "})
			}
			visitForDisplayParts(node.AsTypePredicateNode().ParameterName)
			if node.Type() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " is "})
				visitForDisplayParts(node.Type())
			}
		case ast.KindTypeReference:
			visitForDisplayParts(node.AsTypeReferenceNode().TypeName)
			if len(node.TypeArguments()) > 0 {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
				visitDisplayPartList(node.TypeArguments(), ",")
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
			}
		case ast.KindTypeParameter:
			if len(node.ModifierNodes()) > 0 {
				visitDisplayPartList(node.ModifierNodes(), "")
			}
			visitForDisplayParts(node.Name())
			if node.AsTypeParameter().Constraint != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " extends "})
				visitForDisplayParts(node.AsTypeParameter().Constraint)
			}
			if node.AsTypeParameter().DefaultType != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " = "})
				visitForDisplayParts(node.AsTypeParameter().DefaultType)
			}
		case ast.KindParameter:
			if len(node.ModifierNodes()) > 0 {
				visitDisplayPartList(node.ModifierNodes(), " ")
			}
			if node.AsParameterDeclaration().DotDotDotToken != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
			}
			visitForDisplayParts(node.Name())
			if node.QuestionToken() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
			}
			if node.Type() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
				visitForDisplayParts(node.Type())
			}
		case ast.KindConstructorType:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "new "})
			visitParametersAndTypeParameters(node)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: " => "})
			visitForDisplayParts(node.Type())
		case ast.KindTypeQuery:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "typeof "})
			visitForDisplayParts(node.AsTypeQueryNode().ExprName)
			if len(node.TypeArguments()) > 0 {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
				visitDisplayPartList(node.TypeArguments(), ", ")
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
			}
		case ast.KindTypeLiteral:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{"})
			if len(node.Members()) > 0 {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
				visitDisplayPartList(node.Members(), "; ")
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "}"})
		case ast.KindArrayType:
			visitForDisplayParts(node.AsArrayTypeNode().ElementType)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "[]"})
		case ast.KindTupleType:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
			visitDisplayPartList(node.Elements(), ", ")
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
		case ast.KindNamedTupleMember:
			if node.AsNamedTupleMember().DotDotDotToken != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
			}
			visitForDisplayParts(node.Name())
			if node.QuestionToken() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
			visitForDisplayParts(node.Type())
		case ast.KindOptionalType:
			visitForDisplayParts(node.Type())
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
		case ast.KindRestType:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
			visitForDisplayParts(node.Type())
		case ast.KindUnionType:
			if node.AsUnionTypeNode().Types != nil {
				visitDisplayPartList(node.AsUnionTypeNode().Types.Nodes, " | ")
			}
		case ast.KindIntersectionType:
			if node.AsIntersectionTypeNode().Types != nil {
				visitDisplayPartList(node.AsIntersectionTypeNode().Types.Nodes, " & ")
			}
		case ast.KindConditionalType:
			visitForDisplayParts(node.AsConditionalTypeNode().CheckType)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: " extends "})
			visitForDisplayParts(node.AsConditionalTypeNode().ExtendsType)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: " ? "})
			visitForDisplayParts(node.AsConditionalTypeNode().TrueType)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: " : "})
			visitForDisplayParts(node.AsConditionalTypeNode().FalseType)
		case ast.KindInferType:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "infer "})
			visitForDisplayParts(node.AsInferTypeNode().TypeParameter)
		case ast.KindParenthesizedType:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "("})
			visitForDisplayParts(node.Type())
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
		case ast.KindTypeOperator:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: scanner.TokenToString(node.AsTypeOperatorNode().Operator)})
			visitForDisplayParts(node.Type())
		case ast.KindIndexedAccessType:
			visitForDisplayParts(node.AsIndexedAccessTypeNode().ObjectType)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
			visitForDisplayParts(node.AsIndexedAccessTypeNode().IndexType)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
		case ast.KindMappedType:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{ "})
			if node.AsMappedTypeNode().ReadonlyToken != nil {
				if node.AsMappedTypeNode().ReadonlyToken.Kind == ast.KindPlusToken {
					parts = append(parts, &lsproto.InlayHintLabelPart{Value: "+"})
				} else if node.AsMappedTypeNode().ReadonlyToken.Kind == ast.KindMinusToken {
					parts = append(parts, &lsproto.InlayHintLabelPart{Value: "-"})
				}
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "readonly "})
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
			visitForDisplayParts(node.AsMappedTypeNode().TypeParameter)
			if node.AsMappedTypeNode().NameType != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " as "})
				visitForDisplayParts(node.AsMappedTypeNode().NameType)
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
			if node.QuestionToken() != nil {
				if node.QuestionToken().Kind == ast.KindPlusToken {
					parts = append(parts, &lsproto.InlayHintLabelPart{Value: "+"})
				} else if node.QuestionToken().Kind == ast.KindMinusToken {
					parts = append(parts, &lsproto.InlayHintLabelPart{Value: "-"})
				}
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
			if node.Type() != nil {
				visitForDisplayParts(node.Type())
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "; }"})
		case ast.KindLiteralType:
			visitForDisplayParts(node.AsLiteralTypeNode().Literal)
		case ast.KindFunctionType:
			visitParametersAndTypeParameters(node)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: " => "})
			visitForDisplayParts(node.Type())
		case ast.KindImportType:
			if node.AsImportTypeNode().IsTypeOf {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "typeof "})
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "import("})
			visitForDisplayParts(node.AsImportTypeNode().Argument)
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
			if node.AsImportTypeNode().Qualifier != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
				visitForDisplayParts(node.AsImportTypeNode().Qualifier)
			}
			if len(node.TypeArguments()) > 0 {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
				visitDisplayPartList(node.TypeArguments(), ", ")
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
			}
		case ast.KindPropertySignature:
			if len(node.ModifierNodes()) > 0 {
				visitDisplayPartList(node.ModifierNodes(), " ")
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
			}
			visitForDisplayParts(node.Name())
			if node.PostfixToken() != nil {
				parts = append(
					parts,
					&lsproto.InlayHintLabelPart{
						Value: scanner.TokenToString(node.PostfixToken().Kind),
					})
			}
			if node.Type() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
				visitForDisplayParts(node.Type())
			}
		case ast.KindIndexSignature:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
			visitDisplayPartList(node.Parameters(), ", ")
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
			if node.Type() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
				visitForDisplayParts(node.Type())
			}
		case ast.KindMethodSignature:
			if len(node.ModifierNodes()) > 0 {
				visitDisplayPartList(node.ModifierNodes(), " ")
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
			}
			visitForDisplayParts(node.Name())
			if node.PostfixToken() != nil {
				parts = append(
					parts,
					&lsproto.InlayHintLabelPart{
						Value: scanner.TokenToString(node.PostfixToken().Kind),
					})
			}
			visitParametersAndTypeParameters(node)
			if node.Type() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
				visitForDisplayParts(node.Type())
			}
		case ast.KindCallSignature:
			visitParametersAndTypeParameters(node)
			if node.Type() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
				visitForDisplayParts(node.Type())
			}
		case ast.KindConstructSignature:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "new "})
			visitParametersAndTypeParameters(node)
			if node.Type() != nil {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
				visitForDisplayParts(node.Type())
			}
		case ast.KindArrayBindingPattern:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
			visitDisplayPartList(node.Elements(), ", ")
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
		case ast.KindObjectBindingPattern:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{"})
			if len(node.Elements()) > 0 {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
				visitDisplayPartList(node.Elements(), ", ")
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
			}
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "}"})
		case ast.KindBindingElement:
			visitForDisplayParts(node.Name())
		case ast.KindPrefixUnaryExpression:
			parts = append(
				parts,
				&lsproto.InlayHintLabelPart{
					Value: scanner.TokenToString(node.AsPrefixUnaryExpression().Operator),
				})
			visitForDisplayParts(node.AsPrefixUnaryExpression().Operand)
		case ast.KindTemplateLiteralType:
			visitForDisplayParts(node.AsTemplateLiteralTypeNode().Head)
			for _, span := range node.AsTemplateLiteralTypeNode().TemplateSpans.Nodes {
				visitForDisplayParts(span)
			}
		case ast.KindTemplateHead:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
		case ast.KindTemplateLiteralTypeSpan:
			visitForDisplayParts(node.Type())
			visitForDisplayParts(node.AsTemplateLiteralTypeSpan().Literal)
		case ast.KindTemplateMiddle, ast.KindTemplateTail:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
		case ast.KindThisType:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "this"})
		case ast.KindComputedPropertyName:
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
			visitForDisplayParts(node.Expression())
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
		default:
			debug.FailBadSyntaxKind(node)
		}
	}

	visitDisplayPartList = func(nodes []*ast.Node, separator string) {
		for i, n := range nodes {
			if i > 0 {
				parts = append(parts, &lsproto.InlayHintLabelPart{Value: separator})
			}
			visitForDisplayParts(n)
		}
	}

	visitParametersAndTypeParameters = func(node *ast.SignatureDeclaration) {
		if len(node.TypeParameters()) > 0 {
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
			visitDisplayPartList(node.TypeParameters(), ", ")
			parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
		}
		parts = append(parts, &lsproto.InlayHintLabelPart{Value: "("})
		visitDisplayPartList(node.Parameters(), ", ")
		parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
	}

	visitForDisplayParts(node)
	return parts
}

func (s *inlayHintState) getNodeDisplayPart(text string, node *ast.Node) *lsproto.InlayHintLabelPart {
	file := ast.GetSourceFileOfNode(node)
	return &lsproto.InlayHintLabelPart{
		Value: text,
		Location: &lsproto.Location{
			Uri:   lsconv.FileNameToDocumentURI(file.FileName()),
			Range: s.converters.ToLSPRange(file, node.Loc),
		},
	}
}

func (s *inlayHintState) getLiteralText(node *ast.LiteralLikeNode) string {
	switch node.Kind {
	case ast.KindStringLiteral:
		if s.quotePreference == quotePreferenceSingle {
			return `'` + printer.EscapeString(node.Text(), printer.QuoteCharSingleQuote) + `'`
		}
		return `"` + printer.EscapeString(node.Text(), printer.QuoteCharDoubleQuote) + `"`
	case ast.KindTemplateHead, ast.KindTemplateMiddle, ast.KindTemplateTail:
		rawText := node.RawText()
		if rawText == "" {
			rawText = printer.EscapeString(node.Text(), printer.QuoteCharBacktick)
		}
		switch node.Kind {
		case ast.KindTemplateHead:
			return "`" + rawText + "${"
		case ast.KindTemplateMiddle:
			return "}" + rawText + "${"
		case ast.KindTemplateTail:
			return "}" + rawText + "`"
		}
	}
	return node.Text()
}

type parameterInfo struct {
	parameter       *ast.IdentifierNode
	name            string
	isRestParameter bool
}

func (s *inlayHintState) getParameterIdentifierInfoAtPosition(signature *checker.Signature, pos int) *parameterInfo {
	parameters := signature.Parameters()
	paramCount := len(parameters) - core.IfElse(signature.HasRestParameter(), 1, 0)
	if pos < paramCount {
		param := parameters[pos]
		paramId := getParameterDeclarationIdentifier(param)
		if paramId == nil {
			return nil
		}
		return &parameterInfo{
			parameter:       paramId,
			name:            paramId.Text(),
			isRestParameter: false,
		}
	}

	var restParameter *ast.Symbol
	var restId *ast.IdentifierNode
	if paramCount < len(parameters) {
		restParameter = parameters[paramCount]
		restId = getParameterDeclarationIdentifier(restParameter)
	}
	if restId == nil {
		return nil
	}

	restType := s.checker.GetTypeOfSymbol(restParameter)
	if restType.IsTupleType() {
		associatedNames := make([]*ast.Node, 0, len(restType.Target().AsTupleType().ElementInfos()))
		for _, elementInfo := range restType.Target().AsTupleType().ElementInfos() {
			labeledElement := elementInfo.LabeledDeclaration()
			associatedNames = append(associatedNames, labeledElement)
		}
		index := pos - paramCount
		if index < len(associatedNames) {
			associatedName := associatedNames[index]
			if associatedName != nil {
				debug.Assert(ast.IsIdentifier(associatedName.Name()))
				var isRestTupleElement bool
				if ast.IsNamedTupleMember(associatedName) {
					isRestTupleElement = associatedName.AsNamedTupleMember().DotDotDotToken != nil
				} else {
					isRestTupleElement = associatedName.AsParameterDeclaration().DotDotDotToken != nil
				}
				return &parameterInfo{
					parameter:       associatedName.Name(),
					name:            associatedName.Name().Text(),
					isRestParameter: isRestTupleElement,
				}
			}
		}

		return nil
	}

	if pos == paramCount {
		return &parameterInfo{
			parameter:       restId,
			name:            restParameter.Name,
			isRestParameter: true,
		}
	}
	return nil
}

func getParameterDeclarationIdentifier(symbol *ast.Symbol) *ast.IdentifierNode {
	if symbol.ValueDeclaration != nil && ast.IsParameter(symbol.ValueDeclaration) && ast.IsIdentifier(symbol.ValueDeclaration.Name()) {
		return symbol.ValueDeclaration.Name()
	}
	return nil
}

func identifierOrAccessExpressionPostfixMatchesParameterName(expr *ast.Expression, parameterName string) bool {
	if ast.IsIdentifier(expr) {
		return expr.Text() == parameterName
	}
	if ast.IsPropertyAccessExpression(expr) {
		return expr.Name().Text() == parameterName
	}
	return false
}

func (s *inlayHintState) leadingCommentsContainsParameterName(node *ast.Node, name string) bool {
	if !scanner.IsIdentifierText(name, s.file.LanguageVariant) {
		return false
	}

	ranges := getLeadingCommentRangesOfNode(node, s.file)
	fileText := s.file.Text()
	for r := range ranges {
		commentText := strings.TrimFunc(fileText[r.Pos():r.End()], func(r rune) bool {
			return unicode.IsSpace(r) || r == '/' || r == '*'
		})
		if commentText == name {
			return true
		}
	}

	return false
}

func (s *inlayHintState) getTypeAnnotationPosition(decl *ast.FunctionLikeDeclaration) int {
	closeParenToken := astnav.FindChildOfKind(decl, ast.KindCloseParenToken, s.file)
	if closeParenToken != nil {
		return closeParenToken.End()
	}
	return decl.ParameterList().End()
}

func isAnyInlayHintEnabled(preferences *lsutil.InlayHintsPreferences) bool {
	return *preferences != lsutil.InlayHintsPreferences{}
}
