"""
A library to create a robot that can be controlled by the calling code,'
allowing creation and testing of a control algorithm.
"""

# UI scaling: if you have a high-resolution display, try increasing this.
UI_SCALE = 1

# Output mode: one of "tk", "ascii", "unicode".
OUTPUT_MODE = "tk"

from tkinter import Tk, Canvas, PhotoImage, StringVar, Label
import threading
import time
import random
import queue
from typing import Any, List, Tuple

# size (in pixles) of a square on the screen
squarex: int = 56
squarey: int = 56


class RobotStoppedException(RuntimeError):
    """
    Exception indicating continued robot control after calling .exit() or closing the window.
    """


class Direction:
    """
    Class to represent a compass direction in the game.
    """

    def __init__(self, string: str):
        self.string = string

    def __repr__(self) -> str:
        return self.string

    def __str__(self) -> str:
        return self.string

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, Direction) and self.string == other.string

    def __hash__(self) -> int:
        return hash(self.string)


N = Direction("N")
S = Direction("S")
E = Direction("E")
W = Direction("W")


class Square:
    """
    Class representing one of the items that can be on a game square, or seen by the sensor.
    """

    def __init__(self, string: str):
        self.string = string

    def __repr__(self) -> str:
        return self.string

    def __str__(self) -> str:
        return self.string

    def __eq__(self, other: object) -> bool:
        return isinstance(other, Square) and self.string == other.string

    def __hash__(self) -> int:
        return hash(self.string)


WALL = Square("WALL")
COIN = Square("COIN")
BLOCK = Square("BLOCK")
BLANK = Square("BLANK")
ROBOT = Square("ROBOT")

# type aliases
Pos = Tuple[int, int]
Layout = List[Tuple[int, int, Square]]
BoardContent = List[Square]

# text-mode output details
ASCII_REPR = {BLANK: " ", ROBOT: "R", COIN: "$", BLOCK: "#"}
UNICODE_REPR = {BLANK: " ", ROBOT: "\U00002605", COIN: "\U000025CF", BLOCK: "\U000025AB"}


# test boards
class TestBoard:
    """
    One of the pre-defined test boards provideed.
    """

    def __init__(self, size: Pos, initpos: Pos, layout: Layout):
        self.size = size
        self.initpos = initpos
        self.layout = layout


testboards: List[TestBoard] = []

# 0: one arbitrarily placed coin, no blocks
testboards.append(TestBoard((10, 10), (0, 0), [(1, 7, COIN)]))

# 1: four arbitrarily placed coins, no blocks
testboards.append(
    TestBoard(
        (10, 10), (4, 6), [(5, 5, COIN), (1, 7, COIN), (2, 5, COIN), (8, 2, COIN)]
    )
)

# 2: one block and one coin; different rows/cols
testboards.append(TestBoard((10, 10), (2, 8), [(5, 5, COIN), (1, 7, BLOCK)]))

# 3: one block and one coin; same row
testboards.append(TestBoard((10, 10), (3, 3), [(6, 4, COIN), (3, 4, BLOCK)]))

# 4: one block and one coin; same col
testboards.append(TestBoard((10, 10), (0, 9), [(7, 5, COIN), (7, 2, BLOCK)]))

# 5: one block with coins on each compass point
testboards.append(
    TestBoard(
        (11, 11),
        (2, 9),
        [(4, 5, COIN), (6, 5, COIN), (5, 4, COIN), (5, 6, COIN), (5, 5, BLOCK)],
    )
)

# 6: coin between two blocks (same row)
testboards.append(
    TestBoard((7, 7), (3, 1), [(4, 3, COIN), (2, 3, BLOCK), (5, 3, BLOCK)])
)

# 7: coin between two blocks (same col)
testboards.append(
    TestBoard((12, 12), (0, 8), [(8, 6, COIN), (8, 7, BLOCK), (8, 3, BLOCK)])
)

# 8: coin surrounded by three blocks
testboards.append(
    TestBoard(
        (10, 10), (1, 1), [(5, 5, COIN), (5, 4, BLOCK), (5, 7, BLOCK), (6, 5, BLOCK)]
    )
)

# 9: coin surrounded by three blocks
testboards.append(
    TestBoard(
        (10, 10), (7, 1), [(5, 5, COIN), (4, 5, BLOCK), (6, 5, BLOCK), (5, 4, BLOCK)]
    )
)

# 10: coin surrounded by four blocks
testboards.append(
    TestBoard(
        (10, 10),
        (1, 1),
        [(5, 5, COIN), (4, 5, BLOCK), (6, 5, BLOCK), (5, 4, BLOCK), (5, 7, BLOCK)],
    )
)

# 11: coin surrounded by five blocks
testboards.append(
    TestBoard(
        (10, 10),
        (1, 3),
        [
            (5, 5, COIN),
            (4, 5, BLOCK),
            (6, 5, BLOCK),
            (5, 4, BLOCK),
            (5, 7, BLOCK),
            (4, 6, BLOCK),
        ],
    )
)

# 12: plus sign of blocks with a coin in each corner
testboards.append(
    TestBoard(
        (10, 10),
        (0, 0),
        [
            (4, 4, COIN),
            (6, 4, COIN),
            (4, 6, COIN),
            (6, 6, COIN),
            (5, 5, BLOCK),
            (4, 5, BLOCK),
            (6, 5, BLOCK),
            (5, 4, BLOCK),
            (5, 6, BLOCK),
        ],
    )
)

# 13: one block in each corner, with coins surrounding each
testboards.append(
    TestBoard(
        (11, 11),
        (5, 5),
        [
            (0, 1, COIN),
            (1, 0, COIN),
            (10, 9, COIN),
            (9, 10, COIN),
            (9, 0, COIN),
            (0, 9, COIN),
            (1, 10, COIN),
            (10, 1, COIN),
            (0, 0, BLOCK),
            (0, 10, BLOCK),
            (10, 10, BLOCK),
            (10, 0, BLOCK),
        ],
    )
)

# 14: nasty board 1
testboards.append(
    TestBoard(
        (11, 11),
        (2, 10),
        [
            (8, 2, COIN),
            (1, 9, COIN),
            (9, 9, COIN),
            (8, 9, COIN),
            (10, 9, COIN),
            (9, 7, COIN),
            (8, 7, COIN),
            (10, 7, COIN),
            (5, 5, BLOCK),
            (5, 6, BLOCK),
            (5, 7, BLOCK),
            (5, 8, BLOCK),
            (5, 9, BLOCK),
            (5, 10, BLOCK),
            (7, 7, BLOCK),
            (7, 9, BLOCK),
            (7, 5, BLOCK),
            (8, 5, BLOCK),
            (9, 5, BLOCK),
            (10, 5, BLOCK),
            (10, 8, BLOCK),
            (9, 8, BLOCK),
            (8, 8, BLOCK),
            (7, 8, BLOCK),
        ],
    )
)


# 15: nasty board 2
def layout_15() -> Layout:
    layout = []
    for x in [1, 5, 9]:
        for y in range(10):
            layout.append((x, y, BLOCK))
        layout.append((x, 10, COIN))
    for x in [3, 7]:
        for y in range(10):
            layout.append((x, y + 1, BLOCK))
        layout.append((x, 0, COIN))
    layout.append((10, 0, COIN))
    return layout


testboards.append(TestBoard((11, 11), (0, 0), layout_15()))


# 16: nasty board 3: maze
def layout_16() -> Layout:
    return [
        (1, 1, BLOCK),
        (1, 2, BLOCK),
        (1, 3, BLOCK),
        (1, 4, BLOCK),
        (2, 1, BLOCK),
        (3, 1, BLOCK),
        (3, 2, BLOCK),
        (0, 4, BLOCK),
        (2, 4, BLOCK),
        (3, 4, BLOCK),
        (4, 4, BLOCK),
        (6, 4, BLOCK),
        (7, 4, BLOCK),
        (8, 4, BLOCK),
        (5, 0, BLOCK),
        (5, 1, BLOCK),
        (5, 2, BLOCK),
        (6, 2, BLOCK),
        (7, 2, BLOCK),
        (7, 3, BLOCK),
        (9, 2, BLOCK),
        (8, 0, BLOCK),
        (7, 0, BLOCK),
        (0, 6, BLOCK),
        (1, 6, BLOCK),
        (3, 6, BLOCK),
        (3, 7, BLOCK),
        (3, 8, BLOCK),
        (0, 8, BLOCK),
        (1, 8, BLOCK),
        (2, 8, BLOCK),
        (4, 6, BLOCK),
        (5, 6, BLOCK),
        (6, 6, BLOCK),
        (7, 6, BLOCK),
        (9, 6, BLOCK),
        (5, 8, BLOCK),
        (6, 8, BLOCK),
        (7, 8, BLOCK),
        (7, 9, BLOCK),
        (8, 8, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (1, 1, BLOCK),
        (2, 2, COIN),
        (0, 5, COIN),
        (6, 0, COIN),
        (9, 0, COIN),
        (0, 9, COIN),
        (2, 7, COIN),
        (5, 9, COIN),
        (8, 9, COIN),
        (7, 5, COIN),
        (6, 7, COIN),
    ]


testboards.append(TestBoard((10, 10), (0, 0), layout_16()))


class Robot:
    """
    A robot game that can be played by calling .move and .sensor and
    other methods as needed to collect all of the coins.
    """

    def __init__(
        self,
        coins: int = 5,
        blocks: int = 2,
        width: int = 10,
        height: int = 10,
        robotposition: Pos = (-1, -1),
        boardlayout: Layout = [],
        testboard: int = -1,
        delay: float = 0.25,
        ui_scale: int = UI_SCALE,
    ):
        if testboard == -1:
            # create a random board layout
            self.sizex = width
            self.sizey = height
            if boardlayout == []:
                self.board_content = self.__create_random_board(coins, blocks)
                self.__coins = coins
            else:
                (self.board_content, self.__coins) = self.__create_board_layout(
                    boardlayout
                )

            (self.__robot_x, self.__robot_y) = self.__place_robot(self.board_content)
        else:
            # test board: set that up.
            self.sizex, self.sizey = testboards[testboard].size
            (self.board_content, self.__coins) = self.__create_board_layout(
                testboards[testboard].layout
            )
            (self.__robot_x, self.__robot_y) = testboards[testboard].initpos

        # if user gives robot position, honour that.
        if list(robotposition) != [-1, -1]:
            if (
                self.board_content[self.index(robotposition[0], robotposition[1])]
                == BLANK
            ):
                (self.__robot_x, self.__robot_y) = robotposition
            else:
                raise ValueError("Robot placed on occupied square.")

        self.board_content[self.index(self.__robot_x, self.__robot_y)] = ROBOT

        # set up the GUI
        self.__running = True
        self.__lock = threading.Lock()
        self.__delay = delay
        if OUTPUT_MODE == "tk":
            self.__ui_scale = ui_scale
            self.__queue: queue.Queue = queue.Queue()
            self.__queue.put(("coins", self.__coins))
            self.__thread = threading.Thread(target=self.__create_window, args=())
            self.__thread.name = "tkinter thread"
            self.__thread.start()
            time.sleep(self.__delay)

    # Utility functions

    def __del__(self) -> None:
        self.__on_close()

    def index(self, x: int, y: int) -> int:
        """
        Return the position in a board array of (x,y).
        """
        return self.sizex * y + x

    def __empty_board(self) -> BoardContent:
        # create empty board
        board = [BLANK] * (self.sizex * self.sizey)
        return board

    def __create_random_board(self, coins: int = 5, blocks: int = 0) -> BoardContent:
        """
        Create a random board layout.
        """
        # create empty board
        board = self.__empty_board()

        # place blocks
        for _ in range(blocks):
            while True:
                x = random.randint(0, self.sizex - 1)
                y = random.randint(0, self.sizey - 1)
                if board[self.index(x, y)] == BLANK:
                    break
            board[self.index(x, y)] = BLOCK

        # place coins
        for _ in range(coins):
            while True:
                x = random.randint(0, self.sizex - 1)
                y = random.randint(0, self.sizey - 1)
                if board[self.index(x, y)] == BLANK:
                    break
            board[self.index(x, y)] = COIN

        return board

    def __create_board_layout(self, layout: Layout) -> Tuple[BoardContent, int]:
        """
        Create a board layout as specified.
        """
        # create empty board
        board = self.__empty_board()
        coins = 0

        # place their objects
        for x, y, obj in layout:
            if board[self.index(x, y)] == BLANK or board[self.index(x, y)] == obj:
                board[self.index(x, y)] = obj
                if obj == COIN:
                    coins += 1
            else:
                raise ValueError(
                    "Multiple objects put on the same block in boardlayout."
                )

        return (board, coins)

    def __place_robot(self, board: BoardContent) -> Pos:
        """
        Determine an initial position for the robot.
        """
        while True:
            x = random.randint(0, self.sizex - 1)
            y = random.randint(0, self.sizey - 1)
            if board[self.index(x, y)] == BLANK:
                break

        return (x, y)

    # window/GUI handling methods

    def __create_window(self) -> None:
        # create Tkinter window and the canvas
        if OUTPUT_MODE != "tk":
            raise ValueError()
        self.__root = Tk()
        self.__root.title("Robot")
        self.__root.protocol("WM_DELETE_WINDOW", self.__on_close)
        self.__c = Canvas(
            self.__root,
            width=squarex * self.sizex * self.__ui_scale,
            height=squarey * self.sizey * self.__ui_scale,
        )
        self.__c.grid(row=0, column=0)

        self.robot_image = PhotoImage(data=ROBOT_IMGDATA).zoom(self.__ui_scale)
        self.coin_image = PhotoImage(data=COIN_IMGDATA).zoom(self.__ui_scale)
        self.wall_image = PhotoImage(data=WALL_IMGDATA).zoom(self.__ui_scale)

        # create the labels
        self.__coinstr = StringVar()
        Label(self.__root, textvariable=self.__coinstr).grid(row=1, column=0)

        # draw the window
        self.__draw_initial_board(self.board_content)
        self.__periodic_call()

        # throw mainloop in another thread, so we can get on with it.
        self.__root.mainloop()

    def __periodic_call(self) -> None:
        """
        Check every 100 ms if there is something new in the queue.
        """
        self.__process_incoming()
        self.__root.after(100, self.__periodic_call)

    def __on_close(self) -> None:
        with self.__lock:
            self.__running = False
        if OUTPUT_MODE == "tk":
            self.__root.quit()

    def __process_incoming(self) -> None:
        """
        Handle all the messages currently in the queue (if any).
        """
        while self.__queue.qsize():
            try:
                action = self.__queue.get()
                # Check contents of message and do what it says
                if action[0] == "empty":
                    self.__draw_empty(action[1], action[2])
                elif action[0] == "robot":
                    self.__draw_robot(action[1], action[2])
                elif action[0] == "coins":
                    self.__coinstr.set("Coins left: " + str(action[1]))
                else:
                    raise NotImplementedError()

            except queue.Empty:
                pass

    def __draw_empty(self, x: int, y: int) -> None:
        self.__c.create_rectangle(
            x * squarex * self.__ui_scale,
            y * squarey * self.__ui_scale,
            (x+1) * squarex * self.__ui_scale,
            (y+1) * squarey * self.__ui_scale,
            fill="white",
        )

    def __draw_robot(self, x: int, y: int) -> None:
        self.__draw_empty(x, y)
        self.__c.create_image(
            (x+0.5) * squarex * self.__ui_scale,
            (y+0.5) * squarey * self.__ui_scale,
            image=self.robot_image,
        )

    def __draw_coin(self, x: int, y: int) -> None:
        self.__draw_empty(x, y)
        self.__c.create_image(
            (x+0.5) * squarex * self.__ui_scale,
            (y+0.5) * squarey * self.__ui_scale,
            image=self.coin_image,
        )

    def __draw_block(self, x: int, y: int) -> None:
        self.__draw_empty(x, y)
        self.__c.create_image(
            (x+0.5) * squarex * self.__ui_scale,
            (y+0.5) * squarey * self.__ui_scale,
            image=self.wall_image,
        )

    def __draw_initial_board(self, board: BoardContent) -> None:
        for y in range(self.sizey):
            for x in range(self.sizex):
                if board[self.index(x, y)] == BLANK:
                    self.__draw_empty(x, y)
                elif board[self.index(x, y)] == ROBOT:
                    self.__draw_robot(x, y)
                elif board[self.index(x, y)] == COIN:
                    self.__draw_coin(x, y)
                elif board[self.index(x, y)] == BLOCK:
                    self.__draw_block(x, y)
                else:
                    raise NotImplementedError()
    
    def __draw_text_board(self, characters: dict[Square, str]=ASCII_REPR) -> None:
        output = ['']
        output.append(' ' + '-' * self.sizex + ' ')
        for y in range(self.sizey):
            line = ''.join(characters[self.board_content[self.index(x, y)]] for x in range(self.sizex))
            output.append("|"+line+"|")
        output.append(' ' + '-' * self.sizex + ' ')
        output.append(f' Coins left: {self.__coins}')
        print('\n'.join(output))

    # public methods

    def move(self, direction: Direction) -> None:
        """
        robot.move(direction): move the robot one square in the given
        direction (N,E,S,W).
        """
        with self.__lock:
            if not self.__running:
                raise RobotStoppedException("The robot window has been closed.")
        old_x = self.__robot_x
        old_y = self.__robot_y
        if direction == N:
            new_x = old_x
            new_y = old_y - 1
        elif direction == E:
            new_x = old_x + 1
            new_y = old_y
        elif direction == S:
            new_x = old_x
            new_y = old_y + 1
        elif direction == W:
            new_x = old_x - 1
            new_y = old_y
        else:
            raise ValueError("Unknown direction")

        if self.board_content[self.index(new_x, new_y)] == BLOCK:
            # thump
            raise RuntimeError("Hit a block")

        if new_x < 0 or new_x >= self.sizex or new_y < 0 or new_y >= self.sizey:
            # off the edge
            raise RuntimeError("Past edge of board")

        if self.board_content[self.index(new_x, new_y)] == COIN:
            # ran into a coin
            self.__coins -= 1
            if OUTPUT_MODE == "tk":
                self.__queue.put(("coins", self.__coins))

        # update data structure
        self.board_content[self.index(old_x, old_y)] = BLANK
        self.board_content[self.index(new_x, new_y)] = ROBOT
        # update position
        self.__robot_x = new_x
        self.__robot_y = new_y
        if OUTPUT_MODE == "tk":
            # update screen
            self.__queue.put(("empty", old_x, old_y))
            self.__queue.put(("robot", new_x, new_y))
        elif OUTPUT_MODE == "ascii":
            self.__draw_text_board()
        elif OUTPUT_MODE == "unicode":
            self.__draw_text_board(characters=UNICODE_REPR)
        else:
            raise ValueError("OUTPUT_MODE is not set to an understood value.")

        # control animation speed
        time.sleep(self.__delay)

    def sensor(self, direction: Direction) -> Tuple[Square, int]:
        """
        (object, distance) = robot.sensor(direction): check the sensor in
        the given direction (N,E,S,W).

        Returns the object (WALL, COIN, BLOCK) and the distance to it (1
        means you're adjacent).
        """
        with self.__lock:
            if not self.__running:
                raise RobotStoppedException("The robot window has been closed.")
        x = self.__robot_x
        y = self.__robot_y
        dist = 0

        while not (x < 0 or x >= self.sizex or y < 0 or y >= self.sizey):
            if direction == N:
                y = y - 1
            elif direction == E:
                x = x + 1
            elif direction == S:
                y = y + 1
            elif direction == W:
                x = x - 1
            else:
                raise ValueError("Unknown direction")

            dist += 1
            if x < 0 or x >= self.sizex or y < 0 or y >= self.sizey:
                return (WALL, dist)

            if self.board_content[self.index(x, y)] != BLANK:
                if self.board_content[self.index(x, y)] == BLOCK:
                    return (BLOCK, dist)
                if self.board_content[self.index(x, y)] == COIN:
                    return (COIN, dist)
                raise ValueError("Unknown thing on board.")

        return (WALL, dist)

    def current_position(self) -> Pos:
        """
        Return the current x and y position of the robot.
        """
        return (self.__robot_x, self.__robot_y)

    def board_size(self) -> Pos:
        """
        Return the number of squares horizontally and vertically in the
        playing field.
        """
        return (self.sizex, self.sizey)

    def coins_left(self) -> int:
        """
        Return the number of coins still on the board.
        """
        return self.__coins

    def exit(self, delay: int = 1) -> None:
        """
        Close the robot window and exit the game after a specified delay.
        """
        time.sleep(delay)
        self.__on_close()


# image data for robot image. Image source: https://www.flaticon.com/free-icon/robot_3070648
ROBOT_IMGDATA = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAHlBMVEUAAAAKCgtYXGYAAAB7go91eoYAAACKkJ0AAAAAAACZAAsGAAAACnRSTlMC9v+l//9e////pK26qQAAAWZJREFUeJytU8tOwzAQjLFwr1knhCuuWnF1VIl7KyyuUDl3R/xAQUj9fXbtNH40SEgwhzT2ZGf21ar6C/pm+Z5JVZd30FHAaUUhI8R7LvHwDogn/EjpSNxDxcFDV/ARCQENOgzGDujSg4taoxb9xihpD43jXZqQA7AHgI0BEGlqzOvX009C8JTQVYooVZQowJK5SVMK6K011pirdk31lQ7kcUERIJ8nqCyEQXtce2z3WR0ArQpCch/FBJ5Vqx7PiE+5l3gMOfNmR8SJ3ldE7Prgw6nVrdoMiANK4QhmgqceOiEqdnfJ6o1OgRg7IszgYYkYuykpDI5SJOzTYjVqsfY15H6LBNOMiuQah4lSwYOkmONzXzKprFntMbxsC4LLl7PHV97dn+eBqzyhKwgBa+ZEvb5aBuq9E3PHcxNPlPe4P/RY+rP9IwHzI8eIs8bZl/XhtzeU7sNVCC215gtLLS6EW7D/Hb4BgbU/F8a9aIMAAAAASUVORK5CYII="
# image data for wall image. Image source: https://www.flaticon.com/free-icon/wall_698633
WALL_IMGDATA = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAHlBMVEXnbVUTCAZcJh+LNi2TRDW7WEUTBwZiIx8AAAAAAADFD/qVAAAACnRSTlP//v////+ibv//TQPSuQAAAPBJREFUeJzNVEEOgyAQXNH2XMDYq2iTXjX0AWJ8gE/QpB9o/38oLspqUkx6qnNYFwaS3WFcGLnF8OIe1/MUMxBad61UNs4QdakfnQIJFlKAh6p7Gw1MWyfJ10SDxDNEwM+E3COCVSXrGxxvKMBuU0WdC4MfiItS6ybWHlWCEUY6mzPKL8ALD1lTzl1VDkJRbg5KiBBBzXJZrxbwpm6rmPK7Iyq2cKzIY0dsZc8jL3vQPrtv/lfiq+HQDKkiMYQRaAa0T96Sejfm7IOGm622tl1Y9s1D9VTCQX0VNpzuEzddWmnHyzJqlNVqiJbfK6LGsw+TZEjxnQK6kAAAAABJRU5ErkJggg=="
# image data for coin image. Image source: https://www.flaticon.com/free-icon/dollar_1490853
COIN_IMGDATA = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAHlBMVEX7qQP+3VQAAAD+6pT5zhr00AfwzQT+3U4AAAAAAADhSHg1AAAACnRSTlP//QD++B1rbf//sqBY/AAAAZFJREFUeJx1lM1qwzAMx5WPbtcIB3Itge4coz5ACnmAMUrPG/QBRt7/MH3ZTkPmQ5v8f5FkyZJh9HX6XhDxHdJ7eniILOv3FXwmHRG24FH0RPT3stUD1Rk8b6Isy9zyH9GUwIcY9DPIqgIRrQ7EoGexFdYQxcGAGIgOzVV+AyGuCu5soH5CVG8RsRNwcYM5gYo3UDMQT2pQEalH0B2DeOpdJ+r8aWLQ3jyCgFiAhFAHpGv2xxo4hHuaQmzMF+eyAnvyly5EpvZR7ODuIRqS7VoQIP74eQQiDuIKCgiTZcIWHttigC8uUgYckDKoNkDzOARNSnwPtCbXI7BxtgOBgpXkBbRyUF4S3W5OMGrm15zgYUmQS7IrYrE4LLuAo4NisMrRUs7C8+COrbkZfPOlGSSEtI8XYtM+Bs6pNyrXxdNqIJpQWhS/pKkb8s4KKYI19XiOaMTGoMI0BjI46SBMH/KohXx4omMatfFHU1i49jq3Q55a2TGVgS7jPHI9YtZP2ysDisHLlTGOb/9cMvlaOqX3P6kuZ9oKK9FaAAAAAElFTkSuQmCC"
