// Copyright (c) 2019 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package txscript

import (
	"encoding/binary"
	"fmt"
)

// opcodeArrayRef is used to break initialization cycles.
var opcodeArrayRef *[256]opcode

func init() {
	opcodeArrayRef = &opcodeArray
}

// ScriptTokenizer provides a facility for easily and efficiently tokenizing
// transaction scripts without creating allocations.  Each successive opcode is
// parsed with the Next function, which returns false when iteration is
// complete, either due to successfully tokenizing the entire script or
// encountering a parse error.  In the case of failure, the Err function may be
// used to obtain the specific parse error.
//
// Upon successfully parsing an opcode, the opcode and data associated with it
// may be obtained via the Opcode and Data functions, respectively.
//
// The ByteIndex function may be used to obtain the tokenizer's current offset
// into the raw script.
type ScriptTokenizer struct {
	script  []byte
	version uint16
	offset  int32
	op      *opcode
	data    []byte
	err     error
}

// Done returns true when either all opcodes have been exhausted or a parse
// failure was encountered and therefore the state has an associated error.
func (t *ScriptTokenizer) Done() bool {
	return t.err != nil || t.offset >= int32(len(t.script))
}

// Next attempts to parse the next opcode and returns whether or not it was
// successful.  It will not be successful if invoked when already at the end of
// the script, a parse failure is encountered, or an associated error already
// exists due to a previous parse failure.
//
// In the case of a true return, the parsed opcode and data can be obtained with
// the associated functions and the offset into the script will either point to
// the next opcode or the end of the script if the final opcode was parsed.
//
// In the case of a false return, the parsed opcode and data will be the last
// successfully parsed values (if any) and the offset into the script will
// either point to the failing opcode or the end of the script if the function
// was invoked when already at the end of the script.
//
// Invoking this function when already at the end of the script is not
// considered an error and will simply return false.
func (t *ScriptTokenizer) Next() bool {
	if t.Done() {
		return false
	}

	op := &opcodeArrayRef[t.script[t.offset]]
	switch {
	// No additional data.  Note that some of the opcodes, notably OP_1NEGATE,
	// OP_0, and OP_[1-16] represent the data themselves.
	case op.length == 1:
		t.offset++
		t.op = op
		t.data = nil
		return true

	// Data pushes of specific lengths -- OP_DATA_[1-75].
	case op.length > 1:
		script := t.script[t.offset:]
		if len(script) < op.length {
			str := fmt.Sprintf("opcode %s requires %d bytes, but script only "+
				"has %d remaining", op.name, op.length, len(script))
			t.err = scriptError(ErrMalformedPush, str)
			return false
		}

		// Move the offset forward and set the opcode and data accordingly.
		t.offset += int32(op.length)
		t.op = op
		t.data = script[1:op.length]
		return true

	// Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}.
	case op.length < 0:
		script := t.script[t.offset+1:]
		if len(script) < -op.length {
			str := fmt.Sprintf("opcode %s requires %d bytes, but script only "+
				"has %d remaining", op.name, -op.length, len(script))
			t.err = scriptError(ErrMalformedPush, str)
			return false
		}

		// Next -length bytes are little endian length of data.
		var dataLen int32
		switch op.length {
		case -1:
			dataLen = int32(script[0])
		case -2:
			dataLen = int32(binary.LittleEndian.Uint16(script[:2]))
		case -4:
			dataLen = int32(binary.LittleEndian.Uint32(script[:4]))
		default:
			// In practice it should be impossible to hit this
			// check as each op code is predefined, and only uses
			// the specified lengths.
			str := fmt.Sprintf("invalid opcode length %d", op.length)
			t.err = scriptError(ErrMalformedPush, str)
			return false
		}

		// Move to the beginning of the data.
		script = script[-op.length:]

		// Disallow entries that do not fit script or were sign extended.
		if dataLen > int32(len(script)) || dataLen < 0 {
			str := fmt.Sprintf("opcode %s pushes %d bytes, but script only "+
				"has %d remaining", op.name, dataLen, len(script))
			t.err = scriptError(ErrMalformedPush, str)
			return false
		}

		// Move the offset forward and set the opcode and data accordingly.
		t.offset += 1 + int32(-op.length) + dataLen
		t.op = op
		t.data = script[:dataLen]
		return true
	}

	// The only remaining case is an opcode with length zero which is
	// impossible.
	panic("unreachable")
}

// Script returns the full script associated with the tokenizer.
func (t *ScriptTokenizer) Script() []byte {
	return t.script
}

// ByteIndex returns the current offset into the full script that will be parsed
// next and therefore also implies everything before it has already been parsed.
func (t *ScriptTokenizer) ByteIndex() int32 {
	return t.offset
}

// Opcode returns the current opcode associated with the tokenizer.
func (t *ScriptTokenizer) Opcode() byte {
	return t.op.value
}

// Data returns the data associated with the most recently successfully parsed
// opcode.
func (t *ScriptTokenizer) Data() []byte {
	return t.data
}

// Err returns any errors currently associated with the tokenizer.  This will
// only be non-nil in the case a parsing error was encountered.
func (t *ScriptTokenizer) Err() error {
	return t.err
}

// MakeScriptTokenizer returns a new instance of a script tokenizer.  Passing
// an unsupported script version will result in the returned tokenizer
// immediately having an err set accordingly.
//
// See the docs for ScriptTokenizer for more details.
func MakeScriptTokenizer(scriptVersion uint16, script []byte) ScriptTokenizer {
	// Only version 0 scripts are currently supported.
	var err error
	if scriptVersion != 0 {
		str := fmt.Sprintf("script version %d is not supported", scriptVersion)
		err = scriptError(ErrUnsupportedScriptVersion, str)

	}
	return ScriptTokenizer{version: scriptVersion, script: script, err: err}
}