package goja

import (
	"fmt"
	"sort"

	"github.com/projectdiscovery/goja/token"

	"github.com/projectdiscovery/goja/ast"
	"github.com/projectdiscovery/goja/file"
	"github.com/projectdiscovery/goja/unistring"
)

type blockType int

const (
	blockLoop blockType = iota
	blockLoopEnum
	blockTry
	blockLabel
	blockSwitch
	blockWith
	blockScope
	blockIterScope
	blockOptChain
)

const (
	maskConst     = 1 << 31
	maskVar       = 1 << 30
	maskDeletable = 1 << 29
	maskStrict    = maskDeletable

	maskTyp = maskConst | maskVar | maskDeletable
)

type varType byte

const (
	varTypeVar varType = iota
	varTypeLet
	varTypeStrictConst
	varTypeConst
)

const thisBindingName = " this" // must not be a valid identifier

type CompilerError struct {
	Message string
	File    *file.File
	Offset  int
}

type CompilerSyntaxError struct {
	CompilerError
}

type CompilerReferenceError struct {
	CompilerError
}

type srcMapItem struct {
	pc     int
	srcPos int
}

// Program is an internal, compiled representation of code which is produced by the Compile function.
// This representation is not linked to a runtime in any way and can be used concurrently.
// It is always preferable to use a Program over a string when running code as it skips the compilation step.
type Program struct {
	code []instruction

	funcName unistring.String
	src      *file.File
	srcMap   []srcMapItem
}

type compiler struct {
	p     *Program
	scope *scope
	block *block

	classScope *classScope

	enumGetExpr compiledEnumGetExpr

	evalVM *vm // VM used to evaluate constant expressions
	ctxVM  *vm // VM in which an eval() code is compiled

	codeScratchpad []instruction

	stringCache map[unistring.String]Value
}

type binding struct {
	scope        *scope
	name         unistring.String
	accessPoints map[*scope]*[]int
	isConst      bool
	isStrict     bool
	isArg        bool
	isVar        bool
	inStash      bool
}

func (b *binding) getAccessPointsForScope(s *scope) *[]int {
	m := b.accessPoints[s]
	if m == nil {
		a := make([]int, 0, 1)
		m = &a
		if b.accessPoints == nil {
			b.accessPoints = make(map[*scope]*[]int)
		}
		b.accessPoints[s] = m
	}
	return m
}

func (b *binding) markAccessPointAt(pos int) {
	scope := b.scope.c.scope
	m := b.getAccessPointsForScope(scope)
	*m = append(*m, pos-scope.base)
}

func (b *binding) markAccessPointAtScope(scope *scope, pos int) {
	m := b.getAccessPointsForScope(scope)
	*m = append(*m, pos-scope.base)
}

func (b *binding) markAccessPoint() {
	scope := b.scope.c.scope
	m := b.getAccessPointsForScope(scope)
	*m = append(*m, len(scope.prg.code)-scope.base)
}

func (b *binding) emitGet() {
	b.markAccessPoint()
	if b.isVar && !b.isArg {
		b.scope.c.emit(loadStack(0))
	} else {
		b.scope.c.emit(loadStackLex(0))
	}
}

func (b *binding) emitGetAt(pos int) {
	b.markAccessPointAt(pos)
	if b.isVar && !b.isArg {
		b.scope.c.p.code[pos] = loadStack(0)
	} else {
		b.scope.c.p.code[pos] = loadStackLex(0)
	}
}

func (b *binding) emitGetP() {
	if b.isVar && !b.isArg {
		// no-op
	} else {
		// make sure TDZ is checked
		b.markAccessPoint()
		b.scope.c.emit(loadStackLex(0), pop)
	}
}

func (b *binding) emitSet() {
	if b.isConst {
		if b.isStrict || b.scope.c.scope.strict {
			b.scope.c.emit(throwAssignToConst)
		}
		return
	}
	b.markAccessPoint()
	if b.isVar && !b.isArg {
		b.scope.c.emit(storeStack(0))
	} else {
		b.scope.c.emit(storeStackLex(0))
	}
}

func (b *binding) emitSetP() {
	if b.isConst {
		if b.isStrict || b.scope.c.scope.strict {
			b.scope.c.emit(throwAssignToConst)
		}
		return
	}
	b.markAccessPoint()
	if b.isVar && !b.isArg {
		b.scope.c.emit(storeStackP(0))
	} else {
		b.scope.c.emit(storeStackLexP(0))
	}
}

func (b *binding) emitInitP() {
	if !b.isVar && b.scope.outer == nil {
		b.scope.c.emit(initGlobalP(b.name))
	} else {
		b.markAccessPoint()
		b.scope.c.emit(initStackP(0))
	}
}

func (b *binding) emitInit() {
	if !b.isVar && b.scope.outer == nil {
		b.scope.c.emit(initGlobal(b.name))
	} else {
		b.markAccessPoint()
		b.scope.c.emit(initStack(0))
	}
}

func (b *binding) emitInitAt(pos int) {
	if !b.isVar && b.scope.outer == nil {
		b.scope.c.p.code[pos] = initGlobal(b.name)
	} else {
		b.markAccessPointAt(pos)
		b.scope.c.p.code[pos] = initStack(0)
	}
}

func (b *binding) emitInitAtScope(scope *scope, pos int) {
	if !b.isVar && scope.outer == nil {
		scope.c.p.code[pos] = initGlobal(b.name)
	} else {
		b.markAccessPointAtScope(scope, pos)
		scope.c.p.code[pos] = initStack(0)
	}
}

func (b *binding) emitInitPAtScope(scope *scope, pos int) {
	if !b.isVar && scope.outer == nil {
		scope.c.p.code[pos] = initGlobalP(b.name)
	} else {
		b.markAccessPointAtScope(scope, pos)
		scope.c.p.code[pos] = initStackP(0)
	}
}

func (b *binding) emitGetVar(callee bool) {
	b.markAccessPoint()
	if b.isVar && !b.isArg {
		b.scope.c.emit(&loadMixed{name: b.name, callee: callee})
	} else {
		b.scope.c.emit(&loadMixedLex{name: b.name, callee: callee})
	}
}

func (b *binding) emitResolveVar(strict bool) {
	b.markAccessPoint()
	if b.isVar && !b.isArg {
		b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: varTypeVar})
	} else {
		var typ varType
		if b.isConst {
			if b.isStrict {
				typ = varTypeStrictConst
			} else {
				typ = varTypeConst
			}
		} else {
			typ = varTypeLet
		}
		b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: typ})
	}
}

func (b *binding) moveToStash() {
	if b.isArg && !b.scope.argsInStash {
		b.scope.moveArgsToStash()
	} else {
		b.inStash = true
		b.scope.needStash = true
	}
}

func (b *binding) useCount() (count int) {
	for _, a := range b.accessPoints {
		count += len(*a)
	}
	return
}

type scope struct {
	c          *compiler
	prg        *Program
	outer      *scope
	nested     []*scope
	boundNames map[unistring.String]*binding
	bindings   []*binding
	base       int
	numArgs    int

	// function type. If not funcNone, this is a function or a top-level lexical environment
	funcType funcType

	// in strict mode
	strict bool
	// eval top-level scope
	eval bool
	// at least one inner scope has direct eval() which can lookup names dynamically (by name)
	dynLookup bool
	// at least one binding has been marked for placement in stash
	needStash bool

	// is a variable environment, i.e. the target for dynamically created var bindings
	variable bool
	// a function scope that has at least one direct eval() and non-strict, so the variables can be added dynamically
	dynamic bool
	// arguments have been marked for placement in stash (functions only)
	argsInStash bool
	// need 'arguments' object (functions only)
	argsNeeded bool
}

type block struct {
	typ        blockType
	label      unistring.String
	cont       int
	breaks     []int
	conts      []int
	outer      *block
	breaking   *block // set when the 'finally' block is an empty break statement sequence
	needResult bool
}

func (c *compiler) leaveScopeBlock(enter *enterBlock) {
	c.updateEnterBlock(enter)
	leave := &leaveBlock{
		stackSize: enter.stackSize,
		popStash:  enter.stashSize > 0,
	}
	c.emit(leave)
	for _, pc := range c.block.breaks {
		c.p.code[pc] = leave
	}
	c.block.breaks = nil
	c.leaveBlock()
}

func (c *compiler) leaveBlock() {
	lbl := len(c.p.code)
	for _, item := range c.block.breaks {
		c.p.code[item] = jump(lbl - item)
	}
	if t := c.block.typ; t == blockLoop || t == blockLoopEnum {
		for _, item := range c.block.conts {
			c.p.code[item] = jump(c.block.cont - item)
		}
	}
	c.block = c.block.outer
}

func (e *CompilerSyntaxError) Error() string {
	if e.File != nil {
		return fmt.Sprintf("SyntaxError: %s at %s", e.Message, e.File.Position(e.Offset))
	}
	return fmt.Sprintf("SyntaxError: %s", e.Message)
}

func (e *CompilerReferenceError) Error() string {
	return fmt.Sprintf("ReferenceError: %s", e.Message)
}

func (c *compiler) newScope() {
	strict := false
	if c.scope != nil {
		strict = c.scope.strict
	}
	c.scope = &scope{
		c:      c,
		prg:    c.p,
		outer:  c.scope,
		strict: strict,
	}
}

func (c *compiler) newBlockScope() {
	c.newScope()
	if outer := c.scope.outer; outer != nil {
		outer.nested = append(outer.nested, c.scope)
	}
	c.scope.base = len(c.p.code)
}

func (c *compiler) popScope() {
	c.scope = c.scope.outer
}

func (c *compiler) emitLiteralString(s String) {
	key := s.string()
	if c.stringCache == nil {
		c.stringCache = make(map[unistring.String]Value)
	}
	internVal := c.stringCache[key]
	if internVal == nil {
		c.stringCache[key] = s
		internVal = s
	}

	c.emit(loadVal{internVal})
}

func (c *compiler) emitLiteralValue(v Value) {
	if s, ok := v.(String); ok {
		c.emitLiteralString(s)
		return
	}

	c.emit(loadVal{v})
}

func newCompiler() *compiler {
	c := &compiler{
		p: &Program{},
	}

	c.enumGetExpr.init(c, file.Idx(0))

	return c
}

func (p *Program) dumpCode(logger func(format string, args ...interface{})) {
	p._dumpCode("", logger)
}

func (p *Program) _dumpCode(indent string, logger func(format string, args ...interface{})) {
	dumpInitFields := func(initFields *Program) {
		i := indent + ">"
		logger("%s ---- init_fields:", i)
		initFields._dumpCode(i, logger)
		logger("%s ----", i)
	}
	for pc, ins := range p.code {
		logger("%s %d: %T(%v)", indent, pc, ins, ins)
		var prg *Program
		switch f := ins.(type) {
		case newFuncInstruction:
			prg = f.getPrg()
		case *newDerivedClass:
			if f.initFields != nil {
				dumpInitFields(f.initFields)
			}
			prg = f.ctor
		case *newClass:
			if f.initFields != nil {
				dumpInitFields(f.initFields)
			}
			prg = f.ctor
		case *newStaticFieldInit:
			if f.initFields != nil {
				dumpInitFields(f.initFields)
			}
		}
		if prg != nil {
			prg._dumpCode(indent+">", logger)
		}
	}
}

func (p *Program) sourceOffset(pc int) int {
	i := sort.Search(len(p.srcMap), func(idx int) bool {
		return p.srcMap[idx].pc > pc
	}) - 1
	if i >= 0 {
		return p.srcMap[i].srcPos
	}

	return 0
}

func (p *Program) addSrcMap(srcPos int) {
	if len(p.srcMap) > 0 && p.srcMap[len(p.srcMap)-1].srcPos == srcPos {
		return
	}
	p.srcMap = append(p.srcMap, srcMapItem{pc: len(p.code), srcPos: srcPos})
}

func (s *scope) lookupName(name unistring.String) (binding *binding, noDynamics bool) {
	noDynamics = true
	toStash := false
	for curScope := s; ; curScope = curScope.outer {
		if curScope.outer != nil {
			if b, exists := curScope.boundNames[name]; exists {
				if toStash && !b.inStash {
					b.moveToStash()
				}
				binding = b
				return
			}
		} else {
			noDynamics = false
			return
		}
		if curScope.dynamic {
			noDynamics = false
		}
		if name == "arguments" && curScope.funcType != funcNone && curScope.funcType != funcArrow {
			if curScope.funcType == funcClsInit {
				s.c.throwSyntaxError(0, "'arguments' is not allowed in class field initializer or static initialization block")
			}
			curScope.argsNeeded = true
			binding, _ = curScope.bindName(name)
			return
		}
		if curScope.isFunction() {
			toStash = true
		}
	}
}

func (s *scope) lookupThis() (*binding, bool) {
	toStash := false
	for curScope := s; curScope != nil; curScope = curScope.outer {
		if curScope.outer == nil {
			if curScope.eval {
				return nil, true
			}
		}
		if b, exists := curScope.boundNames[thisBindingName]; exists {
			if toStash && !b.inStash {
				b.moveToStash()
			}
			return b, false
		}
		if curScope.isFunction() {
			toStash = true
		}
	}
	return nil, false
}

func (s *scope) ensureBoundNamesCreated() {
	if s.boundNames == nil {
		s.boundNames = make(map[unistring.String]*binding)
	}
}

func (s *scope) addBinding(offset int) *binding {
	if len(s.bindings) >= (1<<24)-1 {
		s.c.throwSyntaxError(offset, "Too many variables")
	}
	b := &binding{
		scope: s,
	}
	s.bindings = append(s.bindings, b)
	return b
}

func (s *scope) bindNameLexical(name unistring.String, unique bool, offset int) (*binding, bool) {
	if b := s.boundNames[name]; b != nil {
		if unique {
			s.c.throwSyntaxErrorf(offset, "Identifier '%s' has already been declared", name)
		}
		return b, false
	}
	b := s.addBinding(offset)
	b.name = name
	s.ensureBoundNamesCreated()
	s.boundNames[name] = b
	return b, true
}

func (s *scope) createThisBinding() *binding {
	thisBinding, _ := s.bindNameLexical(thisBindingName, false, 0)
	thisBinding.isVar = true // don't check on load
	return thisBinding
}

func (s *scope) bindName(name unistring.String) (*binding, bool) {
	if !s.isFunction() && !s.variable && s.outer != nil {
		return s.outer.bindName(name)
	}
	b, created := s.bindNameLexical(name, false, 0)
	if created {
		b.isVar = true
	}
	return b, created
}

func (s *scope) bindNameShadow(name unistring.String) (*binding, bool) {
	if !s.isFunction() && s.outer != nil {
		return s.outer.bindNameShadow(name)
	}

	_, exists := s.boundNames[name]
	b := &binding{
		scope: s,
		name:  name,
	}
	s.bindings = append(s.bindings, b)
	s.ensureBoundNamesCreated()
	s.boundNames[name] = b
	return b, !exists
}

func (s *scope) nearestFunction() *scope {
	for sc := s; sc != nil; sc = sc.outer {
		if sc.isFunction() {
			return sc
		}
	}
	return nil
}

func (s *scope) nearestThis() *scope {
	for sc := s; sc != nil; sc = sc.outer {
		if sc.eval || sc.isFunction() && sc.funcType != funcArrow {
			return sc
		}
	}
	return nil
}

func (s *scope) finaliseVarAlloc(stackOffset int) (stashSize, stackSize int) {
	argsInStash := false
	if f := s.nearestFunction(); f != nil {
		argsInStash = f.argsInStash
	}
	stackIdx, stashIdx := 0, 0
	allInStash := s.isDynamic()
	var derivedCtor bool
	if fs := s.nearestThis(); fs != nil && fs.funcType == funcDerivedCtor {
		derivedCtor = true
	}
	for i, b := range s.bindings {
		var this bool
		if b.name == thisBindingName {
			this = true
		}
		if allInStash || b.inStash {
			for scope, aps := range b.accessPoints {
				var level uint32
				for sc := scope; sc != nil && sc != s; sc = sc.outer {
					if sc.needStash || sc.isDynamic() {
						level++
					}
				}
				if level > 255 {
					s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded")
				}
				idx := (level << 24) | uint32(stashIdx)
				base := scope.base
				code := scope.prg.code
				if this {
					if derivedCtor {
						for _, pc := range *aps {
							ap := &code[base+pc]
							switch (*ap).(type) {
							case loadStack:
								*ap = loadThisStash(idx)
							case initStack:
								*ap = initStash(idx)
							case initStackP:
								*ap = initStashP(idx)
							case resolveThisStack:
								*ap = resolveThisStash(idx)
							case _ret:
								*ap = cret(idx)
							default:
								s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'")
							}
						}
					} else {
						for _, pc := range *aps {
							ap := &code[base+pc]
							switch (*ap).(type) {
							case loadStack:
								*ap = loadStash(idx)
							case initStack:
								*ap = initStash(idx)
							case initStackP:
								*ap = initStashP(idx)
							default:
								s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'")
							}
						}
					}
				} else {
					for _, pc := range *aps {
						ap := &code[base+pc]
						switch i := (*ap).(type) {
						case loadStack:
							*ap = loadStash(idx)
						case storeStack:
							*ap = storeStash(idx)
						case storeStackP:
							*ap = storeStashP(idx)
						case loadStackLex:
							*ap = loadStashLex(idx)
						case storeStackLex:
							*ap = storeStashLex(idx)
						case storeStackLexP:
							*ap = storeStashLexP(idx)
						case initStackP:
							*ap = initStashP(idx)
						case initStack:
							*ap = initStash(idx)
						case *loadMixed:
							i.idx = idx
						case *loadMixedLex:
							i.idx = idx
						case *resolveMixed:
							i.idx = idx
						default:
							s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i)
						}
					}
				}
			}
			stashIdx++
		} else {
			var idx int
			if !this {
				if i < s.numArgs {
					idx = -(i + 1)
				} else {
					stackIdx++
					idx = stackIdx + stackOffset
				}
			}
			for scope, aps := range b.accessPoints {
				var level int
				for sc := scope; sc != nil && sc != s; sc = sc.outer {
					if sc.needStash || sc.isDynamic() {
						level++
					}
				}
				if level > 255 {
					s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded")
				}
				code := scope.prg.code
				base := scope.base
				if this {
					if derivedCtor {
						for _, pc := range *aps {
							ap := &code[base+pc]
							switch (*ap).(type) {
							case loadStack:
								*ap = loadThisStack{}
							case initStack:
								// no-op
							case initStackP:
								// no-op
							case resolveThisStack:
								// no-op
							case _ret:
								// no-op, already in the right place
							default:
								s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'")
							}
						}
					} /*else {
						no-op
					}*/
				} else if argsInStash {
					for _, pc := range *aps {
						ap := &code[base+pc]
						switch i := (*ap).(type) {
						case loadStack:
							*ap = loadStack1(idx)
						case storeStack:
							*ap = storeStack1(idx)
						case storeStackP:
							*ap = storeStack1P(idx)
						case loadStackLex:
							*ap = loadStack1Lex(idx)
						case storeStackLex:
							*ap = storeStack1Lex(idx)
						case storeStackLexP:
							*ap = storeStack1LexP(idx)
						case initStackP:
							*ap = initStack1P(idx)
						case initStack:
							*ap = initStack1(idx)
						case *loadMixed:
							*ap = &loadMixedStack1{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
						case *loadMixedLex:
							*ap = &loadMixedStack1Lex{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
						case *resolveMixed:
							*ap = &resolveMixedStack1{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict}
						default:
							s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i)
						}
					}
				} else {
					for _, pc := range *aps {
						ap := &code[base+pc]
						switch i := (*ap).(type) {
						case loadStack:
							*ap = loadStack(idx)
						case storeStack:
							*ap = storeStack(idx)
						case storeStackP:
							*ap = storeStackP(idx)
						case loadStackLex:
							*ap = loadStackLex(idx)
						case storeStackLex:
							*ap = storeStackLex(idx)
						case storeStackLexP:
							*ap = storeStackLexP(idx)
						case initStack:
							*ap = initStack(idx)
						case initStackP:
							*ap = initStackP(idx)
						case *loadMixed:
							*ap = &loadMixedStack{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
						case *loadMixedLex:
							*ap = &loadMixedStackLex{name: i.name, idx: idx, level: uint8(level), callee: i.callee}
						case *resolveMixed:
							*ap = &resolveMixedStack{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict}
						default:
							s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i)
						}
					}
				}
			}
		}
	}
	for _, nested := range s.nested {
		nested.finaliseVarAlloc(stackIdx + stackOffset)
	}
	return stashIdx, stackIdx
}

func (s *scope) moveArgsToStash() {
	for _, b := range s.bindings {
		if !b.isArg {
			break
		}
		b.inStash = true
	}
	s.argsInStash = true
	s.needStash = true
}

func (c *compiler) trimCode(delta int) {
	src := c.p.code[delta:]
	newCode := make([]instruction, len(src))
	copy(newCode, src)
	if cap(c.codeScratchpad) < cap(c.p.code) {
		c.codeScratchpad = c.p.code[:0]
	}
	c.p.code = newCode
}

func (s *scope) trimCode(delta int) {
	s.c.trimCode(delta)
	if delta != 0 {
		srcMap := s.c.p.srcMap
		for i := range srcMap {
			srcMap[i].pc -= delta
		}
		s.adjustBase(-delta)
	}
}

func (s *scope) adjustBase(delta int) {
	s.base += delta
	for _, nested := range s.nested {
		nested.adjustBase(delta)
	}
}

func (s *scope) makeNamesMap() map[unistring.String]uint32 {
	l := len(s.bindings)
	if l == 0 {
		return nil
	}
	names := make(map[unistring.String]uint32, l)
	for i, b := range s.bindings {
		idx := uint32(i)
		if b.isConst {
			idx |= maskConst
			if b.isStrict {
				idx |= maskStrict
			}
		}
		if b.isVar {
			idx |= maskVar
		}
		names[b.name] = idx
	}
	return names
}

func (s *scope) isDynamic() bool {
	return s.dynLookup || s.dynamic
}

func (s *scope) isFunction() bool {
	return s.funcType != funcNone && !s.eval
}

func (s *scope) deleteBinding(b *binding) {
	idx := 0
	for i, bb := range s.bindings {
		if bb == b {
			idx = i
			goto found
		}
	}
	return
found:
	delete(s.boundNames, b.name)
	copy(s.bindings[idx:], s.bindings[idx+1:])
	l := len(s.bindings) - 1
	s.bindings[l] = nil
	s.bindings = s.bindings[:l]
}

func (c *compiler) compile(in *ast.Program, strict, inGlobal bool, evalVm *vm) {
	c.ctxVM = evalVm

	eval := evalVm != nil
	c.p.src = in.File
	c.newScope()
	scope := c.scope
	scope.dynamic = true
	scope.eval = eval
	if !strict && len(in.Body) > 0 {
		strict = c.isStrict(in.Body) != nil
	}
	scope.strict = strict
	ownVarScope := eval && strict
	ownLexScope := !inGlobal || eval
	if ownVarScope {
		c.newBlockScope()
		scope = c.scope
		scope.variable = true
	}
	if eval && !inGlobal {
		for s := evalVm.stash; s != nil; s = s.outer {
			if ft := s.funcType; ft != funcNone && ft != funcArrow {
				scope.funcType = ft
				break
			}
		}
	}
	funcs := c.extractFunctions(in.Body)
	c.createFunctionBindings(funcs)
	numFuncs := len(scope.bindings)
	if inGlobal && !ownVarScope {
		if numFuncs == len(funcs) {
			c.compileFunctionsGlobalAllUnique(funcs)
		} else {
			c.compileFunctionsGlobal(funcs)
		}
	}
	c.compileDeclList(in.DeclarationList, false)
	numVars := len(scope.bindings) - numFuncs
	vars := make([]unistring.String, len(scope.bindings))
	for i, b := range scope.bindings {
		vars[i] = b.name
	}
	if len(vars) > 0 && !ownVarScope && ownLexScope {
		if inGlobal {
			c.emit(&bindGlobal{
				vars:      vars[numFuncs:],
				funcs:     vars[:numFuncs],
				deletable: eval,
			})
		} else {
			c.emit(&bindVars{names: vars, deletable: eval})
		}
	}
	var enter *enterBlock
	if c.compileLexicalDeclarations(in.Body, ownVarScope || !ownLexScope) {
		if ownLexScope {
			c.block = &block{
				outer:      c.block,
				typ:        blockScope,
				needResult: true,
			}
			enter = &enterBlock{}
			c.emit(enter)
		}
	}
	if len(scope.bindings) > 0 && !ownLexScope {
		var lets, consts []unistring.String
		for _, b := range c.scope.bindings[numFuncs+numVars:] {
			if b.isConst {
				consts = append(consts, b.name)
			} else {
				lets = append(lets, b.name)
			}
		}
		c.emit(&bindGlobal{
			vars:   vars[numFuncs:],
			funcs:  vars[:numFuncs],
			lets:   lets,
			consts: consts,
		})
	}
	if !inGlobal || ownVarScope {
		c.compileFunctions(funcs)
	}
	c.compileStatements(in.Body, true)
	if enter != nil {
		c.leaveScopeBlock(enter)
		c.popScope()
	}

	scope.finaliseVarAlloc(0)
	c.stringCache = nil
}

func (c *compiler) compileDeclList(v []*ast.VariableDeclaration, inFunc bool) {
	for _, value := range v {
		c.createVarBindings(value, inFunc)
	}
}

func (c *compiler) extractLabelled(st ast.Statement) ast.Statement {
	if st, ok := st.(*ast.LabelledStatement); ok {
		return c.extractLabelled(st.Statement)
	}
	return st
}

func (c *compiler) extractFunctions(list []ast.Statement) (funcs []*ast.FunctionDeclaration) {
	for _, st := range list {
		var decl *ast.FunctionDeclaration
		switch st := c.extractLabelled(st).(type) {
		case *ast.FunctionDeclaration:
			decl = st
		case *ast.LabelledStatement:
			if st1, ok := st.Statement.(*ast.FunctionDeclaration); ok {
				decl = st1
			} else {
				continue
			}
		default:
			continue
		}
		funcs = append(funcs, decl)
	}
	return
}

func (c *compiler) createFunctionBindings(funcs []*ast.FunctionDeclaration) {
	s := c.scope
	if s.outer != nil {
		unique := !s.isFunction() && !s.variable && s.strict
		if !unique {
			hasNonStandard := false
			for _, decl := range funcs {
				if !decl.Function.Async && !decl.Function.Generator {
					s.bindNameLexical(decl.Function.Name.Name, false, int(decl.Function.Name.Idx1())-1)
				} else {
					hasNonStandard = true
				}
			}
			if hasNonStandard {
				for _, decl := range funcs {
					if decl.Function.Async || decl.Function.Generator {
						s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1)
					}
				}
			}
		} else {
			for _, decl := range funcs {
				s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1)
			}
		}
	} else {
		for _, decl := range funcs {
			s.bindName(decl.Function.Name.Name)
		}
	}
}

func (c *compiler) compileFunctions(list []*ast.FunctionDeclaration) {
	for _, decl := range list {
		c.compileFunction(decl)
	}
}

func (c *compiler) compileFunctionsGlobalAllUnique(list []*ast.FunctionDeclaration) {
	for _, decl := range list {
		c.compileFunctionLiteral(decl.Function, false).emitGetter(true)
	}
}

func (c *compiler) compileFunctionsGlobal(list []*ast.FunctionDeclaration) {
	m := make(map[unistring.String]int, len(list))
	for i := len(list) - 1; i >= 0; i-- {
		name := list[i].Function.Name.Name
		if _, exists := m[name]; !exists {
			m[name] = i
		}
	}
	idx := 0
	for i, decl := range list {
		name := decl.Function.Name.Name
		if m[name] == i {
			c.compileFunctionLiteral(decl.Function, false).emitGetter(true)
			c.scope.bindings[idx] = c.scope.boundNames[name]
			idx++
		} else {
			leave := c.enterDummyMode()
			c.compileFunctionLiteral(decl.Function, false).emitGetter(false)
			leave()
		}
	}
}

func (c *compiler) createVarIdBinding(name unistring.String, offset int, inFunc bool) {
	if c.scope.strict {
		c.checkIdentifierLName(name, offset)
		c.checkIdentifierName(name, offset)
	}
	if !inFunc || name != "arguments" {
		c.scope.bindName(name)
	}
}

func (c *compiler) createBindings(target ast.Expression, createIdBinding func(name unistring.String, offset int)) {
	switch target := target.(type) {
	case *ast.Identifier:
		createIdBinding(target.Name, int(target.Idx)-1)
	case *ast.ObjectPattern:
		for _, prop := range target.Properties {
			switch prop := prop.(type) {
			case *ast.PropertyShort:
				createIdBinding(prop.Name.Name, int(prop.Name.Idx)-1)
			case *ast.PropertyKeyed:
				c.createBindings(prop.Value, createIdBinding)
			default:
				c.throwSyntaxErrorf(int(target.Idx0()-1), "unsupported property type in ObjectPattern: %T", prop)
			}
		}
		if target.Rest != nil {
			c.createBindings(target.Rest, createIdBinding)
		}
	case *ast.ArrayPattern:
		for _, elt := range target.Elements {
			if elt != nil {
				c.createBindings(elt, createIdBinding)
			}
		}
		if target.Rest != nil {
			c.createBindings(target.Rest, createIdBinding)
		}
	case *ast.AssignExpression:
		c.createBindings(target.Left, createIdBinding)
	default:
		c.throwSyntaxErrorf(int(target.Idx0()-1), "unsupported binding target: %T", target)
	}
}

func (c *compiler) createVarBinding(target ast.Expression, inFunc bool) {
	c.createBindings(target, func(name unistring.String, offset int) {
		c.createVarIdBinding(name, offset, inFunc)
	})
}

func (c *compiler) createVarBindings(v *ast.VariableDeclaration, inFunc bool) {
	for _, item := range v.List {
		c.createVarBinding(item.Target, inFunc)
	}
}

func (c *compiler) createLexicalIdBinding(name unistring.String, isConst bool, offset int) *binding {
	if name == "let" {
		c.throwSyntaxError(offset, "let is disallowed as a lexically bound name")
	}
	if c.scope.strict {
		c.checkIdentifierLName(name, offset)
		c.checkIdentifierName(name, offset)
	}
	b, _ := c.scope.bindNameLexical(name, true, offset)
	if isConst {
		b.isConst, b.isStrict = true, true
	}
	return b
}

func (c *compiler) createLexicalIdBindingFuncBody(name unistring.String, isConst bool, offset int, calleeBinding *binding) *binding {
	if name == "let" {
		c.throwSyntaxError(offset, "let is disallowed as a lexically bound name")
	}
	if c.scope.strict {
		c.checkIdentifierLName(name, offset)
		c.checkIdentifierName(name, offset)
	}
	paramScope := c.scope.outer
	parentBinding := paramScope.boundNames[name]
	if parentBinding != nil {
		if parentBinding != calleeBinding && (name != "arguments" || !paramScope.argsNeeded) {
			c.throwSyntaxErrorf(offset, "Identifier '%s' has already been declared", name)
		}
	}
	b, _ := c.scope.bindNameLexical(name, true, offset)
	if isConst {
		b.isConst, b.isStrict = true, true
	}
	return b
}

func (c *compiler) createLexicalBinding(target ast.Expression, isConst bool) {
	c.createBindings(target, func(name unistring.String, offset int) {
		c.createLexicalIdBinding(name, isConst, offset)
	})
}

func (c *compiler) createLexicalBindings(lex *ast.LexicalDeclaration) {
	for _, d := range lex.List {
		c.createLexicalBinding(d.Target, lex.Token == token.CONST)
	}
}

func (c *compiler) compileLexicalDeclarations(list []ast.Statement, scopeDeclared bool) bool {
	for _, st := range list {
		if lex, ok := st.(*ast.LexicalDeclaration); ok {
			if !scopeDeclared {
				c.newBlockScope()
				scopeDeclared = true
			}
			c.createLexicalBindings(lex)
		} else if cls, ok := st.(*ast.ClassDeclaration); ok {
			if !scopeDeclared {
				c.newBlockScope()
				scopeDeclared = true
			}
			c.createLexicalIdBinding(cls.Class.Name.Name, false, int(cls.Class.Name.Idx)-1)
		}
	}
	return scopeDeclared
}

func (c *compiler) compileLexicalDeclarationsFuncBody(list []ast.Statement, calleeBinding *binding) {
	for _, st := range list {
		if lex, ok := st.(*ast.LexicalDeclaration); ok {
			isConst := lex.Token == token.CONST
			for _, d := range lex.List {
				c.createBindings(d.Target, func(name unistring.String, offset int) {
					c.createLexicalIdBindingFuncBody(name, isConst, offset, calleeBinding)
				})
			}
		} else if cls, ok := st.(*ast.ClassDeclaration); ok {
			c.createLexicalIdBindingFuncBody(cls.Class.Name.Name, false, int(cls.Class.Name.Idx)-1, calleeBinding)
		}
	}
}

func (c *compiler) compileFunction(v *ast.FunctionDeclaration) {
	name := v.Function.Name.Name
	b := c.scope.boundNames[name]
	if b == nil || b.isVar {
		e := &compiledIdentifierExpr{
			name: v.Function.Name.Name,
		}
		e.init(c, v.Function.Idx0())
		e.emitSetter(c.compileFunctionLiteral(v.Function, false), false)
	} else {
		c.compileFunctionLiteral(v.Function, false).emitGetter(true)
		b.emitInitP()
	}
}

func (c *compiler) compileStandaloneFunctionDecl(v *ast.FunctionDeclaration) {
	if v.Function.Async {
		c.throwSyntaxError(int(v.Idx0())-1, "Async functions can only be declared at top level or inside a block.")
	}
	if v.Function.Generator {
		c.throwSyntaxError(int(v.Idx0())-1, "Generators can only be declared at top level or inside a block.")
	}
	if c.scope.strict {
		c.throwSyntaxError(int(v.Idx0())-1, "In strict mode code, functions can only be declared at top level or inside a block.")
	}
	c.throwSyntaxError(int(v.Idx0())-1, "In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement.")
}

func (c *compiler) emit(instructions ...instruction) {
	c.p.code = append(c.p.code, instructions...)
}

func (c *compiler) throwSyntaxError(offset int, msg string) {
	panic(&CompilerSyntaxError{
		CompilerError: CompilerError{
			File:    c.p.src,
			Offset:  offset,
			Message: msg,
		},
	})
}

func (c *compiler) throwSyntaxErrorf(offset int, format string, args ...interface{}) {
	panic(&CompilerSyntaxError{
		CompilerError: CompilerError{
			File:    c.p.src,
			Offset:  offset,
			Message: fmt.Sprintf(format, args...),
		},
	})
}

func (c *compiler) isStrict(list []ast.Statement) *ast.StringLiteral {
	for _, st := range list {
		if st, ok := st.(*ast.ExpressionStatement); ok {
			if e, ok := st.Expression.(*ast.StringLiteral); ok {
				if e.Literal == `"use strict"` || e.Literal == `'use strict'` {
					return e
				}
			} else {
				break
			}
		} else {
			break
		}
	}
	return nil
}

func (c *compiler) isStrictStatement(s ast.Statement) *ast.StringLiteral {
	if s, ok := s.(*ast.BlockStatement); ok {
		return c.isStrict(s.List)
	}
	return nil
}

func (c *compiler) checkIdentifierName(name unistring.String, offset int) {
	switch name {
	case "implements", "interface", "let", "package", "private", "protected", "public", "static", "yield":
		c.throwSyntaxError(offset, "Unexpected strict mode reserved word")
	}
}

func (c *compiler) checkIdentifierLName(name unistring.String, offset int) {
	switch name {
	case "eval", "arguments":
		c.throwSyntaxError(offset, "Assignment to eval or arguments is not allowed in strict mode")
	}
}

// Enter a 'dummy' compilation mode. Any code produced after this method is called will be discarded after
// leaveFunc is called with no additional side effects. This is useful for compiling code inside a
// constant falsy condition 'if' branch or a loop (i.e 'if (false) { ... } or while (false) { ... }).
// Such code should not be included in the final compilation result as it's never called, but it must
// still produce compilation errors if there are any.
// TODO: make sure variable lookups do not de-optimise parent scopes
func (c *compiler) enterDummyMode() (leaveFunc func()) {
	savedBlock, savedProgram := c.block, c.p
	if savedBlock != nil {
		c.block = &block{
			typ:      savedBlock.typ,
			label:    savedBlock.label,
			outer:    savedBlock.outer,
			breaking: savedBlock.breaking,
		}
	}
	c.p = &Program{
		src: c.p.src,
	}
	c.newScope()
	return func() {
		c.block, c.p = savedBlock, savedProgram
		c.popScope()
	}
}

func (c *compiler) compileStatementDummy(statement ast.Statement) {
	leave := c.enterDummyMode()
	c.compileStatement(statement, false)
	leave()
}

func (c *compiler) assert(cond bool, offset int, msg string, args ...interface{}) {
	if !cond {
		c.throwSyntaxErrorf(offset, "Compiler bug: "+msg, args...)
	}
}

func privateIdString(desc unistring.String) unistring.String {
	return asciiString("#").Concat(stringValueFromRaw(desc)).string()
}

type privateName struct {
	idx                  int
	isStatic             bool
	isMethod             bool
	hasGetter, hasSetter bool
}

type resolvedPrivateName struct {
	name     unistring.String
	idx      uint32
	level    uint8
	isStatic bool
	isMethod bool
}

func (r *resolvedPrivateName) string() unistring.String {
	return privateIdString(r.name)
}

type privateEnvRegistry struct {
	fields, methods []unistring.String
}

type classScope struct {
	c            *compiler
	privateNames map[unistring.String]*privateName

	instanceEnv, staticEnv privateEnvRegistry

	outer *classScope
}

func (r *privateEnvRegistry) createPrivateMethodId(name unistring.String) int {
	r.methods = append(r.methods, name)
	return len(r.methods) - 1
}

func (r *privateEnvRegistry) createPrivateFieldId(name unistring.String) int {
	r.fields = append(r.fields, name)
	return len(r.fields) - 1
}

func (s *classScope) declarePrivateId(name unistring.String, kind ast.PropertyKind, isStatic bool, offset int) {
	pn := s.privateNames[name]
	if pn != nil {
		if pn.isStatic == isStatic {
			switch kind {
			case ast.PropertyKindGet:
				if pn.hasSetter && !pn.hasGetter {
					pn.hasGetter = true
					return
				}
			case ast.PropertyKindSet:
				if pn.hasGetter && !pn.hasSetter {
					pn.hasSetter = true
					return
				}
			}
		}
		s.c.throwSyntaxErrorf(offset, "Identifier '#%s' has already been declared", name)
		panic("unreachable")
	}
	var env *privateEnvRegistry
	if isStatic {
		env = &s.staticEnv
	} else {
		env = &s.instanceEnv
	}

	pn = &privateName{
		isStatic:  isStatic,
		hasGetter: kind == ast.PropertyKindGet,
		hasSetter: kind == ast.PropertyKindSet,
	}
	if kind != ast.PropertyKindValue {
		pn.idx = env.createPrivateMethodId(name)
		pn.isMethod = true
	} else {
		pn.idx = env.createPrivateFieldId(name)
	}

	if s.privateNames == nil {
		s.privateNames = make(map[unistring.String]*privateName)
	}
	s.privateNames[name] = pn
}

func (s *classScope) getDeclaredPrivateId(name unistring.String) *privateName {
	if n := s.privateNames[name]; n != nil {
		return n
	}
	s.c.assert(false, 0, "getDeclaredPrivateId() for undeclared id")
	panic("unreachable")
}

func (c *compiler) resolvePrivateName(name unistring.String, offset int) (*resolvedPrivateName, *privateId) {
	level := 0
	for s := c.classScope; s != nil; s = s.outer {
		if len(s.privateNames) > 0 {
			if pn := s.privateNames[name]; pn != nil {
				return &resolvedPrivateName{
					name:     name,
					idx:      uint32(pn.idx),
					level:    uint8(level),
					isStatic: pn.isStatic,
					isMethod: pn.isMethod,
				}, nil
			}
			level++
		}
	}
	if c.ctxVM != nil {
		for s := c.ctxVM.privEnv; s != nil; s = s.outer {
			if id := s.names[name]; id != nil {
				return nil, id
			}
		}
	}
	c.throwSyntaxErrorf(offset, "Private field '#%s' must be declared in an enclosing class", name)
	panic("unreachable")
}
