package lsutil

import (
	"strings"
	"unicode"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/astnav"
	"github.com/microsoft/typescript-go/internal/compiler"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/scanner"
	"github.com/microsoft/typescript-go/internal/stringutil"
	"github.com/microsoft/typescript-go/internal/tspath"
)

func ProbablyUsesSemicolons(file *ast.SourceFile) bool {
	withSemicolon := 0
	withoutSemicolon := 0
	nStatementsToObserve := 5

	var visit func(node *ast.Node) bool
	visit = func(node *ast.Node) bool {
		if node.Flags&ast.NodeFlagsReparsed != 0 {
			return false
		}
		if SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
			lastToken := GetLastToken(node, file)
			if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
				withSemicolon++
			} else {
				withoutSemicolon++
			}
		} else if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
			lastToken := GetLastToken(node, file)
			if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
				withSemicolon++
			} else if lastToken != nil && lastToken.Kind != ast.KindCommaToken {
				lastTokenLine := scanner.GetECMALineOfPosition(
					file,
					astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/),
				)
				nextTokenLine := scanner.GetECMALineOfPosition(
					file,
					scanner.SkipTrivia(file.Text(), lastToken.End()),
				)
				// Avoid counting missing semicolon in single-line objects:
				// `function f(p: { x: string /*no semicolon here is insignificant*/ }) {`
				if lastTokenLine != nextTokenLine {
					withoutSemicolon++
				}
			}
		}

		if withSemicolon+withoutSemicolon >= nStatementsToObserve {
			return true
		}

		return node.ForEachChild(visit)
	}

	file.ForEachChild(visit)

	// One statement missing a semicolon isn't sufficient evidence to say the user
	// doesn't want semicolons, because they may not even be done writing that statement.
	if withSemicolon == 0 && withoutSemicolon <= 1 {
		return true
	}

	// When both kinds of observation exist, treat the file as using semicolons when the
	// ratio withSemicolon/withoutSemicolon exceeds 1/nStatementsToObserve (real arithmetic),
	// implemented as an integer inequality to avoid truncation.
	if withoutSemicolon == 0 {
		return true
	}
	return withSemicolon*nStatementsToObserve > withoutSemicolon
}

func ShouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) core.Tristate {
	for _, node := range file.Imports() {
		if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] {
			if strings.HasPrefix(node.Text(), "node:") {
				return core.TSTrue
			} else {
				return core.TSFalse
			}
		}
	}

	return program.UsesUriStyleNodeCoreModules()
}

func QuotePreferenceFromString(str *ast.StringLiteral) QuotePreference {
	if str.TokenFlags&ast.TokenFlagsSingleQuote != 0 {
		return QuotePreferenceSingle
	}
	return QuotePreferenceDouble
}

func GetQuotePreference(sourceFile *ast.SourceFile, preferences UserPreferences) QuotePreference {
	if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" {
		if preferences.QuotePreference == "single" {
			return QuotePreferenceSingle
		}
		return QuotePreferenceDouble
	}
	// ignore synthetic import added when importHelpers: true
	firstModuleSpecifier := core.Find(sourceFile.Imports(), func(n *ast.Node) bool {
		return ast.IsStringLiteral(n) && !ast.NodeIsSynthesized(n.Parent)
	})
	if firstModuleSpecifier != nil {
		return QuotePreferenceFromString(firstModuleSpecifier.AsStringLiteral())
	}
	return QuotePreferenceDouble
}

func ModuleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, forceCapitalize bool) string {
	return ModuleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), forceCapitalize)
}

func ModuleSpecifierToValidIdentifier(moduleSpecifier string, forceCapitalize bool) string {
	baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index"))
	res := []rune{}
	lastCharWasValid := true
	baseNameRunes := []rune(baseName)
	if len(baseNameRunes) > 0 && scanner.IsIdentifierStart(baseNameRunes[0]) {
		if forceCapitalize {
			res = append(res, unicode.ToUpper(baseNameRunes[0]))
		} else {
			res = append(res, baseNameRunes[0])
		}
	} else {
		lastCharWasValid = false
	}

	for i := 1; i < len(baseNameRunes); i++ {
		isValid := scanner.IsIdentifierPart(baseNameRunes[i])
		if isValid {
			if !lastCharWasValid {
				res = append(res, unicode.ToUpper(baseNameRunes[i]))
			} else {
				res = append(res, baseNameRunes[i])
			}
		}
		lastCharWasValid = isValid
	}

	// Need `"_"` to ensure result isn't empty.
	resString := string(res)
	if resString != "" && !IsNonContextualKeyword(scanner.StringToToken(resString)) {
		return resString
	}
	return "_" + resString
}

func IsNonContextualKeyword(token ast.Kind) bool {
	return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token)
}
