package op

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

	"github.com/derekparker/delve/dwarf/util"
)

const (
	DW_OP_addr           = 0x3
	DW_OP_call_frame_cfa = 0x9c
	DW_OP_plus           = 0x22
	DW_OP_consts         = 0x11
	DW_OP_plus_uconsts   = 0x23
)

type stackfn func(*bytes.Buffer, []int64, int64) ([]int64, error)

var oplut = map[byte]stackfn{
	DW_OP_call_frame_cfa: callframecfa,
	DW_OP_plus:           plus,
	DW_OP_consts:         consts,
	DW_OP_addr:           addr,
	DW_OP_plus_uconsts:   plusuconsts,
}

func ExecuteStackProgram(cfa int64, instructions []byte) (int64, error) {
	stack := make([]int64, 0, 3)
	buf := bytes.NewBuffer(instructions)

	for opcode, err := buf.ReadByte(); err == nil; opcode, err = buf.ReadByte() {
		fn, ok := oplut[opcode]
		if !ok {
			return 0, fmt.Errorf("invalid instruction %#v", opcode)
		}

		stack, err = fn(buf, stack, cfa)
		if err != nil {
			return 0, err
		}
	}

	if len(stack) == 0 {
		return 0, errors.New("empty OP stack")
	}

	return stack[len(stack)-1], nil
}

func callframecfa(buf *bytes.Buffer, stack []int64, cfa int64) ([]int64, error) {
	if cfa == 0 {
		return stack, fmt.Errorf("Could not retrieve CFA for current PC")
	}
	return append(stack, int64(cfa)), nil
}

func addr(buf *bytes.Buffer, stack []int64, cfa int64) ([]int64, error) {
	return append(stack, int64(binary.LittleEndian.Uint64(buf.Next(8)))), nil
}

func plus(buf *bytes.Buffer, stack []int64, cfa int64) ([]int64, error) {
	var (
		slen   = len(stack)
		digits = stack[slen-2 : slen]
		st     = stack[:slen-2]
	)

	return append(st, digits[0]+digits[1]), nil
}

func plusuconsts(buf *bytes.Buffer, stack []int64, cfa int64) ([]int64, error) {
	slen := len(stack)
	num, _ := util.DecodeULEB128(buf)
	stack[slen-1] = stack[slen-1] + int64(num)
	return stack, nil
}

func consts(buf *bytes.Buffer, stack []int64, cfa int64) ([]int64, error) {
	num, _ := util.DecodeSLEB128(buf)
	return append(stack, num), nil
}
