package goja

import (
	"fmt"
	"github.com/dop251/goja/ast"
	"github.com/dop251/goja/file"
	"strconv"
)

const (
	blockLoop = iota
	blockTry
	blockBranch
	blockSwitch
	blockWith
)

type CompilerError struct {
	Message string
	File    *SrcFile
	Offset  int
}

type CompilerSyntaxError struct {
	CompilerError
}

type CompilerReferenceError struct {
	CompilerError
}

type srcMapItem struct {
	pc     int
	srcPos int
}

type Program struct {
	code   []instruction
	values []Value

	funcName string
	src      *SrcFile
	srcMap   []srcMapItem
}

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

	enumGetExpr compiledEnumGetExpr

	evalVM *vm
}

type scope struct {
	names      map[string]uint32
	outer      *scope
	strict     bool
	eval       bool
	lexical    bool
	dynamic    bool
	accessed   bool
	argsNeeded bool
	thisNeeded bool

	namesMap    map[string]string
	lastFreeTmp int
}

type block struct {
	typ        int
	label      string
	needResult bool
	cont       int
	breaks     []int
	conts      []int
	outer      *block
}

func (c *compiler) leaveBlock() {
	lbl := len(c.p.code)
	for _, item := range c.block.breaks {
		c.p.code[item] = jump(lbl - item)
	}
	if c.block.typ == blockLoop {
		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{
		outer:    c.scope,
		names:    make(map[string]uint32),
		strict:   strict,
		namesMap: make(map[string]string),
	}
}

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

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

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

	c.newScope()
	c.scope.dynamic = true
	return c
}

func (p *Program) defineLiteralValue(val Value) uint32 {
	for idx, v := range p.values {
		if v.SameAs(val) {
			return uint32(idx)
		}
	}
	idx := uint32(len(p.values))
	p.values = append(p.values, val)
	return idx
}

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{})) {
	logger("values: %+v", p.values)
	for pc, ins := range p.code {
		logger("%s %d: %T(%v)", indent, pc, ins, ins)
		if f, ok := ins.(*newFunc); ok {
			f.prg._dumpCode(indent+">", logger)
		}
	}
}

func (p *Program) sourceOffset(pc int) int {
	// TODO use sort.Search()
	for i, item := range p.srcMap {
		if item.pc > pc {
			if i > 0 {
				return p.srcMap[i-1].srcPos
			}
			return 0
		}
	}
	return p.srcMap[len(p.srcMap)-1].srcPos
}

func (s *scope) isFunction() bool {
	if !s.lexical {
		return s.outer != nil
	}
	return s.outer.isFunction()
}

func (s *scope) lookupName(name string) (idx uint32, found, noDynamics bool) {
	var level uint32 = 0
	noDynamics = true
	for curScope := s; curScope != nil; curScope = curScope.outer {
		if curScope != s {
			curScope.accessed = true
		}
		if curScope.dynamic {
			noDynamics = false
		} else {
			var mapped string
			if m, exists := curScope.namesMap[name]; exists {
				mapped = m
			} else {
				mapped = name
			}
			if i, exists := curScope.names[mapped]; exists {
				idx = i | (level << 24)
				found = true
				return
			}
		}
		if name == "arguments" && !s.lexical && s.isFunction() {
			s.argsNeeded = true
			s.accessed = true
			idx, _ = s.bindName(name)
			found = true
			return
		}
		level++
	}
	return
}

func (s *scope) bindName(name string) (uint32, bool) {
	if s.lexical {
		return s.outer.bindName(name)
	}

	if idx, exists := s.names[name]; exists {
		return idx, false
	}
	idx := uint32(len(s.names))
	s.names[name] = idx
	return idx, true
}

func (s *scope) bindNameShadow(name string) (uint32, bool) {
	if s.lexical {
		return s.outer.bindName(name)
	}

	unique := true

	if idx, exists := s.names[name]; exists {
		unique = false
		// shadow the var
		delete(s.names, name)
		n := strconv.Itoa(int(idx))
		s.names[n] = idx
	}
	idx := uint32(len(s.names))
	s.names[name] = idx
	return idx, unique
}

func (c *compiler) markBlockStart() {
	c.blockStart = len(c.p.code)
}

func (c *compiler) compile(in *ast.Program) {
	c.p.src = NewSrcFile(in.File.Name(), in.File.Source())

	if len(in.Body) > 0 {
		if !c.scope.strict {
			c.scope.strict = c.isStrict(in.Body)
		}
	}

	c.compileDeclList(in.DeclarationList, false)
	c.compileFunctions(in.DeclarationList)

	if len(in.Body) > 0 {
		for _, st := range in.Body[:len(in.Body)-1] {
			c.compileStatement(st, false)
		}

		c.compileStatement(in.Body[len(in.Body)-1], true)
	} else {
		c.compileStatement(&ast.EmptyStatement{}, true)
	}

	c.p.code = append(c.p.code, halt)
	code := c.p.code
	c.p.code = make([]instruction, 0, len(code)+len(c.scope.names)+2)
	if c.scope.eval {
		if !c.scope.strict {
			c.emit(jne(2), newStash)
		} else {
			c.emit(pop, newStash)
		}
	}
	l := len(c.p.code)
	c.p.code = c.p.code[:l+len(c.scope.names)]
	for name, nameIdx := range c.scope.names {
		c.p.code[l+int(nameIdx)] = bindName(name)
	}

	c.p.code = append(c.p.code, code...)
	for i, _ := range c.p.srcMap {
		c.p.srcMap[i].pc += len(c.scope.names)
	}

}

func (c *compiler) compileDeclList(v []ast.Declaration, inFunc bool) {
	for _, value := range v {
		switch value := value.(type) {
		case *ast.FunctionDeclaration:
			c.compileFunctionDecl(value)
		case *ast.VariableDeclaration:
			c.compileVarDecl(value, inFunc)
		default:
			panic(fmt.Errorf("Unsupported declaration: %T", value))
		}
	}
}

func (c *compiler) compileFunctions(v []ast.Declaration) {
	for _, value := range v {
		if value, ok := value.(*ast.FunctionDeclaration); ok {
			c.compileFunction(value)
		}
	}
}

func (c *compiler) compileVarDecl(v *ast.VariableDeclaration, inFunc bool) {
	for _, item := range v.List {
		if c.scope.strict {
			c.checkIdentifierLName(item.Name, int(item.Idx)-1)
			c.checkIdentifierName(item.Name, int(item.Idx)-1)
		}
		if !inFunc || item.Name != "arguments" {
			idx, ok := c.scope.bindName(item.Name)
			_ = idx
			//log.Printf("Define var: %s: %x", item.Name, idx)
			if !ok {
				// TODO: error
			}
		}
	}
}

func (c *compiler) addDecls() []instruction {
	code := make([]instruction, len(c.scope.names))
	for name, nameIdx := range c.scope.names {
		code[nameIdx] = bindName(name)
	}
	return code
}

func (c *compiler) convertInstrToStashless(instr uint32, args int) (newIdx int, convert bool) {
	level := instr >> 24
	idx := instr & 0x00FFFFFF
	if level > 0 {
		level--
		newIdx = int((level << 24) | idx)
	} else {
		iidx := int(idx)
		if iidx < args {
			newIdx = -iidx - 1
		} else {
			newIdx = iidx - args + 1
		}
		convert = true
	}
	return
}

func (c *compiler) convertFunctionToStashless(code []instruction, args int) {
	code[0] = enterFuncStashless{stackSize: uint32(len(c.scope.names) - args), args: uint32(args)}
	for pc := 1; pc < len(code); pc++ {
		instr := code[pc]
		if instr == ret {
			code[pc] = retStashless
		}
		switch instr := instr.(type) {
		case getLocal:
			if newIdx, convert := c.convertInstrToStashless(uint32(instr), args); convert {
				code[pc] = loadStack(newIdx)
			} else {
				code[pc] = getLocal(newIdx)
			}
		case setLocal:
			if newIdx, convert := c.convertInstrToStashless(uint32(instr), args); convert {
				code[pc] = storeStack(newIdx)
			} else {
				code[pc] = setLocal(newIdx)
			}
		case setLocalP:
			if newIdx, convert := c.convertInstrToStashless(uint32(instr), args); convert {
				code[pc] = storeStackP(newIdx)
			} else {
				code[pc] = setLocalP(newIdx)
			}
		case getVar:
			level := instr.idx >> 24
			idx := instr.idx & 0x00FFFFFF
			level--
			instr.idx = level<<24 | idx
			code[pc] = instr
		case setVar:
			level := instr.idx >> 24
			idx := instr.idx & 0x00FFFFFF
			level--
			instr.idx = level<<24 | idx
			code[pc] = instr
		}
	}
}

func (c *compiler) compileFunctionDecl(v *ast.FunctionDeclaration) {
	idx, ok := c.scope.bindName(v.Function.Name.Name)
	if !ok {
		// TODO: error
	}
	_ = idx
	// log.Printf("Define function: %s: %x", v.Function.Name.Name, idx)
}

func (c *compiler) compileFunction(v *ast.FunctionDeclaration) {
	e := &compiledIdentifierExpr{
		name: v.Function.Name.Name,
	}
	e.init(c, v.Function.Idx0())
	e.emitSetter(c.compileFunctionLiteral(v.Function, false))
	c.emit(pop)
}

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

func (c *compiler) throwSyntaxError(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) bool {
	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 true
				}
			} else {
				break
			}
		} else {
			break
		}
	}
	return false
}

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

func (c *compiler) checkIdentifierName(name 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 string, offset int) {
	switch name {
	case "eval", "arguments":
		c.throwSyntaxError(offset, "Assignment to eval or arguments is not allowed in strict mode")
	}
}
