"""
This example shows how two computers can interact with one another by using
a module called  simpleMQTT  to send/receive messages to/from each other.

See   m0_simplest_example.py   for a simpler example.

This example adds to the  m0  example in that this example:
  -- Uses PyGame.
  -- Shows the following solution to the problem in which two objects
     need each other:
        a = Thing_1()
        b = Thing_2(a)
        a.set_b(b)
     See the accompanying video for details.

WATCH THE VIDEO associated with this example to:
  1. Learn how to RUN this example.
  2. Understand what this example DOES.
  3. Understand the CODE in this example.

But briefly:
  -- Each computer runs this program (but see the call to main below).
  -- Each computer's PyGame screen shows a black Player and a red Zombie.
       -- The user moves the Player by using the arrow keys.
       -- Doing so ALSO moves the ZOMBIE on the OTHER computer to match
          the PLAYER's movement on THIS computer (and vice versa).

Authors: David Mutchler, Dave Fisher, Sana Ebrahimi, Mohammad Noureddine
  and their colleagues at Rose-Hulman Institute of Technology,
  licensed under CC BY-NC-SA 4.0.  To view a copy of this license, visit
      https://creativecommons.org/licenses/by-nc-sa/4.0.
"""

import sys
import time

import pygame

import simpleMQTT as mq


# -----------------------------------------------------------------------------
# See Note 1 below.
# -----------------------------------------------------------------------------
class Controller:
    """ Receives and acts upon messages received from the other computer. """

    def __init__(self, zombie):
        self.zombie = zombie  # type: Player

    # noinspection PyUnusedLocal
    def act_on_message_received(self, message, sender_id):
        """
        Moves this Controller's "zombie" Player to the position
        that was sent by the other computer.
        Parameters:
          -- message: Must be a string that represents two non-negative
                      integers separated by one or more spaces, e.g. "100 38"
          -- sender_id: The number of the computer sending the message
                        (unused by this method)
          :type message:   str
          :type sender_id: int
        """
        x = float(message.split()[0])
        y = float(message.split()[1])
        self.zombie.move_to(x, y)


class Player:
    """ A Player in the game.  For now, simply a filled circle. """

    def __init__(self, screen, x, y, speed_x, speed_y, color):
        self.screen = screen
        self.x = x
        self.y = y
        self.speed_x = speed_x
        self.speed_y = speed_y
        self.color = pygame.Color(color)

        # ---------------------------------------------------------------------
        # See Note 2 below.
        # ---------------------------------------------------------------------
        # noinspection PyTypeChecker
        self.sender = None  # type: mq.Sender

    def set_sender(self, sender):
        self.sender = sender

    def draw(self):
        """ Draws this Player, for now as a filled circle of radius 10. """
        pygame.draw.circle(self.screen, self.color, (self.x, self.y), 10)

    def move_by_keys(self):
        """
        Moves this Player via the four arrow keys. Never called on the Zombie.
        """
        pressed_keys = pygame.key.get_pressed()
        if pressed_keys[pygame.K_UP]:
            self.y = self.y - self.speed_y
        if pressed_keys[pygame.K_DOWN]:
            self.y = self.y + self.speed_y
        if pressed_keys[pygame.K_LEFT]:
            self.x = self.x - self.speed_x
        if pressed_keys[pygame.K_RIGHT]:
            self.x = self.x + self.speed_x

        # ---------------------------------------------------------------------
        # See Note 3 below.
        #   The following statement tells the other computer to move
        #   its "zombie" to the place where this Player is.
        # ---------------------------------------------------------------------
        self.sender.send_message("{} {}".format(self.x, self.y))

    def move_to(self, x, y):
        """ Moves this Player to the given position. """
        self.x = x
        self.y = y


###############################################################################
# Run this program on two computers.
#   On one of them, run by calling:  main(1)
#   On the other,   run by calling:  main(2)
###############################################################################
def main(who_am_i):
    pygame.init()
    clock = pygame.time.Clock()
    pygame.display.set_caption("Computer {}".format(who_am_i))
    screen = pygame.display.set_mode((400, 300))

    # -------------------------------------------------------------------------
    # There are two instances of the Player class: a Player and a Zombie.
    # The Player moves per the arrow keys.  The Zombie moves per messages
    # sent from the other computer.
    #
    # The Player is black and starts:
    #   -- on the LEFT side of the PyGame screen if this is computer 1
    #   -- on the RIGHT side of the PyGame screen if this is computer 2
    # The Zombie is red and starts in the MIDDLE of the PyGame screen
    # -------------------------------------------------------------------------
    if who_am_i == 1:
        player_x = screen.get_width() * 0.25
    else:
        player_x = screen.get_width() * 0.75
    zombie_x = screen.get_width() * 0.5
    y = screen.get_height() * 0.5
    speed = 2  # Controls the effect of the arrow keys; irrelevant for Zombie
    player = Player(screen, player_x, y, speed, speed, "black")
    zombie = Player(screen, zombie_x, y, 0, 0, "red")

    # -------------------------------------------------------------------------
    # See Note 4 below.
    # -------------------------------------------------------------------------
    unique_id = "csse120-david-mutchler"
    sender = mq.Sender(who_am_i)
    controller = Controller(zombie)
    receiver = mq.Receiver(controller)
    mq.activate(unique_id, sender, receiver)

    # -------------------------------------------------------------------------
    # See Note 5 below.
    # -------------------------------------------------------------------------
    sender.verbosity = receiver.verbosity = 3  # Set to 0 for no debug messages

    # ---------------------------------------------------------------------
    # See Note 2 below.
    # ---------------------------------------------------------------------
    player.set_sender(sender)

    background = pygame.color.Color("grey")
    # -------------------------------------------------------------------------
    # See Note 6 below
    # -------------------------------------------------------------------------
    while True:
        screen.fill(background)
        clock.tick(60)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

        player.move_by_keys()
        player.draw()
        zombie.draw()

        pygame.display.update()


# -----------------------------------------------------------------------------
# See Note 7 below.
# -----------------------------------------------------------------------------
if __name__ == '__main__':
    try:
        main(2)  # On one computer, use 1.  On the other computer, use 2.
    except Exception:
        mq.print_in_red("ERROR - While running this program,")
        mq.print_in_red("your code raised the following exception:")
        print()
        time.sleep(1)
        raise

###############################################################################
# NOTES mentioned in the simpleMQTT code above.
#
# Note 1:
#    You MUST have some object to receive messages.
#    That object MUST be an instance of a class that has a method named
#        act_on_message_received
#    The method MUST be spelled exactly as shown above.
#    The method MUST have exactly 3 parameters: self, message, sender_id.
#
#    In this example, the class for that object is called Controller.
#    You can call it whatever you want.
#
#    The   act_on_message_received   method can do whatever you want it to do.
#    In this example:
#      -- The Controller stores the "Zombie"
#           when the Controller is constructed (see its  __init__)
#      -- When a message arrives (via the  act_on_message_received  method,
#           the Controller decodes the message into x and y coordinates
#           and sets the Zombie's position to that (x, y).
#   The "decoding" the STRING message into NUMBERS x and y is done by:
#      1. Apply the  split  method to the message to SPLIT the STRING
#         at white space (here, space characters) into a LIST of STRINGS.
#           Example:   "100 38".split()  =   ["100", "38"]
#      2. Access the items at indices 0 (for x) and 1 (for y).
#           Examples:  "100 38".split()[0]  =>  ["100", "38"][0]  =>  "100"
#                      "100 38".split()[1]  =>  ["100", "38"][1]  =>  "38"
#      3. Convert from a string to a FLOAT.  Example:
#            float("100 38".split()[0]) => ["100", "38"][0] => "100" => 100.0
#         (This program stores coordinates as floats and sends them that way.)
#
#
# Note 2:
#    The  Player  object needs a Sender to send the Player's position
#    to the other computer, for the OTHER computer to use as ITS Zombie.
#
#    It so happens that the Player is constructed in this example BEFORE
#    the Sender is constructed.  (It did not HAVE to be that way, but in
#    some other example it might need to be.)  So it is IMPOSSIBLE to
#    have the Sender be an argument to the Player's __init__.
#
#    Instead, the Player stores   self.sender   as None  TEMPORARILY.
#    Soon after the Player is constructed, the Sender is constructed,
#    and at THAT time the Player applies its  set_sender  method
#    to store the REAL Sender in self.sender.
#    This happens before any messages need to be sent, so all is well.
#
# Note 3:
#    This shows why the Player must have the Sender as an instance variable.
#
# Note 4:
#    All this is the same as the  m0  example EXCEPT that here the Controller
#    takes an argument that is an OBJECT (the Player that is the Zombie)
#    that the Controller asks to move when the Controller acts on a message.
#
#    [The rest of this note is repeated from  m0  example.]
#    You MUST have a unique_id. It distinguishes YOUR simpleMQTT-enabled
#    program from OTHER simpleMQTT-enabled programs that may be running
#    at the same time as yours and with the same Broker as yours.
#    Choose a string that is unique to you,
#    e.g. something involving your name and/or your program's name.
#
#    You MUST construct a Sender.
#    Its first argument MUST be a positive integer that denotes the
#    "computer number" for the computer on which you are running this program.
#    Computers are numbered 1, 2, 3, ... in simpleMQTT.
#
#    If you want to receive messages, you MUST construct an object like the
#    Controller object described in Note 1, and you MUST construct a Receiver
#    whose first argument is that Controller object.
#
#   You MUST call the   mq.activate  function to enable communication.
#
# Note 5:  [same as the corresponding note in the  m0  example]
#    Printing the messages sent and received is invaluable for debugging.
#    By default, Senders and Receivers print all messages.
#      FYI: Setting  verbosity  to  0  disables printing of debugging messages.
#      ADVICE: Do NOT set verbosity to 0 until your program WORKS CORRECTLY.
#
# Note 6:
#    The code enters the "game loop" here.  In this example, the program
#    repeatedly moves the Player per the arrow keys and draws both Player
#    objects (the arrow-key-driven Player and the Zombie).
#    Meanwhile, the Receiver is listening in the background, sending each
#    message received to the Controller, which changes the position of
#    the Zombie, hence "moving" it the next time the Zombie is drawn.
#
#    In this simple example, no attempt is made to "untangle" the printing.
#
# Note 7:  [same as the corresponding note in the  m0  example]
#   Run the program on two computers, or twice on your computer (see the video).
#     On one of the computers, run by calling:  main(1)
#     On the other computer,   run by calling:  main(2)
#   The argument is the computer's "number".
#   It helps prevent a computer running this program from talking to itself.
#
#   The  if __name__ == '__main__'  makes external programs able to run main.
#   The  try .. except  helps avoid intermingling ordinary and error output.
###############################################################################