#
# A class which represents the Program Counter and decodes instructions
# to be executed by the ZPU.  Implements section 4 of Z-code specification.
#
# For the license of this file, please consult the LICENSE file in the
# root directory of this distribution.
#

from .zlogging import log


class ZOperationError(Exception):
    "General exception for ZOperation class"

    pass


# Constants defining the known instruction types. These types are
# related to the number of operands the opcode has: for each operand
# count, there is a separate opcode table, and the actual opcode
# number is an index into that table.
OPCODE_0OP = 0
OPCODE_1OP = 1
OPCODE_2OP = 2
OPCODE_VAR = 3
OPCODE_EXT = 4

# Mapping of those constants to strings describing the opcode
# classes. Used for pretty-printing only.
OPCODE_STRINGS = {
    OPCODE_0OP: "0OP",
    OPCODE_1OP: "1OP",
    OPCODE_2OP: "2OP",
    OPCODE_VAR: "VAR",
    OPCODE_EXT: "EXT",
}

# Constants defining the possible operand types.
LARGE_CONSTANT = 0x0
SMALL_CONSTANT = 0x1
VARIABLE = 0x2
ABSENT = 0x3


class ZOpDecoder:
    def __init__(self, zmem, zstack):
        ""
        self._memory = zmem
        self._stack = zstack
        self._parse_map = {}
        self.program_counter = self._memory.read_word(0x6)

    def _get_pc(self):
        byte = self._memory[self.program_counter]
        self.program_counter += 1
        return byte

    def get_next_instruction(self):
        """Decode the opcode & operands currently pointed to by the
        program counter, and appropriately increment the program counter
        afterwards. A decoded operation is returned to the caller in the form:

           [opcode-class, opcode-number, [operand, operand, operand, ...]]

        If the opcode has no operands, the operand list is present but empty."""

        opcode = self._get_pc()

        log(f"Decode opcode {opcode:x}")

        # Determine the opcode type, and hand off further parsing.
        if self._memory.version >= 5 and opcode == 0xBE:
            # Extended opcode
            return self._parse_opcode_extended()

        if not (opcode & 0x80):
            return self._parse_opcode_long(opcode)
        elif not (opcode & 0x40):
            return self._parse_opcode_short(opcode)
        else:
            return self._parse_opcode_variable(opcode)

    def _parse_opcode_long(self, opcode):
        """Parse an opcode of the long form."""
        # Long opcodes are always 2OP. The types of the two operands are
        # encoded in bits 5 and 6 of the opcode.
        log("Opcode is long")
        LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
        operands = [
            self._parse_operand(LONG_OPERAND_TYPES[(opcode >> 6) & 1]),
            self._parse_operand(LONG_OPERAND_TYPES[(opcode >> 5) & 1]),
        ]
        return (OPCODE_2OP, opcode & 0x1F, operands)

    def _parse_opcode_short(self, opcode):
        """Parse an opcode of the short form."""
        # Short opcodes can have either 1 operand, or no operand.
        log("Opcode is short")
        operand_type = (opcode >> 4) & 0x03
        operand = self._parse_operand(operand_type)
        if operand is None:  # 0OP variant
            log("Opcode is 0OP variant")
            return (OPCODE_0OP, opcode & 0x0F, [])
        else:
            log("Opcode is 1OP variant")
            return (OPCODE_1OP, opcode & 0x0F, [operand])

    def _parse_opcode_variable(self, opcode):
        """Parse an opcode of the variable form."""
        log("Opcode is variable")
        if (opcode >> 5) & 1:
            log("Variable opcode of VAR kind")
            opcode_type = OPCODE_VAR
        else:
            log("Variable opcode of 2OP kind")
            opcode_type = OPCODE_2OP

        opcode_num = opcode & 0x1F

        # Read all type bytes FIRST, before parsing any operands.
        # call_vs2 (VAR:12) and call_vn2 (VAR:26) have two type bytes;
        # all others have one. Both type bytes must be read before
        # operand data starts in the stream.
        operand_types = self._read_type_byte()
        if opcode_type == OPCODE_VAR and opcode_num in (0xC, 0x1A):
            log("Opcode has second operand byte")
            operand_types += self._read_type_byte()

        operands = self._parse_operand_list(operand_types)
        return (opcode_type, opcode_num, operands)

    def _parse_opcode_extended(self):
        """Parse an extended opcode (v5+ feature)."""
        log("Opcode is extended")

        # Read the extended opcode number
        opcode_num = self._get_pc()
        log(f"Extended opcode number: {opcode_num:x}")

        # Parse the operand types byte and retrieve operands
        operands = self._parse_operands_byte()

        return (OPCODE_EXT, opcode_num, operands)

    def _parse_operand(self, operand_type):
        """Read and return an operand of the given type.

        This assumes that the operand is in memory, at the address pointed
        by the Program Counter."""
        assert operand_type <= 0x3

        if operand_type == LARGE_CONSTANT:
            log("Operand is large constant")
            operand = self._memory.read_word(self.program_counter)
            self.program_counter += 2
        elif operand_type == SMALL_CONSTANT:
            log("Operand is small constant")
            operand = self._get_pc()
        elif operand_type == VARIABLE:
            variable_number = self._get_pc()
            log(f"Operand is variable {variable_number}")
            if variable_number == 0:
                log("Operand value comes from stack")
                operand = self._stack.pop_stack()  # TODO: make sure this is right.
            elif variable_number < 16:
                log("Operand value comes from local variable")
                operand = self._stack.get_local_variable(variable_number - 1)
            else:
                log("Operand value comes from global variable")
                operand = self._memory.read_global(variable_number)
        elif operand_type == ABSENT:
            log("Operand is absent")
            operand = None
        if operand is not None:
            log(f"Operand value: {operand}")

        return operand

    def _read_type_byte(self):
        """Read one operand type byte and return a list of type codes."""
        operand_byte = self._get_pc()
        return [
            (operand_byte >> 6) & 0x03,
            (operand_byte >> 4) & 0x03,
            (operand_byte >> 2) & 0x03,
            operand_byte & 0x03,
        ]

    def _parse_operand_list(self, operand_types):
        """Parse operands from a list of type codes, stopping at ABSENT."""
        operands = []
        for operand_type in operand_types:
            operand = self._parse_operand(operand_type)
            if operand is None:
                break
            operands.append(operand)
        return operands

    def _parse_operands_byte(self):
        """Read one type byte and parse its operands. Used by extended
        opcodes and other callers that always have a single type byte."""
        return self._parse_operand_list(self._read_type_byte())

    # Public funcs that the ZPU may also need to call, depending on the
    # opcode being executed:

    def get_zstring(self):
        """For string opcodes, return the address of the zstring pointed
        to by the PC.  Increment PC just past the text."""

        start_addr = self.program_counter

        while True:
            byte = self._memory[self.program_counter]
            self.program_counter += 2
            if (byte >> 7) & 1:
                break

        return start_addr

    def get_store_address(self):
        """For store opcodes, read byte pointed to by PC and return the
        variable number in which the operation result should be stored.
        Increment the PC as necessary."""
        return self._get_pc()

    def get_branch_offset(self):
        """For branching opcodes, examine address pointed to by PC, and
        return two values: first, either True or False (indicating whether
        to branch if true or branch if false), and second, the address to
        jump to.  Increment the PC as necessary."""

        byte = self._get_pc()
        branch_if_true = bool((byte >> 7) & 1)
        if (byte >> 6) & 1:
            branch_offset = byte & 0x3F
        else:
            # We need to do a little magic here. The branch offset is
            # written as a signed 14-bit number, with signed meaning '-n' is
            # written as '65536-n'. Or in this case, as we have 14 bits,
            # '16384-n'.
            #
            # So, if the MSB (ie. bit 13) is set, we have a negative
            # number. We take the value, and substract 16384 to get the
            # actual offset as a negative integer.
            #
            # If the MSB is not set, we just extract the value and return it.
            #
            # Can you spell "Weird" ?
            branch_offset = self._get_pc() + ((byte & 0x1F) << 8)
            if (byte >> 5) & 1:
                branch_offset -= 8192

        log(f"Branch if {branch_if_true} to offset {branch_offset:+d}")
        return branch_if_true, branch_offset
