package format

import (
	"slices"

	"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/scanner"
)

func rangeIsOnOneLine(node core.TextRange, file *ast.SourceFile) bool {
	startLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, node.Pos())
	endLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, node.End())
	return startLine == endLine
}

func getOpenTokenForList(node *ast.Node, list *ast.NodeList) ast.Kind {
	switch node.Kind {
	case ast.KindConstructor,
		ast.KindFunctionDeclaration,
		ast.KindFunctionExpression,
		ast.KindMethodDeclaration,
		ast.KindMethodSignature,
		ast.KindArrowFunction,
		ast.KindCallSignature,
		ast.KindConstructSignature,
		ast.KindFunctionType,
		ast.KindConstructorType,
		ast.KindGetAccessor,
		ast.KindSetAccessor:
		if node.TypeParameterList() == list {
			return ast.KindLessThanToken
		} else if node.ParameterList() == list {
			return ast.KindOpenParenToken
		}
	case ast.KindCallExpression, ast.KindNewExpression:
		if node.TypeArgumentList() == list {
			return ast.KindLessThanToken
		} else if node.ArgumentList() == list {
			return ast.KindOpenParenToken
		}
	case ast.KindClassDeclaration,
		ast.KindClassExpression,
		ast.KindInterfaceDeclaration,
		ast.KindTypeAliasDeclaration:
		if node.TypeParameterList() == list {
			return ast.KindLessThanToken
		}
	case ast.KindTypeReference,
		ast.KindTaggedTemplateExpression,
		ast.KindTypeQuery,
		ast.KindExpressionWithTypeArguments,
		ast.KindImportType:
		if node.TypeArgumentList() == list {
			return ast.KindLessThanToken
		}
	case ast.KindTypeLiteral:
		return ast.KindOpenBraceToken
	}

	return ast.KindUnknown
}

func getCloseTokenForOpenToken(kind ast.Kind) ast.Kind {
	// TODO: matches strada - seems like it could handle more pairs of braces, though? [] notably missing
	switch kind {
	case ast.KindOpenParenToken:
		return ast.KindCloseParenToken
	case ast.KindLessThanToken:
		return ast.KindGreaterThanToken
	case ast.KindOpenBraceToken:
		return ast.KindCloseBraceToken
	}
	return ast.KindUnknown
}

func GetLineStartPositionForPosition(position int, sourceFile *ast.SourceFile) int {
	lineStarts := scanner.GetECMALineStarts(sourceFile)
	line, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, position)
	return int(lineStarts[line])
}

/**
 * Tests whether `child` is a grammar error on `parent`.
 * In strada, this also checked node arrays, but it is never actually called with one in practice.
 */
func isGrammarError(parent *ast.Node, child *ast.Node) bool {
	if ast.IsTypeParameterDeclaration(parent) {
		return child == parent.AsTypeParameter().Expression
	}
	if ast.IsPropertySignatureDeclaration(parent) {
		return child == parent.AsPropertySignatureDeclaration().Initializer
	}
	if ast.IsPropertyDeclaration(parent) {
		return ast.IsAutoAccessorPropertyDeclaration(parent) && child == parent.AsPropertyDeclaration().PostfixToken && child.Kind == ast.KindQuestionToken
	}
	if ast.IsPropertyAssignment(parent) {
		pa := parent.AsPropertyAssignment()
		mods := pa.Modifiers()
		return child == pa.PostfixToken || (mods != nil && isGrammarErrorElement(&mods.NodeList, child, ast.IsModifierLike))
	}
	if ast.IsShorthandPropertyAssignment(parent) {
		sp := parent.AsShorthandPropertyAssignment()
		mods := sp.Modifiers()
		return child == sp.EqualsToken || child == sp.PostfixToken || (mods != nil && isGrammarErrorElement(&mods.NodeList, child, ast.IsModifierLike))
	}
	if ast.IsMethodDeclaration(parent) {
		return child == parent.AsMethodDeclaration().PostfixToken && child.Kind == ast.KindExclamationToken
	}
	if ast.IsConstructorDeclaration(parent) {
		return child == parent.AsConstructorDeclaration().Type || isGrammarErrorElement(parent.AsConstructorDeclaration().TypeParameters, child, ast.IsTypeParameterDeclaration)
	}
	if ast.IsGetAccessorDeclaration(parent) {
		return isGrammarErrorElement(parent.AsGetAccessorDeclaration().TypeParameters, child, ast.IsTypeParameterDeclaration)
	}
	if ast.IsSetAccessorDeclaration(parent) {
		return child == parent.AsSetAccessorDeclaration().Type || isGrammarErrorElement(parent.AsSetAccessorDeclaration().TypeParameters, child, ast.IsTypeParameterDeclaration)
	}
	if ast.IsNamespaceExportDeclaration(parent) {
		mods := parent.AsNamespaceExportDeclaration().Modifiers()
		return mods != nil && isGrammarErrorElement(&mods.NodeList, child, ast.IsModifierLike)
	}
	return false
}

func isGrammarErrorElement(list *ast.NodeList, child *ast.Node, isPossibleElement func(node *ast.Node) bool) bool {
	if list == nil || len(list.Nodes) == 0 {
		return false
	}
	if !isPossibleElement(child) {
		return false
	}
	return slices.Contains(list.Nodes, child)
}

/**
 * Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment).
 * @param expectedTokenKind The kind of the last token constituting the desired parent node.
 */
func findImmediatelyPrecedingTokenOfKind(end int, expectedTokenKind ast.Kind, sourceFile *ast.SourceFile) *ast.Node {
	precedingToken := astnav.FindPrecedingToken(sourceFile, end)
	if precedingToken == nil || precedingToken.Kind != expectedTokenKind || precedingToken.End() != end {
		return nil
	}
	return precedingToken
}

/**
 * Finds the highest node enclosing `node` at the same list level as `node`
 * and whose end does not exceed `node.end`.
 *
 * Consider typing the following
 * ```
 * let x = 1;
 * while (true) {
 * }
 * ```
 * Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding
 * variable declaration.
 */
func findOutermostNodeWithinListLevel(node *ast.Node) *ast.Node {
	current := node
	for current != nil &&
		current.Parent != nil &&
		current.Parent.End() == node.End() &&
		!isListElement(current.Parent, current) {
		current = current.Parent
	}

	return current
}

// Returns true if node is a element in some list in parent
// i.e. parent is class declaration with the list of members and node is one of members.
func isListElement(parent *ast.Node, node *ast.Node) bool {
	switch parent.Kind {
	case ast.KindClassDeclaration, ast.KindInterfaceDeclaration:
		return node.Loc.ContainedBy(parent.MemberList().Loc)
	case ast.KindModuleDeclaration:
		body := parent.Body()
		return body != nil && body.Kind == ast.KindModuleBlock && node.Loc.ContainedBy(body.StatementList().Loc)
	case ast.KindSourceFile, ast.KindBlock, ast.KindModuleBlock:
		return node.Loc.ContainedBy(parent.StatementList().Loc)
	case ast.KindCatchClause:
		return node.Loc.ContainedBy(parent.AsCatchClause().Block.StatementList().Loc)
	}

	return false
}
