<filename>day11/robot.py
"""
This file is part of Advent of Code 2019.

Coded by: <NAME> (<EMAIL>)
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.
"""

from day11 import ElfCPU, InputInterrupt, OutputInterrupt, AlreadyExecutingError, SystemHaltError
from enum import Enum
from operator import itemgetter


class Move(Enum):
    LEFT = 0
    RIGHT = 1


class Direction(Enum):
    UP = 0
    DOWN = 1
    LEFT = 2
    RIGHT = 3


class Paint(Enum):
    BLACK = 0
    WHITE = 1


class State(Enum):
    PAINTING = 0
    MOVING = 1


# Holds coordinates and color
# {(x,y): 'w', (x,y): 'b'}
# Part one should be BLACK (empty dict), part two should be WHITE
hull_map = {(0,0): Paint.WHITE}


class HullRobot(object):
    def __init__(self, debug: bool = False):
        # The brain
        self._cpu = ElfCPU()

        # Current position x,y
        self._pos_x = 0
        self._pos_y = 0
        self._dir = Direction.UP

        # State
        self._state = State.PAINTING

        # Debug?
        if not isinstance(debug, bool):
            raise TypeError(f"debug must be of type bool, got {debug.__class__}")
        self._debug = debug

        if self._debug:
            print("HullRobot powered on")

    def run(self) -> None:
        """
        Tells robot to begin
        """
        if self._debug:
            print("HullRobot run requested")
        while not self._cpu.is_halted:
            try:
                if self._debug:
                    print("HullRobot CPU executing")
                self._cpu.execute()
            except InputInterrupt:
                if self._debug:
                    print("HullRobot input interrupt")
                self._cpu.input_buffer = self._camera().value
            except OutputInterrupt:
                if self._debug:
                    print("HullRobot output interrupt")
                if self._state == State.PAINTING:
                    # Paint the hull
                    self._paint(Paint(self._cpu.output_buffer))
                    self._state = State.MOVING
                elif self._state == State.MOVING:
                    # Move
                    self._move(Move(self._cpu.output_buffer))
                    self._state = State.PAINTING
                # Clear the output buffer
                del self._cpu.output_buffer

    def load_program(self, code: str) -> None:
        """
        Loads a new program
        """
        if not isinstance(code, str):
            raise TypeError(f"code must be of type str, got {code.__class__}")

        if self._debug:
            print("HullRobot loaded program code")

        self._cpu.load_string(code)
        self._cpu.interrupts = True

    def _move(self, dest: Move) -> None:
        """
        Moves the robot left or right
        """
        if self._dir == Direction.UP:
            if dest == Move.LEFT:
                self._pos_x -= 1
                self._dir = Direction.LEFT
            elif dest == Move.RIGHT:
                self._pos_x += 1
                self._dir = Direction.RIGHT
        elif self._dir == Direction.DOWN:
            if dest == Move.LEFT:
                self._pos_x += 1
                self._dir = Direction.RIGHT
            elif dest == Move.RIGHT:
                self._pos_x -= 1
                self._dir = Direction.LEFT
        elif self._dir == Direction.LEFT:
            if dest == Move.LEFT:
                self._pos_y -= 1
                self._dir = Direction.DOWN
            elif dest == Move.RIGHT:
                self._pos_y += 1
                self._dir = Direction.UP
        elif self._dir == Direction.RIGHT:
            if dest == Move.LEFT:
                self._pos_y += 1
                self._dir = Direction.UP
            elif dest == Move.RIGHT:
                self._pos_y -= 1
                self._dir = Direction.DOWN

        if self._debug:
            print(f"HullRobot moved {'left' if dest == Move.LEFT else 'right'} to ({self._pos_x}, {self._pos_y})")

    def _camera(self) -> Paint:
        """
        Access the robot camera to determine if over black or white panel by
        looking up the hull map.  Anything not explicitly painted by the robot
        is black.
        """
        cam = hull_map.get((self._pos_x, self._pos_y), Paint.BLACK)
        if self._debug:
            print(f"HullRobot camera at ({self._pos_x}, {self._pos_y}) sees "
                  f"{'black' if cam == Paint.BLACK else 'white'}")
        return cam

    def _paint(self, color: Paint) -> None:
        """
        Robot has painted its current location
        """
        try:
            x = hull_map[(self._pos_x, self._pos_y)]
        except KeyError:
            x = None
        if self._debug:
            print(f"HullRobot {'painting' if x is None else 're-painting'} "
                  f"{'black' if color == Paint.BLACK else 'white'} at ({self._pos_x}, {self._pos_y})")
        hull_map[(self._pos_x, self._pos_y)] = color


if __name__ == '__main__':
    # Use argument debug=True for output
    r = HullRobot()
    with open('input.txt') as f:
        r.load_program(f.readline())

    r.run()

    # For part one
    print(f"Total painted squares: {len(hull_map)-1}")

    # Get our top left and bottom right coords
    xleft = min(list(hull_map.keys()),key=itemgetter(0))[0]
    xright = max(list(hull_map.keys()), key=itemgetter(0))[0]
    ytop = max(list(hull_map.keys()), key=itemgetter(1))[1]
    ybottom = min(list(hull_map.keys()), key=itemgetter(1))[1]

    # Iterate the cols
    for y in range(ytop, ybottom-1, -1):
        # Iterate the row
        for x in range(xleft, xright+1, 1):
            color = hull_map.get((x, y), Paint.BLACK)
            print('#' if color == Paint.WHITE else ' ', end='')
        print('')

# EOF
