package checker

import (
	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
	"github.com/microsoft/typescript-go/internal/nodebuilder"
)

func cloneNodeBuilderContext(context *NodeBuilderContext) func() {
	// Make type parameters created within this context not consume the name outside this context
	// The symbol serializer ends up creating many sibling scopes that all need "separate" contexts when
	// it comes to naming things - within a normal `typeToTypeNode` call, the node builder only ever descends
	// through the type tree, so the only cases where we could have used distinct sibling scopes was when there
	// were multiple generic overloads with similar generated type parameter names
	// The effect:
	// When we write out
	// export const x: <T>(x: T) => T
	// export const y: <T>(x: T) => T
	// we write it out like that, rather than as
	// export const x: <T>(x: T) => T
	// export const y: <T_1>(x: T_1) => T_1
	restoreNames := context.typeParameterNames.EnterScope()
	restoreNamesByText := context.typeParameterNamesByText.EnterScope()
	restoreNamesByTextNextNameCount := context.typeParameterNamesByTextNextNameCount.EnterScope()
	restoreSymbolList := context.typeParameterSymbolList.EnterScope()
	return func() {
		restoreNames()
		restoreNamesByText()
		restoreNamesByTextNextNameCount()
		restoreSymbolList()
	}
}

type localsRecord struct {
	name      string
	oldSymbol *ast.Symbol
}

func (b *NodeBuilderImpl) addSymbolTypeToContext(symbol *ast.Symbol, t *Type) func() {
	id := ast.GetSymbolId(symbol)
	oldType, oldTypeExists := b.ctx.enclosingSymbolTypes[id]
	b.ctx.enclosingSymbolTypes[id] = t
	return func() {
		if oldTypeExists {
			b.ctx.enclosingSymbolTypes[id] = oldType
		} else {
			delete(b.ctx.enclosingSymbolTypes, id)
		}
	}
}

func (b *NodeBuilderImpl) enterSignatureScope(signature *Signature) (expandedParams []*ast.Symbol, cleanup func()) {
	expandedParams = b.ch.getExpandedParameters(signature, true /*skipUnionExpanding*/)[0]
	cleanup = b.enterNewScope(signature.declaration, expandedParams, signature.typeParameters, signature.parameters, signature.mapper)
	return expandedParams, cleanup
}

func (b *NodeBuilderImpl) enterNewScope(declaration *ast.Node, expandedParams []*ast.Symbol, typeParameters []*Type, originalParameters []*ast.Symbol, mapper *TypeMapper) func() {
	cleanupContext := cloneNodeBuilderContext(b.ctx)
	// For regular function/method declarations, the enclosing declaration will already be signature.declaration,
	// so this is a no-op, but for arrow functions and function expressions, the enclosing declaration will be
	// the declaration that the arrow function / function expression is assigned to.
	//
	// If the parameters or return type include "typeof globalThis.paramName", using the wrong scope will lead
	// us to believe that we can emit "typeof paramName" instead, even though that would refer to the parameter,
	// not the global. Make sure we are in the right scope by changing the enclosingDeclaration to the function.
	//
	// We can't use the declaration directly; it may be in another file and so we may lose access to symbols
	// accessible to the current enclosing declaration, or gain access to symbols not accessible to the current
	// enclosing declaration. To keep this chain accurate, insert a fake scope into the chain which makes the
	// function's parameters visible.
	var cleanupParams func()
	var cleanupTypeParams func()
	oldEnclosingDecl := b.ctx.enclosingDeclaration
	oldMapper := b.ctx.mapper
	if mapper != nil {
		b.ctx.mapper = mapper
	}
	if b.ctx.enclosingDeclaration != nil && declaration != nil {
		// As a performance optimization, reuse the same fake scope within this chain.
		// This is especially needed when we are working on an excessively deep type;
		// if we don't do this, then we spend all of our time adding more and more
		// scopes that need to be searched in isSymbolAccessible later. Since all we
		// really want to do is to mark certain names as unavailable, we can just keep
		// all of the names we're introducing in one large table and push/pop from it as
		// needed; isSymbolAccessible will walk upward and find the closest "fake" scope,
		// which will conveniently report on any and all faked scopes in the chain.
		//
		// It'd likely be better to store this somewhere else for isSymbolAccessible, but
		// since that API _only_ uses the enclosing declaration (and its parents), this is
		// seems like the best way to inject names into that search process.
		//
		// Note that we only check the most immediate enclosingDeclaration; the only place we
		// could potentially add another fake scope into the chain is right here, so we don't
		// traverse all ancestors.
		pushFakeScope := func(kind string, addAll func(addSymbol func(name string, symbol *ast.Symbol))) func() {
			// We only ever need to look two declarations upward.
			debug.Assert(b.ctx.enclosingDeclaration != nil)
			var existingFakeScope *ast.Node
			if b.links.Has(b.ctx.enclosingDeclaration) {
				links := b.links.Get(b.ctx.enclosingDeclaration)
				if links.fakeScopeForSignatureDeclaration != nil && *links.fakeScopeForSignatureDeclaration == kind {
					existingFakeScope = b.ctx.enclosingDeclaration
				}
			}
			if existingFakeScope == nil && b.ctx.enclosingDeclaration.Parent != nil {
				if b.links.Has(b.ctx.enclosingDeclaration.Parent) {
					links := b.links.Get(b.ctx.enclosingDeclaration.Parent)
					if links.fakeScopeForSignatureDeclaration != nil && *links.fakeScopeForSignatureDeclaration == kind {
						existingFakeScope = b.ctx.enclosingDeclaration.Parent
					}
				}
			}
			debug.Assert(existingFakeScope == nil || ast.IsBlock(existingFakeScope))

			var locals ast.SymbolTable
			if existingFakeScope != nil {
				locals = existingFakeScope.Locals()
			}
			if locals == nil {
				locals = make(ast.SymbolTable)
			}
			newLocals := []string{}
			oldLocals := []localsRecord{}
			addAll(func(name string, symbol *ast.Symbol) {
				// Add cleanup information only if we don't own the fake scope
				if existingFakeScope != nil {
					oldSymbol, ok := locals[name]
					if !ok || oldSymbol == nil {
						newLocals = append(newLocals, name)
					} else {
						oldLocals = append(oldLocals, localsRecord{name, oldSymbol})
					}
				}
				locals[name] = symbol
			})

			if existingFakeScope == nil {
				// Use a Block for this; the type of the node doesn't matter so long as it
				// has locals, and this is cheaper/easier than using a function-ish Node.
				fakeScope := b.f.NewBlock(b.f.NewNodeList([]*ast.Node{}), false)
				b.links.Get(fakeScope).fakeScopeForSignatureDeclaration = &kind
				data := fakeScope.LocalsContainerData()
				data.Locals = locals
				fakeScope.Parent = b.ctx.enclosingDeclaration
				b.ctx.enclosingDeclaration = fakeScope
				return nil
			} else {
				// We did not create the current scope, so we have to clean it up
				undo := func() {
					for _, s := range newLocals {
						delete(locals, s)
					}
					for _, s := range oldLocals {
						locals[s.name] = s.oldSymbol
					}
				}
				return undo
			}
		}

		if expandedParams == nil || !core.Some(expandedParams, func(p *ast.Symbol) bool { return p != nil }) {
			cleanupParams = nil
		} else {
			cleanupParams = pushFakeScope("params", func(add func(name string, symbol *ast.Symbol)) {
				if expandedParams == nil {
					return
				}
				for pIndex, param := range expandedParams {
					var originalParam *ast.Symbol
					if pIndex < len(originalParameters) {
						originalParam = originalParameters[pIndex]
					}
					if originalParameters != nil && originalParam != param {
						// Can't reference the expanded parameter name, just the original, unless we've expanded the param list for some reason
						if originalParam != nil {
							add(originalParam.Name, originalParam)
						}
					} else if !core.Some(param.Declarations, func(d *ast.Node) bool {
						var bindElement func(e *ast.BindingElement)
						var bindPattern func(e *ast.BindingPattern)

						bindPatternWorker := func(p *ast.BindingPattern) {
							for _, e := range p.Elements.Nodes {
								switch e.Kind {
								case ast.KindOmittedExpression:
									return
								case ast.KindBindingElement:
									bindElement(e.AsBindingElement())
									return
								default:
									panic("Unhandled binding element kind")
								}
							}
						}

						bindElementWorker := func(e *ast.BindingElement) {
							if e.Name() != nil && ast.IsBindingPattern(e.Name()) {
								bindPattern(e.Name().AsBindingPattern())
								return
							}
							symbol := b.ch.getSymbolOfDeclaration(e.AsNode())
							if symbol != nil { // omitted expressions are now parsed as nameless binding patterns and also have no symbol
								add(symbol.Name, symbol)
							}
						}
						bindElement = bindElementWorker
						bindPattern = bindPatternWorker

						if ast.IsParameterDeclaration(d) && d.Name() != nil && ast.IsBindingPattern(d.Name()) {
							bindPattern(d.Name().AsBindingPattern())
							return true
						}
						return false
					}) {
						add(param.Name, param)
					}
				}
			})
		}

		if b.ctx.flags&nodebuilder.FlagsGenerateNamesForShadowedTypeParams != 0 && typeParameters != nil && core.Some(typeParameters, func(p *Type) bool { return p != nil }) {
			cleanupTypeParams = pushFakeScope("typeParams", func(add func(name string, symbol *ast.Symbol)) {
				if typeParameters == nil {
					return
				}
				for _, typeParam := range typeParameters {
					if typeParam == nil {
						continue
					}
					typeParamName := b.typeParameterToName(typeParam).Text
					add(typeParamName, typeParam.symbol)
				}
			})
		}

	}

	return func() {
		if cleanupParams != nil {
			cleanupParams()
		}
		if cleanupTypeParams != nil {
			cleanupTypeParams()
		}
		cleanupContext()
		b.ctx.enclosingDeclaration = oldEnclosingDecl
		b.ctx.mapper = oldMapper
	}
}
