package format

import (
	"iter"
	"slices"
	"unicode/utf8"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/astnav"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/scanner"
	"github.com/microsoft/typescript-go/internal/stringutil"
)

func GetIndentationForNode(n *ast.Node, ignoreActualIndentationRange *core.TextRange, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
	startline, startpos := scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
	return getIndentationForNodeWorker(n, startline, startpos, ignoreActualIndentationRange /*indentationDelta*/, 0, sourceFile /*isNextChild*/, false, options)
}

// GetIndentation computes the expected indentation for a position in a source file.
// This is the Go port of SmartIndenter.getIndentation from TypeScript.
func GetIndentation(position int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings, assumeNewLineBeforeCloseBrace bool) int {
	if position > len(sourceFile.Text()) {
		return options.BaseIndentSize // past EOF
	}

	// no indentation when the indent style is set to none,
	// so we can return fast
	if options.IndentStyle == lsutil.IndentStyleNone {
		return 0
	}

	precedingToken := astnav.FindPrecedingTokenEx(sourceFile, position, nil /*startNode*/, true /*excludeJSDoc*/)

	enclosingCommentRange := getRangeOfEnclosingComment(sourceFile, position, precedingToken)
	if enclosingCommentRange != nil && enclosingCommentRange.Kind == ast.KindMultiLineCommentTrivia {
		return getCommentIndent(sourceFile, position, options, enclosingCommentRange)
	}

	if precedingToken == nil {
		return options.BaseIndentSize
	}

	// no indentation in string/regex/template literals
	if isStringOrRegularExpressionOrTemplateLiteral(precedingToken.Kind) {
		tokenStart := scanner.GetTokenPosOfNode(precedingToken, sourceFile, false)
		if tokenStart <= position && position < precedingToken.End() {
			return 0
		}
	}

	lineAtPosition := scanner.GetECMALineOfPosition(sourceFile, position)

	// indentation is first non-whitespace character in a previous line
	// for block indentation, we should look for a line which contains something that's not
	// whitespace.
	currentToken := astnav.GetTokenAtPosition(sourceFile, position)
	// For object literals, we want indentation to work just like with blocks.
	// If the `{` starts in any position (even in the middle of a line), then
	// the following indentation should treat `{` as the start of that line (including leading whitespace).
	// ```
	//     const a: { x: undefined, y: undefined } = {}       // leading 4 whitespaces and { starts in the middle of line
	// ->
	//     const a: { x: undefined, y: undefined } = {
	//         x: undefined,
	//         y: undefined,
	//     }
	// ---------------------
	//     const a: {x : undefined, y: undefined } =
	//      {}
	// ->
	//     const a: { x: undefined, y: undefined } =
	//      {                                                  // leading 5 whitespaces and { starts at 6 column
	//          x: undefined,
	//          y: undefined,
	//      }
	// ```
	isObjectLiteral := currentToken.Kind == ast.KindOpenBraceToken && currentToken.Parent != nil && currentToken.Parent.Kind == ast.KindObjectLiteralExpression
	if options.IndentStyle == lsutil.IndentStyleBlock || isObjectLiteral {
		return getBlockIndent(sourceFile, position, options)
	}

	if precedingToken.Kind == ast.KindCommaToken && precedingToken.Parent != nil && precedingToken.Parent.Kind != ast.KindBinaryExpression {
		// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
		actualIndentation := getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options)
		if actualIndentation != -1 {
			return actualIndentation
		}
	}

	containerList := getListByPosition(position, precedingToken.Parent, sourceFile)
	// use list position if the preceding token is before any list items
	if containerList != nil && !precedingToken.Loc.ContainedBy(containerList.Loc) {
		useTheSameBaseIndentation := currentToken.Parent != nil && (currentToken.Parent.Kind == ast.KindFunctionExpression || currentToken.Parent.Kind == ast.KindArrowFunction)
		indentSize := 0
		if !useTheSameBaseIndentation {
			indentSize = options.IndentSize
		}
		res := getActualIndentationForListStartLine(containerList, sourceFile, options)
		if res == -1 {
			return indentSize
		}
		return res + indentSize
	}

	return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options)
}

func getCommentIndent(sourceFile *ast.SourceFile, position int, options lsutil.FormatCodeSettings, enclosingCommentRange *ast.CommentRange) int {
	previousLine := scanner.GetECMALineOfPosition(sourceFile, position) - 1
	commentStartLine := scanner.GetECMALineOfPosition(sourceFile, enclosingCommentRange.Pos())

	debug.Assert(commentStartLine >= 0, "commentStartLine >= 0")

	if previousLine <= commentStartLine {
		lineStarts := scanner.GetECMALineStarts(sourceFile)
		return FindFirstNonWhitespaceColumn(int(lineStarts[commentStartLine]), position, sourceFile, options)
	}

	lineStarts := scanner.GetECMALineStarts(sourceFile)
	startPositionOfLine := int(lineStarts[previousLine])
	character, column := findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options)

	if column == 0 {
		return column
	}

	firstNonWhitespaceCharacterCode := sourceFile.Text()[startPositionOfLine+character]
	if firstNonWhitespaceCharacterCode == '*' {
		return column - 1
	}
	return column
}

func getLeadingCommentRangesOfNode(node *ast.Node, file *ast.SourceFile) iter.Seq[ast.CommentRange] {
	if node.Kind == ast.KindJsxText {
		return nil
	}
	return scanner.GetLeadingCommentRanges(&ast.NodeFactory{}, file.Text(), node.Pos())
}

func getRangeOfEnclosingComment(
	sourceFile *ast.SourceFile,
	position int,
	precedingToken *ast.Node,
) *ast.CommentRange {
	tokenAtPosition := astnav.GetTokenAtPosition(sourceFile, position)
	jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc)
	if jsdoc != nil {
		tokenAtPosition = jsdoc.Parent
	}
	tokenStart := astnav.GetStartOfNode(tokenAtPosition, sourceFile, false /*includeJSDoc*/)
	if tokenStart <= position && position < tokenAtPosition.End() {
		return nil
	}

	// Between two consecutive tokens, all comments are either trailing on the former
	// or leading on the latter (and none are in both lists).
	var trailingRangesOfPreviousToken iter.Seq[ast.CommentRange]
	if precedingToken != nil {
		trailingRangesOfPreviousToken = scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, sourceFile.Text(), precedingToken.End())
	}
	leadingRangesOfNextToken := getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile)
	commentRanges := core.ConcatenateSeq(trailingRangesOfPreviousToken, leadingRangesOfNextToken)
	for commentRange := range commentRanges {
		if commentRange.ContainsExclusive(position) ||
			position == commentRange.End() &&
				(commentRange.Kind == ast.KindSingleLineCommentTrivia || position == len(sourceFile.Text())) {
			return &commentRange
		}
	}
	return nil
}

func getBlockIndent(sourceFile *ast.SourceFile, position int, options lsutil.FormatCodeSettings) int {
	// move backwards until we find a line with a non-whitespace character,
	// then find the first non-whitespace character for that line.
	current := position
	for current > 0 {
		ch, size := utf8.DecodeRuneInString(sourceFile.Text()[current:])
		if !stringutil.IsWhiteSpaceLike(ch) {
			break
		}
		current -= size
	}

	lineStart := GetLineStartPositionForPosition(current, sourceFile)
	return FindFirstNonWhitespaceColumn(lineStart, current, sourceFile, options)
}

func getActualIndentationForListItemBeforeComma(commaToken *ast.Node, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
	// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
	if commaToken.Parent == nil {
		return -1
	}
	containingList := GetContainingList(commaToken, sourceFile)
	if containingList == nil {
		return -1
	}
	commaIndex := core.FindIndex(containingList.Nodes, func(n *ast.Node) bool { return n == commaToken })
	if commaIndex > 0 {
		return deriveActualIndentationFromList(containingList, commaIndex-1, sourceFile, options)
	}
	return -1
}

type nextTokenKind int

const (
	nextTokenKindUnknown    nextTokenKind = 0
	nextTokenKindOpenBrace  nextTokenKind = 1
	nextTokenKindCloseBrace nextTokenKind = 2
)

func nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken *ast.Node, current *ast.Node, lineAtPosition int, sourceFile *ast.SourceFile) nextTokenKind {
	nextToken := astnav.FindNextToken(precedingToken, current, sourceFile)
	if nextToken == nil {
		return nextTokenKindUnknown
	}

	if nextToken.Kind == ast.KindOpenBraceToken {
		// open braces are always indented at the parent level
		return nextTokenKindOpenBrace
	} else if nextToken.Kind == ast.KindCloseBraceToken {
		// close braces are indented at the parent level if they are located on the same line with cursor
		nextTokenStartLine := getStartLineForNode(nextToken, sourceFile)
		if lineAtPosition == nextTokenStartLine {
			return nextTokenKindCloseBrace
		}
		return nextTokenKindUnknown
	}

	return nextTokenKindUnknown
}

func getSmartIndent(sourceFile *ast.SourceFile, position int, precedingToken *ast.Node, lineAtPosition int, assumeNewLineBeforeCloseBrace bool, options lsutil.FormatCodeSettings) int {
	// try to find node that can contribute to indentation and includes 'position' starting from 'precedingToken'
	// if such node is found - compute initial indentation for 'position' inside this node
	var previous *ast.Node
	current := precedingToken

	for current != nil {
		if lsutil.PositionBelongsToNode(current, position, sourceFile) && ShouldIndentChildNode(options, current, previous, sourceFile, true) {
			currentStartLine, currentStartChar := getStartLineAndCharacterForNode(current, sourceFile)
			ntk := nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile)
			var indentationDelta int
			if ntk != nextTokenKindUnknown {
				// handle cases when codefix is about to be inserted before the close brace
				if assumeNewLineBeforeCloseBrace && ntk == nextTokenKindCloseBrace {
					indentationDelta = options.IndentSize
				}
				// else 0
			} else {
				if lineAtPosition != currentStartLine {
					indentationDelta = options.IndentSize
				}
			}
			return getIndentationForNodeWorker(current, currentStartLine, currentStartChar, nil, indentationDelta, sourceFile, true, options)
		}

		// check if current node is a list item - if yes, take indentation from it
		// do not consider parent-child line sharing yet:
		// function foo(a
		//    | preceding node 'a' does share line with its parent but indentation is expected
		actualIndentation := getActualIndentationForListItem(current, sourceFile, options, true /*listIndentsChild*/)
		if actualIndentation != -1 {
			return actualIndentation
		}

		previous = current
		current = current.Parent
	}
	// no parent was found - return the base indentation of the SourceFile
	return options.BaseIndentSize
}

func getIndentationForNodeWorker(
	current *ast.Node,
	currentStartLine int,
	currentStartCharacter int,
	ignoreActualIndentationRange *core.TextRange,
	indentationDelta int,
	sourceFile *ast.SourceFile,
	isNextChild bool,
	options lsutil.FormatCodeSettings,
) int {
	parent := current.Parent

	// Walk up the tree and collect indentation for parent-child node pairs. Indentation is not added if
	// * parent and child nodes start on the same line, or
	// * parent is an IfStatement and child starts on the same line as an 'else clause'.
	for parent != nil {
		useActualIndentation := true
		if ignoreActualIndentationRange != nil {
			start := scanner.GetTokenPosOfNode(current, sourceFile, false)
			useActualIndentation = start < ignoreActualIndentationRange.Pos() || start > ignoreActualIndentationRange.End()
		}

		containingListOrParentStartLine, containingListOrParentStartCharacter := getContainingListOrParentStart(parent, current, sourceFile)
		parentAndChildShareLine := containingListOrParentStartLine == currentStartLine ||
			childStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStartLine, sourceFile)

		if useActualIndentation {
			// check if current node is a list item - if yes, take indentation from it
			var firstListChild *ast.Node
			containerList := GetContainingList(current, sourceFile)
			if containerList != nil {
				firstListChild = core.FirstOrNil(containerList.Nodes)
			}
			// A list indents its children if the children begin on a later line than the list itself:
			//
			// f1(               L0 - List start
			//   {               L1 - First child start: indented, along with all other children
			//     prop: 0
			//   },
			//   {
			//     prop: 1
			//   }
			// )
			//
			// f2({             L0 - List start and first child start: children are not indented.
			//   prop: 0             Object properties are indented only one level, because the list
			// }, {                  itself contributes nothing.
			//   prop: 1        L3 - The indentation of the second object literal is best understood by
			// })                    looking at the relationship between the list and *first* list item.
			var listIndentsChild bool
			if firstListChild != nil {
				listLine := getStartLineForNode(firstListChild, sourceFile)
				listIndentsChild = listLine > containingListOrParentStartLine
			}
			actualIndentation := getActualIndentationForListItem(current, sourceFile, options, listIndentsChild)
			if actualIndentation != -1 {
				return actualIndentation + indentationDelta
			}

			// try to fetch actual indentation for current node from source text
			actualIndentation = getActualIndentationForNode(current, parent, currentStartLine, currentStartCharacter, parentAndChildShareLine, sourceFile, options)
			if actualIndentation != -1 {
				return actualIndentation + indentationDelta
			}
		}

		// increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line
		if ShouldIndentChildNode(options, parent, current, sourceFile, isNextChild) && !parentAndChildShareLine {
			indentationDelta += options.IndentSize
		}

		// In our AST, a call argument's `parent` is the call-expression, not the argument list.
		// We would like to increase indentation based on the relationship between an argument and its argument-list,
		// so we spoof the starting position of the (parent) call-expression to match the (non-parent) argument-list.
		// But, the spoofed start-value could then cause a problem when comparing the start position of the call-expression
		// to *its* parent (in the case of an iife, an expression statement), adding an extra level of indentation.
		//
		// Instead, when at an argument, we unspoof the starting position of the enclosing call expression
		// *after* applying indentation for the argument.

		useTrueStart := isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStartLine, sourceFile)

		current = parent
		parent = current.Parent

		if useTrueStart {
			currentStartLine, currentStartCharacter = scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(current, sourceFile, false))
		} else {
			currentStartLine = containingListOrParentStartLine
			currentStartCharacter = containingListOrParentStartCharacter
		}
	}

	return indentationDelta + options.BaseIndentSize
}

/*
* Function returns -1 if actual indentation for node should not be used (i.e because node is nested expression)
 */
func getActualIndentationForNode(current *ast.Node, parent *ast.Node, cuurentLine int, currentChar int, parentAndChildShareLine bool, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
	// actual indentation is used for statements\declarations if one of cases below is true:
	// - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually
	// - parent and child are not on the same line
	useActualIndentation := (ast.IsDeclaration(current) || ast.IsStatementButNotDeclaration(current)) && (parent.Kind == ast.KindSourceFile || !parentAndChildShareLine)

	if !useActualIndentation {
		return -1
	}

	return findColumnForFirstNonWhitespaceCharacterInLine(cuurentLine, currentChar, sourceFile, options)
}

func isArgumentAndStartLineOverlapsExpressionBeingCalled(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
	if !(ast.IsCallExpression(parent) && slices.Contains(parent.Arguments(), child)) {
		return false
	}
	expressionOfCallExpressionEnd := parent.Expression().End()
	expressionOfCallExpressionEndLine := scanner.GetECMALineOfPosition(sourceFile, expressionOfCallExpressionEnd)
	return expressionOfCallExpressionEndLine == childStartLine
}

func getActualIndentationForListItem(node *ast.Node, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings, listIndentsChild bool) int {
	if node.Parent != nil && node.Parent.Kind == ast.KindVariableDeclarationList {
		// VariableDeclarationList has no wrapping tokens
		return -1
	}
	containingList := GetContainingList(node, sourceFile)
	if containingList != nil {
		index := core.FindIndex(containingList.Nodes, func(e *ast.Node) bool { return e == node })
		if index != -1 {
			result := deriveActualIndentationFromList(containingList, index, sourceFile, options)
			if result != -1 {
				return result
			}
		}
		delta := 0
		if listIndentsChild {
			delta = options.IndentSize
		}
		res := getActualIndentationForListStartLine(containingList, sourceFile, options)
		if res == -1 {
			return delta
		}
		return res + delta
	}
	return -1
}

func getActualIndentationForListStartLine(list *ast.NodeList, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
	if list == nil {
		return -1
	}
	line, char := scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, list.Loc.Pos())
	return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options)
}

func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
	debug.Assert(list != nil && index >= 0 && index < len(list.Nodes))

	node := list.Nodes[index]

	// walk toward the start of the list starting from current node and check if the line is the same for all items.
	// if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i]

	line, char := getStartLineAndCharacterForNode(node, sourceFile)

	for i := index; i >= 0; i-- {
		if list.Nodes[i].Kind == ast.KindCommaToken {
			continue
		}
		// skip list items that ends on the same line with the current list element
		prevEndLine := scanner.GetECMALineOfPosition(sourceFile, list.Nodes[i].End())
		if prevEndLine != line {
			return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options)
		}

		line, char = getStartLineAndCharacterForNode(list.Nodes[i], sourceFile)
	}
	return -1
}

func findColumnForFirstNonWhitespaceCharacterInLine(line int, char int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
	lineStart := scanner.GetECMAPositionOfLineAndByteOffset(sourceFile, line, 0)
	return FindFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options)
}

func FindFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) int {
	_, col := findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options)
	return col
}

/**
* Character is the actual index of the character since the beginning of the line.
* Column - position of the character after expanding tabs to spaces.
* "0\t2$"
* value of 'character' for '$' is 3
* value of 'column' for '$' is 6 (assuming that tab size is 4)
 */
func findFirstNonWhitespaceCharacterAndColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options lsutil.FormatCodeSettings) (character int, column int) {
	column = 0
	text := sourceFile.Text()
	pos := startPos
	for pos < endPos {
		ch, size := utf8.DecodeRuneInString(text[pos:])
		if !stringutil.IsWhiteSpaceSingleLine(ch) {
			break
		}

		if ch == '\t' {
			if options.TabSize > 0 {
				column += options.TabSize + (column % options.TabSize)
			}
		} else {
			column++
		}

		pos += size
	}
	return pos - startPos, column
}

func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
	if parent.Kind == ast.KindIfStatement && parent.AsIfStatement().ElseStatement == child {
		elseKeyword := astnav.FindPrecedingToken(sourceFile, child.Pos())
		debug.Assert(elseKeyword != nil)
		elseKeywordStartLine := getStartLineForNode(elseKeyword, sourceFile)
		return elseKeywordStartLine == childStartLine
	}
	return false
}

func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (line int, character int) {
	return scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
}

func getStartLineForNode(n *ast.Node, sourceFile *ast.SourceFile) int {
	return scanner.GetECMALineOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false))
}

func GetContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
	if node.Parent == nil {
		return nil
	}
	return getListByRange(scanner.GetTokenPosOfNode(node, sourceFile, false), node.End(), node.Parent, sourceFile)
}

func getListByPosition(pos int, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
	if node == nil {
		return nil
	}
	return getListByRange(pos, pos, node, sourceFile)
}

func getListByRange(start int, end int, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
	r := core.NewTextRange(start, end)
	switch node.Kind {
	case ast.KindTypeReference:
		return getList(node.TypeArgumentList(), r, node, sourceFile)
	case ast.KindObjectLiteralExpression:
		return getList(node.PropertyList(), r, node, sourceFile)
	case ast.KindArrayLiteralExpression:
		return getList(node.ElementList(), r, node, sourceFile)
	case ast.KindTypeLiteral:
		return getList(node.MemberList(), r, node, sourceFile)
	case ast.KindFunctionDeclaration,
		ast.KindFunctionExpression,
		ast.KindArrowFunction,
		ast.KindMethodDeclaration,
		ast.KindMethodSignature,
		ast.KindCallSignature,
		ast.KindConstructor,
		ast.KindConstructorType,
		ast.KindConstructSignature:
		tpl := getList(node.TypeParameterList(), r, node, sourceFile)
		if tpl != nil {
			return tpl
		}
		return getList(node.ParameterList(), r, node, sourceFile)
	case ast.KindGetAccessor:
		return getList(node.ParameterList(), r, node, sourceFile)
	case ast.KindClassDeclaration,
		ast.KindClassExpression,
		ast.KindInterfaceDeclaration,
		ast.KindTypeAliasDeclaration,
		ast.KindJSDocTemplateTag:
		return getList(node.TypeParameterList(), r, node, sourceFile)
	case ast.KindNewExpression, ast.KindCallExpression:
		l := getList(node.TypeArgumentList(), r, node, sourceFile)
		if l != nil {
			return l
		}
		return getList(node.ArgumentList(), r, node, sourceFile)
	case ast.KindVariableDeclarationList:
		return getList(node.AsVariableDeclarationList().Declarations, r, node, sourceFile)
	case ast.KindObjectBindingPattern, ast.KindArrayBindingPattern, ast.KindNamedImports, ast.KindNamedExports:
		return getList(node.ElementList(), r, node, sourceFile)
	}
	return nil // TODO: should this be a panic? It isn't in strada.
}

func getList(list *ast.NodeList, r core.TextRange, node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList {
	if list == nil {
		return nil
	}
	if r.ContainedBy(getVisualListRange(node, list.Loc, sourceFile)) {
		return list
	}
	return nil
}

func getVisualListRange(node *ast.Node, list core.TextRange, sourceFile *ast.SourceFile) core.TextRange {
	// In strada, this relied on the services .getChildren method, which manifested synthetic token nodes
	// _however_, the logic boils down to "find the child with the matching span and adjust its start to the
	// previous (possibly token) child's end and its end to the token start of the following element" - basically
	// expanding the range to encompass all the neighboring non-token trivia
	// Now, we perform that logic with the scanner instead
	prior := astnav.FindPrecedingToken(sourceFile, list.Pos())
	var priorEnd int
	if prior == nil {
		priorEnd = list.Pos()
	} else {
		priorEnd = prior.End()
	}
	// Find the token that starts at or after list.End() using the scanner
	scan := scanner.GetScannerForSourceFile(sourceFile, list.End())
	var nextStart int
	if scan.Token() == ast.KindEndOfFile {
		nextStart = list.End()
	} else {
		nextStart = scan.TokenStart()
	}
	return core.NewTextRange(priorEnd, nextStart)
}

func getContainingListOrParentStart(parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile) (line int, character int) {
	containingList := GetContainingList(child, sourceFile)
	var startPos int
	if containingList != nil {
		startPos = containingList.Loc.Pos()
	} else {
		startPos = scanner.GetTokenPosOfNode(parent, sourceFile, false)
	}
	return scanner.GetECMALineAndByteOffsetOfPosition(sourceFile, startPos)
}

func isControlFlowEndingStatement(kind ast.Kind, parentKind ast.Kind) bool {
	switch kind {
	case ast.KindReturnStatement, ast.KindThrowStatement, ast.KindContinueStatement, ast.KindBreakStatement:
		return parentKind != ast.KindBlock
	default:
		return false
	}
}

/**
* True when the parent node should indent the given child by an explicit rule.
* @param isNextChild If true, we are judging indent of a hypothetical child *after* this one, not the current child.
 */
func ShouldIndentChildNode(settings lsutil.FormatCodeSettings, parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile, isNextChildArg ...bool) bool {
	isNextChild := false
	if len(isNextChildArg) > 0 {
		isNextChild = isNextChildArg[0]
	}

	return NodeWillIndentChild(settings, parent, child, sourceFile, false) && !(isNextChild && child != nil && isControlFlowEndingStatement(child.Kind, parent.Kind))
}

func NodeWillIndentChild(settings lsutil.FormatCodeSettings, parent *ast.Node, child *ast.Node, sourceFile *ast.SourceFile, indentByDefault bool) bool {
	childKind := ast.KindUnknown
	if child != nil {
		childKind = child.Kind
	}

	switch parent.Kind {
	case ast.KindExpressionStatement,
		ast.KindClassDeclaration,
		ast.KindClassExpression,
		ast.KindInterfaceDeclaration,
		ast.KindEnumDeclaration,
		ast.KindTypeAliasDeclaration,
		ast.KindArrayLiteralExpression,
		ast.KindBlock,
		ast.KindModuleBlock,
		ast.KindObjectLiteralExpression,
		ast.KindTypeLiteral,
		ast.KindMappedType,
		ast.KindTupleType,
		ast.KindParenthesizedExpression,
		ast.KindPropertyAccessExpression,
		ast.KindCallExpression,
		ast.KindNewExpression,
		ast.KindVariableStatement,
		ast.KindExportAssignment,
		ast.KindReturnStatement,
		ast.KindConditionalExpression,
		ast.KindArrayBindingPattern,
		ast.KindObjectBindingPattern,
		ast.KindJsxOpeningElement,
		ast.KindJsxOpeningFragment,
		ast.KindJsxSelfClosingElement,
		ast.KindJsxExpression,
		ast.KindMethodSignature,
		ast.KindCallSignature,
		ast.KindConstructSignature,
		ast.KindParameter,
		ast.KindFunctionType,
		ast.KindConstructorType,
		ast.KindParenthesizedType,
		ast.KindTaggedTemplateExpression,
		ast.KindAwaitExpression,
		ast.KindNamedExports,
		ast.KindNamedImports,
		ast.KindExportSpecifier,
		ast.KindImportSpecifier,
		ast.KindPropertyDeclaration,
		ast.KindCaseClause,
		ast.KindDefaultClause:
		return true
	case ast.KindCaseBlock:
		return settings.IndentSwitchCase.IsTrueOrUnknown()
	case ast.KindVariableDeclaration, ast.KindPropertyAssignment, ast.KindBinaryExpression:
		if settings.IndentMultiLineObjectLiteralBeginningOnBlankLine.IsFalseOrUnknown() && sourceFile != nil && childKind == ast.KindObjectLiteralExpression {
			return rangeIsOnOneLine(child.Loc, sourceFile)
		}
		if parent.Kind == ast.KindBinaryExpression && sourceFile != nil && childKind == ast.KindJsxElement {
			parentStartLine := scanner.GetECMALineOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), parent.Pos()))
			childStartLine := scanner.GetECMALineOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), child.Pos()))
			return parentStartLine != childStartLine
		}
		if parent.Kind != ast.KindBinaryExpression {
			return true
		}
		return indentByDefault
	case ast.KindDoStatement,
		ast.KindWhileStatement,
		ast.KindForInStatement,
		ast.KindForOfStatement,
		ast.KindForStatement,
		ast.KindIfStatement,
		ast.KindFunctionDeclaration,
		ast.KindFunctionExpression,
		ast.KindMethodDeclaration,
		ast.KindConstructor,
		ast.KindGetAccessor,
		ast.KindSetAccessor:
		return childKind != ast.KindBlock
	case ast.KindArrowFunction:
		if sourceFile != nil && childKind == ast.KindParenthesizedExpression {
			return rangeIsOnOneLine(child.Loc, sourceFile)
		}
		return childKind != ast.KindBlock
	case ast.KindExportDeclaration:
		return childKind != ast.KindNamedExports
	case ast.KindImportDeclaration:
		return childKind != ast.KindImportClause || (child.AsImportClause().NamedBindings != nil && child.AsImportClause().NamedBindings.Kind != ast.KindNamedImports)
	case ast.KindJsxElement:
		return childKind != ast.KindJsxClosingElement
	case ast.KindJsxFragment:
		return childKind != ast.KindJsxClosingFragment
	case ast.KindIntersectionType, ast.KindUnionType, ast.KindSatisfiesExpression:
		if childKind == ast.KindTypeLiteral || childKind == ast.KindTupleType || childKind == ast.KindMappedType {
			return false
		}
		return indentByDefault
	case ast.KindTryStatement:
		if childKind == ast.KindBlock {
			return false
		}
		return indentByDefault
	}

	// No explicit rule for given nodes so the result will follow the default value argument
	return indentByDefault
}

// A multiline conditional typically increases the indentation of its whenTrue and whenFalse children:
//
// condition
//
//	? whenTrue
//	: whenFalse;
//
// However, that indentation does not apply if the subexpressions themselves span multiple lines,
// applying their own indentation:
//
//	(() => {
//	  return complexCalculationForCondition();
//	})() ? {
//
//	  whenTrue: 'multiline object literal'
//	} : (
//
//	whenFalse('multiline parenthesized expression')
//
// );
//
// In these cases, we must discard the indentation increase that would otherwise be applied to the
// whenTrue and whenFalse children to avoid double-indenting their contents. To identify this scenario,
// we check for the whenTrue branch beginning on the line that the condition ends, and the whenFalse
// branch beginning on the line that the whenTrue branch ends.
func childIsUnindentedBranchOfConditionalExpression(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
	if parent.Kind == ast.KindConditionalExpression && (child == parent.AsConditionalExpression().WhenTrue || child == parent.AsConditionalExpression().WhenFalse) {
		conditionEndLine := scanner.GetECMALineOfPosition(sourceFile, parent.AsConditionalExpression().Condition.End())
		if child == parent.AsConditionalExpression().WhenTrue {
			return childStartLine == conditionEndLine
		} else {
			// On the whenFalse side, we have to look at the whenTrue side, because if that one was
			// indented, whenFalse must also be indented:
			//
			// const y = true
			//   ? 1 : (          L1: whenTrue indented because it's on a new line
			//     0              L2: indented two stops, one because whenTrue was indented
			//   );                   and one because of the parentheses spanning multiple lines
			trueStartLine := getStartLineForNode(parent.AsConditionalExpression().WhenTrue, sourceFile)
			trueEndLine := scanner.GetECMALineOfPosition(sourceFile, parent.AsConditionalExpression().WhenTrue.End())
			return conditionEndLine == trueStartLine && trueEndLine == childStartLine
		}
	}
	return false
}

func argumentStartsOnSameLineAsPreviousArgument(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool {
	if ast.IsCallExpression(parent) || ast.IsNewExpression(parent) {
		if len(parent.Arguments()) == 0 {
			return false
		}
		currentIndex := core.FindIndex(parent.Arguments(), func(n *ast.Node) bool { return n == child })
		if currentIndex == -1 {
			// If it's not one of the arguments, don't look past this
			return false
		}
		if currentIndex == 0 {
			return false // Can't look at previous node if first
		}

		previousNode := parent.Arguments()[currentIndex-1]
		lineOfPreviousNode := scanner.GetECMALineOfPosition(sourceFile, previousNode.End())
		if childStartLine == lineOfPreviousNode {
			return true
		}
	}
	return false
}
