"""
This file is part of Advent of Code 2019.

Coded by: Samuel Michaels (samuel.michaels@protonmail.com)
11 December 2019

NO COPYRIGHT
This work is dedicated to the public domain.  All rights have been
waived worldwide under copyright law, including all related and
neighboring rights, to the extent allowed by law.

You may copy, modify, distribute, and perform the work, even for
commercial purposes, all without asking permission.  See the
accompanying COPYRIGHT document.
"""

import re
from functools import partial
from threading import RLock
from typing import Union


class AlreadyExecutingError(Exception):
    """
    Raised when something is blocked because we're already executing
    """
    pass


class SystemHaltError(Exception):
    """
    Raised when execution is attempted after halting
    """
    pass


class SystemBlockError(Exception):
    """
    Raised when a change is attempted during a blocked operation
    """
    pass


class InvalidInstructionError(Exception):
    """
    Raised when an invalid instruction is decoded
    """
    pass


class ProtectionFaultError(Exception):
    """
    Raised when an invalid memory operation occurs
    """
    pass


class IntegerOverflow(Exception):
    """
    Raised when an integer overflows
    """
    pass


class InputInterrupt(Exception):
    """
    Raised when an input is requested
    """
    pass


class OutputInterrupt(Exception):
    """
    Raised when an output is requested
    """
    pass


class OutputOverflow(Exception):
    """
    Raised when the output buffer is already full
    """
    pass


class InputOverflow(Exception):
    """
    Raised when the input buffer is already full
    """
    pass


class ElfCPU(object):
    def __init__(self, interrupts: bool = True, ram_size: int = 2**11):
        self._lock = RLock()

        # Main memory (program + user)
        self._memory_map = None

        # CPU is executing
        self._running = False

        # CPU has been halted from a halt instruction
        self._halt = False

        # If the CPU is blocked during operation
        self._block = False

        # reset vector 0
        self._program_counter = 0

        # input buffer
        self._input_buffer = None

        # output buffer
        self._output_buffer = None

        # use interrupts or stdio
        if not isinstance(interrupts, bool):
            raise TypeError(f"interrupts must be of type bool, got {interrupts.__class__}")
        self._interrupts = interrupts

        # RAM size (program code + ram size)
        if not isinstance(ram_size, int):
            raise TypeError(f"ram_size must be of type int, got {ram_size.__class__}")
        if not 0 < ram_size <= (2**64)-1:
            raise ValueError(f"ram_size must be in range 1 to (2**64)-1")
        self._ram_size = ram_size

        # Relative base (op code 9)
        self._relative_base = 0

    def load_string(self, codes: str) -> None:
        """
        Loads the int codes from a comma separated string
        """
        if not isinstance(codes, str):
            raise TypeError(f"codes must be of type string, got {codes.__class__}")

        # convert the comma separated string into an array of ints
        int_codes = [int(x) for x in codes.strip().split(',')]

        with self._lock:
            if self._running:
                raise AlreadyExecutingError

            # Reset CPU
            self.reset()

            # Load codes
            self._memory_map = int_codes

            # Initialize RAM to 0
            [self._memory_map.append(0) for _ in range(self._ram_size - len(self._memory_map))]

    def reset(self):
        """
        Reset the processor
        """
        with self._lock:
            self._running = False
            self._halt = False
            self._block = False
            self._memory_map = []
            self._program_counter = 0
            self._input_buffer = None
            self._output_buffer = None
            self._interrupts = False
            self._relative_base = 0

    def _validate_addr(self, addr) -> bool:
        if not isinstance(addr, int):
            raise TypeError(f"addr must be of type int, got {addr.__class__}")
        if not 0 <= addr < len(self._memory_map):
            raise ValueError(f"addr '{addr}' outside of memory range of 0 to {len(self._memory_map) - 1}")
        return True

    def peek(self, addr: int) -> int:
        """
        Peeks at an address
        """
        self._validate_addr(addr)
        with self._lock:
            return self._memory_map[addr]

    def poke(self, addr: int, val: int) -> int:
        """
        Pokes a value into an address, returns the value
        """

        self._validate_addr(addr)

        if not isinstance(val, int):
            raise TypeError(f"addr must be of type int, got {addr.__class__}")

        # 64-bit Elf CPU
        if not 0 <= val <= (2**64) - 1:
            raise ValueError(f"value must be 0 to 2**64 - 1")

        with self._lock:
            if self._running:
                raise AlreadyExecutingError
            self._memory_map[addr] = val

        return val

    def _do_fault(self, exc):
        """
        Handles the internal CPU state and then raises exception.  Assumes a lock
        is already in place
        """
        self._running = False
        raise exc from None

    def step(self):
        """
        Executes the next instruction
        """
        with self._lock:
            if self._running and not self._block:
                raise AlreadyExecutingError
            if self._halt:
                raise SystemHaltError

            self._block = False
            self._running = True
            self._cpu_cycle()
            self._running = False

    def execute(self):
        """
        Resumes normal execution
        """
        with self._lock:
            if self._running and not self._block:
                raise AlreadyExecutingError
            if self._halt:
                raise SystemHaltError
            self._running = True
            self._block = False

            # main CPU loop
            while self._running and not self._halt:
                self._cpu_cycle()
            self._running = False

    def _cpu_cycle(self):
        """
        One CPU cycle
        """
        instr = -1
        pc_start = self._program_counter
        try:
            instr = self._memory_map[self._program_counter]
        except IndexError:
            self._do_fault(ProtectionFaultError(f"pc:{self._program_counter}"))

        # Advance the pc because we read an int
        self._program_counter += 1

        # Get the function for the op code
        op = self._decode_instr(instr)

        if not op:
            self._do_fault(InvalidInstructionError(f"instr:{instr} @ addr:{pc_start}"))

        # Execute instruction
        op()

    def _decode_instr(self, instr: int):
        """
        Decodes an instruction to find its function, returns None if invalid
        """
        instr = str(instr).zfill(5)
        op_code = int(instr[3:])
        # 0 = positional, 1 = immediate, 2 = relative
        if not re.match(r'^[012]{3}$', instr[0:3]):
            return None

        a = int(instr[2])
        b = int(instr[1])
        c = int(instr[0])

        # I don't like this pythonic switch
        # TODO add if/else structure and fail on immediate write destinations
        decoder = {
            1: partial(self._op_add, a_mode=a, b_mode=b, dst_mode=c),
            2: partial(self._op_mul, a_mode=a, b_mode=b, dst_mode=c),
            3: partial(self._op_input, dst_mode=a),
            4: partial(self._op_output, a_mode=a),
            5: partial(self._op_jmp, if_true=True, a_mode=a, dst_mode=b),
            6: partial(self._op_jmp, if_true=False, a_mode=a, dst_mode=b),
            7: partial(self._op_cmp, eq=False, a_mode=a, b_mode=b, dst_mode=c),
            8: partial(self._op_cmp, eq=True, a_mode=a, b_mode=b, dst_mode=c),
            9: partial(self._op_relative_base, a_mode=a),
            99: self._op_hlt
        }
        return decoder.get(op_code, None)
    
    @staticmethod
    def _relchar(mode: int) -> str:
        if mode == 0:
            return ''
        elif mode == 1:
            return '#'
        elif mode == 2:
            return 'r'
        else:
            return ''

    def _resolve_addr(self, a: int, mode: int) -> int:
        """
        Resolves an address based on mode but does not dereference
        """
        addr = None
        if mode == 0:
            # positional
            addr = a
        elif mode == 1:
            # immediate
            addr = a
        elif mode == 2:
            # relative
            a += self._relative_base
            addr = a
        return addr

    def _dereference(self, a: int, mode: int):
        """
        Dereferences address
        """
        addr = None
        try:
            if mode == 0:
                # positional
                addr = self._memory_map[a]
            elif mode == 1:
                # immediate
                addr = a
            elif mode == 2:
                # relative
                addr = self._memory_map[a + self._relative_base]
        except IndexError:
            self._do_fault(ProtectionFaultError(f"invalid dereference {a}\n"
                                                f"pc:{self._program_counter}"))
        return addr

    def _get_parameters(self, params: int) -> tuple:
        """
        Retrieves the op code params
        """
        fetched = []
        for p in range(params):
            a = None
            try:
                a = self._memory_map[self._program_counter]
                self._program_counter += 1
            except IndexError:
                self._do_fault(ProtectionFaultError(f"invalid read of address {self._program_counter}\n"
                                                    f"pc:{self._program_counter}"))
            fetched.append(a)
        return tuple(fetched)

    def _set_memory(self, dst: int, data: int) -> None:
        """
        Writes data to the dst location
        """
        try:
            self._memory_map[dst] = data
        except IndexError:
            self._do_fault(ProtectionFaultError(f"memory access violation @ {dst}\n"
                                                f"pc:{self._program_counter}"))

    def _op_relative_base(self, a_mode: int):
        """
        Executes the relative base op code
        """

        # Get params tuple
        a, = self._get_parameters(1)

        self._relative_base += self._dereference(a, a_mode)

    def _op_jmp(self, if_true: bool, a_mode: int, dst_mode: int):
        """
        Executes jump to dst or [dst] if a is non-zero
        if (a|[a]) != 0
            pc:=(dst|[dst])
        """

        # Get params tuple
        a, dst = self._get_parameters(2)
        a = self._dereference(a, a_mode)

        # Trying to minimize dupe code here
        do_jump = False
        if if_true and a != 0:
            # jump-if-true
            do_jump = True
        elif not if_true and a == 0:
            # jump-if-false
            do_jump = True

        if do_jump:
            self._program_counter = self._dereference(dst, dst_mode)

    def _op_cmp(self, eq: bool, a_mode: int, b_mode: int, dst_mode: int):
        """
        Executes less than
        """

        # Get params tuple
        a, b, dst = self._get_parameters(3)
        a = self._dereference(a, a_mode)
        b = self._dereference(b, b_mode)
        # Writes are literal
        dst = self._resolve_addr(dst, dst_mode)

        # Trying to minimize dupe code
        if eq and a == b:
            # equals
            val = 1
        elif not eq and a < b:
            # less than
            val = 1
        else:
            val = 0

        # Write result
        self._set_memory(dst, val)

    def _op_input(self, dst_mode: int):
        """
            3
        Executes the input op code.  This operation blocks until completion.
        """

        # Get params tuple
        dst, = self._get_parameters(1)
        # Writes are literal
        dst = self._resolve_addr(dst, dst_mode)

        if self._interrupts:
            # Check for data, call interrupt if none
            if self._input_buffer is None:
                self._program_counter -= 2
                self._block = True
                raise InputInterrupt
            data = self._validate_int(self._input_buffer)
            self._input_buffer = None
        else:
            # This operation blocks!!!
            self._block = True
            while True:
                data = input("> ")
                try:
                    data = self._validate_int(data)
                except TypeError:
                    # Likely won't happen, but just in case...
                    continue
                except IntegerOverflow:
                    pass
                break
            self._block = False

        # Write data
        self._set_memory(dst, data)

    def _op_output(self, a_mode: int):
        """
            4
        Outputs data
        """

        # Get params tuple
        a, = self._get_parameters(1)
        a = self._dereference(a, a_mode)

        if self._interrupts:
            if self._output_buffer is not None:
                self._program_counter -= 2
                raise OutputOverflow
            self._output_buffer = a
            self._block = True
            raise OutputInterrupt
        else:
            print(a)

    def _op_add(self, a_mode: int, b_mode: int, dst_mode: int):
        """
        Executes the add op code
        dst:=a|[a] + b|[b]
        """

        # Get params tuple
        a, b, dst = self._get_parameters(3)
        a = self._dereference(a, a_mode)
        b = self._dereference(b, b_mode)
        # Writes are literal
        dst = self._resolve_addr(dst, dst_mode)

        # Write memory
        self._set_memory(dst, a + b)

    def _op_mul(self, a_mode: int, b_mode: int, dst_mode: int):
        """
        Executes the mul op code
        [dst]:=a|[a] * b|[b]
        """

        # Get params tuple
        a, b, dst = self._get_parameters(3)
        a = self._dereference(a, a_mode)
        b = self._dereference(b, b_mode)
        # Writes are literal
        dst = self._resolve_addr(dst, dst_mode)

        # Write memory
        self._set_memory(dst, a * b)

    def _op_hlt(self):
        """
        Executes the halt op code
        """
        self._halt = True

    @staticmethod
    def _validate_int(i: Union[int, str]) -> int:
        """
        Validates an integer, throws an excetion
        """
        if isinstance(i, str):
            i = int(i)
        if not isinstance(i, int):
            raise TypeError(f"integer must be of type int, got {i.__class__}")
        if i.bit_length() > 64:
            raise IntegerOverflow
        # The exception might get passed, so continue processing
        return i

    @property
    def is_running(self):
        with self._lock:
            return self._running

    @is_running.setter
    def is_running(self, x):
        raise NotImplementedError("cannot set internal attribute")

    @is_running.deleter
    def is_running(self):
        raise NotImplementedError("cannot delete internal attribute")

    @property
    def is_halted(self):
        with self._lock:
            return self._halt

    @is_halted.setter
    def is_halted(self, x):
        raise NotImplementedError("cannot set internal attribute")

    @is_halted.deleter
    def is_halted(self):
        raise NotImplementedError("cannot delete internal attribute")

    @property
    def pc(self):
        with self._lock:
            return self._program_counter

    @pc.setter
    def pc(self, x):
        raise NotImplementedError("cannot set internal attribute")

    @pc.deleter
    def pc(self):
        raise NotImplementedError("cannot delete internal attribute")

    @property
    def interrupts(self):
        return self._interrupts

    @interrupts.setter
    def interrupts(self, x):
        with self._lock:
            self._interrupts = x

    @interrupts.deleter
    def interrupts(self):
        raise NotImplementedError("cannot delete internal attribute")

    @property
    def input_buffer(self):
        raise NotImplementedError("cannot read internal buffer")

    @input_buffer.setter
    def input_buffer(self, x):
        with self._lock:
            if self._input_buffer:
                raise InputOverflow
            # Don't validate - the code handles that
            self._input_buffer = x

    @input_buffer.deleter
    def input_buffer(self):
        raise NotImplementedError("cannot delete internal attribute")

    @property
    def output_buffer(self):
        with self._lock:
            return self._output_buffer

    @output_buffer.setter
    def output_buffer(self, x):
        raise NotImplementedError("cannot set internal attribute")

    @output_buffer.deleter
    def output_buffer(self):
        with self._lock:
            self._output_buffer = None


if __name__ == '__main__':
    e = ElfCPU()
    with open('input.txt') as f:
        e.load_string(f.readline())
    e.interrupts = False
    e.execute()

# EOF
