package module

import (
	"strings"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/diagnostics"
	"github.com/microsoft/typescript-go/internal/semver"
	"github.com/microsoft/typescript-go/internal/tspath"
)

var typeScriptVersion = semver.MustParse(core.Version())

const InferredTypesContainingFile = "__inferred type names__.ts"

func IsApplicableVersionedTypesKey(key string) bool {
	if !strings.HasPrefix(key, "types@") {
		return false
	}
	range_, ok := semver.TryParseVersionRange(key[len("types@"):])
	if !ok {
		return false
	}
	return range_.Test(&typeScriptVersion)
}

func ParseNodeModuleFromPath(resolved string, isFolder bool) string {
	path := tspath.NormalizePath(resolved)
	idx := strings.LastIndex(path, "/node_modules/")
	if idx == -1 {
		return ""
	}

	indexAfterNodeModules := idx + len("/node_modules/")
	indexAfterPackageName := moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules, isFolder)
	if path[indexAfterNodeModules] == '@' {
		indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName, isFolder)
	}
	return path[:indexAfterPackageName]
}

func ParsePackageName(moduleName string) (packageName, rest string) {
	idx := strings.Index(moduleName, "/")
	if len(moduleName) > 0 && moduleName[0] == '@' {
		offset := idx + 1
		idx = strings.Index(moduleName[offset:], "/")
		if idx != -1 {
			idx += offset
		}
	}
	if idx == -1 {
		return moduleName, ""
	}
	return moduleName[:idx], moduleName[idx+1:]
}

func MangleScopedPackageName(packageName string) string {
	if len(packageName) > 0 && packageName[0] == '@' {
		idx := strings.Index(packageName, "/")
		if idx == -1 {
			return packageName
		}
		return packageName[1:idx] + "__" + packageName[idx+1:]
	}
	return packageName
}

func UnmangleScopedPackageName(packageName string) string {
	before, after, ok := strings.Cut(packageName, "__")
	if ok {
		return "@" + before + "/" + after
	}
	return packageName
}

func GetTypesPackageName(packageName string) string {
	return "@types/" + MangleScopedPackageName(packageName)
}

func GetPackageNameFromTypesPackageName(mangledName string) string {
	withoutAtTypePrefix := strings.TrimPrefix(mangledName, "@types/")
	if withoutAtTypePrefix != mangledName {
		return UnmangleScopedPackageName(withoutAtTypePrefix)
	}
	return mangledName
}

func ComparePatternKeys(a, b string) int {
	aPatternIndex := strings.Index(a, "*")
	bPatternIndex := strings.Index(b, "*")
	baseLenA := len(a)
	if aPatternIndex != -1 {
		baseLenA = aPatternIndex + 1
	}
	baseLenB := len(b)
	if bPatternIndex != -1 {
		baseLenB = bPatternIndex + 1
	}

	if baseLenA > baseLenB {
		return -1
	}
	if baseLenB > baseLenA {
		return 1
	}
	if aPatternIndex == -1 {
		return 1
	}
	if bPatternIndex == -1 {
		return -1
	}
	if len(a) > len(b) {
		return -1
	}
	if len(b) > len(a) {
		return 1
	}
	return 0
}

// Returns a DiagnosticMessage if we won't include a resolved module due to its extension.
// The DiagnosticMessage's parameters are the imported module name, and the filename it resolved to.
// This returns a diagnostic even if the module will be an untyped module.
func GetResolutionDiagnostic(options *core.CompilerOptions, resolvedModule *ResolvedModule, file *ast.SourceFile) *diagnostics.Message {
	needJsx := func() *diagnostics.Message {
		if options.Jsx != core.JsxEmitNone {
			return nil
		}
		return diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set
	}

	needAllowJs := func() *diagnostics.Message {
		if options.GetAllowJS() || !options.NoImplicitAny.DefaultIfUnknown(options.Strict).IsTrue() {
			return nil
		}
		return diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type
	}

	needResolveJsonModule := func() *diagnostics.Message {
		if options.GetResolveJsonModule() {
			return nil
		}
		return diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used
	}

	needAllowArbitraryExtensions := func() *diagnostics.Message {
		if file.IsDeclarationFile || options.AllowArbitraryExtensions.IsTrue() {
			return nil
		}
		return diagnostics.Module_0_was_resolved_to_1_but_allowArbitraryExtensions_is_not_set
	}

	switch resolvedModule.Extension {
	case tspath.ExtensionTs, tspath.ExtensionDts,
		tspath.ExtensionMts, tspath.ExtensionDmts,
		tspath.ExtensionCts, tspath.ExtensionDcts:
		// These are always allowed.
		return nil
	case tspath.ExtensionTsx:
		return needJsx()
	case tspath.ExtensionJsx:
		if message := needJsx(); message != nil {
			return message
		}
		return needAllowJs()
	case tspath.ExtensionJs, tspath.ExtensionMjs, tspath.ExtensionCjs:
		return needAllowJs()
	case tspath.ExtensionJson:
		return needResolveJsonModule()
	default:
		return needAllowArbitraryExtensions()
	}
}

// TryGetJSExtensionForFile maps TS/JS/DTS extensions to the output JS-side extension.
// Returns an empty string if the extension is unsupported.
func TryGetJSExtensionForFile(fileName string, options *core.CompilerOptions) string {
	ext := tspath.TryGetExtensionFromPath(fileName)
	switch ext {
	case tspath.ExtensionTs, tspath.ExtensionDts:
		return tspath.ExtensionJs
	case tspath.ExtensionTsx:
		if options.Jsx == core.JsxEmitPreserve {
			return tspath.ExtensionJsx
		}
		return tspath.ExtensionJs
	case tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionJson:
		return ext
	case tspath.ExtensionDmts, tspath.ExtensionMts, tspath.ExtensionMjs:
		return tspath.ExtensionMjs
	case tspath.ExtensionDcts, tspath.ExtensionCts, tspath.ExtensionCjs:
		return tspath.ExtensionCjs
	default:
		return ""
	}
}
