from ...core.enum.ea_mode import EAMode
from ...core.enum.op_size import OpSize
from ...core.enum import ea_mode_bin
from ...simulator.m68k import M68K
from ...core.opcodes.opcode import Opcode
from ...core.util.split_bits import split_bits
from ...core.util import opcode_util
from ..util.parsing import parse_assembly_parameter
from ..models.assembly_parameter import AssemblyParameter
from ..models.memory_value import MemoryValue
from ..enum.register import Register
from typing import Union
[docs]class Jsr(Opcode):
    """
    JSR: Jump to Subroutine
    Operation: SP - 4 -> SP; PC -> (SP); Destination Address -> PC
    Assembler Syntax: JSR <ea>
    Attributes: Unsized
    Description: Pushes the long-word address of the instruction immediately following the JSR instruction
                 onto the system stack. Program execution then continues
                 at the address specified in the instruction.
    Condition Codes: Not affected
    Instruction Format: 0100111010 Signature xxx EAMode xxx EARegister
    Instruction Field:
        Effective Address field - Specifies the address of the next instruction.
                                  Only control addressing modes can be used as listed in the following tables.
        Valid Modes - (An), (xxx).W, (xxx).L
    """
    def __init__(self, params: list):
        assert len(params) == 1
        assert isinstance(params[0], AssemblyParameter)
        # check ea param is valid
        assert params[0].mode == EAMode.ARI or params[0].mode == EAMode.AWA or params[0].mode == EAMode.ALA
        self.dest = params[0]
[docs]    def assemble(self) -> bytearray:
        """
        Assembles this opcode into hex to be inserted into memory
        :return: The hex version of this opcode
        """
        # 0100111010 Signature xxx EAMode xxx EARegister
        # ret_opcode is the binary value which represents the assembled instruction
        ret_opcode = 0b0100111010 << 6
        ret_opcode |= ea_mode_bin.parse_from_ea_mode_modefirst(self.dest) << 0
        ret_bytes = bytearray(ret_opcode.to_bytes(2, byteorder='big', signed=False))
        if self.dest.mode == EAMode.AWA or self.dest.mode == EAMode.ALA:
            size = OpSize.WORD if self.dest.mode == EAMode.AWA else OpSize.LONG
            ret_bytes.extend(opcode_util.ea_to_binary_post_op(self.dest, size).get_value_bytearray())
        return ret_bytes 
[docs]    def execute(self, simulator: M68K):
        """
        Executes this command in a simulator
        :param simulator: The simulator to execute the command on
        :return: None
        """
        # increment the program counter by the length of the instruction (1 word)
        to_increment = OpSize.WORD.value
        # repeat for the dest
        if self.dest.mode in [EAMode.AbsoluteLongAddress]:
            to_increment += OpSize.LONG.value
        elif self.dest.mode in [EAMode.AbsoluteWordAddress]:
            to_increment += OpSize.WORD.value
        # set the program counter value
        simulator.increment_program_counter(to_increment)
        # get the value of src from the simulator
        dest_val = self.dest.get_value(simulator, OpSize.LONG)
        sp_val = simulator.get_register(Register.A7)
        new_sp_val = sp_val.get_value_unsigned() - 4
        # SP – 4 -> SP
        simulator.set_register(Register.A7, MemoryValue(OpSize.LONG, unsigned_int=new_sp_val))
        pc_val = simulator.get_program_counter_value()
        # PC -> (SP)
        AssemblyParameter(EAMode.ALA, new_sp_val).set_value(simulator, MemoryValue(OpSize.LONG, unsigned_int=pc_val))
        # Destination Address -> PC
        simulator.set_register(Register.PC, dest_val) 
    def __str__(self):
        # Makes this a bit easier to read in doctest output
        return 'Jsr command: dest {}'.format(self.dest)
[docs]    @classmethod
    def command_matches(cls, command: str) -> bool:
        """
        Checks whether a command string is an instance of this command type
        :param command: The command string to check (e.g. 'MOVE.B', 'LEA', etc.)
        :return: Whether the string is an instance of this command type
        """
        return opcode_util.command_matches(command, 'JSR') 
[docs]    @classmethod
    def get_word_length(cls, command: str, parameters: str) -> int:
        """
        >>> Jsr.get_word_length('JSR', '(A5)')
        1
        >>> Jsr.get_word_length('JSR', '-(A7)')
        1
        >>> Jsr.get_word_length('JSR', '($BBBB).W')
        2
        >>> Jsr.get_word_length('JSR', '($1000).W')
        2
        >>> Jsr.get_word_length('JSR', '($FFFF).L')
        3
        >>> Jsr.get_word_length('JSR', '($8000).L')
        3
        Gets what the end length of this command will be in memory
        :param command: The text of the command itself (e.g. "LEA", "MOVE.B", etc.)
        :param parameters: The parameters after the command
        :return: The length of the bytes in memory in words, as well as a list of warnings or errors encountered
        """
        dest = parse_assembly_parameter(parameters.strip())  # Parse the destination
        length = 1  # Always 1 word not counting additions to end
        if dest.mode == EAMode.AWA:  # Appends a word
            length += 1
        if dest.mode == EAMode.ALA:  # Appends a long, so 2 words
            length += 2
        return length 
[docs]    @classmethod
    def is_valid(cls, command: str, parameters: str) -> (bool, list):
        """
        Tests whether the given command is valid
        >>> Jsr.is_valid('JSR', '(A0)')[0]
        True
        >>> Jsr.is_valid('JSR', '(A5)')[0]
        True
        >>> Jsr.is_valid('JSR', '#5, D1')[0]
        False
        >>> Jsr.is_valid('JSR.W', '(A1)')[0]
        True
        >>> Jsr.is_valid('JS', '($1000).W')[0]
        False
        >>> Jsr.is_valid('JSR.', '(A4)')[0]
        False
        >>> Jsr.is_valid('JSR', '($7000).W')[0]
        True
        :param command: The command itself (e.g. 'MOVE.B', 'LEA', etc.)
        :param parameters: The parameters after the command (such as the source and destination of a move)
        :return: Whether the given command is valid and a list of issues/warnings encountered
        """
        return opcode_util.n_param_is_valid(command, parameters, "JSR", 1, param_invalid_modes=[[EAMode.DRD,
                                                                                                 EAMode.ARD,
                                                                                                 EAMode.ARIPD,
                                                                                                 EAMode.ARIPI,
                                                                                                 EAMode.IMM]])[:2] 
[docs]    @classmethod
    def disassemble_instruction(cls, data: bytearray) -> Union[Opcode, None]:
        """
        This has a non-JSR opcode
        >>> Jsr.disassemble_instruction(bytearray.fromhex('0280'))
        JSR (A0)
        >>> op = Jsr.disassemble_instruction(bytearray.fromhex('4E90'))
        >>> str(op.dest)
        'EA Mode: EAMode.ARI, Data: 0'
        JSR (A5)
        >>> op = Jsr.disassemble_instruction(bytearray.fromhex('4E95'))
        >>> str(op.dest)
        'EA Mode: EAMode.ARI, Data: 5'
        JSR $4000
        >>> op = Jsr.disassemble_instruction(bytearray.fromhex('4EB84000'))
        >>> str(op.dest)
        'EA Mode: EAMode.AWA, Data: 16384'
        JSR $FFFF7000
        >>> op = Jsr.disassemble_instruction(bytearray.fromhex('4EB87000'))
        >>> str(op.dest)
        'EA Mode: EAMode.AWA, Data: 28672'
        Parses some raw data into an instance of the opcode class
        :param data: The data used to convert into an opcode instance
        :return: The constructed instance or none if there was an error and
            the amount of data in words that was used (e.g. extra for immediate
            data) or 0 for not a match
        """
        assert len(data) >= 2, 'Opcode size is at least one word'
        first_word = int.from_bytes(data[0:2], 'big')
        [opcode_bin,
         ea_mode_binary,
         ea_reg_bin] = split_bits(first_word, [10, 3, 3])
        if opcode_bin != 0b0100111010:
            return None
        if ea_mode_binary == 0b111:
            mode = EAMode.AWA if ea_reg_bin == 0 else EAMode.ALA
            dest = AssemblyParameter(mode, int.from_bytes(data[2:], byteorder='big', signed=False))
        elif ea_mode_binary == 0b010:
            dest = AssemblyParameter(EAMode.ARI, ea_reg_bin)
        else:
            return None
        return cls([dest]) 
[docs]    @classmethod
    def from_str(cls, command: str, parameters: str):
        """
        Parses a JSR command from text.
        >>> str(Jsr.from_str('JSR', '(A0)'))
        'Jsr command: dest EA Mode: EAMode.ARI, Data: 0'
        >>> str(Jsr.from_str('JSR', '($4000).W'))
        'Jsr command: dest EA Mode: EAMode.AWA, Data: 16384'
        :param command: The command itself (e.g. 'MOVE.B', 'LEA', etc.)
        :param parameters: The parameters after the command (such as the source and destination of a move)
        :return: The parsed command
        """
        return opcode_util.n_param_from_str(command, parameters, Jsr, 1, None)