package vm

import (
	"errors"

	"github.com/CityOfZion/neo-go/pkg/io"
)

// Context represents the current execution context of the VM.
type Context struct {
	// Instruction pointer.
	ip int

	// The next instruction pointer.
	nextip int

	// The raw program script.
	prog []byte

	// Breakpoints.
	breakPoints []int

	// Return value count, -1 is unspecified.
	rvcount int

	// Evaluation stack pointer.
	estack *Stack

	// Alt stack pointer.
	astack *Stack
}

// NewContext returns a new Context object.
func NewContext(b []byte) *Context {
	return &Context{
		prog:        b,
		breakPoints: []int{},
		rvcount:     -1,
	}
}

// Next returns the next instruction to execute with its parameter if any. After
// its invocation the instruction pointer points to the instruction being
// returned.
func (c *Context) Next() (Instruction, []byte, error) {
	c.ip = c.nextip
	if c.ip >= len(c.prog) {
		return RET, nil, nil
	}
	r := io.NewBinReaderFromBuf(c.prog[c.ip:])

	var instrbyte byte
	r.ReadLE(&instrbyte)
	instr := Instruction(instrbyte)
	c.nextip++

	var numtoread int
	switch instr {
	case PUSHDATA1, SYSCALL:
		var n byte
		r.ReadLE(&n)
		numtoread = int(n)
		c.nextip++
	case PUSHDATA2:
		var n uint16
		r.ReadLE(&n)
		numtoread = int(n)
		c.nextip += 2
	case PUSHDATA4:
		var n uint32
		r.ReadLE(&n)
		if n > MaxItemSize {
			return instr, nil, errors.New("parameter is too big")
		}
		numtoread = int(n)
		c.nextip += 4
	case JMP, JMPIF, JMPIFNOT, CALL, CALLED, CALLEDT:
		numtoread = 2
	case CALLI:
		numtoread = 4
	case APPCALL, TAILCALL:
		numtoread = 20
	case CALLE, CALLET:
		numtoread = 22
	default:
		if instr >= PUSHBYTES1 && instr <= PUSHBYTES75 {
			numtoread = int(instr)
		} else {
			// No parameters, can just return.
			return instr, nil, nil
		}
	}
	parameter := make([]byte, numtoread)
	r.ReadLE(parameter)
	if r.Err != nil {
		return instr, nil, errors.New("failed to read instruction parameter")
	}
	c.nextip += numtoread
	return instr, parameter, nil
}

// IP returns the absolute instruction without taking 0 into account.
// If that program starts the ip = 0 but IP() will return 1, cause its
// the first instruction.
func (c *Context) IP() int {
	return c.ip + 1
}

// LenInstr returns the number of instructions loaded.
func (c *Context) LenInstr() int {
	return len(c.prog)
}

// CurrInstr returns the current instruction and opcode.
func (c *Context) CurrInstr() (int, Instruction) {
	return c.ip, Instruction(c.prog[c.ip])
}

// Copy returns an new exact copy of c.
func (c *Context) Copy() *Context {
	ctx := new(Context)
	*ctx = *c
	return ctx
}

// Program returns the loaded program.
func (c *Context) Program() []byte {
	return c.prog
}

// Value implements StackItem interface.
func (c *Context) Value() interface{} {
	return c
}

func (c *Context) atBreakPoint() bool {
	for _, n := range c.breakPoints {
		if n == c.ip {
			return true
		}
	}
	return false
}

func (c *Context) String() string {
	return "execution context"
}