"""
Capstone Team Project.  Code to run on a ROBOT (NOT a laptop).

This code defines the   DriveSystem   class  that is used to make a robot move.

Authors:  Your professors (for the framework)
    and PUT_YOUR_NAME_HERE.
Winter term, 2019-2020.
"""
# TODO: 1. In the above, put the names of EACH team member who contributes
#  (in any way) to this module.

# -----------------------------------------------------------------------------
# TODO: 2. Note below how to write an IMPORT statement
#  that imports a module that is in the  LIBS  sub-folder.
#  This module uses code that is in the "low-level" api in rosebot_ev3dev_api.
#  Change this _TODO_ to DONE after you have seen how to do it.
# -----------------------------------------------------------------------------
import libs.rosebot_ev3dev_api as ev3dev
import time


###############################################################################
#    DriveSystem
###############################################################################
class DriveSystem(object):
    """
    Controls the robot's motion via methods that include:
      go                         stop
      go_straight_for_seconds    go_straight_for_inches
      spin_in_place_for_seconds  spin_in_place_for_degrees
      turn_for_seconds           turn_for_degrees
    """
    # -------------------------------------------------------------------------
    # TODO: 3. Read and digest the following NOTE:
    #   To "go straight" means that both wheels move at the same speed.
    #     -- Positive speeds should make the robot move forward.
    #     -- Negative speeds should make the robot move backward.
    #   To "spin_in_place" means that the wheels move at speeds X and -X.
    #     -- Positive speeds should make the robot spin clockwise
    #          (i.e., left motor goes at speed X, right motor at speed -X).
    #     -- Negative speeds should make the robot spin counter-clockwise
    #          (i.e., left motor goes at speed -X, right motor at speed X).
    #   To "turn" means that one wheel does NOT move and the other DOES move:
    #     -- Positive speeds should make only the left motor move
    #          (and hence the turn is clockwise).
    #     -- Negative speeds should make only the right motor move
    #          (and hence the turn is counter-clockwise).
    #   All distances (inches or degrees) should be POSITIVE numbers.
    #   The RoseBot's "wheels" have diameter about 1.3 inches.
    #  _
    #  Once you understand the above, change this _TODO_ to DONE.
    # -------------------------------------------------------------------------
    def __init__(self, left_motor_port="B", right_motor_port="C"):
        """
        Constructs two Motor objects (for the left and right wheels), plugged
        into the given ports. Each Motor object must be a "large" motor.

        Stores those Motor objects as:
           self.left_motor
           self.right_motor

        :type  left_motor_port:  str
        :type  right_motor_port: str
          Each port must be "A", "B", "C", or "D" (defaults to "B" and "C").
        """
        # ---------------------------------------------------------------------
        # TODO: 4. Read the following, ASKING QUESTIONS AS NEEDED.
        #  Once you understand the code, change this _TODO_ to DONE.
        # ---------------------------------------------------------------------
        self.left_motor = ev3dev.Motor(left_motor_port, motor_type="large")
        self.right_motor = ev3dev.Motor(right_motor_port, motor_type="large")

    def go(self, left_wheel_speed, right_wheel_speed):
        """
        Makes the left and right wheel motors spin at the given speeds.
          (More accurately, at the given duty-cycle, which is a percent of
          the maximum possible speed given the current battery level.)

        Speeds are expected to be integers between -100 and 100,
          where positive means forward, negative means backward, and zero (0)
          means to coast to a stop (also see the  stop  method below).

        :type  left_wheel_speed:  int
        :type  right_wheel_speed: int
        """
        # ---------------------------------------------------------------------
        # TODO: 5. Implement this method, using code like this:
        #      self.left_motor.YOU_FIGURE_OUT_WHAT_GOES_HERE
        #      self.right_motor.YOU_FIGURE_OUT_WHAT_GOES_HERE
        #  The "dot trick" should make it clear what to put in the YOU_FIGURE...
        # ---------------------------------------------------------------------
        self.left_motor.turn_on(left_wheel_speed)
        self.right_motor.turn_on(right_wheel_speed)

    def stop(self, stop_action="brake"):
        """
        Stops the left and right wheel motors.
          By default uses applies a "brake" in stopping, as opposed to
          setting a speed of 0 which allows the motor to "coast" to a stop.

        :type  stop_action: str
        """
        # ---------------------------------------------------------------------
        # TODO: 6. Implement this method, using code like this:
        #      self.left_motor.YOU_FIGURE_OUT_WHAT_GOES_HERE
        #      self.right_motor.YOU_FIGURE_OUT_WHAT_GOES_HERE
        #  The "dot trick" should make it clear what to put in the YOU_FIGURE...
        # ---------------------------------------------------------------------
        self.left_motor.turn_off()
        self.right_motor.turn_off()

    def go_straight_for_seconds(self, seconds, speed=50, stop_action="brake"):
        """
        Makes the robot go straight for the given number of seconds
        at the given speed, stopping using the given stop_action.

        Speeds are expected to be integers between -100 and 100,
          where positive means forward and negative means backward.

        Prints an error message (and goes nowhere) if seconds <= 0
          or speed == 0.

        :type  seconds: float | int
        :type  speed:   int
        :type  stop_action: str
        """
        # Error handling:
        if has_bad_arguments("go_straight_for_seconds", seconds, speed):
            return -1

        # ---------------------------------------------------------------------
        # TODO: 7. Implement this method, using three lines of code like this:
        #    1. Start both wheel-motors moving at the specified speed
        #         (using the   go   method).
        #    2. "Sleep" (do nothing else) while the robot is moving,
        #         for the specified number of seconds,
        #         using the   time.sleep   function.
        #    3.  Stop the wheel-motors (using the  stop   method).
        # ---------------------------------------------------------------------
        self.go(speed, speed)
        time.sleep(seconds)
        self.stop()

    def go_straight_for_inches(self, inches, speed=50, stop_action="brake"):
        """
        Makes the robot go straight at the given speed for the given number
        of inches.  Uses the encoder (degrees_traveled sensor, "position")
        built into the motors to tell when to stop moving.

        Speeds are expected to be integers between -100 and 100,
          where positive means forward and negative means backward.

        Prints an error message (and goes nowhere) if inches <= 0
          or speed == 0.

        :type  inches: float | int
        :type  speed:   int
        :type  stop_action: str
        """
        # Error handling:
        if has_bad_arguments("go_straight_for_inches", inches, speed):
            return -1

        # ---------------------------------------------------------------------
        # TODO: 8. Implement this method, using the following algorithm:
        #   1. Compute the number of DEGREES that the motors should SPIN
        #        in order to go the given number of INCHES.
        #      Find this value by trial-and-error, starting with a conversion
        #        value of 1 inch = 80 degrees to spin.
        #   2. Set a variable to the current value of the "encoder" of a wheel.
        #        Either wheel is fine, using code that includes
        #        something like this:  self.left_motor.get_position()
        #   3. Start both wheel-motors moving at the specified speed
        #         (using the   go   method).
        #   4. Repeatedly:
        #        a. Set a variable to the current value of the "encoder"
        #              of the wheel you used in Step 2.
        #        b. Compute the absolute value of the difference between
        #             the motor's position before you started the robot moving
        #             and the motor's current position.
        #           If that computed value is greater than or equal to
        #             the computed number of degrees that the motor should spin,
        #             STOP the motors and break out of the loop.
        #        c. Sleep 0.05 seconds in order to avoid "flooding" the
        #             hardware/software of the encoder.
        # ---------------------------------------------------------------------

    def spin_in_place_for_seconds(self, seconds, speed=50, stop_action="brake"):
        """
        Makes the robot spin in place for the given number of seconds
        at the given speed, stopping using the given stop_action.

        Spinning in place means that the left motor spins at the given speed
        and the right motor spins at the negative of the given speed.
        Hence a positive speed causes the robot to spin clockwise and
        a negative speed causes the robot to spin counter-clockwise.

        Speeds are expected to be integers between -100 and 100.

        Prints an error message (and goes nowhere) if seconds <= 0
          or speed == 0.

        :type  seconds: float | int
        :type  speed:   int
        :type  stop_action: str
        """
        # Error handling:
        if has_bad_arguments("spin_in_place_for_seconds", seconds, speed):
            return -1

        # ---------------------------------------------------------------------
        # TODO: 9. Implement this method, using three lines of code like this:
        #    1. Start both wheel-motors moving:
        #         the left_motor at the given speed, and
        #         the right_motor at the negative of the given speed
        #           (using the   go   method).
        #    2. "Sleep" (do nothing else) while the robot is moving,
        #         for the specified number of seconds,
        #         using the   time.sleep   function.
        #    3.  Stop the wheel-motors (using the  stop   method).
        # ---------------------------------------------------------------------
        self.go(speed, -speed)
        time.sleep(seconds)
        seconds.stop()

    def spin_in_place_for_degrees(self, degrees, speed=50, stop_action="brake"):
        """
        Makes the robot spin in place for the given number of degrees
        at the given speed, stopping using the given stop_action.

        Note that DEGREES is the number of degrees that the ROBOT should spin.

        Uses the encoder (degrees_traveled sensor, "position")
        built into the motors to tell when to stop moving.

        Spinning in place means that the left motor spins at the given speed
        and the right motor spins at the negative of the given speed.
        Hence a positive speed causes the robot to spin clockwise and
        a negative speed causes the robot to spin counter-clockwise.

        Speeds are expected to be integers between -100 and 100.

        Prints an error message (and goes nowhere) if degrees <= 0
          or speed == 0.

        :type  degrees: float | int
        :type  speed:   int
        :type  stop_action: str
        """
        # Error handling:
        if has_bad_arguments("spin_in_place_for_degrees", degrees, speed):
            return -1

        # ---------------------------------------------------------------------
        # TODO: 10. Implement this method, using the following algorithm:
        #   1. Compute the number of DEGREES that the MOTORS should SPIN
        #        in order for the ROBOT to spin the given number of DEGREES.
        #      Find this value by trial-and-error, starting with a conversion
        #        value of 1 robot degree = 6 degrees for the motor to spin.
        #   2. Set a variable to the current value of the "encoder" of a wheel.
        #        Either wheel is fine, using code that includes
        #        something like this:  self.left_motor.get_position()
        #   3. Start the left motor spinning at the given speed and the right
        #        motor spinning at the negative of the given speed
        #         (using the   go   method).
        #   4. Repeatedly:
        #        a. Set a variable to the current value of the "encoder"
        #              of the wheel you used in Step 2.
        #        b. Compute the absolute value of the difference between
        #             the motor's position before you started the robot moving
        #             and the motor's current position.
        #           If that computed value is greater than or equal to
        #             the computed number of degrees that the MOTOR should spin,
        #             STOP the motors and break out of the loop.
        #        c. Sleep 0.05 seconds in order to avoid "flooding" the
        #             hardware/software of the encoder.
        # ---------------------------------------------------------------------

    def turn_for_seconds(self, seconds, speed=50, stop_action="brake"):
        """
        Makes the robot turn for the given number of seconds
        at the given speed, stopping using the given stop_action.

        If the speed is positive, then turning means that the left motor
        spins at the given speed and the right motor does NOT spin.
        Hence the robot turns to the RIGHT (clockwise) in this case.

        If the speed is negative, then turning means that the left motor does
        NOT spin and the right motor spins at the negative of the given speed.
        Hence the robot turns to the LEFT (counter-clockwise) in this case.

        Speeds are expected to be integers between -100 and 100.

        Prints an error message (and goes nowhere) if seconds <= 0
          or speed == 0.

        :type  seconds: float | int
        :type  speed:   int
        :type  stop_action: str
        """
        # Error handling:
        if has_bad_arguments("turn_for_seconds", seconds, speed):
            return -1

        # ---------------------------------------------------------------------
        # TODO: 11. Implement this method, using three lines of code like this:
        #    1. Start both wheel-motors moving:
        #         If the speed is positive, the left motor spins at the given
        #           speed and the right motor spins at speed 0.
        #         If the speed is negative, the left motor spins at speed 0 and
        #           the right motor spins at the negative of the given speed.
        #         In both cases, use the   go   method.
        #    2. "Sleep" (do nothing else) while the robot is moving,
        #         for the specified number of seconds,
        #         using the   time.sleep   function.
        #    3.  Stop the wheel-motors (using the  stop   method).
        # ---------------------------------------------------------------------
        if speed > 0:
            self.go(speed, 0)
        else:
            self.go(0, speed)
        time.sleep(seconds)
        self.stop()

    def turn_for_degrees(self, degrees, speed=50, stop_action="brake"):
        """
        Makes the robot turn for the given number of degrees
        at the given speed, stopping using the given stop_action.

        Note that DEGREES is the number of degrees that the ROBOT should turn.

        Uses the encoder (degrees_traveled sensor, "position")
        built into the motors to tell when to stop moving.

        If the speed is positive, then turning means that the left motor
        spins at the given speed and the right motor does NOT spin.
        Hence the robot turns to the RIGHT (clockwise) in this case.

        If the speed is negative, then turning means that the left motor does
        NOT spin and the right motor spins at the negative of the given speed.
        Hence the robot turns to the LEFT (counter-clockwise) in this case.

        Speeds are expected to be integers between -100 and 100.

        Prints an error message (and goes nowhere) if degrees <= 0
          or speed == 0.

        :type  degrees: float | int
        :type  speed:   int
        :type  stop_action: str
        """
        # Error handling:
        if has_bad_arguments("turn_for_degrees", degrees, speed):
            return -1

        # ---------------------------------------------------------------------
        # TODO: 12. Implement this method, using the following algorithm:
        #   1. Compute the number of DEGREES that the relevant MOTOR should SPIN
        #        in order for the ROBOT to turn the given number of DEGREES.
        #      Find this value by trial-and-error, starting with a conversion
        #        value of 1 robot degree = 6 degrees for the motor to spin.
        #   2. Set a variable to the current value of the "encoder" of
        #        the relevant wheel.
        #   3. Start both wheel-motors moving:
        #         If the speed is positive, the left motor spins at the given
        #           speed and the right motor spins at speed 0.
        #         If the speed is negative, the left motor spins at speed 0 and
        #           the right motor spins at the negative of the given speed.
        #         In both cases, use the   go   method.
        #   4. Repeatedly:
        #        a. Set a variable to the current value of the "encoder"
        #              of the wheel you used in Step 2.
        #        b. Compute the absolute value of the difference between
        #             the motor's position before you started the robot moving
        #             and the motor's current position.
        #           If that computed value is greater than or equal to
        #             the computed number of degrees that the MOTOR should spin,
        #             STOP the motors and break out of the loop.
        #        c. Sleep 0.05 seconds in order to avoid "flooding" the
        #             hardware/software of the encoder.
        # ---------------------------------------------------------------------


def has_bad_arguments(method_name, seconds, speed):
    # Error handling:
    if seconds < 0:
        message = ("ERROR: in calling \"" + method_name + "\",\n"
                   + "the first argument is the seconds to move.\n"
                   + "It must be a POSITIVE number.\n"
                   + "The actual value for the first argument was:\n"
                   + "   " + str(seconds)
                   + "No movement done!")
        print(message)
        return True

    if speed == 0:
        message = ("ERROR: in calling \"" + method_name + "\",\n"
                   + "the second argument is the speed at which to move.\n"
                   + "It must be a NON-ZERO number, but in fact was 0.\n"
                   + "No movement done!")
        print(message)
        return True

    return False