package ls

import (
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
)

type ScriptElementKind string

const (
	ScriptElementKindUnknown ScriptElementKind = ""
	ScriptElementKindWarning ScriptElementKind = "warning"
	// predefined type (void) or keyword (class)
	ScriptElementKindKeyword ScriptElementKind = "keyword"
	// top level script node
	ScriptElementKindScriptElement ScriptElementKind = "script"
	// module foo {}
	ScriptElementKindModuleElement ScriptElementKind = "module"
	// class X {}
	ScriptElementKindClassElement ScriptElementKind = "class"
	// var x = class X {}
	ScriptElementKindLocalClassElement ScriptElementKind = "local class"
	// interface Y {}
	ScriptElementKindInterfaceElement ScriptElementKind = "interface"
	// type T = ...
	ScriptElementKindTypeElement ScriptElementKind = "type"
	// enum E {}
	ScriptElementKindEnumElement       ScriptElementKind = "enum"
	ScriptElementKindEnumMemberElement ScriptElementKind = "enum member"
	// Inside module and script only.
	// const v = ...
	ScriptElementKindVariableElement ScriptElementKind = "var"
	// Inside function.
	ScriptElementKindLocalVariableElement ScriptElementKind = "local var"
	// using foo = ...
	ScriptElementKindVariableUsingElement ScriptElementKind = "using"
	// await using foo = ...
	ScriptElementKindVariableAwaitUsingElement ScriptElementKind = "await using"
	// Inside module and script only.
	// function f() {}
	ScriptElementKindFunctionElement ScriptElementKind = "function"
	// Inside function.
	ScriptElementKindLocalFunctionElement ScriptElementKind = "local function"
	// class X { [public|private]* foo() {} }
	ScriptElementKindMemberFunctionElement ScriptElementKind = "method"
	// class X { [public|private]* [get|set] foo:number; }
	ScriptElementKindMemberGetAccessorElement ScriptElementKind = "getter"
	ScriptElementKindMemberSetAccessorElement ScriptElementKind = "setter"
	// class X { [public|private]* foo:number; }
	// interface Y { foo:number; }
	ScriptElementKindMemberVariableElement ScriptElementKind = "property"
	// class X { [public|private]* accessor foo: number; }
	ScriptElementKindMemberAccessorVariableElement ScriptElementKind = "accessor"
	// class X { constructor() { } }
	// class X { static { } }
	ScriptElementKindConstructorImplementationElement ScriptElementKind = "constructor"
	// interface Y { ():number; }
	ScriptElementKindCallSignatureElement ScriptElementKind = "call"
	// interface Y { []:number; }
	ScriptElementKindIndexSignatureElement ScriptElementKind = "index"
	// interface Y { new():Y; }
	ScriptElementKindConstructSignatureElement ScriptElementKind = "construct"
	// function foo(*Y*: string)
	ScriptElementKindParameterElement     ScriptElementKind = "parameter"
	ScriptElementKindTypeParameterElement ScriptElementKind = "type parameter"
	ScriptElementKindPrimitiveType        ScriptElementKind = "primitive type"
	ScriptElementKindLabel                ScriptElementKind = "label"
	ScriptElementKindAlias                ScriptElementKind = "alias"
	ScriptElementKindConstElement         ScriptElementKind = "const"
	ScriptElementKindLetElement           ScriptElementKind = "let"
	ScriptElementKindDirectory            ScriptElementKind = "directory"
	ScriptElementKindExternalModuleName   ScriptElementKind = "external module name"
	// String literal
	ScriptElementKindString ScriptElementKind = "string"
	// Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}"
	ScriptElementKindLink ScriptElementKind = "link"
	// Jsdoc @link: in `{@link C link text}`, the entity name "C"
	ScriptElementKindLinkName ScriptElementKind = "link name"
	// Jsdoc @link: in `{@link C link text}`, the link text "link text"
	ScriptElementKindLinkText ScriptElementKind = "link text"
)

type ScriptElementKindModifier string

const (
	ScriptElementKindModifierNone       ScriptElementKindModifier = ""
	ScriptElementKindModifierPublic     ScriptElementKindModifier = "public"
	ScriptElementKindModifierPrivate    ScriptElementKindModifier = "private"
	ScriptElementKindModifierProtected  ScriptElementKindModifier = "protected"
	ScriptElementKindModifierExported   ScriptElementKindModifier = "export"
	ScriptElementKindModifierAmbient    ScriptElementKindModifier = "declare"
	ScriptElementKindModifierStatic     ScriptElementKindModifier = "static"
	ScriptElementKindModifierAbstract   ScriptElementKindModifier = "abstract"
	ScriptElementKindModifierOptional   ScriptElementKindModifier = "optional"
	ScriptElementKindModifierDeprecated ScriptElementKindModifier = "deprecated"
	ScriptElementKindModifierDts        ScriptElementKindModifier = ".d.ts"
	ScriptElementKindModifierTs         ScriptElementKindModifier = ".ts"
	ScriptElementKindModifierTsx        ScriptElementKindModifier = ".tsx"
	ScriptElementKindModifierJs         ScriptElementKindModifier = ".js"
	ScriptElementKindModifierJsx        ScriptElementKindModifier = ".jsx"
	ScriptElementKindModifierJson       ScriptElementKindModifier = ".json"
	ScriptElementKindModifierDmts       ScriptElementKindModifier = ".d.mts"
	ScriptElementKindModifierMts        ScriptElementKindModifier = ".mts"
	ScriptElementKindModifierMjs        ScriptElementKindModifier = ".mjs"
	ScriptElementKindModifierDcts       ScriptElementKindModifier = ".d.cts"
	ScriptElementKindModifierCts        ScriptElementKindModifier = ".cts"
	ScriptElementKindModifierCjs        ScriptElementKindModifier = ".cjs"
)

var fileExtensionKindModifiers = []ScriptElementKindModifier{
	ScriptElementKindModifierDts,
	ScriptElementKindModifierTs,
	ScriptElementKindModifierTsx,
	ScriptElementKindModifierJs,
	ScriptElementKindModifierJsx,
	ScriptElementKindModifierJson,
	ScriptElementKindModifierDmts,
	ScriptElementKindModifierMts,
	ScriptElementKindModifierMjs,
	ScriptElementKindModifierDcts,
	ScriptElementKindModifierCts,
	ScriptElementKindModifierCjs,
}

func getSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
	result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location)
	if result != ScriptElementKindUnknown {
		return result
	}

	flags := checker.GetCombinedLocalAndExportSymbolFlags(symbol)
	if flags&ast.SymbolFlagsClass != 0 {
		decl := ast.GetDeclarationOfKind(symbol, ast.KindClassExpression)
		if decl != nil {
			return ScriptElementKindLocalClassElement
		}
		return ScriptElementKindClassElement
	}
	if flags&ast.SymbolFlagsEnum != 0 {
		return ScriptElementKindEnumElement
	}
	if flags&ast.SymbolFlagsTypeAlias != 0 {
		return ScriptElementKindTypeElement
	}
	if flags&ast.SymbolFlagsInterface != 0 {
		return ScriptElementKindInterfaceElement
	}
	if flags&ast.SymbolFlagsTypeParameter != 0 {
		return ScriptElementKindTypeParameterElement
	}
	if flags&ast.SymbolFlagsEnumMember != 0 {
		return ScriptElementKindEnumMemberElement
	}
	if flags&ast.SymbolFlagsAlias != 0 {
		return ScriptElementKindAlias
	}
	if flags&ast.SymbolFlagsModule != 0 {
		return ScriptElementKindModuleElement
	}

	return ScriptElementKindUnknown
}

func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
	roots := typeChecker.GetRootSymbols(symbol)
	// If this is a method from a mapped type, leave as a method so long as it still has a call signature, as opposed to e.g.
	// `{ [K in keyof I]: number }`.
	if len(roots) == 1 &&
		roots[0].Flags&ast.SymbolFlagsMethod != 0 &&
		len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0 {
		return ScriptElementKindMemberFunctionElement
	}

	if typeChecker.IsUndefinedSymbol(symbol) {
		return ScriptElementKindVariableElement
	}
	if typeChecker.IsArgumentsSymbol(symbol) {
		return ScriptElementKindLocalVariableElement
	}
	if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) ||
		ast.IsThisInTypeQuery(location) {
		return ScriptElementKindParameterElement
	}

	flags := checker.GetCombinedLocalAndExportSymbolFlags(symbol)
	if flags&ast.SymbolFlagsVariable != 0 {
		if isFirstDeclarationOfSymbolParameter(symbol) {
			return ScriptElementKindParameterElement
		} else if symbol.ValueDeclaration != nil && ast.IsVarConst(symbol.ValueDeclaration) {
			return ScriptElementKindConstElement
		} else if symbol.ValueDeclaration != nil && ast.IsVarUsing(symbol.ValueDeclaration) {
			return ScriptElementKindVariableUsingElement
		} else if symbol.ValueDeclaration != nil && ast.IsVarAwaitUsing(symbol.ValueDeclaration) {
			return ScriptElementKindVariableAwaitUsingElement
		} else if core.Some(symbol.Declarations, ast.IsLet) {
			return ScriptElementKindLetElement
		}
		if isLocalVariableOrFunction(symbol) {
			return ScriptElementKindLocalVariableElement
		}
		return ScriptElementKindVariableElement
	}
	if flags&ast.SymbolFlagsFunction != 0 {
		if isLocalVariableOrFunction(symbol) {
			return ScriptElementKindLocalFunctionElement
		}
		return ScriptElementKindFunctionElement
	}
	// FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag.
	// So, even when the location is just on the declaration of setter, this function returns getter.
	if flags&ast.SymbolFlagsGetAccessor != 0 {
		return ScriptElementKindMemberGetAccessorElement
	}
	if flags&ast.SymbolFlagsSetAccessor != 0 {
		return ScriptElementKindMemberSetAccessorElement
	}
	if flags&ast.SymbolFlagsMethod != 0 {
		return ScriptElementKindMemberFunctionElement
	}
	if flags&ast.SymbolFlagsConstructor != 0 {
		return ScriptElementKindConstructorImplementationElement
	}
	if flags&ast.SymbolFlagsSignature != 0 {
		return ScriptElementKindIndexSignatureElement
	}

	if flags&ast.SymbolFlagsProperty != 0 {
		if flags&ast.SymbolFlagsTransient != 0 &&
			symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 {
			// If union property is result of union of non method (property/accessors/variables), it is labeled as property
			var unionPropertyKind ScriptElementKind
			for _, rootSymbol := range roots {
				if rootSymbol.Flags&(ast.SymbolFlagsPropertyOrAccessor|ast.SymbolFlagsVariable) != 0 {
					unionPropertyKind = ScriptElementKindMemberVariableElement
					break
				}
			}
			if unionPropertyKind == ScriptElementKindUnknown {
				// If this was union of all methods,
				// make sure it has call signatures before we can label it as method.
				typeOfUnionProperty := typeChecker.GetTypeOfSymbolAtLocation(symbol, location)
				if len(typeChecker.GetCallSignatures(typeOfUnionProperty)) > 0 {
					return ScriptElementKindMemberFunctionElement
				}
				return ScriptElementKindMemberVariableElement
			}
			return unionPropertyKind
		}

		return ScriptElementKindMemberVariableElement
	}

	return ScriptElementKindUnknown
}

func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool {
	var declaration *ast.Node
	if len(symbol.Declarations) > 0 {
		declaration = symbol.Declarations[0]
	}
	result := ast.FindAncestorOrQuit(declaration, func(n *ast.Node) ast.FindAncestorResult {
		if ast.IsParameter(n) {
			return ast.FindAncestorTrue
		}
		if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) {
			return ast.FindAncestorFalse
		}
		return ast.FindAncestorQuit
	})

	return result != nil
}

func isLocalVariableOrFunction(symbol *ast.Symbol) bool {
	if symbol.Parent != nil {
		return false // This is exported symbol
	}

	for _, decl := range symbol.Declarations {
		// Function expressions are local
		if decl.Kind == ast.KindFunctionExpression {
			return true
		}

		if decl.Kind != ast.KindVariableDeclaration && decl.Kind != ast.KindFunctionDeclaration {
			continue
		}

		// If the parent is not source file or module block, it is a local variable.
		parent := decl.Parent
		for ; !ast.IsFunctionBlock(parent); parent = parent.Parent {
			// Reached source file or module block
			if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock {
				break
			}
		}

		if ast.IsFunctionBlock(parent) {
			// Parent is in function block.
			return true
		}
	}
	return false
}

func getSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) collections.Set[ScriptElementKindModifier] {
	if symbol == nil {
		return collections.Set[ScriptElementKindModifier]{}
	}

	modifiers := getNormalizedSymbolModifiers(typeChecker, symbol)
	if symbol.Flags&ast.SymbolFlagsAlias != 0 {
		resolvedSymbol := typeChecker.GetAliasedSymbol(symbol)
		if resolvedSymbol != symbol {
			aliasModifiers := getNormalizedSymbolModifiers(typeChecker, resolvedSymbol)
			for modifier := range aliasModifiers.Keys() {
				modifiers.Add(modifier)
			}
		}
	}
	if symbol.Flags&ast.SymbolFlagsOptional != 0 {
		modifiers.Add(ScriptElementKindModifierOptional)
	}

	return modifiers
}

func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) collections.Set[ScriptElementKindModifier] {
	var modifierSet collections.Set[ScriptElementKindModifier]
	if len(symbol.Declarations) > 0 {
		declaration := symbol.Declarations[0]
		declarations := symbol.Declarations[1:]
		// omit deprecated flag if some declarations are not deprecated
		var excludeFlags ast.ModifierFlags
		if len(declarations) > 0 &&
			typeChecker.IsDeprecatedDeclaration(declaration) && // !!! include jsdoc node flags
			core.Some(declarations, func(d *ast.Node) bool { return !typeChecker.IsDeprecatedDeclaration(d) }) {
			excludeFlags = ast.ModifierFlagsDeprecated
		} else {
			excludeFlags = ast.ModifierFlagsNone
		}
		modifierSet = getNodeModifiers(declaration, excludeFlags)
	}

	return modifierSet
}

func getNodeModifiers(node *ast.Node, excludeFlags ast.ModifierFlags) collections.Set[ScriptElementKindModifier] {
	var result collections.Set[ScriptElementKindModifier]
	var flags ast.ModifierFlags
	if ast.IsDeclaration(node) {
		flags = ast.GetCombinedModifierFlags(node) & ^excludeFlags // !!! include jsdoc node flags
	}

	if flags&ast.ModifierFlagsPrivate != 0 {
		result.Add(ScriptElementKindModifierPrivate)
	}
	if flags&ast.ModifierFlagsProtected != 0 {
		result.Add(ScriptElementKindModifierProtected)
	}
	if flags&ast.ModifierFlagsPublic != 0 {
		result.Add(ScriptElementKindModifierPublic)
	}
	if flags&ast.ModifierFlagsStatic != 0 {
		result.Add(ScriptElementKindModifierStatic)
	}
	if flags&ast.ModifierFlagsAbstract != 0 {
		result.Add(ScriptElementKindModifierAbstract)
	}
	if flags&ast.ModifierFlagsExport != 0 {
		result.Add(ScriptElementKindModifierExported)
	}
	if flags&ast.ModifierFlagsDeprecated != 0 {
		result.Add(ScriptElementKindModifierDeprecated)
	}
	if flags&ast.ModifierFlagsAmbient != 0 {
		result.Add(ScriptElementKindModifierAmbient)
	}
	if node.Flags&ast.NodeFlagsAmbient != 0 {
		result.Add(ScriptElementKindModifierAmbient)
	}
	if node.Kind == ast.KindExportAssignment {
		result.Add(ScriptElementKindModifierExported)
	}

	return result
}
