package printer

import (
	"fmt"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
)

// Flags enum to track count of temp variables and a few dedicated names
type tempFlags int

const (
	tempFlagsAuto      tempFlags = 0x00000000 // No preferred name
	tempFlagsCountMask tempFlags = 0x0FFFFFFF // Temp variable counter
	tempFlags_i        tempFlags = 0x10000000 // Use/preference flag for '_i'
)

type NameGenerator struct {
	Context                            *EmitContext
	IsFileLevelUniqueNameInCurrentFile func(string, bool) bool   // callback for Printer.isFileLevelUniqueNameInCurrentFile
	GetTextOfNode                      func(*ast.Node) string    // callback for Printer.getTextOfNode
	nodeIdToGeneratedName              map[ast.NodeId]string     // Map of generated names for specific nodes
	nodeIdToGeneratedPrivateName       map[ast.NodeId]string     // Map of generated private names for specific nodes
	autoGeneratedIdToGeneratedName     map[AutoGenerateId]string // Map of generated names for temp and loop variables
	nameGenerationScope                *nameGenerationScope
	privateNameGenerationScope         *nameGenerationScope
	generatedNames                     collections.Set[string] // NOTE: Used to match Strada, but should be moved to nameGenerationScope after port is complete.
}

type nameGenerationScope struct {
	next                   *nameGenerationScope    // The next nameGenerationScope in the stack
	tempFlags              tempFlags               // TempFlags for the current name generation scope.
	formattedNameTempFlags map[string]tempFlags    // TempFlags for the current name generation scope.
	reservedNames          collections.Set[string] // Names reserved in nested name generation scopes.
	// generatedNames         collections.Set[string] // NOTE: generated names should be scoped after Strada port is complete.
}

func (g *NameGenerator) PushScope(reuseTempVariableScope bool) {
	g.privateNameGenerationScope = &nameGenerationScope{next: g.privateNameGenerationScope}
	if !reuseTempVariableScope {
		g.nameGenerationScope = &nameGenerationScope{next: g.nameGenerationScope}
	}
}

func (g *NameGenerator) PopScope(reuseTempVariableScope bool) {
	if g.privateNameGenerationScope != nil {
		g.privateNameGenerationScope = g.privateNameGenerationScope.next
	}
	if !reuseTempVariableScope && g.nameGenerationScope != nil {
		g.nameGenerationScope = g.nameGenerationScope.next
	}
}

func (g *NameGenerator) getScope(privateName bool) **nameGenerationScope {
	return core.IfElse(privateName, &g.privateNameGenerationScope, &g.nameGenerationScope)
}

func (g *NameGenerator) getTempFlags(privateName bool) tempFlags {
	scope := g.getScope(privateName)
	if *scope != nil {
		return (*scope).tempFlags
	}
	return tempFlagsAuto
}

func (g *NameGenerator) setTempFlags(privateName bool, flags tempFlags) {
	scope := g.getScope(privateName)
	if *scope == nil {
		*scope = &nameGenerationScope{}
	}
	(*scope).tempFlags = flags
}

// Gets the TempFlags to use in the current nameGenerationScope for the given key
func (g *NameGenerator) getTempFlagsForFormattedName(privateName bool, formattedNameKey string) tempFlags {
	scope := g.getScope(privateName)
	if *scope != nil {
		if flags, ok := (*scope).formattedNameTempFlags[formattedNameKey]; ok {
			return flags
		}
	}
	return tempFlagsAuto
}

// Sets the TempFlags to use in the current nameGenerationScope for the given key
func (g *NameGenerator) setTempFlagsForFormattedName(privateName bool, formattedNameKey string, flags tempFlags) {
	scope := g.getScope(privateName)
	if *scope == nil {
		*scope = &nameGenerationScope{}
	}
	if (*scope).formattedNameTempFlags == nil {
		(*scope).formattedNameTempFlags = make(map[string]tempFlags)
	}
	(*scope).formattedNameTempFlags[formattedNameKey] = flags
}

func (g *NameGenerator) reserveName(name string, privateName bool, scoped bool, temp bool) {
	scope := g.getScope(privateName)
	if *scope == nil {
		*scope = &nameGenerationScope{}
	}
	if privateName || scoped {
		(*scope).reservedNames.Add(name)
	} else if !temp {
		g.generatedNames.Add(name) // NOTE: Matches Strada, but is incorrect.
		// (*scope).generatedNames.Add(name) // TODO: generated names should be scoped after Strada port is complete.
	}
}

// Generate the text for a generated identifier or private identifier
func (g *NameGenerator) GenerateName(name *ast.MemberName) string {
	if g.Context != nil {
		if autoGenerate, ok := g.Context.autoGenerate[name]; ok {
			if autoGenerate.Flags.IsNode() {
				// Node names generate unique names based on their original node
				// and are cached based on that node's id.
				return g.generateNameForNodeCached(g.Context.GetNodeForGeneratedName(name), ast.IsPrivateIdentifier(name), autoGenerate.Flags, autoGenerate.Prefix, autoGenerate.Suffix)
			} else {
				// Auto, Loop, and Unique names are cached based on their unique autoGenerateId.
				if autoGeneratedName, ok := g.autoGeneratedIdToGeneratedName[autoGenerate.Id]; ok {
					return autoGeneratedName
				}
				if g.autoGeneratedIdToGeneratedName == nil {
					g.autoGeneratedIdToGeneratedName = make(map[AutoGenerateId]string)
				}
				autoGeneratedName := g.makeName(name)
				g.autoGeneratedIdToGeneratedName[autoGenerate.Id] = autoGeneratedName
				return autoGeneratedName
			}
		}
	}
	return g.GetTextOfNode(name)
}

func (g *NameGenerator) generateNameForNodeCached(node *ast.Node, privateName bool, flags GeneratedIdentifierFlags, prefix string, suffix string) string {
	nodeId := ast.GetNodeId(node)
	cache := core.IfElse(privateName, &g.nodeIdToGeneratedPrivateName, &g.nodeIdToGeneratedName)
	if *cache == nil {
		*cache = make(map[ast.NodeId]string)
	}

	if name, ok := (*cache)[nodeId]; ok {
		return name
	}

	name := g.generateNameForNode(node, privateName, flags, prefix, suffix)
	(*cache)[nodeId] = name
	return name
}

func (g *NameGenerator) generateNameForNode(node *ast.Node, privateName bool, flags GeneratedIdentifierFlags, prefix string, suffix string) string {
	switch node.Kind {
	case ast.KindIdentifier, ast.KindPrivateIdentifier:
		return g.makeUniqueName(g.GetTextOfNode(node), nil /*checkFn*/, flags.IsOptimistic(), flags.IsReservedInNestedScopes(), privateName, prefix, suffix)
	case ast.KindModuleDeclaration, ast.KindEnumDeclaration:
		if privateName || len(prefix) > 0 || len(suffix) > 0 {
			panic("Generated name for a module or enum cannot be private and may have neither a prefix nor suffix")
		}
		return g.generateNameForModuleOrEnum(node)
	case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindExportDeclaration:
		if privateName || len(prefix) > 0 || len(suffix) > 0 {
			panic("Generated name for an import or export cannot be private and may have neither a prefix nor suffix")
		}
		return g.generateNameForImportOrExportDeclaration(node)
	case ast.KindFunctionDeclaration, ast.KindClassDeclaration:
		if privateName || len(prefix) > 0 || len(suffix) > 0 {
			panic("Generated name for a class or function declaration cannot be private and may have neither a prefix nor suffix")
		}
		name := node.Name()
		if name != nil && !(g.Context == nil && g.Context.HasAutoGenerateInfo(name)) {
			return g.generateNameForNode(name, false /*privateName*/, flags, "" /*prefix*/, "" /*suffix*/)
		}
		return g.generateNameForExportDefault()
	case ast.KindExportAssignment:
		if privateName || len(prefix) > 0 || len(suffix) > 0 {
			panic("Generated name for an export assignment cannot be private and may have neither a prefix nor suffix")
		}
		return g.generateNameForExportDefault()
	case ast.KindClassExpression:
		if privateName || len(prefix) > 0 || len(suffix) > 0 {
			panic("Generated name for a class expression cannot be private and may have neither a prefix nor suffix")
		}
		return g.generateNameForClassExpression()
	case ast.KindMethodDeclaration, ast.KindGetAccessor, ast.KindSetAccessor:
		return g.generateNameForMethodOrAccessor(node, privateName, prefix, suffix)
	case ast.KindComputedPropertyName:
		return g.makeTempVariableName(tempFlagsAuto, true /*reservedInNestedScopes*/, privateName, prefix, suffix)
	default:
		return g.makeTempVariableName(tempFlagsAuto, false /*reservedInNestedScopes*/, privateName, prefix, suffix)
	}
}

func (g *NameGenerator) generateNameForModuleOrEnum(node *ast.Node /* ModuleDeclaration | EnumDeclaration */) string {
	name := g.GetTextOfNode(node.Name())
	// Use module/enum name itself if it is unique, otherwise make a unique variation
	if isUniqueLocalName(name, node) {
		return name
	} else {
		return g.makeUniqueName(name, nil /*checkFn*/, false /*optimistic*/, false /*scoped*/, false /*privateName*/, "" /*prefix*/, "" /*suffix*/)
	}
}

func (g *NameGenerator) generateNameForImportOrExportDeclaration(node *ast.Node /* ImportDeclaration | ExportDeclaration */) string {
	expr := ast.GetExternalModuleName(node)
	baseName := "module"
	if ast.IsStringLiteral(expr) {
		baseName = makeIdentifierFromModuleName(expr.Text())
	}
	return g.makeUniqueName(baseName, nil /*checkFn*/, false /*optimistic*/, false /*scoped*/, false /*privateName*/, "" /*prefix*/, "" /*suffix*/)
}

func (g *NameGenerator) generateNameForExportDefault() string {
	return g.makeUniqueName("default", nil /*checkFn*/, false /*optimistic*/, false /*scoped*/, false /*privateName*/, "" /*prefix*/, "" /*suffix*/)
}

func (g *NameGenerator) generateNameForClassExpression() string {
	return g.makeUniqueName("class", nil /*checkFn*/, false /*optimistic*/, false /*scoped*/, false /*privateName*/, "" /*prefix*/, "" /*suffix*/)
}

func (g *NameGenerator) generateNameForMethodOrAccessor(node *ast.Node /* MethodDeclaration | AccessorDeclaration */, privateName bool, prefix string, suffix string) string {
	if ast.IsIdentifier(node.Name()) {
		return g.generateNameForNodeCached(node.Name(), privateName, GeneratedIdentifierFlagsNone, prefix, suffix)
	}
	return g.makeTempVariableName(tempFlagsAuto, false /*reservedInNestedScopes*/, privateName, prefix, suffix)
}

func (g *NameGenerator) makeName(name *ast.Node) string {
	if g.Context != nil {
		if autoGenerate, ok := g.Context.autoGenerate[name]; ok {
			switch autoGenerate.Flags.Kind() {
			case GeneratedIdentifierFlagsAuto:
				return g.makeTempVariableName(tempFlagsAuto, autoGenerate.Flags.IsReservedInNestedScopes(), ast.IsPrivateIdentifier(name), autoGenerate.Prefix, autoGenerate.Suffix)
			case GeneratedIdentifierFlagsLoop:
				debug.Assert(ast.IsIdentifier(name))
				return g.makeTempVariableName(tempFlags_i, autoGenerate.Flags.IsReservedInNestedScopes(), false /*privateName*/, autoGenerate.Prefix, autoGenerate.Suffix)
			case GeneratedIdentifierFlagsUnique:
				return g.makeUniqueName(
					name.Text(),
					core.IfElse(autoGenerate.Flags.IsFileLevel(), g.IsFileLevelUniqueNameInCurrentFile, nil),
					autoGenerate.Flags.IsOptimistic(),
					autoGenerate.Flags.IsReservedInNestedScopes(),
					ast.IsPrivateIdentifier(name),
					autoGenerate.Prefix,
					autoGenerate.Suffix,
				)
			}
		}
	}
	return g.GetTextOfNode(name)
}

// Return the next available name in the pattern _a ... _z, _0, _1, ...
// TempFlags._i may be used to express a preference for that dedicated name.
// Note that names generated by makeTempVariableName and makeUniqueName will never conflict.
func (g *NameGenerator) makeTempVariableName(flags tempFlags, reservedInNestedScopes bool, privateName bool, prefix string, suffix string) string {
	var tempFlags tempFlags
	var key string
	simple := len(prefix) == 0 && len(suffix) == 0
	if simple {
		tempFlags = g.getTempFlags(privateName)
	} else {
		// Generate a key to use to acquire a TempFlags counter based on the fixed portions of the generated name.
		key = FormatGeneratedName(privateName, prefix, "" /*base*/, suffix)
		if privateName {
			key = ensureLeadingHash(key)
		}
		tempFlags = g.getTempFlagsForFormattedName(privateName, key)
	}

	if flags != 0 && tempFlags&flags == 0 {
		fullName := FormatGeneratedName(privateName, prefix, "_i", suffix)
		if g.isUniqueName(fullName, privateName) {
			tempFlags |= flags
			g.reserveName(fullName, privateName, reservedInNestedScopes, true /*temp*/)
			if simple {
				g.setTempFlags(privateName, tempFlags)
			} else {
				g.setTempFlagsForFormattedName(privateName, key, tempFlags)
			}
			return fullName
		}
	}

	for {
		count := tempFlags & tempFlagsCountMask
		tempFlags++
		// Skip over 'i' and 'n'
		if count != 8 && count != 13 {
			var name string
			if count < 26 {
				name = fmt.Sprintf("_%c", 'a'+byte(count))
			} else {
				name = fmt.Sprintf("_%d", count-26)
			}
			fullName := FormatGeneratedName(privateName, prefix, name, suffix)
			if g.isUniqueName(fullName, privateName) {
				g.reserveName(fullName, privateName, reservedInNestedScopes, true /*temp*/)
				if simple {
					g.setTempFlags(privateName, tempFlags)
				} else {
					g.setTempFlagsForFormattedName(privateName, key, tempFlags)
				}
				return fullName
			}
		}
	}
}

// Generate a name that is unique within the current file and doesn't conflict with any names
// in global scope. The name is formed by adding an '_n' suffix to the specified base name,
// where n is a positive integer. Note that names generated by makeTempVariableName and
// makeUniqueName are guaranteed to never conflict.
// If `optimistic` is set, the first instance will use 'baseName' verbatim instead of 'baseName_1'
func (g *NameGenerator) makeUniqueName(baseName string, checkFn func(name string, privateName bool) bool, optimistic bool, scoped bool, privateName bool, prefix string, suffix string) string {
	baseName = removeLeadingHash(baseName)
	if optimistic {
		fullName := FormatGeneratedName(privateName, prefix, baseName, suffix)
		if g.checkUniqueName(fullName, privateName, checkFn) {
			g.reserveName(fullName, privateName, scoped, false /*temp*/)
			return fullName
		}
	}

	// Find the first unique 'name_n', where n is a positive integer
	if len(baseName) > 0 && baseName[len(baseName)-1] != '_' {
		baseName += "_"
	}

	i := 1
	for {
		fullName := FormatGeneratedName(privateName, prefix, fmt.Sprintf("%s%d", baseName, i), suffix)
		if g.checkUniqueName(fullName, privateName, checkFn) {
			g.reserveName(fullName, privateName, scoped, false /*temp*/)
			return fullName
		}
		i++
	}
}

func (g *NameGenerator) MakeFileLevelOptimisticUniqueName(name string) string {
	return g.makeUniqueName(name, g.IsFileLevelUniqueNameInCurrentFile, true /*optimistic*/, false /*scoped*/, false /*privateName*/, "" /*prefix*/, "" /*suffix*/)
}

func (g *NameGenerator) checkUniqueName(name string, privateName bool, checkFn func(name string, privateName bool) bool) bool {
	if checkFn != nil {
		return checkFn(name, privateName)
	} else {
		return g.isUniqueName(name, privateName)
	}
}

func nextContainer(node *ast.Node) *ast.Node {
	data := node.LocalsContainerData()
	if data != nil {
		return data.NextContainer
	}
	return nil
}

func isUniqueLocalName(name string, container *ast.Node) bool {
	node := container
	for node != nil && ast.IsNodeDescendantOf(node, container) && node.LocalsContainerData() != nil {
		locals := node.Locals()
		if locals != nil {
			// We conservatively include alias symbols to cover cases where they're emitted as locals
			if local, ok := locals[name]; ok && local.Flags&(ast.SymbolFlagsValue|ast.SymbolFlagsExportValue|ast.SymbolFlagsAlias) != 0 {
				return false
			}
		}
		node = nextContainer(node)
	}
	return true
}

func (g *NameGenerator) isUniqueName(name string, privateName bool) bool {
	return (g.IsFileLevelUniqueNameInCurrentFile == nil || g.IsFileLevelUniqueNameInCurrentFile(name, privateName)) &&
		!g.isReservedName(name, privateName)
}

func (g *NameGenerator) isReservedName(name string, privateName bool) bool {
	scope := g.getScope(privateName)

	// NOTE: The following matches Strada, but is incorrect.
	if g.generatedNames.Has(name) {
		return true
	}

	// TODO: generated names should be scoped after Strada port is complete.
	////if *scope != nil {
	////	if (*scope).generatedNames.Has(name) {
	////		return true
	////	}
	////}

	for *scope != nil {
		if (*scope).reservedNames.Has(name) {
			return true
		}
		scope = &(*scope).next
	}
	return false
}
