package scparser

import (
	"encoding/binary"
	"errors"
	"fmt"

	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

var errNoInstParam = errors.New("failed to read instruction parameter")

// Context represents bytecode parser context.
type Context struct {
	// Instruction pointer.
	ip int
	// The next instruction pointer.
	nextip int
	// Program.
	prog []byte
}

// NewContext creates a new parsing context for the given program with the
// given initial offset.
func NewContext(b []byte, pos int) *Context {
	return &Context{
		nextip: pos,
		prog:   b,
	}
}

// NextIP returns the next instruction pointer.
func (c *Context) NextIP() int {
	return c.nextip
}

// Jump unconditionally moves the next instruction pointer to the specified location.
func (c *Context) Jump(pos int) {
	if pos < 0 || pos >= len(c.prog) {
		panic("instruction offset is out of range")
	}
	c.nextip = pos
}

// Next returns the next instruction to execute with its parameter if any.
// The parameter is not copied and shouldn't be written to. After its invocation,
// the instruction pointer points to the instruction returned.
func (c *Context) Next() (opcode.Opcode, []byte, error) {
	var err error

	c.ip = c.nextip
	prog := c.prog
	if c.ip >= len(prog) {
		return opcode.RET, nil, nil
	}

	var instrbyte = prog[c.ip]
	instr := opcode.Opcode(instrbyte)
	if !opcode.IsValid(instr) {
		return instr, nil, fmt.Errorf("incorrect opcode %s", instr.String())
	}
	c.nextip++

	var numtoread int
	switch instr {
	case opcode.PUSHDATA1:
		if c.nextip >= len(prog) {
			err = errNoInstParam
		} else {
			numtoread = int(prog[c.nextip])
			c.nextip++
		}
	case opcode.PUSHDATA2:
		if c.nextip+1 >= len(prog) {
			err = errNoInstParam
		} else {
			numtoread = int(binary.LittleEndian.Uint16(prog[c.nextip : c.nextip+2]))
			c.nextip += 2
		}
	case opcode.PUSHDATA4:
		if c.nextip+3 >= len(prog) {
			err = errNoInstParam
		} else {
			var n = binary.LittleEndian.Uint32(prog[c.nextip : c.nextip+4])
			if n > stackitem.MaxSize {
				return instr, nil, errors.New("parameter is too big")
			}
			numtoread = int(n)
			c.nextip += 4
		}
	case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT, opcode.JMPEQ, opcode.JMPNE,
		opcode.JMPGT, opcode.JMPGE, opcode.JMPLT, opcode.JMPLE,
		opcode.CALL, opcode.ISTYPE, opcode.CONVERT, opcode.NEWARRAYT,
		opcode.ENDTRY,
		opcode.INITSSLOT, opcode.LDSFLD, opcode.STSFLD, opcode.LDARG, opcode.STARG, opcode.LDLOC, opcode.STLOC:
		numtoread = 1
	case opcode.INITSLOT, opcode.TRY, opcode.CALLT:
		numtoread = 2
	case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL, opcode.JMPEQL, opcode.JMPNEL,
		opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLTL, opcode.JMPLEL,
		opcode.ENDTRYL,
		opcode.CALLL, opcode.SYSCALL, opcode.PUSHA:
		numtoread = 4
	case opcode.TRYL:
		numtoread = 8
	default:
		if instr <= opcode.PUSHINT256 {
			numtoread = 1 << instr
		} else {
			// No parameters, can just return.
			return instr, nil, nil
		}
	}
	if c.nextip+numtoread-1 >= len(prog) {
		err = errNoInstParam
	}
	if err != nil {
		return instr, nil, err
	}
	parameter := prog[c.nextip : c.nextip+numtoread]
	c.nextip += numtoread
	return instr, parameter, nil
}

// IP returns the current instruction offset in the context script.
func (c *Context) IP() int {
	return c.ip
}

// 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, opcode.Opcode) {
	return c.ip, opcode.Opcode(c.prog[c.ip])
}

// NextInstr returns the next instruction offset and opcode at that position.
// If the next instruction offset points past the end of the program the opcode
// returned is [opcode.RET].
func (c *Context) NextInstr() (int, opcode.Opcode) {
	op := opcode.RET
	if c.nextip < len(c.prog) {
		op = opcode.Opcode(c.prog[c.nextip])
	}
	return c.nextip, op
}

// CalcJumpOffset returns an absolute (wrt bytecode loaded into [Context])
// and a relative (wrt current [Context.IP]) offset of JMP/CALL/TRY
// instructions (and their long forms). 1 or 4 byte parameters are accepted.
func (c *Context) CalcJumpOffset(parameter []byte) (int, int, error) {
	var rOffset int32
	switch l := len(parameter); l {
	case 1:
		rOffset = int32(int8(parameter[0]))
	case 4:
		rOffset = int32(binary.LittleEndian.Uint32(parameter))
	default:
		_, curr := c.CurrInstr()
		return 0, 0, fmt.Errorf("invalid %s parameter length: %d", curr, l)
	}
	offset := c.IP() + int(rOffset)
	if offset < 0 || offset > c.LenInstr() {
		return 0, 0, fmt.Errorf("invalid offset %d ip at %d", offset, c.IP())
	}

	return offset, int(rOffset), nil
}
