"""
This module lets you practice
  -- IMPLEMENTING CLASSES and 
  -- the ITERATE-THROUGH-A-SEQUENCE pattern.

Authors: David Mutchler, Sana Ebrahimi, Mohammed Noureddine, Vibha Alangar,
         Matt Boutell, Dave Fisher, Mark Hays, their colleagues, and
         PUT_YOUR_NAME_HERE.
"""  # TODO: 1. PUT YOUR NAME IN THE ABOVE LINE.

import math
import time
from typing import List

import testing_helper


# -----------------------------------------------------------------------------
# TODO: 2. Watch the VIDEO for this module listed in the Follow-Me section
#  of the Preparation for this session.  It introduces the ConceptMap class
#  that you will implement in this module.
#    After you have watched that video, mark this _TODO_ as DONE.
# -----------------------------------------------------------------------------

###############################################################################
# The Point and Circle classes are already implemented.
###############################################################################
class Point:
    """ A Point in 2-space, with x and y-coordinates. """

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def clone(self):
        """
        Returns a new Point with the same x and y-coordinates as this Point. """
        return Point(self.x, self.y)

    def distance_from(self, another_point):
        """
        Returns the distance this Point is from the given Point.
          :type another_point: Point
          :rtype float
        """
        delta_x = self.x - another_point.x
        delta_y = self.y - another_point.y
        return math.sqrt((delta_x * delta_x) + (delta_y * delta_y))


class Circle:
    """ A circle with center and radius. """

    def __init__(self, center, radius):
        """
        Stores the given radius and a clone of the given center.
        Type hints:
          :type center: Point
          :type radius: float
        """
        self.center = center.clone()
        self.radius = radius


###############################################################################
# You implement the ConceptMap class.
###############################################################################
class ConceptMap:
    """
    This is the beginning of a class that might eventually model Concept Maps.
    Currently it contains only a sequence of Circle objects
    (that might eventually be augmented to model Concepts).
      ** This is an ARTIFICIAL example that exists solely to give you practice
      ** at implementing classes AND iterating through sequences.
    """

    def __init__(self, circles):
        """
        Stores the given sequence of Circle objects.
          :type circles: List[Circle]
        """
        self.circles = circles  # type: List[Circle]

    def sum_of_x_coordinates(self):
        """
        Examines each of the Circle objects that this ConceptMap has.
        Returns the sum of the x-coordinates of the centers of those Circles.
        For example:
            circles1 = [Circle(Point(11, 12), 10),
                        Circle(Point(14, 13), 8),
                        Circle(Point(10, 20), 5),
                        Circle(Point(30, 15), 12),
                        Circle(Point(50, 35), 33)]
            concept_map1 = ConceptMap(circles1)

            concept_map1.sum_of_x_coordinates()
               returns  11 + 14 + 10 + 30 + 50, which is 115.
        """
        # ---------------------------------------------------------------------
        # TODO: 3. Follow along with the video to implement and test this
        #   method.  Tests have been written for you (below) and include
        #   the example in the above doc-string.
        # ---------------------------------------------------------------------

    def number_of_small_circles(self, threshold):
        """
        Examines each of the Circle objects that this ConceptMap has.
        Returns the number of those Circle objects
        whose radius is less than the given threshold.
        For example:
            circles1 = [Circle(Point(11, 12), 10),
                        Circle(Point(14, 15), 8),
                        Circle(Point(10, 20), 5),
                        Circle(Point(30, 15), 12),
                        Circle(Point(50, 35), 33)]
            concept_map1 = ConceptMap(circles1)

            concept_map1.number_of_small_circles(15)  returns  4
            concept_map1.number_of_small_circles(10)  returns  2
            concept_map1.number_of_small_circles(80)  returns  5
            concept_map1.number_of_small_circles(4)   returns  0
        """
        # ---------------------------------------------------------------------
        # TODO: 4. Implement and test this method.  Tests have been written
        #   for you (below) and include the example in the above doc-string.
        # ---------------------------------------------------------------------

    def product_of_sums_of_x_and_y_coordinates(self):
        """
        Examines each of the Circle objects that this ConceptMap has.
        Computes, for each of those Circle objects,
        the sum of the x and y-coordinates of its center.
        Returns the product of those sums.
        For example:
            circles1 = [Circle(Point(11, 12), 10),
                        Circle(Point(14, 13), 8),
                        Circle(Point(10, 20), 5),
                        Circle(Point(30, 15), 12),
                        Circle(Point(50, 35), 33)]
            concept_map1 = ConceptMap(circles1)

            concept_map1.product_of_sums_of_x_and_y_coordinates()
               returns
                 (11 + 12) * (14 + 13) * (10 + 20) * (30 + 15) * (50 + 35),
               that is,
                    23 * 27 * 30 * 45 * 85,
               which is 71259750.
        """
        # ---------------------------------------------------------------------
        # TODO: 5. Implement and test this method.  Tests have been written
        #   for you (below) and include the example in the above doc-string.
        # ---------------------------------------------------------------------

    def sum_of_some_indices(self):
        """
        Examines each of the Circle objects that this ConceptMap has.
        Returns the sum of the INDICES of those Circle objects
        whose center has x-coordinate bigger than its y-coordinate.
        For example:
            circles1 = [Circle(Point(11, 12), 10),
                        Circle(Point(14, 13), 8),
                        Circle(Point(10, 20), 5),
                        Circle(Point(30, 15), 12),
                        Circle(Point(50, 35), 33)]
            concept_map1 = ConceptMap(circles1)

            concept_map1.sum_of_some_indices()
               returns  1 + 3 + 4,   which is 8
        """
        # ---------------------------------------------------------------------
        # TODO: 3. Follow along with the video to implement and test this
        #   method.  Tests have been written for you (below) and include
        #   the example in the above doc-string.
        # ---------------------------------------------------------------------

    def product_of_some_radii(self):
        """
        Examines the Circle objects that this ConceptMap has,
        but looking only at those at odd-numbered indices (1, 3, 5, ...)
        Returns the product of the radii of those Circles.
        For example:
            circles1 = [Circle(Point(11, 12), 10),
                        Circle(Point(14, 13), 8),
                        Circle(Point(10, 20), 5),
                        Circle(Point(30, 15), 12),
                        Circle(Point(50, 35), 33)]
            concept_map1 = ConceptMap(circles1)

            concept_map1.product_of_some_radii()
               returns  8 * 12, which is 96
        """
        # ---------------------------------------------------------------------
        # TODO: 6. Implement and test this method.  Tests have been written
        #   for you (below) and include the example in the above doc-string.
        # ---------------------------------------------------------------------

    def sum_of_distances(self, point):
        """
        Examines the Circle objects that this ConceptMap has.
        For each such Circle object, examines its center and computes
        the distance that that center is from the given point
            [USE THE Point distance_from METHOD TO DO SO!].
        Adds up all those distances, and returns the resulting sum.
        For example:
            circles1 = [Circle(Point(11, 12), 10),
                        Circle(Point(14, 13), 8),
                        Circle(Point(10, 20), 5),
                        Circle(Point(30, 15), 12),
                        Circle(Point(50, 35), 33)]
            concept_map1 = ConceptMap(circles1)

            concept_map1.sum_of_distances(Point(14, 12))
               returns
              [    distance from (11, 12) to (14, 12)
                +  distance from (14, 13) to (14, 12)
                +  distance from (10, 20) to (14, 12)
                +  distance from (30, 15) to (14, 12)
                +  distance from (50, 35) to (14, 12)]
               which is (approximately)
              [ 3  +  1  +  8.94  +  16.28  +  42.72], which is about 71.943111.
        """
        # ---------------------------------------------------------------------
        # TODO: 6. Implement and test this method.  Tests have been written
        #   for you (below) and include the example in the above doc-string.
        # ---------------------------------------------------------------------
        total = 0
        for k in range(len(self.circles)):
            print(self.circles[k].center.distance_from(point))
            total = total + self.circles[k].center.distance_from(point)
        return total


###############################################################################
# Code for TESTING is below here.
###############################################################################
def main():
    """
    Tests the methods of the ConceptMap class defined above.
    Each test uses the same two ConceptMap examples as test cases.
    """
    circles1 = [Circle(Point(11, 12), 10),
                Circle(Point(14, 13), 8),
                Circle(Point(10, 20), 5),
                Circle(Point(30, 15), 12),
                Circle(Point(50, 35), 33)]
    concept_map1 = ConceptMap(circles1)

    circles2 = [Circle(Point(30, 20), 10),
                Circle(Point(100, 200), 40),
                Circle(Point(22, 10), 5)]
    concept_map2 = ConceptMap(circles2)

    print()
    print("The TESTS all use the same two ConceptMap examples as test cases:")
    testing_helper.print_colored("""
    circles1 = [Circle(Point(11, 12), 10),
                Circle(Point(14, 13), 8),
                Circle(Point(10, 20), 5),
                Circle(Point(30, 15), 12),
                Circle(Point(50, 35), 33)]
    concept_map1 = ConceptMap(circles1)

    circles2 = [Circle(Point(30, 20), 10),
                Circle(Point(100, 200), 40),
                Circle(Point(22, 10), 5)]
    concept_map2 = ConceptMap(circles2)
    """, color="blue")

    run_test_sum_of_x_coordinates(concept_map1, concept_map2)
    run_test_number_of_small_circles(concept_map1, concept_map2)
    run_test_product_of_sums_of_x_and_y_coordinates(concept_map1, concept_map2)
    run_test_sum_of_some_indices(concept_map1, concept_map2)
    run_test_product_of_some_radii(concept_map1, concept_map2)
    run_test_sum_of_distances(concept_map1, concept_map2)


def run_test_sum_of_x_coordinates(concept_map1, concept_map2):
    """
    What comes in: two ConceptMap objects.
    Side effects:  Tests the   sum_of_x_coordinates   method twice:
      once using the given concept_map1 and then using the given concept_map2.
    """
    print()
    print("--------------------------------------------------")
    print("Testing the  sum_of_x_coordinates   method:")
    print("--------------------------------------------------")

    # Test using concept_map1:
    expected = 115
    actual = concept_map1.sum_of_x_coordinates()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")

    # Test using concept_map2:
    expected = 152
    actual = concept_map2.sum_of_x_coordinates()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")


def run_test_number_of_small_circles(concept_map1, concept_map2):
    """
    What comes in: two ConceptMap objects.
    Side effects:  Tests the   number_of_small_circles   method twice:
      once using the given concept_map1 and then using the given concept_map2.
    """
    print()
    print("--------------------------------------------------")
    print("Testing the  number_of_small_circles   method:")
    print("--------------------------------------------------")

    # Test using concept_map1:
    expected = 4
    actual = concept_map1.number_of_small_circles(15)
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")

    # Test using concept_map2:
    expected = 0
    actual = concept_map2.number_of_small_circles(5)
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")


def run_test_product_of_sums_of_x_and_y_coordinates(concept_map1, concept_map2):
    """
    What comes in: two ConceptMap objects.
    Side effects:  Tests the   product_of_sums_of_x_and_y_coordinates   method
      twice: once using the given concept_map1
      and then again using the given concept_map2.
    """
    print()
    print("--------------------------------------------------")
    print("Testing the method:")
    print("   product_of_sums_of_x_and_y_coordinates")
    print("--------------------------------------------------")

    # Test using concept_map1:
    expected = 71259750
    actual = concept_map1.product_of_sums_of_x_and_y_coordinates()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")

    # Test using concept_map2:
    expected = 480000
    actual = concept_map2.product_of_sums_of_x_and_y_coordinates()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")


def run_test_sum_of_some_indices(concept_map1, concept_map2):
    """
    What comes in: two ConceptMap objects.
    Side effects:  Tests the   sum_of_some_indices   method twice:
      once using the given concept_map1 and then using the given concept_map2.
    """
    print()
    print("--------------------------------------------------")
    print("Testing the  sum_of_some_indices   method:")
    print("--------------------------------------------------")

    # Test using concept_map1:
    expected = 8
    actual = concept_map1.sum_of_some_indices()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")

    # Test using concept_map2:
    expected = 2
    actual = concept_map2.sum_of_some_indices()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")


def run_test_product_of_some_radii(concept_map1, concept_map2):
    """
    What comes in: two ConceptMap objects.
    Side effects:  Tests the   product_of_some_radii   method twice:
      once using the given concept_map1 and then using the given concept_map2.
    """
    print()
    print("--------------------------------------------------")
    print("Testing the  product_of_some_radii   method:")
    print("--------------------------------------------------")

    # Test using concept_map1:
    expected = 96
    actual = concept_map1.product_of_some_radii()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")

    # Test using concept_map2:
    expected = 40
    actual = concept_map2.product_of_some_radii()
    print("Expected:", expected)
    print("Actual:  ", actual)
    if actual != expected:
        testing_helper.print_colored("FAILED the above test", color="red")


def run_test_sum_of_distances(concept_map1, concept_map2):
    """
    What comes in: two ConceptMap objects.
    Side effects:  Tests the   sum_of_distances   method twice:
      once using the given concept_map1 and then using the given concept_map2.
    """
    print()
    print("--------------------------------------------------")
    print("Testing the  sum_of_distances   method:")
    print("--------------------------------------------------")

    # Test using concept_map1:
    expected = 71.943111
    actual = concept_map1.sum_of_distances(Point(14, 12))
    print("Expected:", expected, "(approximately)")
    print("Actual:  ", actual)
    if actual is None or abs(actual - expected) > 0.00001:
        testing_helper.print_colored("FAILED the above test", color="red")

    # Test using concept_map2:
    expected = 398.519518
    actual = concept_map2.sum_of_distances(Point(100, 200))
    print("Expected:", expected, "(approximately)")
    print("Actual:  ", actual)
    if actual is None or abs(actual - expected) > 0.00001:
        testing_helper.print_colored("FAILED the above test", color="red")


# -----------------------------------------------------------------------------
# Calls  main  to start the ball rolling.
# The   try .. except   prevents error messages on the console from being
# intermingled with ordinary output to the console.
# -----------------------------------------------------------------------------
try:
    main()
except Exception:
    testing_helper.print_colored("ERROR - While running this test,",
                                 color="red")
    testing_helper.print_colored("your code raised the following exception:",
                                 color="red")
    print()
    time.sleep(1)
    raise