package compiler

import (
	"encoding/binary"
	"errors"
	"fmt"
	"go/ast"
	"go/constant"
	"go/token"
	"go/types"
	"math"
	"sort"
	"strings"

	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/vm"
	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
	"golang.org/x/tools/go/loader"
)

// The identifier of the entry function. Default set to Main.
const mainIdent = "Main"

type codegen struct {
	// Information about the program with all its dependencies.
	buildInfo *buildInfo

	// prog holds the output buffer.
	prog *io.BufBinWriter

	// Type information.
	typeInfo *types.Info

	// A mapping of func identifiers with their scope.
	funcs map[string]*funcScope

	// A mapping of lambda functions into their scope.
	lambda map[string]*funcScope

	// Current funcScope being converted.
	scope *funcScope

	globals map[string]int

	// A mapping from label's names to their ids.
	labels map[labelWithType]uint16
	// A list of nested label names together with evaluation stack depth.
	labelList []labelWithStackSize

	// A label for the for-loop being currently visited.
	currentFor string
	// A label for the switch statement being visited.
	currentSwitch string
	// A label to be used in the next statement.
	nextLabel string

	// sequencePoints is mapping from method name to a slice
	// containing info about mapping from opcode's offset
	// to a text span in the source file.
	sequencePoints map[string][]DebugSeqPoint

	// Label table for recording jump destinations.
	l []int
}

type labelOffsetType byte

const (
	labelStart labelOffsetType = iota // labelStart is a default label type
	labelEnd                          // labelEnd is a type for labels that are targets for break
	labelPost                         // labelPost is a type for labels that are targets for continue
)

type labelWithType struct {
	name string
	typ  labelOffsetType
}

type labelWithStackSize struct {
	name string
	sz   int
}

type varType int

const (
	varGlobal varType = iota
	varLocal
	varArgument
)

// newLabel creates a new label to jump to
func (c *codegen) newLabel() (l uint16) {
	li := len(c.l)
	if li > math.MaxUint16 {
		c.prog.Err = errors.New("label number is too big")
		return
	}
	l = uint16(li)
	c.l = append(c.l, -1)
	return
}

// newNamedLabel creates a new label with a specified name.
func (c *codegen) newNamedLabel(typ labelOffsetType, name string) (l uint16) {
	l = c.newLabel()
	lt := labelWithType{name: name, typ: typ}
	c.labels[lt] = l
	return
}

func (c *codegen) setLabel(l uint16) {
	c.l[l] = c.pc() + 1
}

// pc returns the program offset off the last instruction.
func (c *codegen) pc() int {
	return c.prog.Len() - 1
}

func (c *codegen) emitLoadConst(t types.TypeAndValue) {
	if c.prog.Err != nil {
		return
	}

	typ, ok := t.Type.Underlying().(*types.Basic)
	if !ok {
		c.prog.Err = fmt.Errorf("compiler doesn't know how to convert this constant: %v", t)
		return
	}

	switch typ.Kind() {
	case types.Int, types.UntypedInt, types.Uint,
		types.Int8, types.Uint8,
		types.Int16, types.Uint16,
		types.Int32, types.Uint32, types.Int64, types.Uint64:
		val, _ := constant.Int64Val(t.Value)
		emit.Int(c.prog.BinWriter, val)
	case types.String, types.UntypedString:
		val := constant.StringVal(t.Value)
		emit.String(c.prog.BinWriter, val)
	case types.Bool, types.UntypedBool:
		val := constant.BoolVal(t.Value)
		emit.Bool(c.prog.BinWriter, val)
	default:
		c.prog.Err = fmt.Errorf("compiler doesn't know how to convert this basic type: %v", t)
		return
	}
}

func (c *codegen) emitLoadField(i int) {
	emit.Int(c.prog.BinWriter, int64(i))
	emit.Opcode(c.prog.BinWriter, opcode.PICKITEM)
}

func (c *codegen) emitStoreStructField(i int) {
	emit.Int(c.prog.BinWriter, int64(i))
	emit.Opcode(c.prog.BinWriter, opcode.ROT)
	emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
}

// getVarIndex returns variable type and position in corresponding slot,
// according to current scope.
func (c *codegen) getVarIndex(name string) (varType, int) {
	if c.scope != nil {
		if i, ok := c.scope.arguments[name]; ok {
			return varArgument, i
		} else if i, ok := c.scope.locals[name]; ok {
			return varLocal, i
		}
	}
	if i, ok := c.globals[name]; ok {
		return varGlobal, i
	}

	return varLocal, c.scope.newVariable(varLocal, name)
}

func getBaseOpcode(t varType) (opcode.Opcode, opcode.Opcode) {
	switch t {
	case varGlobal:
		return opcode.LDSFLD0, opcode.STSFLD0
	case varLocal:
		return opcode.LDLOC0, opcode.STLOC0
	case varArgument:
		return opcode.LDARG0, opcode.STARG0
	default:
		panic("invalid type")
	}
}

// emitLoadVar loads specified variable to the evaluation stack.
func (c *codegen) emitLoadVar(name string) {
	t, i := c.getVarIndex(name)
	base, _ := getBaseOpcode(t)
	if i < 7 {
		emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
	} else {
		emit.Instruction(c.prog.BinWriter, base+7, []byte{byte(i)})
	}
}

// emitStoreVar stores top value from the evaluation stack in the specified variable.
func (c *codegen) emitStoreVar(name string) {
	if name == "_" {
		emit.Opcode(c.prog.BinWriter, opcode.DROP)
		return
	}
	t, i := c.getVarIndex(name)
	_, base := getBaseOpcode(t)
	if i < 7 {
		emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
	} else {
		emit.Instruction(c.prog.BinWriter, base+7, []byte{byte(i)})
	}
}

func (c *codegen) emitDefault(t types.Type) {
	switch t := t.Underlying().(type) {
	case *types.Basic:
		info := t.Info()
		switch {
		case info&types.IsInteger != 0:
			emit.Int(c.prog.BinWriter, 0)
		case info&types.IsString != 0:
			emit.Bytes(c.prog.BinWriter, []byte{})
		case info&types.IsBoolean != 0:
			emit.Bool(c.prog.BinWriter, false)
		default:
			emit.Opcode(c.prog.BinWriter, opcode.PUSHNULL)
		}
	case *types.Slice:
		if isCompoundSlice(t) {
			emit.Opcode(c.prog.BinWriter, opcode.NEWARRAY0)
		} else {
			emit.Int(c.prog.BinWriter, 0)
			emit.Opcode(c.prog.BinWriter, opcode.NEWBUFFER)
		}
	case *types.Struct:
		num := t.NumFields()
		emit.Int(c.prog.BinWriter, int64(num))
		emit.Opcode(c.prog.BinWriter, opcode.NEWSTRUCT)
		for i := 0; i < num; i++ {
			emit.Opcode(c.prog.BinWriter, opcode.DUP)
			emit.Int(c.prog.BinWriter, int64(i))
			c.emitDefault(t.Field(i).Type())
			emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
		}
	default:
		emit.Opcode(c.prog.BinWriter, opcode.PUSHNULL)
	}
}

// convertGlobals traverses the AST and only converts global declarations.
// If we call this in convertFuncDecl then it will load all global variables
// into the scope of the function.
func (c *codegen) convertGlobals(f ast.Node) {
	ast.Inspect(f, func(node ast.Node) bool {
		switch n := node.(type) {
		case *ast.FuncDecl:
			return false
		case *ast.GenDecl:
			// constants are loaded directly so there is no need
			// to store them as a local variables
			if n.Tok != token.CONST {
				ast.Walk(c, n)
			}
		}
		return true
	})
}

func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl) {
	var (
		f            *funcScope
		ok, isLambda bool
	)

	f, ok = c.funcs[decl.Name.Name]
	if ok {
		// If this function is a syscall we will not convert it to bytecode.
		if isSyscall(f) {
			return
		}
		c.setLabel(f.label)
	} else if f, ok = c.lambda[decl.Name.Name]; ok {
		isLambda = ok
		c.setLabel(f.label)
	} else {
		f = c.newFunc(decl)
	}

	f.rng.Start = uint16(c.prog.Len())
	c.scope = f
	ast.Inspect(decl, c.scope.analyzeVoidCalls) // @OPTIMIZE

	// All globals copied into the scope of the function need to be added
	// to the stack size of the function.
	sizeLoc := f.countLocals()
	if sizeLoc > 255 {
		c.prog.Err = errors.New("maximum of 255 local variables is allowed")
	}
	sizeArg := f.countArgs()
	if sizeArg > 255 {
		c.prog.Err = errors.New("maximum of 255 local variables is allowed")
	}
	if sizeLoc != 0 || sizeArg != 0 {
		emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(sizeLoc), byte(sizeArg)})
	}

	// We need to handle methods, which in Go, is just syntactic sugar.
	// The method receiver will be passed in as first argument.
	// We check if this declaration has a receiver and load it into scope.
	//
	// FIXME: For now we will hard cast this to a struct. We can later fine tune this
	// to support other types.
	if decl.Recv != nil {
		for _, arg := range decl.Recv.List {
			// only create an argument here, it will be stored via INITSLOT
			c.scope.newVariable(varArgument, arg.Names[0].Name)
		}
	}

	// Load the arguments in scope.
	for _, arg := range decl.Type.Params.List {
		for _, id := range arg.Names {
			// only create an argument here, it will be stored via INITSLOT
			c.scope.newVariable(varArgument, id.Name)
		}
	}

	ast.Walk(c, decl.Body)

	// If we have reached the end of the function without encountering `return` statement,
	// we should clean alt.stack manually.
	// This can be the case with void and named-return functions.
	if !lastStmtIsReturn(decl) {
		c.saveSequencePoint(decl.Body)
		emit.Opcode(c.prog.BinWriter, opcode.RET)
	}

	f.rng.End = uint16(c.prog.Len() - 1)

	if !isLambda {
		for _, f := range c.lambda {
			c.convertFuncDecl(file, f.decl)
		}
		c.lambda = make(map[string]*funcScope)
	}
}

func (c *codegen) Visit(node ast.Node) ast.Visitor {
	if c.prog.Err != nil {
		return nil
	}
	switch n := node.(type) {

	// General declarations.
	// var (
	//     x = 2
	// )
	case *ast.GenDecl:
		for _, spec := range n.Specs {
			switch t := spec.(type) {
			case *ast.ValueSpec:
				for _, id := range t.Names {
					if c.scope == nil {
						// it is a global declaration
						c.newGlobal(id.Name)
					} else {
						c.scope.newLocal(id.Name)
					}
					c.registerDebugVariable(id.Name, t.Type)
				}
				for i := range t.Names {
					if len(t.Values) != 0 {
						ast.Walk(c, t.Values[i])
					} else {
						c.emitDefault(c.typeOf(t.Type))
					}
					c.emitStoreVar(t.Names[i].Name)
				}
			}
		}
		return nil

	case *ast.AssignStmt:
		multiRet := len(n.Rhs) != len(n.Lhs)
		c.saveSequencePoint(n)
		// Assign operations are grouped https://github.com/golang/go/blob/master/src/go/types/stmt.go#L160
		isAssignOp := token.ADD_ASSIGN <= n.Tok && n.Tok <= token.AND_NOT_ASSIGN
		if isAssignOp {
			// RHS can contain exactly one expression, thus there is no need to iterate.
			ast.Walk(c, n.Lhs[0])
			ast.Walk(c, n.Rhs[0])
			c.convertToken(n.Tok)
		}
		for i := 0; i < len(n.Lhs); i++ {
			switch t := n.Lhs[i].(type) {
			case *ast.Ident:
				if n.Tok == token.DEFINE {
					if !multiRet {
						c.registerDebugVariable(t.Name, n.Rhs[i])
					}
					if t.Name != "_" {
						c.scope.newLocal(t.Name)
					}
				}
				if !isAssignOp && (i == 0 || !multiRet) {
					ast.Walk(c, n.Rhs[i])
				}
				c.emitStoreVar(t.Name)

			case *ast.SelectorExpr:
				if !isAssignOp {
					ast.Walk(c, n.Rhs[i])
				}
				strct, ok := c.typeOf(t.X).Underlying().(*types.Struct)
				if !ok {
					c.prog.Err = fmt.Errorf("nested selector assigns not supported yet")
					return nil
				}
				ast.Walk(c, t.X)                      // load the struct
				i := indexOfStruct(strct, t.Sel.Name) // get the index of the field
				c.emitStoreStructField(i)             // store the field

			// Assignments to index expressions.
			// slice[0] = 10
			case *ast.IndexExpr:
				if !isAssignOp {
					ast.Walk(c, n.Rhs[i])
				}
				ast.Walk(c, t.X)
				ast.Walk(c, t.Index)
				emit.Opcode(c.prog.BinWriter, opcode.ROT)
				emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
			}
		}
		return nil

	case *ast.SliceExpr:
		name := n.X.(*ast.Ident).Name
		c.emitLoadVar(name)

		if n.Low != nil {
			ast.Walk(c, n.Low)
		} else {
			emit.Opcode(c.prog.BinWriter, opcode.PUSH0)
		}

		if n.High != nil {
			ast.Walk(c, n.High)
		} else {
			emit.Opcode(c.prog.BinWriter, opcode.OVER)
			emit.Opcode(c.prog.BinWriter, opcode.SIZE)
		}

		emit.Opcode(c.prog.BinWriter, opcode.OVER)
		emit.Opcode(c.prog.BinWriter, opcode.SUB)
		emit.Opcode(c.prog.BinWriter, opcode.SUBSTR)

		return nil

	case *ast.ReturnStmt:
		l := c.newLabel()
		c.setLabel(l)

		cnt := 0
		for i := range c.labelList {
			cnt += c.labelList[i].sz
		}
		c.dropItems(cnt)

		if len(n.Results) == 0 {
			results := c.scope.decl.Type.Results
			if results.NumFields() != 0 {
				// function with named returns
				for i := len(results.List) - 1; i >= 0; i-- {
					names := results.List[i].Names
					for j := len(names) - 1; j >= 0; j-- {
						c.emitLoadVar(names[j].Name)
					}
				}
			}
		} else {
			// first result should be on top of the stack
			for i := len(n.Results) - 1; i >= 0; i-- {
				ast.Walk(c, n.Results[i])
			}
		}

		c.saveSequencePoint(n)
		emit.Opcode(c.prog.BinWriter, opcode.RET)
		return nil

	case *ast.IfStmt:
		lIf := c.newLabel()
		lElse := c.newLabel()
		lElseEnd := c.newLabel()

		if n.Cond != nil {
			ast.Walk(c, n.Cond)
			emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, lElse)
		}

		c.setLabel(lIf)
		ast.Walk(c, n.Body)
		if n.Else != nil {
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, lElseEnd)
		}

		c.setLabel(lElse)
		if n.Else != nil {
			ast.Walk(c, n.Else)
		}
		c.setLabel(lElseEnd)
		return nil

	case *ast.SwitchStmt:
		ast.Walk(c, n.Tag)

		eqOpcode := c.getEqualityOpcode(n.Tag)
		switchEnd, label := c.generateLabel(labelEnd)

		lastSwitch := c.currentSwitch
		c.currentSwitch = label
		c.pushStackLabel(label, 1)

		startLabels := make([]uint16, len(n.Body.List))
		for i := range startLabels {
			startLabels[i] = c.newLabel()
		}
		for i := range n.Body.List {
			lEnd := c.newLabel()
			lStart := startLabels[i]
			cc := n.Body.List[i].(*ast.CaseClause)

			if l := len(cc.List); l != 0 { // if not `default`
				for j := range cc.List {
					emit.Opcode(c.prog.BinWriter, opcode.DUP)
					ast.Walk(c, cc.List[j])
					emit.Opcode(c.prog.BinWriter, eqOpcode)
					if j == l-1 {
						emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, lEnd)
					} else {
						emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, lStart)
					}
				}
			}

			c.setLabel(lStart)
			last := len(cc.Body) - 1
			for j, stmt := range cc.Body {
				if j == last && isFallthroughStmt(stmt) {
					emit.Jmp(c.prog.BinWriter, opcode.JMPL, startLabels[i+1])
					break
				}
				ast.Walk(c, stmt)
			}
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, switchEnd)
			c.setLabel(lEnd)
		}

		c.setLabel(switchEnd)
		c.dropStackLabel()

		c.currentSwitch = lastSwitch

		return nil

	case *ast.FuncLit:
		l := c.newLabel()
		c.newLambda(l, n)
		buf := make([]byte, 4)
		binary.LittleEndian.PutUint16(buf, l)
		emit.Instruction(c.prog.BinWriter, opcode.PUSHA, buf)
		return nil

	case *ast.BasicLit:
		c.emitLoadConst(c.typeAndValueOf(n))
		return nil

	case *ast.Ident:
		if tv := c.typeAndValueOf(n); tv.Value != nil {
			c.emitLoadConst(tv)
		} else {
			c.emitLoadVar(n.Name)
		}
		return nil

	case *ast.CompositeLit:
		typ := c.typeOf(n.Type).Underlying()
		switch n.Type.(type) {
		case *ast.Ident, *ast.SelectorExpr, *ast.MapType:
			switch typ.(type) {
			case *types.Struct:
				c.convertStruct(n)
			case *types.Map:
				c.convertMap(n)
			}
		default:
			ln := len(n.Elts)
			// ByteArrays needs a different approach than normal arrays.
			if isByteSlice(typ) {
				c.convertByteArray(n)
				return nil
			}
			for i := ln - 1; i >= 0; i-- {
				ast.Walk(c, n.Elts[i])
			}
			emit.Int(c.prog.BinWriter, int64(ln))
			emit.Opcode(c.prog.BinWriter, opcode.PACK)
		}

		return nil

	case *ast.BinaryExpr:
		switch n.Op {
		case token.LAND:
			next := c.newLabel()
			end := c.newLabel()
			ast.Walk(c, n.X)
			emit.Jmp(c.prog.BinWriter, opcode.JMPIFL, next)
			emit.Opcode(c.prog.BinWriter, opcode.PUSHF)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, end)
			c.setLabel(next)
			ast.Walk(c, n.Y)
			c.setLabel(end)
			return nil

		case token.LOR:
			next := c.newLabel()
			end := c.newLabel()
			ast.Walk(c, n.X)
			emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, next)
			emit.Opcode(c.prog.BinWriter, opcode.PUSHT)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, end)
			c.setLabel(next)
			ast.Walk(c, n.Y)
			c.setLabel(end)
			return nil

		default:
			// The AST package will try to resolve all basic literals for us.
			// If the typeinfo.Value is not nil we know that the expr is resolved
			// and needs no further action. e.g. x := 2 + 2 + 2 will be resolved to 6.
			// NOTE: Constants will also be automatically resolved be the AST parser.
			// example:
			// const x = 10
			// x + 2 will results into 12
			tinfo := c.typeAndValueOf(n)
			if tinfo.Value != nil {
				c.emitLoadConst(tinfo)
				return nil
			}

			ast.Walk(c, n.X)
			ast.Walk(c, n.Y)

			switch {
			case n.Op == token.ADD:
				// VM has separate opcodes for number and string concatenation
				if isString(tinfo.Type) {
					emit.Opcode(c.prog.BinWriter, opcode.CAT)
				} else {
					emit.Opcode(c.prog.BinWriter, opcode.ADD)
				}
			case n.Op == token.EQL:
				// VM has separate opcodes for number and string equality
				op := c.getEqualityOpcode(n.X)
				emit.Opcode(c.prog.BinWriter, op)
			case n.Op == token.NEQ:
				// VM has separate opcodes for number and string equality
				if isString(c.typeOf(n.X)) {
					emit.Opcode(c.prog.BinWriter, opcode.NOTEQUAL)
				} else {
					emit.Opcode(c.prog.BinWriter, opcode.NUMNOTEQUAL)
				}
			default:
				c.convertToken(n.Op)
			}
			return nil
		}

	case *ast.CallExpr:
		var (
			f         *funcScope
			ok        bool
			name      string
			numArgs   = len(n.Args)
			isBuiltin = isBuiltin(n.Fun)
		)

		switch fun := n.Fun.(type) {
		case *ast.Ident:
			f, ok = c.funcs[fun.Name]
			if !ok && !isBuiltin {
				name = fun.Name
			}
		case *ast.SelectorExpr:
			// If this is a method call we need to walk the AST to load the struct locally.
			// Otherwise this is a function call from a imported package and we can call it
			// directly.
			if c.typeInfo.Selections[fun] != nil {
				ast.Walk(c, fun.X)
				// Dont forget to add 1 extra argument when its a method.
				numArgs++
			}

			f, ok = c.funcs[fun.Sel.Name]
			// @FIXME this could cause runtime errors.
			f.selector = fun.X.(*ast.Ident)
			if !ok {
				c.prog.Err = fmt.Errorf("could not resolve function %s", fun.Sel.Name)
				return nil
			}
		case *ast.ArrayType:
			// For now we will assume that there are only byte slice conversions.
			// E.g. []byte("foobar") or []byte(scriptHash).
			ast.Walk(c, n.Args[0])
			c.emitConvert(vm.BufferT)
			return nil
		}

		c.saveSequencePoint(n)

		args := transformArgs(n.Fun, n.Args)

		// Handle the arguments
		for _, arg := range args {
			ast.Walk(c, arg)
		}
		// Do not swap for builtin functions.
		if !isBuiltin {
			c.emitReverse(numArgs)
		}

		// Check builtin first to avoid nil pointer on funcScope!
		switch {
		case isBuiltin:
			// Use the ident to check, builtins are not in func scopes.
			// We can be sure builtins are of type *ast.Ident.
			c.convertBuiltin(n)
		case name != "":
			// Function was not found thus is can be only an invocation of func-typed variable.
			c.emitLoadVar(name)
			emit.Opcode(c.prog.BinWriter, opcode.CALLA)
		case isSyscall(f):
			c.convertSyscall(n, f.selector.Name, f.name)
		default:
			emit.Call(c.prog.BinWriter, opcode.CALLL, f.label)
		}

		return nil

	case *ast.SelectorExpr:
		strct, ok := c.typeOf(n.X).Underlying().(*types.Struct)
		if !ok {
			c.prog.Err = fmt.Errorf("selectors are supported only on structs")
			return nil
		}
		ast.Walk(c, n.X) // load the struct
		i := indexOfStruct(strct, n.Sel.Name)
		c.emitLoadField(i) // load the field
		return nil

	case *ast.UnaryExpr:
		ast.Walk(c, n.X)
		// From https://golang.org/ref/spec#Operators
		// there can be only following unary operators
		// "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
		// of which last three are not used in SC
		switch n.Op {
		case token.ADD:
			// +10 == 10, no need to do anything in this case
		case token.SUB:
			emit.Opcode(c.prog.BinWriter, opcode.NEGATE)
		case token.NOT:
			emit.Opcode(c.prog.BinWriter, opcode.NOT)
		case token.XOR:
			emit.Opcode(c.prog.BinWriter, opcode.INVERT)
		default:
			c.prog.Err = fmt.Errorf("invalid unary operator: %s", n.Op)
			return nil
		}
		return nil

	case *ast.IncDecStmt:
		ast.Walk(c, n.X)
		c.convertToken(n.Tok)

		// For now only identifiers are supported for (post) for stmts.
		// for i := 0; i < 10; i++ {}
		// Where the post stmt is ( i++ )
		if ident, ok := n.X.(*ast.Ident); ok {
			c.emitStoreVar(ident.Name)
		}
		return nil

	case *ast.IndexExpr:
		// Walk the expression, this could be either an Ident or SelectorExpr.
		// This will load local whatever X is.
		ast.Walk(c, n.X)
		ast.Walk(c, n.Index)
		emit.Opcode(c.prog.BinWriter, opcode.PICKITEM) // just pickitem here

		return nil

	case *ast.BranchStmt:
		var label string
		if n.Label != nil {
			label = n.Label.Name
		} else if n.Tok == token.BREAK {
			label = c.currentSwitch
		} else if n.Tok == token.CONTINUE {
			label = c.currentFor
		}

		cnt := 0
		for i := len(c.labelList) - 1; i >= 0 && c.labelList[i].name != label; i-- {
			cnt += c.labelList[i].sz
		}
		c.dropItems(cnt)

		switch n.Tok {
		case token.BREAK:
			end := c.getLabelOffset(labelEnd, label)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, end)
		case token.CONTINUE:
			post := c.getLabelOffset(labelPost, label)
			emit.Jmp(c.prog.BinWriter, opcode.JMPL, post)
		}

		return nil

	case *ast.LabeledStmt:
		c.nextLabel = n.Label.Name

		ast.Walk(c, n.Stmt)

		return nil

	case *ast.ForStmt:
		fstart, label := c.generateLabel(labelStart)
		fend := c.newNamedLabel(labelEnd, label)
		fpost := c.newNamedLabel(labelPost, label)

		lastLabel := c.currentFor
		lastSwitch := c.currentSwitch
		c.currentFor = label
		c.currentSwitch = label

		// Walk the initializer and condition.
		if n.Init != nil {
			ast.Walk(c, n.Init)
		}

		// Set label and walk the condition.
		c.pushStackLabel(label, 0)
		c.setLabel(fstart)
		if n.Cond != nil {
			ast.Walk(c, n.Cond)

			// Jump if the condition is false
			emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, fend)
		}

		// Walk body followed by the iterator (post stmt).
		ast.Walk(c, n.Body)
		c.setLabel(fpost)
		if n.Post != nil {
			ast.Walk(c, n.Post)
		}

		// Jump back to condition.
		emit.Jmp(c.prog.BinWriter, opcode.JMPL, fstart)
		c.setLabel(fend)
		c.dropStackLabel()

		c.currentFor = lastLabel
		c.currentSwitch = lastSwitch

		return nil

	case *ast.RangeStmt:
		start, label := c.generateLabel(labelStart)
		end := c.newNamedLabel(labelEnd, label)
		post := c.newNamedLabel(labelPost, label)

		lastFor := c.currentFor
		lastSwitch := c.currentSwitch
		c.currentFor = label
		c.currentSwitch = label

		ast.Walk(c, n.X)
		emit.Syscall(c.prog.BinWriter, "Neo.Iterator.Create")

		c.pushStackLabel(label, 1)
		c.setLabel(start)

		emit.Opcode(c.prog.BinWriter, opcode.DUP)
		emit.Syscall(c.prog.BinWriter, "Neo.Enumerator.Next")
		emit.Jmp(c.prog.BinWriter, opcode.JMPIFNOTL, end)

		if n.Key != nil {
			emit.Opcode(c.prog.BinWriter, opcode.DUP)
			emit.Syscall(c.prog.BinWriter, "Neo.Iterator.Key")
			c.emitStoreVar(n.Key.(*ast.Ident).Name)
		}
		if n.Value != nil {
			emit.Opcode(c.prog.BinWriter, opcode.DUP)
			emit.Syscall(c.prog.BinWriter, "Neo.Enumerator.Value")
			c.emitStoreVar(n.Value.(*ast.Ident).Name)
		}

		ast.Walk(c, n.Body)

		c.setLabel(post)

		emit.Jmp(c.prog.BinWriter, opcode.JMPL, start)

		c.setLabel(end)
		c.dropStackLabel()

		c.currentFor = lastFor
		c.currentSwitch = lastSwitch

		return nil

	// We dont really care about assertions for the core logic.
	// The only thing we need is to please the compiler type checking.
	// For this to work properly, we only need to walk the expression
	// not the assertion type.
	case *ast.TypeAssertExpr:
		ast.Walk(c, n.X)
		return nil
	}
	return c
}

func isFallthroughStmt(c ast.Node) bool {
	s, ok := c.(*ast.BranchStmt)
	return ok && s.Tok == token.FALLTHROUGH
}

func (c *codegen) pushStackLabel(name string, size int) {
	c.labelList = append(c.labelList, labelWithStackSize{
		name: name,
		sz:   size,
	})
}

func (c *codegen) dropStackLabel() {
	last := len(c.labelList) - 1
	c.dropItems(c.labelList[last].sz)
	c.labelList = c.labelList[:last]
}

func (c *codegen) dropItems(n int) {
	if n < 4 {
		for i := 0; i < n; i++ {
			emit.Opcode(c.prog.BinWriter, opcode.DROP)
		}
		return
	}

	emit.Int(c.prog.BinWriter, int64(n))
	emit.Opcode(c.prog.BinWriter, opcode.PACK)
	emit.Opcode(c.prog.BinWriter, opcode.DROP)
}

// emitReverse reverses top num items of the stack.
func (c *codegen) emitReverse(num int) {
	switch num {
	case 2:
		emit.Opcode(c.prog.BinWriter, opcode.SWAP)
	case 3:
		emit.Opcode(c.prog.BinWriter, opcode.REVERSE3)
	case 4:
		emit.Opcode(c.prog.BinWriter, opcode.REVERSE4)
	default:
		emit.Int(c.prog.BinWriter, int64(num))
		emit.Opcode(c.prog.BinWriter, opcode.REVERSEN)
	}
}

// generateLabel returns a new label.
func (c *codegen) generateLabel(typ labelOffsetType) (uint16, string) {
	name := c.nextLabel
	if name == "" {
		name = fmt.Sprintf("@%d", len(c.l))
	}

	c.nextLabel = ""
	return c.newNamedLabel(typ, name), name
}

func (c *codegen) getLabelOffset(typ labelOffsetType, name string) uint16 {
	return c.labels[labelWithType{name: name, typ: typ}]
}

func (c *codegen) getEqualityOpcode(expr ast.Expr) opcode.Opcode {
	t, ok := c.typeOf(expr).Underlying().(*types.Basic)
	if ok && t.Info()&types.IsNumeric != 0 {
		return opcode.NUMEQUAL
	}

	return opcode.EQUAL
}

// getByteArray returns byte array value from constant expr.
// Only literals are supported.
func (c *codegen) getByteArray(expr ast.Expr) []byte {
	switch t := expr.(type) {
	case *ast.CompositeLit:
		if !isByteSlice(c.typeOf(t.Type)) {
			return nil
		}
		buf := make([]byte, len(t.Elts))
		for i := 0; i < len(t.Elts); i++ {
			t := c.typeAndValueOf(t.Elts[i])
			val, _ := constant.Int64Val(t.Value)
			buf[i] = byte(val)
		}
		return buf
	case *ast.CallExpr:
		if tv := c.typeAndValueOf(t.Args[0]); tv.Value != nil {
			val := constant.StringVal(tv.Value)
			return []byte(val)
		}

		return nil
	default:
		return nil
	}
}

func (c *codegen) convertSyscall(expr *ast.CallExpr, api, name string) {
	api, ok := syscalls[api][name]
	if !ok {
		c.prog.Err = fmt.Errorf("unknown VM syscall api: %s", name)
		return
	}
	switch name {
	case "Notify":
		numArgs := len(expr.Args)
		emit.Int(c.prog.BinWriter, int64(numArgs))
		emit.Opcode(c.prog.BinWriter, opcode.PACK)
	}
	emit.Syscall(c.prog.BinWriter, api)

	// This NOP instruction is basically not needed, but if we do, we have a
	// one to one matching avm file with neo-python which is very nice for debugging.
	emit.Opcode(c.prog.BinWriter, opcode.NOP)
}

func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
	var name string
	switch t := expr.Fun.(type) {
	case *ast.Ident:
		name = t.Name
	case *ast.SelectorExpr:
		name = t.Sel.Name
	}

	switch name {
	case "len":
		emit.Opcode(c.prog.BinWriter, opcode.SIZE)
	case "append":
		arg := expr.Args[0]
		typ := c.typeInfo.Types[arg].Type
		if isByteSlice(typ) {
			emit.Opcode(c.prog.BinWriter, opcode.CAT)
		} else {
			emit.Opcode(c.prog.BinWriter, opcode.OVER)
			emit.Opcode(c.prog.BinWriter, opcode.SWAP)
			emit.Opcode(c.prog.BinWriter, opcode.APPEND)
		}
	case "panic":
		arg := expr.Args[0]
		if isExprNil(arg) {
			emit.Opcode(c.prog.BinWriter, opcode.DROP)
			emit.Opcode(c.prog.BinWriter, opcode.THROW)
		} else if isString(c.typeInfo.Types[arg].Type) {
			ast.Walk(c, arg)
			emit.Syscall(c.prog.BinWriter, "Neo.Runtime.Log")
			emit.Opcode(c.prog.BinWriter, opcode.THROW)
		} else {
			c.prog.Err = errors.New("panic should have string or nil argument")
		}
	case "ToInteger", "ToByteArray", "ToBool":
		typ := vm.IntegerT
		switch name {
		case "ToByteArray":
			typ = vm.ByteArrayT
		case "ToBool":
			typ = vm.BooleanT
		}
		c.emitConvert(typ)
	case "SHA256":
		emit.Syscall(c.prog.BinWriter, "Neo.Crypto.SHA256")
	case "AppCall":
		c.emitReverse(len(expr.Args))
		buf := c.getByteArray(expr.Args[0])
		if len(buf) != 20 {
			c.prog.Err = errors.New("invalid script hash")
		}
		emit.Syscall(c.prog.BinWriter, "System.Contract.Call")
	case "Equals":
		emit.Opcode(c.prog.BinWriter, opcode.EQUAL)
	case "FromAddress":
		// We can be sure that this is a ast.BasicLit just containing a simple
		// address string. Note that the string returned from calling Value will
		// contain double quotes that need to be stripped.
		addressStr := expr.Args[0].(*ast.BasicLit).Value
		addressStr = strings.Replace(addressStr, "\"", "", 2)
		uint160, err := address.StringToUint160(addressStr)
		if err != nil {
			c.prog.Err = err
			return
		}
		bytes := uint160.BytesBE()
		emit.Bytes(c.prog.BinWriter, bytes)
		c.emitConvert(vm.BufferT)
	}
}

// transformArgs returns a list of function arguments
// which should be put on stack.
// There are special cases for builtins:
// 1. With FromAddress, parameter conversion is happening at compile-time
//    so there is no need to push parameters on stack and perform an actual call
// 2. With panic, generated code depends on if argument was nil or a string so
//    it should be handled accordingly.
func transformArgs(fun ast.Expr, args []ast.Expr) []ast.Expr {
	switch f := fun.(type) {
	case *ast.SelectorExpr:
		if f.Sel.Name == "FromAddress" {
			return args[1:]
		}
	case *ast.Ident:
		if f.Name == "panic" {
			return args[1:]
		}
	}

	return args
}

// emitConvert converts top stack item to the specified type.
func (c *codegen) emitConvert(typ vm.StackItemType) {
	emit.Instruction(c.prog.BinWriter, opcode.CONVERT, []byte{byte(typ)})
}

func (c *codegen) convertByteArray(lit *ast.CompositeLit) {
	buf := make([]byte, len(lit.Elts))
	for i := 0; i < len(lit.Elts); i++ {
		t := c.typeAndValueOf(lit.Elts[i])
		val, _ := constant.Int64Val(t.Value)
		buf[i] = byte(val)
	}
	emit.Bytes(c.prog.BinWriter, buf)
	c.emitConvert(vm.BufferT)
}

func (c *codegen) convertMap(lit *ast.CompositeLit) {
	emit.Opcode(c.prog.BinWriter, opcode.NEWMAP)
	for i := range lit.Elts {
		elem := lit.Elts[i].(*ast.KeyValueExpr)
		emit.Opcode(c.prog.BinWriter, opcode.DUP)
		ast.Walk(c, elem.Key)
		ast.Walk(c, elem.Value)
		emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
	}
}

func (c *codegen) convertStruct(lit *ast.CompositeLit) {
	// Create a new structScope to initialize and store
	// the positions of its variables.
	strct, ok := c.typeOf(lit).Underlying().(*types.Struct)
	if !ok {
		c.prog.Err = fmt.Errorf("the given literal is not of type struct: %v", lit)
		return
	}

	emit.Opcode(c.prog.BinWriter, opcode.NOP)
	emit.Int(c.prog.BinWriter, int64(strct.NumFields()))
	emit.Opcode(c.prog.BinWriter, opcode.NEWSTRUCT)

	// We need to locally store all the fields, even if they are not initialized.
	// We will initialize all fields to their "zero" value.
	for i := 0; i < strct.NumFields(); i++ {
		sField := strct.Field(i)
		fieldAdded := false

		// Fields initialized by the program.
		for _, field := range lit.Elts {
			f := field.(*ast.KeyValueExpr)
			fieldName := f.Key.(*ast.Ident).Name

			if sField.Name() == fieldName {
				emit.Opcode(c.prog.BinWriter, opcode.DUP)

				pos := indexOfStruct(strct, fieldName)
				emit.Int(c.prog.BinWriter, int64(pos))

				ast.Walk(c, f.Value)

				emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
				fieldAdded = true
				break
			}
		}
		if fieldAdded {
			continue
		}

		emit.Opcode(c.prog.BinWriter, opcode.DUP)
		emit.Int(c.prog.BinWriter, int64(i))
		c.emitDefault(sField.Type())
		emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
	}
}

func (c *codegen) convertToken(tok token.Token) {
	switch tok {
	case token.ADD_ASSIGN:
		emit.Opcode(c.prog.BinWriter, opcode.ADD)
	case token.SUB_ASSIGN:
		emit.Opcode(c.prog.BinWriter, opcode.SUB)
	case token.MUL_ASSIGN:
		emit.Opcode(c.prog.BinWriter, opcode.MUL)
	case token.QUO_ASSIGN:
		emit.Opcode(c.prog.BinWriter, opcode.DIV)
	case token.REM_ASSIGN:
		emit.Opcode(c.prog.BinWriter, opcode.MOD)
	case token.ADD:
		emit.Opcode(c.prog.BinWriter, opcode.ADD)
	case token.SUB:
		emit.Opcode(c.prog.BinWriter, opcode.SUB)
	case token.MUL:
		emit.Opcode(c.prog.BinWriter, opcode.MUL)
	case token.QUO:
		emit.Opcode(c.prog.BinWriter, opcode.DIV)
	case token.REM:
		emit.Opcode(c.prog.BinWriter, opcode.MOD)
	case token.LSS:
		emit.Opcode(c.prog.BinWriter, opcode.LT)
	case token.LEQ:
		emit.Opcode(c.prog.BinWriter, opcode.LTE)
	case token.GTR:
		emit.Opcode(c.prog.BinWriter, opcode.GT)
	case token.GEQ:
		emit.Opcode(c.prog.BinWriter, opcode.GTE)
	case token.EQL:
		emit.Opcode(c.prog.BinWriter, opcode.NUMEQUAL)
	case token.NEQ:
		emit.Opcode(c.prog.BinWriter, opcode.NUMNOTEQUAL)
	case token.DEC:
		emit.Opcode(c.prog.BinWriter, opcode.DEC)
	case token.INC:
		emit.Opcode(c.prog.BinWriter, opcode.INC)
	case token.NOT:
		emit.Opcode(c.prog.BinWriter, opcode.NOT)
	case token.AND:
		emit.Opcode(c.prog.BinWriter, opcode.AND)
	case token.OR:
		emit.Opcode(c.prog.BinWriter, opcode.OR)
	case token.SHL:
		emit.Opcode(c.prog.BinWriter, opcode.SHL)
	case token.SHR:
		emit.Opcode(c.prog.BinWriter, opcode.SHR)
	case token.XOR:
		emit.Opcode(c.prog.BinWriter, opcode.XOR)
	default:
		c.prog.Err = fmt.Errorf("compiler could not convert token: %s", tok)
		return
	}
}

func (c *codegen) newFunc(decl *ast.FuncDecl) *funcScope {
	f := newFuncScope(decl, c.newLabel())
	c.funcs[f.name] = f
	return f
}

func (c *codegen) newLambda(u uint16, lit *ast.FuncLit) {
	name := fmt.Sprintf("lambda@%d", u)
	c.lambda[name] = newFuncScope(&ast.FuncDecl{
		Name: ast.NewIdent(name),
		Type: lit.Type,
		Body: lit.Body,
	}, u)
}

func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error {
	// Resolve the entrypoint of the program.
	main, mainFile := resolveEntryPoint(mainIdent, pkg)
	if main == nil {
		c.prog.Err = fmt.Errorf("could not find func main. Did you forget to declare it? ")
		return c.prog.Err
	}

	funUsage := analyzeFuncUsage(info.program.AllPackages)

	// Bring all imported functions into scope.
	for _, pkg := range info.program.AllPackages {
		for _, f := range pkg.Files {
			c.resolveFuncDecls(f)
		}
	}

	c.traverseGlobals(mainFile)

	// convert the entry point first.
	c.convertFuncDecl(mainFile, main)

	// sort map keys to generate code deterministically.
	keys := make([]*types.Package, 0, len(info.program.AllPackages))
	for p := range info.program.AllPackages {
		keys = append(keys, p)
	}
	sort.Slice(keys, func(i, j int) bool { return keys[i].Path() < keys[j].Path() })

	// Generate the code for the program.
	for _, k := range keys {
		pkg := info.program.AllPackages[k]
		c.typeInfo = &pkg.Info

		for _, f := range pkg.Files {
			for _, decl := range f.Decls {
				switch n := decl.(type) {
				case *ast.FuncDecl:
					// Don't convert the function if it's not used. This will save a lot
					// of bytecode space.
					if n.Name.Name != mainIdent && funUsage.funcUsed(n.Name.Name) {
						c.convertFuncDecl(f, n)
					}
				}
			}
		}
	}

	return c.prog.Err
}

func newCodegen(info *buildInfo, pkg *loader.PackageInfo) *codegen {
	return &codegen{
		buildInfo: info,
		prog:      io.NewBufBinWriter(),
		l:         []int{},
		funcs:     map[string]*funcScope{},
		lambda:    map[string]*funcScope{},
		globals:   map[string]int{},
		labels:    map[labelWithType]uint16{},
		typeInfo:  &pkg.Info,

		sequencePoints: make(map[string][]DebugSeqPoint),
	}
}

// CodeGen compiles the program to bytecode.
func CodeGen(info *buildInfo) ([]byte, *DebugInfo, error) {
	pkg := info.program.Package(info.initialPackage)
	c := newCodegen(info, pkg)

	if err := c.compile(info, pkg); err != nil {
		return nil, nil, err
	}

	buf := c.prog.Bytes()
	if err := c.writeJumps(buf); err != nil {
		return nil, nil, err
	}
	return buf, c.emitDebugInfo(), nil
}

func (c *codegen) resolveFuncDecls(f *ast.File) {
	for _, decl := range f.Decls {
		switch n := decl.(type) {
		case *ast.FuncDecl:
			if n.Name.Name != mainIdent {
				c.newFunc(n)
			}
		}
	}
}

func (c *codegen) writeJumps(b []byte) error {
	ctx := vm.NewContext(b)
	for op, _, err := ctx.Next(); err == nil && ctx.NextIP() < len(b); op, _, err = ctx.Next() {
		switch op {
		case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL,
			opcode.JMPEQ, opcode.JMPNE,
			opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT:
			panic("short jumps are not yet supported")
		case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL,
			opcode.JMPEQL, opcode.JMPNEL,
			opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
			opcode.CALLL, opcode.PUSHA:
			// we can't use arg returned by ctx.Next() because it is copied
			nextIP := ctx.NextIP()
			arg := b[nextIP-4:]

			index := binary.LittleEndian.Uint16(arg)
			if int(index) > len(c.l) {
				return fmt.Errorf("unexpected label number: %d (max %d)", index, len(c.l))
			}
			var offset int
			if op == opcode.PUSHA {
				offset = c.l[index]
			} else {
				offset = c.l[index] - nextIP + 5
			}
			if offset > math.MaxInt32 || offset < math.MinInt32 {
				return fmt.Errorf("label offset is too big at the instruction %d: %d (max %d, min %d)",
					nextIP-5, offset, math.MaxInt32, math.MinInt32)
			}
			binary.LittleEndian.PutUint32(arg, uint32(offset))
		}
	}
	return nil
}