package format

import (
	"strings"

	"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/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/scanner"
)

///
/// Contexts
///

type (
	optionSelector                  = func(options *FormatCodeSettings) core.Tristate
	anyOptionSelector[T comparable] = func(options *FormatCodeSettings) T
)

func semicolonOption(options *FormatCodeSettings) SemicolonPreference { return options.Semicolons }
func insertSpaceAfterCommaDelimiterOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterCommaDelimiter
}

func insertSpaceAfterSemicolonInForStatementsOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterSemicolonInForStatements
}

func insertSpaceBeforeAndAfterBinaryOperatorsOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceBeforeAndAfterBinaryOperators
}

func insertSpaceAfterConstructorOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterConstructor
}

func insertSpaceAfterKeywordsInControlFlowStatementsOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterKeywordsInControlFlowStatements
}

func insertSpaceAfterFunctionKeywordForAnonymousFunctionsOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterFunctionKeywordForAnonymousFunctions
}

func insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesisOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis
}

func insertSpaceAfterOpeningAndBeforeClosingNonemptyBracketsOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets
}

func insertSpaceAfterOpeningAndBeforeClosingNonemptyBracesOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces
}

func insertSpaceAfterOpeningAndBeforeClosingEmptyBracesOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces
}

func insertSpaceAfterOpeningAndBeforeClosingTemplateStringBracesOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces
}

func insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBracesOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces
}

func insertSpaceAfterTypeAssertionOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceAfterTypeAssertion
}

func insertSpaceBeforeFunctionParenthesisOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceBeforeFunctionParenthesis
}

func placeOpenBraceOnNewLineForFunctionsOption(options *FormatCodeSettings) core.Tristate {
	return options.PlaceOpenBraceOnNewLineForFunctions
}

func placeOpenBraceOnNewLineForControlBlocksOption(options *FormatCodeSettings) core.Tristate {
	return options.PlaceOpenBraceOnNewLineForControlBlocks
}

func insertSpaceBeforeTypeAnnotationOption(options *FormatCodeSettings) core.Tristate {
	return options.InsertSpaceBeforeTypeAnnotation
}

func indentMultiLineObjectLiteralBeginningOnBlankLineOption(options *FormatCodeSettings) core.Tristate {
	return options.IndentMultiLineObjectLiteralBeginningOnBlankLine
}

func indentSwitchCaseOption(options *FormatCodeSettings) core.Tristate {
	return options.IndentSwitchCase
}

func optionEquals[T comparable](optionName anyOptionSelector[T], optionValue T) contextPredicate {
	return func(context *FormattingContext) bool {
		if context.Options == nil {
			return false
		}
		return optionName(context.Options) == optionValue
	}
}

func isOptionEnabled(optionName optionSelector) contextPredicate {
	return func(context *FormattingContext) bool {
		if context.Options == nil {
			return false
		}
		return optionName(context.Options).IsTrue()
	}
}

func isOptionDisabled(optionName optionSelector) contextPredicate {
	return func(context *FormattingContext) bool {
		if context.Options == nil {
			return true
		}
		return optionName(context.Options).IsFalse()
	}
}

func isOptionDisabledOrUndefined(optionName optionSelector) contextPredicate {
	return func(context *FormattingContext) bool {
		if context.Options == nil {
			return true
		}
		return optionName(context.Options).IsFalseOrUnknown()
	}
}

func isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName optionSelector) contextPredicate {
	return func(context *FormattingContext) bool {
		if context.Options == nil {
			return true
		}
		return optionName(context.Options).IsFalseOrUnknown() || context.TokensAreOnSameLine()
	}
}

func isOptionEnabledOrUndefined(optionName optionSelector) contextPredicate {
	return func(context *FormattingContext) bool {
		if context.Options == nil {
			return true
		}
		return optionName(context.Options).IsTrueOrUnknown()
	}
}

func isForContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindForStatement
}

func isNotForContext(context *FormattingContext) bool {
	return !isForContext(context)
}

func isBinaryOpContext(context *FormattingContext) bool {
	switch context.contextNode.Kind {
	case ast.KindBinaryExpression:
		return context.contextNode.AsBinaryExpression().OperatorToken.Kind != ast.KindCommaToken
	case ast.KindConditionalExpression,
		ast.KindConditionalType,
		ast.KindAsExpression,
		ast.KindExportSpecifier,
		ast.KindImportSpecifier,
		ast.KindTypePredicate,
		ast.KindUnionType,
		ast.KindIntersectionType,
		ast.KindSatisfiesExpression:
		return true

	// equals in binding elements func foo([[x, y] = [1, 2]])
	case ast.KindBindingElement:
		// equals in type X = ...
		fallthrough
	case ast.KindTypeAliasDeclaration:
		// equal in import a = module('a');
		fallthrough
	case ast.KindImportEqualsDeclaration:
		// equal in export = 1
		fallthrough
	case ast.KindExportAssignment:
		// equal in let a = 0
		fallthrough
	case ast.KindVariableDeclaration:
		// equal in p = 0
		fallthrough
	case ast.KindParameter,
		ast.KindEnumMember,
		ast.KindPropertyDeclaration,
		ast.KindPropertySignature:
		return context.currentTokenSpan.Kind == ast.KindEqualsToken || context.nextTokenSpan.Kind == ast.KindEqualsToken
	// "in" keyword in for (let x in []) { }
	case ast.KindForInStatement:
		// "in" keyword in [P in keyof T] T[P]
		fallthrough
	case ast.KindTypeParameter:
		return context.currentTokenSpan.Kind == ast.KindInKeyword || context.nextTokenSpan.Kind == ast.KindInKeyword || context.currentTokenSpan.Kind == ast.KindEqualsToken || context.nextTokenSpan.Kind == ast.KindEqualsToken
	// Technically, "of" is not a binary operator, but format it the same way as "in"
	case ast.KindForOfStatement:
		return context.currentTokenSpan.Kind == ast.KindOfKeyword || context.nextTokenSpan.Kind == ast.KindOfKeyword
	}
	return false
}

func isNotBinaryOpContext(context *FormattingContext) bool {
	return !isBinaryOpContext(context)
}

func isNotTypeAnnotationContext(context *FormattingContext) bool {
	return !isTypeAnnotationContext(context)
}

func isTypeAnnotationContext(context *FormattingContext) bool {
	contextKind := context.contextNode.Kind
	return contextKind == ast.KindPropertyDeclaration ||
		contextKind == ast.KindPropertySignature ||
		contextKind == ast.KindParameter ||
		contextKind == ast.KindVariableDeclaration ||
		ast.IsFunctionLikeKind(contextKind)
}

func isOptionalPropertyContext(context *FormattingContext) bool {
	return ast.IsPropertyDeclaration(context.contextNode) && ast.HasQuestionToken(context.contextNode)
}

func isNonOptionalPropertyContext(context *FormattingContext) bool {
	return !isOptionalPropertyContext(context)
}

func isConditionalOperatorContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindConditionalExpression ||
		context.contextNode.Kind == ast.KindConditionalType
}

func isSameLineTokenOrBeforeBlockContext(context *FormattingContext) bool {
	return context.TokensAreOnSameLine() || isBeforeBlockContext(context)
}

func isBraceWrappedContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindObjectBindingPattern ||
		context.contextNode.Kind == ast.KindMappedType ||
		isSingleLineBlockContext(context)
}

// This check is done before an open brace in a control construct, a function, or a typescript block declaration
func isBeforeMultilineBlockContext(context *FormattingContext) bool {
	return isBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine())
}

func isMultilineBlockContext(context *FormattingContext) bool {
	return isBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine())
}

func isSingleLineBlockContext(context *FormattingContext) bool {
	return isBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine())
}

func isBlockContext(context *FormattingContext) bool {
	return nodeIsBlockContext(context.contextNode)
}

func isBeforeBlockContext(context *FormattingContext) bool {
	return nodeIsBlockContext(context.nextTokenParent)
}

// IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children
func nodeIsBlockContext(node *ast.Node) bool {
	if nodeIsTypeScriptDeclWithBlockContext(node) {
		// This means we are in a context that looks like a block to the user, but in the grammar is actually not a node (it's a class, module, enum, object type literal, etc).
		return true
	}

	switch node.Kind {
	case ast.KindBlock,
		ast.KindCaseBlock,
		ast.KindObjectLiteralExpression,
		ast.KindModuleBlock:
		return true
	}

	return false
}

func isFunctionDeclContext(context *FormattingContext) bool {
	switch context.contextNode.Kind {
	case ast.KindFunctionDeclaration,
		ast.KindMethodDeclaration,
		ast.KindMethodSignature:
		// case ast.KindMemberFunctionDeclaration:
		fallthrough
	case ast.KindGetAccessor,
		ast.KindSetAccessor:
		// case ast.KindMethodSignature:
		fallthrough
	case ast.KindCallSignature,
		ast.KindFunctionExpression,
		ast.KindConstructor,
		ast.KindArrowFunction:
		// case ast.KindConstructorDeclaration:
		// case ast.KindSimpleArrowFunctionExpression:
		// case ast.KindParenthesizedArrowFunctionExpression:
		fallthrough
	case ast.KindInterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one
		return true
	}

	return false
}

func isNotFunctionDeclContext(context *FormattingContext) bool {
	return !isFunctionDeclContext(context)
}

func isFunctionDeclarationOrFunctionExpressionContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindFunctionDeclaration || context.contextNode.Kind == ast.KindFunctionExpression
}

func isTypeScriptDeclWithBlockContext(context *FormattingContext) bool {
	return nodeIsTypeScriptDeclWithBlockContext(context.contextNode)
}

func nodeIsTypeScriptDeclWithBlockContext(node *ast.Node) bool {
	switch node.Kind {
	case ast.KindClassDeclaration,
		ast.KindClassExpression,
		ast.KindInterfaceDeclaration,
		ast.KindEnumDeclaration,
		ast.KindTypeLiteral,
		ast.KindModuleDeclaration,
		ast.KindExportDeclaration,
		ast.KindNamedExports,
		ast.KindImportDeclaration,
		ast.KindNamedImports:
		return true
	}

	return false
}

func isAfterCodeBlockContext(context *FormattingContext) bool {
	switch context.currentTokenParent.Kind {
	case ast.KindClassDeclaration,
		ast.KindModuleDeclaration,
		ast.KindEnumDeclaration,
		ast.KindCatchClause,
		ast.KindModuleBlock,
		ast.KindSwitchStatement:
		return true
	case ast.KindBlock:
		blockParent := context.currentTokenParent.Parent
		// In a codefix scenario, we can't rely on parents being set. So just always return true.
		if blockParent == nil || blockParent.Kind != ast.KindArrowFunction && blockParent.Kind != ast.KindFunctionExpression {
			return true
		}
	}
	return false
}

func isControlDeclContext(context *FormattingContext) bool {
	switch context.contextNode.Kind {
	case ast.KindIfStatement,
		ast.KindSwitchStatement,
		ast.KindForStatement,
		ast.KindForInStatement,
		ast.KindForOfStatement,
		ast.KindWhileStatement,
		ast.KindTryStatement,
		ast.KindDoStatement,
		ast.KindWithStatement:
		// TODO
		// case ast.KindElseClause:
		fallthrough
	case ast.KindCatchClause:
		return true

	default:
		return false
	}
}

func isObjectContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindObjectLiteralExpression
}

func isFunctionCallContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindCallExpression
}

func isNewContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindNewExpression
}

func isFunctionCallOrNewContext(context *FormattingContext) bool {
	return isFunctionCallContext(context) || isNewContext(context)
}

func isPreviousTokenNotComma(context *FormattingContext) bool {
	return context.currentTokenSpan.Kind != ast.KindCommaToken
}

func isNextTokenNotCloseBracket(context *FormattingContext) bool {
	return context.nextTokenSpan.Kind != ast.KindCloseBracketToken
}

func isNextTokenNotCloseParen(context *FormattingContext) bool {
	return context.nextTokenSpan.Kind != ast.KindCloseParenToken
}

func isArrowFunctionContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindArrowFunction
}

func isImportTypeContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindImportType
}

func isNonJsxSameLineTokenContext(context *FormattingContext) bool {
	return context.TokensAreOnSameLine() && context.contextNode.Kind != ast.KindJsxText
}

func isNonJsxTextContext(context *FormattingContext) bool {
	return context.contextNode.Kind != ast.KindJsxText
}

func isNonJsxElementOrFragmentContext(context *FormattingContext) bool {
	return context.contextNode.Kind != ast.KindJsxElement && context.contextNode.Kind != ast.KindJsxFragment
}

func isJsxExpressionContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindJsxExpression || context.contextNode.Kind == ast.KindJsxSpreadAttribute
}

func isNextTokenParentJsxAttribute(context *FormattingContext) bool {
	return context.nextTokenParent.Kind == ast.KindJsxAttribute || (context.nextTokenParent.Kind == ast.KindJsxNamespacedName && context.nextTokenParent.Parent.Kind == ast.KindJsxAttribute)
}

func isJsxAttributeContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindJsxAttribute
}

func isNextTokenParentNotJsxNamespacedName(context *FormattingContext) bool {
	return context.nextTokenParent.Kind != ast.KindJsxNamespacedName
}

func isNextTokenParentJsxNamespacedName(context *FormattingContext) bool {
	return context.nextTokenParent.Kind == ast.KindJsxNamespacedName
}

func isJsxSelfClosingElementContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindJsxSelfClosingElement
}

func isNotBeforeBlockInFunctionDeclarationContext(context *FormattingContext) bool {
	return !isFunctionDeclContext(context) && !isBeforeBlockContext(context)
}

func isEndOfDecoratorContextOnSameLine(context *FormattingContext) bool {
	return context.TokensAreOnSameLine() &&
		ast.HasDecorators(context.contextNode) &&
		nodeIsInDecoratorContext(context.currentTokenParent) &&
		!nodeIsInDecoratorContext(context.nextTokenParent)
}

func nodeIsInDecoratorContext(node *ast.Node) bool {
	for node != nil && ast.IsExpression(node) {
		node = node.Parent
	}
	return node != nil && node.Kind == ast.KindDecorator
}

func isStartOfVariableDeclarationList(context *FormattingContext) bool {
	return context.currentTokenParent.Kind == ast.KindVariableDeclarationList &&
		scanner.GetTokenPosOfNode(context.currentTokenParent, context.SourceFile, false) == context.currentTokenSpan.Loc.Pos()
}

func isNotFormatOnEnter(context *FormattingContext) bool {
	return context.FormattingRequestKind != FormatRequestKindFormatOnEnter
}

func isModuleDeclContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindModuleDeclaration
}

func isObjectTypeContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindTypeLiteral // && context.contextNode.parent.Kind != ast.KindInterfaceDeclaration;
}

func isConstructorSignatureContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindConstructSignature
}

func isTypeArgumentOrParameterOrAssertion(token TextRangeWithKind, parent *ast.Node) bool {
	if token.Kind != ast.KindLessThanToken && token.Kind != ast.KindGreaterThanToken {
		return false
	}
	switch parent.Kind {
	case ast.KindTypeReference,
		ast.KindTypeAssertionExpression,
		ast.KindTypeAliasDeclaration,
		ast.KindClassDeclaration,
		ast.KindClassExpression,
		ast.KindInterfaceDeclaration,
		ast.KindFunctionDeclaration,
		ast.KindFunctionExpression,
		ast.KindArrowFunction,
		ast.KindMethodDeclaration,
		ast.KindMethodSignature,
		ast.KindCallSignature,
		ast.KindConstructSignature,
		ast.KindCallExpression,
		ast.KindNewExpression,
		ast.KindExpressionWithTypeArguments:
		return true
	default:
		return false
	}
}

func isTypeArgumentOrParameterOrAssertionContext(context *FormattingContext) bool {
	return isTypeArgumentOrParameterOrAssertion(context.currentTokenSpan, context.currentTokenParent) ||
		isTypeArgumentOrParameterOrAssertion(context.nextTokenSpan, context.nextTokenParent)
}

func isTypeAssertionContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindTypeAssertionExpression
}

func isNonTypeAssertionContext(context *FormattingContext) bool {
	return !isTypeAssertionContext(context)
}

func isVoidOpContext(context *FormattingContext) bool {
	return context.currentTokenSpan.Kind == ast.KindVoidKeyword && context.currentTokenParent.Kind == ast.KindVoidExpression
}

func isYieldOrYieldStarWithOperand(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindYieldExpression && context.contextNode.Expression() != nil
}

func isNonNullAssertionContext(context *FormattingContext) bool {
	return context.contextNode.Kind == ast.KindNonNullExpression
}

func isNotStatementConditionContext(context *FormattingContext) bool {
	return !isStatementConditionContext(context)
}

func isStatementConditionContext(context *FormattingContext) bool {
	switch context.contextNode.Kind {
	case ast.KindIfStatement,
		ast.KindForStatement,
		ast.KindForInStatement,
		ast.KindForOfStatement,
		ast.KindDoStatement,
		ast.KindWhileStatement:
		return true

	default:
		return false
	}
}

func isSemicolonDeletionContext(context *FormattingContext) bool {
	nextTokenKind := context.nextTokenSpan.Kind
	nextTokenStart := context.nextTokenSpan.Loc.Pos()
	if ast.IsTrivia(nextTokenKind) {
		var nextRealToken *ast.Node
		if context.nextTokenParent == context.currentTokenParent {
			// !!! TODO: very different from strada, but strada's logic here is wonky - find the first ancestor without a parent? that's just the source file.
			nextRealToken = astnav.FindNextToken(context.nextTokenParent, context.SourceFile.AsNode(), context.SourceFile)
		} else {
			nextRealToken = lsutil.GetFirstToken(context.nextTokenParent, context.SourceFile)
		}

		if nextRealToken == nil {
			return true
		}
		nextTokenKind = nextRealToken.Kind
		nextTokenStart = scanner.GetTokenPosOfNode(nextRealToken, context.SourceFile, false)
	}

	startLine := scanner.GetECMALineOfPosition(context.SourceFile, context.currentTokenSpan.Loc.Pos())
	endLine := scanner.GetECMALineOfPosition(context.SourceFile, nextTokenStart)
	if startLine == endLine {
		return nextTokenKind == ast.KindCloseBraceToken || nextTokenKind == ast.KindEndOfFile
	}

	if nextTokenKind == ast.KindSemicolonToken &&
		context.currentTokenSpan.Kind == ast.KindSemicolonToken {
		return true
	}

	if nextTokenKind == ast.KindSemicolonClassElement ||
		nextTokenKind == ast.KindSemicolonToken {
		return false
	}

	if context.contextNode.Kind == ast.KindInterfaceDeclaration ||
		context.contextNode.Kind == ast.KindTypeAliasDeclaration {
		// Can't remove semicolon after `foo`; it would parse as a method declaration:
		//
		// interface I {
		//   foo;
		//   () void
		// }
		return context.currentTokenParent.Kind != ast.KindPropertySignature ||
			context.currentTokenParent.Type() != nil ||
			nextTokenKind != ast.KindOpenParenToken
	}

	if ast.IsPropertyDeclaration(context.currentTokenParent) {
		return context.currentTokenParent.Initializer() == nil
	}

	return context.currentTokenParent.Kind != ast.KindForStatement &&
		context.currentTokenParent.Kind != ast.KindEmptyStatement &&
		context.currentTokenParent.Kind != ast.KindSemicolonClassElement &&
		nextTokenKind != ast.KindOpenBracketToken &&
		nextTokenKind != ast.KindOpenParenToken &&
		nextTokenKind != ast.KindPlusToken &&
		nextTokenKind != ast.KindMinusToken &&
		nextTokenKind != ast.KindSlashToken &&
		nextTokenKind != ast.KindRegularExpressionLiteral &&
		nextTokenKind != ast.KindCommaToken &&
		nextTokenKind != ast.KindTemplateExpression &&
		nextTokenKind != ast.KindTemplateHead &&
		nextTokenKind != ast.KindNoSubstitutionTemplateLiteral &&
		nextTokenKind != ast.KindDotToken
}

func isSemicolonInsertionContext(context *FormattingContext) bool {
	return lsutil.PositionIsASICandidate(context.currentTokenSpan.Loc.End(), context.currentTokenParent, context.SourceFile)
}

func isNotPropertyAccessOnIntegerLiteral(context *FormattingContext) bool {
	return !ast.IsPropertyAccessExpression(context.contextNode) ||
		!ast.IsNumericLiteral(context.contextNode.Expression()) ||
		strings.Contains(context.contextNode.Expression().Text(), ".")
}
