package goja

import (
	"github.com/projectdiscovery/goja/ast"
	"github.com/projectdiscovery/goja/file"
	"github.com/projectdiscovery/goja/token"
	"github.com/projectdiscovery/goja/unistring"
)

func (c *compiler) compileStatement(v ast.Statement, needResult bool) {

	switch v := v.(type) {
	case *ast.BlockStatement:
		c.compileBlockStatement(v, needResult)
	case *ast.ExpressionStatement:
		c.compileExpressionStatement(v, needResult)
	case *ast.VariableStatement:
		c.compileVariableStatement(v)
	case *ast.LexicalDeclaration:
		c.compileLexicalDeclaration(v)
	case *ast.ReturnStatement:
		c.compileReturnStatement(v)
	case *ast.IfStatement:
		c.compileIfStatement(v, needResult)
	case *ast.DoWhileStatement:
		c.compileDoWhileStatement(v, needResult)
	case *ast.ForStatement:
		c.compileForStatement(v, needResult)
	case *ast.ForInStatement:
		c.compileForInStatement(v, needResult)
	case *ast.ForOfStatement:
		c.compileForOfStatement(v, needResult)
	case *ast.WhileStatement:
		c.compileWhileStatement(v, needResult)
	case *ast.BranchStatement:
		c.compileBranchStatement(v)
	case *ast.TryStatement:
		c.compileTryStatement(v, needResult)
	case *ast.ThrowStatement:
		c.compileThrowStatement(v)
	case *ast.SwitchStatement:
		c.compileSwitchStatement(v, needResult)
	case *ast.LabelledStatement:
		c.compileLabeledStatement(v, needResult)
	case *ast.EmptyStatement:
		c.compileEmptyStatement(needResult)
	case *ast.FunctionDeclaration:
		c.compileStandaloneFunctionDecl(v)
		// note functions inside blocks are hoisted to the top of the block and are compiled using compileFunctions()
	case *ast.ClassDeclaration:
		c.compileClassDeclaration(v)
	case *ast.WithStatement:
		c.compileWithStatement(v, needResult)
	case *ast.DebuggerStatement:
	default:
		c.assert(false, int(v.Idx0())-1, "Unknown statement type: %T", v)
		panic("unreachable")
	}
}

func (c *compiler) compileLabeledStatement(v *ast.LabelledStatement, needResult bool) {
	label := v.Label.Name
	if c.scope.strict {
		c.checkIdentifierName(label, int(v.Label.Idx)-1)
	}
	for b := c.block; b != nil; b = b.outer {
		if b.label == label {
			c.throwSyntaxErrorf(int(v.Label.Idx-1), "Label '%s' has already been declared", label)
		}
	}
	switch s := v.Statement.(type) {
	case *ast.ForInStatement:
		c.compileLabeledForInStatement(s, needResult, label)
	case *ast.ForOfStatement:
		c.compileLabeledForOfStatement(s, needResult, label)
	case *ast.ForStatement:
		c.compileLabeledForStatement(s, needResult, label)
	case *ast.WhileStatement:
		c.compileLabeledWhileStatement(s, needResult, label)
	case *ast.DoWhileStatement:
		c.compileLabeledDoWhileStatement(s, needResult, label)
	default:
		c.compileGenericLabeledStatement(s, needResult, label)
	}
}

func (c *compiler) updateEnterBlock(enter *enterBlock) {
	scope := c.scope
	stashSize, stackSize := 0, 0
	if scope.dynLookup {
		stashSize = len(scope.bindings)
		enter.names = scope.makeNamesMap()
	} else {
		for _, b := range scope.bindings {
			if b.inStash {
				stashSize++
			} else {
				stackSize++
			}
		}
	}
	enter.stashSize, enter.stackSize = uint32(stashSize), uint32(stackSize)
}

func (c *compiler) compileTryStatement(v *ast.TryStatement, needResult bool) {
	c.block = &block{
		typ:   blockTry,
		outer: c.block,
	}
	var lp int
	var bodyNeedResult bool
	var finallyBreaking *block
	if v.Finally != nil {
		lp, finallyBreaking = c.scanStatements(v.Finally.List)
	}
	if finallyBreaking != nil {
		c.block.breaking = finallyBreaking
		if lp == -1 {
			bodyNeedResult = finallyBreaking.needResult
		}
	} else {
		bodyNeedResult = needResult
	}
	lbl := len(c.p.code)
	c.emit(nil)
	if needResult {
		c.emit(clearResult)
	}
	c.compileBlockStatement(v.Body, bodyNeedResult)
	var catchOffset int
	if v.Catch != nil {
		lbl2 := len(c.p.code) // jump over the catch block
		c.emit(nil)
		catchOffset = len(c.p.code) - lbl
		if v.Catch.Parameter != nil {
			c.block = &block{
				typ:   blockScope,
				outer: c.block,
			}
			c.newBlockScope()
			list := v.Catch.Body.List
			funcs := c.extractFunctions(list)
			if _, ok := v.Catch.Parameter.(ast.Pattern); ok {
				// add anonymous binding for the catch parameter, note it must be first
				c.scope.addBinding(int(v.Catch.Idx0()) - 1)
			}
			c.createBindings(v.Catch.Parameter, func(name unistring.String, offset int) {
				if c.scope.strict {
					switch name {
					case "arguments", "eval":
						c.throwSyntaxError(offset, "Catch variable may not be eval or arguments in strict mode")
					}
				}
				c.scope.bindNameLexical(name, true, offset)
			})
			enter := &enterBlock{}
			c.emit(enter)
			if pattern, ok := v.Catch.Parameter.(ast.Pattern); ok {
				c.scope.bindings[0].emitGet()
				c.emitPattern(pattern, func(target, init compiledExpr) {
					c.emitPatternLexicalAssign(target, init)
				}, false)
			}
			for _, decl := range funcs {
				c.scope.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1)
			}
			c.compileLexicalDeclarations(list, true)
			c.compileFunctions(funcs)
			c.compileStatements(list, bodyNeedResult)
			c.leaveScopeBlock(enter)
			if c.scope.dynLookup || c.scope.bindings[0].inStash {
				c.p.code[lbl+catchOffset] = &enterCatchBlock{
					names:     enter.names,
					stashSize: enter.stashSize,
					stackSize: enter.stackSize,
				}
			} else {
				enter.stackSize--
			}
			c.popScope()
		} else {
			c.emit(pop)
			c.compileBlockStatement(v.Catch.Body, bodyNeedResult)
		}
		c.p.code[lbl2] = jump(len(c.p.code) - lbl2)
	}
	var finallyOffset int
	if v.Finally != nil {
		c.emit(enterFinally{})
		finallyOffset = len(c.p.code) - lbl // finallyOffset should not include enterFinally
		if bodyNeedResult && finallyBreaking != nil && lp == -1 {
			c.emit(clearResult)
		}
		c.compileBlockStatement(v.Finally, false)
		c.emit(leaveFinally{})
	} else {
		c.emit(leaveTry{})
	}
	c.p.code[lbl] = try{catchOffset: int32(catchOffset), finallyOffset: int32(finallyOffset)}
	c.leaveBlock()
}

func (c *compiler) addSrcMap(node ast.Node) {
	c.p.addSrcMap(int(node.Idx0()) - 1)
}

func (c *compiler) compileThrowStatement(v *ast.ThrowStatement) {
	c.compileExpression(v.Argument).emitGetter(true)
	c.addSrcMap(v)
	c.emit(throw)
}

func (c *compiler) compileDoWhileStatement(v *ast.DoWhileStatement, needResult bool) {
	c.compileLabeledDoWhileStatement(v, needResult, "")
}

func (c *compiler) compileLabeledDoWhileStatement(v *ast.DoWhileStatement, needResult bool, label unistring.String) {
	c.block = &block{
		typ:        blockLoop,
		outer:      c.block,
		label:      label,
		needResult: needResult,
	}

	start := len(c.p.code)
	c.compileStatement(v.Body, needResult)
	c.block.cont = len(c.p.code)
	c.emitExpr(c.compileExpression(v.Test), true)
	c.emit(jeqP(start - len(c.p.code)))
	c.leaveBlock()
}

func (c *compiler) compileForStatement(v *ast.ForStatement, needResult bool) {
	c.compileLabeledForStatement(v, needResult, "")
}

func (c *compiler) compileForHeadLexDecl(decl *ast.LexicalDeclaration, needResult bool) *enterBlock {
	c.block = &block{
		typ:        blockIterScope,
		outer:      c.block,
		needResult: needResult,
	}

	c.newBlockScope()
	enterIterBlock := &enterBlock{}
	c.emit(enterIterBlock)
	c.createLexicalBindings(decl)
	c.compileLexicalDeclaration(decl)
	return enterIterBlock
}

func (c *compiler) compileLabeledForStatement(v *ast.ForStatement, needResult bool, label unistring.String) {
	loopBlock := &block{
		typ:        blockLoop,
		outer:      c.block,
		label:      label,
		needResult: needResult,
	}
	c.block = loopBlock

	var enterIterBlock *enterBlock
	switch init := v.Initializer.(type) {
	case nil:
		// no-op
	case *ast.ForLoopInitializerLexicalDecl:
		enterIterBlock = c.compileForHeadLexDecl(&init.LexicalDeclaration, needResult)
	case *ast.ForLoopInitializerVarDeclList:
		for _, expr := range init.List {
			c.compileVarBinding(expr)
		}
	case *ast.ForLoopInitializerExpression:
		c.compileExpression(init.Expression).emitGetter(false)
	default:
		c.assert(false, int(v.For)-1, "Unsupported for loop initializer: %T", init)
		panic("unreachable")
	}

	if needResult {
		c.emit(clearResult) // initial result
	}

	if enterIterBlock != nil {
		c.emit(jump(1))
	}

	start := len(c.p.code)
	var j int
	testConst := false
	if v.Test != nil {
		expr := c.compileExpression(v.Test)
		if expr.constant() {
			r, ex := c.evalConst(expr)
			if ex == nil {
				if r.ToBoolean() {
					testConst = true
				} else {
					leave := c.enterDummyMode()
					c.compileStatement(v.Body, false)
					if v.Update != nil {
						c.compileExpression(v.Update).emitGetter(false)
					}
					leave()
					goto end
				}
			} else {
				expr.addSrcMap()
				c.emitThrow(ex.val)
				goto end
			}
		} else {
			expr.emitGetter(true)
			j = len(c.p.code)
			c.emit(nil)
		}
	}
	if needResult {
		c.emit(clearResult)
	}
	c.compileStatement(v.Body, needResult)
	loopBlock.cont = len(c.p.code)
	if enterIterBlock != nil {
		c.emit(jump(1))
	}
	if v.Update != nil {
		c.compileExpression(v.Update).emitGetter(false)
	}
	if enterIterBlock != nil {
		if c.scope.needStash || c.scope.isDynamic() {
			c.p.code[start-1] = copyStash{}
			c.p.code[loopBlock.cont] = copyStash{}
		} else {
			if l := len(c.p.code); l > loopBlock.cont {
				loopBlock.cont++
			} else {
				c.p.code = c.p.code[:l-1]
			}
		}
	}
	c.emit(jump(start - len(c.p.code)))
	if v.Test != nil {
		if !testConst {
			c.p.code[j] = jneP(len(c.p.code) - j)
		}
	}
end:
	if enterIterBlock != nil {
		c.leaveScopeBlock(enterIterBlock)
		c.popScope()
	}
	c.leaveBlock()
}

func (c *compiler) compileForInStatement(v *ast.ForInStatement, needResult bool) {
	c.compileLabeledForInStatement(v, needResult, "")
}

func (c *compiler) compileForInto(into ast.ForInto, needResult bool) (enter *enterBlock) {
	switch into := into.(type) {
	case *ast.ForIntoExpression:
		c.compileExpression(into.Expression).emitSetter(&c.enumGetExpr, false)
	case *ast.ForIntoVar:
		if c.scope.strict && into.Binding.Initializer != nil {
			c.throwSyntaxError(int(into.Binding.Initializer.Idx0())-1, "for-in loop variable declaration may not have an initializer.")
		}
		switch target := into.Binding.Target.(type) {
		case *ast.Identifier:
			c.compileIdentifierExpression(target).emitSetter(&c.enumGetExpr, false)
		case ast.Pattern:
			c.emit(enumGet)
			c.emitPattern(target, c.emitPatternVarAssign, false)
		default:
			c.throwSyntaxErrorf(int(target.Idx0()-1), "unsupported for-in var target: %T", target)
		}
	case *ast.ForDeclaration:

		c.block = &block{
			typ:        blockIterScope,
			outer:      c.block,
			needResult: needResult,
		}

		c.newBlockScope()
		enter = &enterBlock{}
		c.emit(enter)
		switch target := into.Target.(type) {
		case *ast.Identifier:
			b := c.createLexicalIdBinding(target.Name, into.IsConst, int(into.Idx)-1)
			c.emit(enumGet)
			b.emitInitP()
		case ast.Pattern:
			c.createLexicalBinding(target, into.IsConst)
			c.emit(enumGet)
			c.emitPattern(target, func(target, init compiledExpr) {
				c.emitPatternLexicalAssign(target, init)
			}, false)
		default:
			c.assert(false, int(into.Idx)-1, "Unsupported ForBinding: %T", into.Target)
		}
	default:
		c.assert(false, int(into.Idx0())-1, "Unsupported for-into: %T", into)
		panic("unreachable")
	}

	return
}

func (c *compiler) compileLabeledForInOfStatement(into ast.ForInto, source ast.Expression, body ast.Statement, iter, needResult bool, label unistring.String) {
	c.block = &block{
		typ:        blockLoopEnum,
		outer:      c.block,
		label:      label,
		needResult: needResult,
	}
	enterPos := -1
	if forDecl, ok := into.(*ast.ForDeclaration); ok {
		c.block = &block{
			typ:        blockScope,
			outer:      c.block,
			needResult: false,
		}
		c.newBlockScope()
		enterPos = len(c.p.code)
		c.emit(jump(1))
		c.createLexicalBinding(forDecl.Target, forDecl.IsConst)
	}
	c.compileExpression(source).emitGetter(true)
	if enterPos != -1 {
		s := c.scope
		used := len(c.block.breaks) > 0 || s.isDynamic()
		if !used {
			for _, b := range s.bindings {
				if b.useCount() > 0 {
					used = true
					break
				}
			}
		}
		if used {
			// We need the stack untouched because it contains the source.
			// This is not the most optimal way, but it's an edge case, hopefully quite rare.
			for _, b := range s.bindings {
				b.moveToStash()
			}
			enter := &enterBlock{}
			c.p.code[enterPos] = enter
			c.leaveScopeBlock(enter)
		} else {
			c.block = c.block.outer
		}
		c.popScope()
	}
	if iter {
		c.emit(iterateP)
	} else {
		c.emit(enumerate)
	}
	if needResult {
		c.emit(clearResult)
	}
	start := len(c.p.code)
	c.block.cont = start
	c.emit(nil)
	enterIterBlock := c.compileForInto(into, needResult)
	if needResult {
		c.emit(clearResult)
	}
	c.compileStatement(body, needResult)
	if enterIterBlock != nil {
		c.leaveScopeBlock(enterIterBlock)
		c.popScope()
	}
	c.emit(jump(start - len(c.p.code)))
	if iter {
		c.p.code[start] = iterNext(len(c.p.code) - start)
	} else {
		c.p.code[start] = enumNext(len(c.p.code) - start)
	}
	c.emit(enumPop, jump(2))
	c.leaveBlock()
	c.emit(enumPopClose)
}

func (c *compiler) compileLabeledForInStatement(v *ast.ForInStatement, needResult bool, label unistring.String) {
	c.compileLabeledForInOfStatement(v.Into, v.Source, v.Body, false, needResult, label)
}

func (c *compiler) compileForOfStatement(v *ast.ForOfStatement, needResult bool) {
	c.compileLabeledForOfStatement(v, needResult, "")
}

func (c *compiler) compileLabeledForOfStatement(v *ast.ForOfStatement, needResult bool, label unistring.String) {
	c.compileLabeledForInOfStatement(v.Into, v.Source, v.Body, true, needResult, label)
}

func (c *compiler) compileWhileStatement(v *ast.WhileStatement, needResult bool) {
	c.compileLabeledWhileStatement(v, needResult, "")
}

func (c *compiler) compileLabeledWhileStatement(v *ast.WhileStatement, needResult bool, label unistring.String) {
	c.block = &block{
		typ:        blockLoop,
		outer:      c.block,
		label:      label,
		needResult: needResult,
	}

	if needResult {
		c.emit(clearResult)
	}
	start := len(c.p.code)
	c.block.cont = start
	expr := c.compileExpression(v.Test)
	testTrue := false
	var j int
	if expr.constant() {
		if t, ex := c.evalConst(expr); ex == nil {
			if t.ToBoolean() {
				testTrue = true
			} else {
				c.compileStatementDummy(v.Body)
				goto end
			}
		} else {
			c.emitThrow(ex.val)
			goto end
		}
	} else {
		expr.emitGetter(true)
		j = len(c.p.code)
		c.emit(nil)
	}
	if needResult {
		c.emit(clearResult)
	}
	c.compileStatement(v.Body, needResult)
	c.emit(jump(start - len(c.p.code)))
	if !testTrue {
		c.p.code[j] = jneP(len(c.p.code) - j)
	}
end:
	c.leaveBlock()
}

func (c *compiler) compileEmptyStatement(needResult bool) {
	if needResult {
		c.emit(clearResult)
	}
}

func (c *compiler) compileBranchStatement(v *ast.BranchStatement) {
	switch v.Token {
	case token.BREAK:
		c.compileBreak(v.Label, v.Idx)
	case token.CONTINUE:
		c.compileContinue(v.Label, v.Idx)
	default:
		c.assert(false, int(v.Idx0())-1, "Unknown branch statement token: %s", v.Token.String())
		panic("unreachable")
	}
}

func (c *compiler) findBranchBlock(st *ast.BranchStatement) *block {
	switch st.Token {
	case token.BREAK:
		return c.findBreakBlock(st.Label, true)
	case token.CONTINUE:
		return c.findBreakBlock(st.Label, false)
	}
	return nil
}

func (c *compiler) findBreakBlock(label *ast.Identifier, isBreak bool) (res *block) {
	if label != nil {
		var found *block
		for b := c.block; b != nil; b = b.outer {
			if res == nil {
				if bb := b.breaking; bb != nil {
					res = bb
					if isBreak {
						return
					}
				}
			}
			if b.label == label.Name {
				found = b
				break
			}
		}
		if !isBreak && found != nil && found.typ != blockLoop && found.typ != blockLoopEnum {
			c.throwSyntaxErrorf(int(label.Idx)-1, "Illegal continue statement: '%s' does not denote an iteration statement", label.Name)
		}
		if res == nil {
			res = found
		}
	} else {
		// find the nearest loop or switch (if break)
	L:
		for b := c.block; b != nil; b = b.outer {
			if bb := b.breaking; bb != nil {
				return bb
			}
			switch b.typ {
			case blockLoop, blockLoopEnum:
				res = b
				break L
			case blockSwitch:
				if isBreak {
					res = b
					break L
				}
			}
		}
	}

	return
}

func (c *compiler) emitBlockExitCode(label *ast.Identifier, idx file.Idx, isBreak bool) *block {
	block := c.findBreakBlock(label, isBreak)
	if block == nil {
		c.throwSyntaxError(int(idx)-1, "Could not find block")
		panic("unreachable")
	}
	contForLoop := !isBreak && block.typ == blockLoop
L:
	for b := c.block; b != block; b = b.outer {
		switch b.typ {
		case blockIterScope:
			// blockIterScope in 'for' loops is shared across iterations, so
			// continue should not pop it.
			if contForLoop && b.outer == block {
				break L
			}
			fallthrough
		case blockScope:
			b.breaks = append(b.breaks, len(c.p.code))
			c.emit(nil)
		case blockTry:
			c.emit(leaveTry{})
		case blockWith:
			c.emit(leaveWith)
		case blockLoopEnum:
			c.emit(enumPopClose)
		}
	}
	return block
}

func (c *compiler) compileBreak(label *ast.Identifier, idx file.Idx) {
	block := c.emitBlockExitCode(label, idx, true)
	block.breaks = append(block.breaks, len(c.p.code))
	c.emit(nil)
}

func (c *compiler) compileContinue(label *ast.Identifier, idx file.Idx) {
	block := c.emitBlockExitCode(label, idx, false)
	block.conts = append(block.conts, len(c.p.code))
	c.emit(nil)
}

func (c *compiler) compileIfBody(s ast.Statement, needResult bool) {
	if !c.scope.strict {
		if s, ok := s.(*ast.FunctionDeclaration); ok && !s.Function.Async && !s.Function.Generator {
			c.compileFunction(s)
			if needResult {
				c.emit(clearResult)
			}
			return
		}
	}
	c.compileStatement(s, needResult)
}

func (c *compiler) compileIfBodyDummy(s ast.Statement) {
	leave := c.enterDummyMode()
	defer leave()
	c.compileIfBody(s, false)
}

func (c *compiler) compileIfStatement(v *ast.IfStatement, needResult bool) {
	test := c.compileExpression(v.Test)
	if needResult {
		c.emit(clearResult)
	}
	if test.constant() {
		r, ex := c.evalConst(test)
		if ex != nil {
			test.addSrcMap()
			c.emitThrow(ex.val)
			return
		}
		if r.ToBoolean() {
			c.compileIfBody(v.Consequent, needResult)
			if v.Alternate != nil {
				c.compileIfBodyDummy(v.Alternate)
			}
		} else {
			c.compileIfBodyDummy(v.Consequent)
			if v.Alternate != nil {
				c.compileIfBody(v.Alternate, needResult)
			} else {
				if needResult {
					c.emit(clearResult)
				}
			}
		}
		return
	}
	test.emitGetter(true)
	jmp := len(c.p.code)
	c.emit(nil)
	c.compileIfBody(v.Consequent, needResult)
	if v.Alternate != nil {
		jmp1 := len(c.p.code)
		c.emit(nil)
		c.p.code[jmp] = jneP(len(c.p.code) - jmp)
		c.compileIfBody(v.Alternate, needResult)
		c.p.code[jmp1] = jump(len(c.p.code) - jmp1)
	} else {
		if needResult {
			c.emit(jump(2))
			c.p.code[jmp] = jneP(len(c.p.code) - jmp)
			c.emit(clearResult)
		} else {
			c.p.code[jmp] = jneP(len(c.p.code) - jmp)
		}
	}
}

func (c *compiler) compileReturnStatement(v *ast.ReturnStatement) {
	if s := c.scope.nearestFunction(); s != nil && s.funcType == funcClsInit {
		c.throwSyntaxError(int(v.Return)-1, "Illegal return statement")
	}
	if v.Argument != nil {
		c.emitExpr(c.compileExpression(v.Argument), true)
	} else {
		c.emit(loadUndef)
	}
	for b := c.block; b != nil; b = b.outer {
		switch b.typ {
		case blockTry:
			c.emit(saveResult, leaveTry{}, loadResult)
		case blockLoopEnum:
			c.emit(enumPopClose)
		}
	}
	if s := c.scope.nearestFunction(); s != nil && s.funcType == funcDerivedCtor {
		b := s.boundNames[thisBindingName]
		c.assert(b != nil, int(v.Return)-1, "Derived constructor, but no 'this' binding")
		b.markAccessPoint()
	}
	c.emit(ret)
}

func (c *compiler) checkVarConflict(name unistring.String, offset int) {
	for sc := c.scope; sc != nil; sc = sc.outer {
		if b, exists := sc.boundNames[name]; exists && !b.isVar && !(b.isArg && sc != c.scope) {
			c.throwSyntaxErrorf(offset, "Identifier '%s' has already been declared", name)
		}
		if sc.isFunction() {
			break
		}
	}
}

func (c *compiler) emitVarAssign(name unistring.String, offset int, init compiledExpr) {
	c.checkVarConflict(name, offset)
	if init != nil {
		b, noDyn := c.scope.lookupName(name)
		if noDyn {
			c.emitNamedOrConst(init, name)
			c.p.addSrcMap(offset)
			b.emitInitP()
		} else {
			c.emitVarRef(name, offset, b)
			c.emitNamedOrConst(init, name)
			c.p.addSrcMap(offset)
			c.emit(initValueP)
		}
	}
}

func (c *compiler) compileVarBinding(expr *ast.Binding) {
	switch target := expr.Target.(type) {
	case *ast.Identifier:
		c.emitVarAssign(target.Name, int(target.Idx)-1, c.compileExpression(expr.Initializer))
	case ast.Pattern:
		c.compileExpression(expr.Initializer).emitGetter(true)
		c.emitPattern(target, c.emitPatternVarAssign, false)
	default:
		c.throwSyntaxErrorf(int(target.Idx0()-1), "unsupported variable binding target: %T", target)
	}
}

func (c *compiler) emitLexicalAssign(name unistring.String, offset int, init compiledExpr) {
	b := c.scope.boundNames[name]
	c.assert(b != nil, offset, "Lexical declaration for an unbound name")
	if init != nil {
		c.emitNamedOrConst(init, name)
		c.p.addSrcMap(offset)
	} else {
		if b.isConst {
			c.throwSyntaxError(offset, "Missing initializer in const declaration")
		}
		c.emit(loadUndef)
	}
	b.emitInitP()
}

func (c *compiler) emitPatternVarAssign(target, init compiledExpr) {
	id := target.(*compiledIdentifierExpr)
	c.emitVarAssign(id.name, id.offset, init)
}

func (c *compiler) emitPatternLexicalAssign(target, init compiledExpr) {
	id := target.(*compiledIdentifierExpr)
	c.emitLexicalAssign(id.name, id.offset, init)
}

func (c *compiler) emitPatternAssign(target, init compiledExpr) {
	if id, ok := target.(*compiledIdentifierExpr); ok {
		b, noDyn := c.scope.lookupName(id.name)
		if noDyn {
			c.emitNamedOrConst(init, id.name)
			b.emitSetP()
		} else {
			c.emitVarRef(id.name, id.offset, b)
			c.emitNamedOrConst(init, id.name)
			c.emit(putValueP)
		}
	} else {
		target.emitRef()
		c.emitExpr(init, true)
		c.emit(putValueP)
	}
}

func (c *compiler) compileLexicalBinding(expr *ast.Binding) {
	switch target := expr.Target.(type) {
	case *ast.Identifier:
		c.emitLexicalAssign(target.Name, int(target.Idx)-1, c.compileExpression(expr.Initializer))
	case ast.Pattern:
		c.compileExpression(expr.Initializer).emitGetter(true)
		c.emitPattern(target, func(target, init compiledExpr) {
			c.emitPatternLexicalAssign(target, init)
		}, false)
	default:
		c.throwSyntaxErrorf(int(target.Idx0()-1), "unsupported lexical binding target: %T", target)
	}
}

func (c *compiler) compileVariableStatement(v *ast.VariableStatement) {
	for _, expr := range v.List {
		c.compileVarBinding(expr)
	}
}

func (c *compiler) compileLexicalDeclaration(v *ast.LexicalDeclaration) {
	for _, e := range v.List {
		c.compileLexicalBinding(e)
	}
}

func (c *compiler) isEmptyResult(st ast.Statement) bool {
	switch st := st.(type) {
	case *ast.EmptyStatement, *ast.VariableStatement, *ast.LexicalDeclaration, *ast.FunctionDeclaration,
		*ast.ClassDeclaration, *ast.BranchStatement, *ast.DebuggerStatement:
		return true
	case *ast.LabelledStatement:
		return c.isEmptyResult(st.Statement)
	case *ast.BlockStatement:
		for _, s := range st.List {
			if _, ok := s.(*ast.BranchStatement); ok {
				return true
			}
			if !c.isEmptyResult(s) {
				return false
			}
		}
		return true
	}
	return false
}

func (c *compiler) scanStatements(list []ast.Statement) (lastProducingIdx int, breakingBlock *block) {
	lastProducingIdx = -1
	for i, st := range list {
		if bs, ok := st.(*ast.BranchStatement); ok {
			if blk := c.findBranchBlock(bs); blk != nil {
				breakingBlock = blk
			}
			break
		}
		if !c.isEmptyResult(st) {
			lastProducingIdx = i
		}
	}
	return
}

func (c *compiler) compileStatementsNeedResult(list []ast.Statement, lastProducingIdx int) {
	if lastProducingIdx >= 0 {
		for _, st := range list[:lastProducingIdx] {
			if _, ok := st.(*ast.FunctionDeclaration); ok {
				continue
			}
			c.compileStatement(st, false)
		}
		c.compileStatement(list[lastProducingIdx], true)
	}
	var leave func()
	defer func() {
		if leave != nil {
			leave()
		}
	}()
	for _, st := range list[lastProducingIdx+1:] {
		if _, ok := st.(*ast.FunctionDeclaration); ok {
			continue
		}
		c.compileStatement(st, false)
		if leave == nil {
			if _, ok := st.(*ast.BranchStatement); ok {
				leave = c.enterDummyMode()
			}
		}
	}
}

func (c *compiler) compileStatements(list []ast.Statement, needResult bool) {
	lastProducingIdx, blk := c.scanStatements(list)
	if blk != nil {
		needResult = blk.needResult
	}
	if needResult {
		c.compileStatementsNeedResult(list, lastProducingIdx)
		return
	}
	for _, st := range list {
		if _, ok := st.(*ast.FunctionDeclaration); ok {
			continue
		}
		c.compileStatement(st, false)
	}
}

func (c *compiler) compileGenericLabeledStatement(v ast.Statement, needResult bool, label unistring.String) {
	c.block = &block{
		typ:        blockLabel,
		outer:      c.block,
		label:      label,
		needResult: needResult,
	}
	c.compileStatement(v, needResult)
	c.leaveBlock()
}

func (c *compiler) compileBlockStatement(v *ast.BlockStatement, needResult bool) {
	var scopeDeclared bool
	funcs := c.extractFunctions(v.List)
	if len(funcs) > 0 {
		c.newBlockScope()
		scopeDeclared = true
	}
	c.createFunctionBindings(funcs)
	scopeDeclared = c.compileLexicalDeclarations(v.List, scopeDeclared)

	var enter *enterBlock
	if scopeDeclared {
		c.block = &block{
			outer:      c.block,
			typ:        blockScope,
			needResult: needResult,
		}
		enter = &enterBlock{}
		c.emit(enter)
	}
	c.compileFunctions(funcs)
	c.compileStatements(v.List, needResult)
	if scopeDeclared {
		c.leaveScopeBlock(enter)
		c.popScope()
	}
}

func (c *compiler) compileExpressionStatement(v *ast.ExpressionStatement, needResult bool) {
	c.emitExpr(c.compileExpression(v.Expression), needResult)
	if needResult {
		c.emit(saveResult)
	}
}

func (c *compiler) compileWithStatement(v *ast.WithStatement, needResult bool) {
	if c.scope.strict {
		c.throwSyntaxError(int(v.With)-1, "Strict mode code may not include a with statement")
		return
	}
	c.compileExpression(v.Object).emitGetter(true)
	c.emit(enterWith)
	c.block = &block{
		outer:      c.block,
		typ:        blockWith,
		needResult: needResult,
	}
	c.newBlockScope()
	c.scope.dynamic = true
	c.compileStatement(v.Body, needResult)
	c.emit(leaveWith)
	c.leaveBlock()
	c.popScope()
}

func (c *compiler) compileSwitchStatement(v *ast.SwitchStatement, needResult bool) {
	c.block = &block{
		typ:        blockSwitch,
		outer:      c.block,
		needResult: needResult,
	}

	c.compileExpression(v.Discriminant).emitGetter(true)

	var funcs []*ast.FunctionDeclaration
	for _, s := range v.Body {
		f := c.extractFunctions(s.Consequent)
		funcs = append(funcs, f...)
	}
	var scopeDeclared bool
	if len(funcs) > 0 {
		c.newBlockScope()
		scopeDeclared = true
		c.createFunctionBindings(funcs)
	}

	for _, s := range v.Body {
		scopeDeclared = c.compileLexicalDeclarations(s.Consequent, scopeDeclared)
	}

	var enter *enterBlock
	var db *binding
	if scopeDeclared {
		c.block = &block{
			typ:        blockScope,
			outer:      c.block,
			needResult: needResult,
		}
		enter = &enterBlock{}
		c.emit(enter)
		// create anonymous variable for the discriminant
		bindings := c.scope.bindings
		var bb []*binding
		if cap(bindings) == len(bindings) {
			bb = make([]*binding, len(bindings)+1)
		} else {
			bb = bindings[:len(bindings)+1]
		}
		copy(bb[1:], bindings)
		db = &binding{
			scope:    c.scope,
			isConst:  true,
			isStrict: true,
		}
		bb[0] = db
		c.scope.bindings = bb
	}

	c.compileFunctions(funcs)

	if needResult {
		c.emit(clearResult)
	}

	jumps := make([]int, len(v.Body))

	for i, s := range v.Body {
		if s.Test != nil {
			if db != nil {
				db.emitGet()
			} else {
				c.emit(dup)
			}
			c.compileExpression(s.Test).emitGetter(true)
			c.emit(op_strict_eq)
			if db != nil {
				c.emit(jneP(2))
			} else {
				c.emit(jneP(3), pop)
			}
			jumps[i] = len(c.p.code)
			c.emit(nil)
		}
	}

	if db == nil {
		c.emit(pop)
	}
	jumpNoMatch := -1
	if v.Default != -1 {
		if v.Default != 0 {
			jumps[v.Default] = len(c.p.code)
			c.emit(nil)
		}
	} else {
		jumpNoMatch = len(c.p.code)
		c.emit(nil)
	}

	for i, s := range v.Body {
		if s.Test != nil || i != 0 {
			c.p.code[jumps[i]] = jump(len(c.p.code) - jumps[i])
		}
		c.compileStatements(s.Consequent, needResult)
	}

	if jumpNoMatch != -1 {
		c.p.code[jumpNoMatch] = jump(len(c.p.code) - jumpNoMatch)
	}
	if enter != nil {
		c.leaveScopeBlock(enter)
		enter.stackSize--
		c.popScope()
	}
	c.leaveBlock()
}

func (c *compiler) compileClassDeclaration(v *ast.ClassDeclaration) {
	c.emitLexicalAssign(v.Class.Name.Name, int(v.Class.Class)-1, c.compileClassLiteral(v.Class, false))
}
