package format

import (
	"slices"
	"unicode/utf8"

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

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

func getIndentationForNodeWorker(
	current *ast.Node,
	currentStartLine int,
	currentStartCharacter int,
	ignoreActualIndentationRange *core.TextRange,
	indentationDelta int,
	sourceFile *ast.SourceFile,
	isNextChild bool,
	options *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.
			listLine, _ := getStartLineAndCharacterForNode(firstListChild, sourceFile)
			listIndentsChild := firstListChild != nil && 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.GetECMALineAndCharacterOfPosition(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 *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(child) && slices.Contains(parent.AsCallExpression().Arguments.Nodes, child)) {
		return false
	}
	expressionOfCallExpressionEnd := parent.Expression().End()
	expressionOfCallExpressionEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd)
	return expressionOfCallExpressionEndLine == childStartLine
}

func getActualIndentationForListItem(node *ast.Node, sourceFile *ast.SourceFile, options *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 *FormatCodeSettings) int {
	if list == nil {
		return -1
	}
	line, char := scanner.GetECMALineAndCharacterOfPosition(sourceFile, list.Loc.Pos())
	return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options)
}

func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile *ast.SourceFile, options *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.GetECMALineAndCharacterOfPosition(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 *FormatCodeSettings) int {
	lineStart := scanner.GetECMAPositionOfLineAndCharacter(sourceFile, line, 0)
	return FindFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options)
}

func FindFirstNonWhitespaceColumn(startPos int, endPos int, sourceFile *ast.SourceFile, options *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 *FormatCodeSettings) (character int, column int) {
	character = 0
	column = 0
	text := sourceFile.Text()
	for pos := startPos; pos < endPos; pos++ {
		ch, size := utf8.DecodeRuneInString(text[pos:])
		if size == 0 && ch == utf8.RuneError {
			continue // multibyte character - TODO: recognize non-tab multicolumn characters? ideographic space?
		}
		if !stringutil.IsWhiteSpaceSingleLine(ch) {
			break
		}

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

		character++
	}
	return character, 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.AssertIsDefined(elseKeyword)
		elseKeywordStartLine, _ := getStartLineAndCharacterForNode(elseKeyword, sourceFile)
		return elseKeywordStartLine == childStartLine
	}
	return false
}

func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (line int, character int) {
	return scanner.GetECMALineAndCharacterOfPosition(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.AsTypeReferenceNode().TypeArguments, r, node, sourceFile)
	case ast.KindObjectLiteralExpression:
		return getList(node.AsObjectLiteralExpression().Properties, r, node, sourceFile)
	case ast.KindArrayLiteralExpression:
		return getList(node.AsArrayLiteralExpression().Elements, r, node, sourceFile)
	case ast.KindTypeLiteral:
		return getList(node.AsTypeLiteralNode().Members, 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.KindNamedImports:
		return getList(node.AsNamedImports().Elements, r, node, sourceFile)
	case ast.KindNamedExports:
		return getList(node.AsNamedExports().Elements, r, node, sourceFile)
	case ast.KindObjectBindingPattern, ast.KindArrayBindingPattern:
		return getList(node.AsBindingPattern().Elements, 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()
	}
	next := astnav.FindNextToken(prior, node, sourceFile)
	var nextStart int
	if next == nil {
		nextStart = list.End()
	} else {
		nextStart = next.Pos()
	}
	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.GetECMALineAndCharacterOfPosition(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 *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 *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.GetECMALineAndCharacterOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), parent.Pos()))
			childStartLine, _ := scanner.GetECMALineAndCharacterOfPosition(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.GetECMALineAndCharacterOfPosition(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, _ := getStartLineAndCharacterForNode(parent.AsConditionalExpression().WhenTrue, sourceFile)
			trueEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(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.GetECMALineAndCharacterOfPosition(sourceFile, previousNode.End())
		if childStartLine == lineOfPreviousNode {
			return true
		}
	}
	return false
}
