# problem3solution.py
#
# ICS H32 Fall 2025
# Exercise Set 4
# INSTRUCTOR SOLUTION
#
# This is a partial solution to Problem 3, in which I've only created
# one kind of shape.  The others are shades of the same colors you see
# in the one that I've implemented.

import math


# The 'shape' interface consists of the following methods:
#
# def centroid(self) -> tuple[float, float]
#     Returns a two-element tuple of floats, representing an x- and
#     y-coordinate where the shape's centroid is.
#
# def move(self, new_centroid: tuple[float, float]) -> None:
#     Given a new centroid, moves the shape so that its centroid is
#     the provided one.
#
# def area(self) -> float:
#     Returns the area of the shape (i.e., the total amount of space
#     within the shape).
#
# def perimeter(self) -> float:
#     Returns the perimeter of the shape (i.e., the distance around
#     the edges of the shape).
#
# def contains(self, point: tuple[float, float]) -> bool:
#     Returns whether a point is contained within the shape.



def distance_between(point1: tuple[float, float], point2: tuple[float, float]) -> float:
    p1x, p1y = point1
    p2x, p2y = point2
    
    return math.sqrt((p1x - p2x) * (p1x - p2x) + (p1y - p2y) * (p1y - p2y))


assert distance_between((0, 0), (1, 1)) == math.sqrt(2)
assert distance_between((1, 1), (0, 0)) == math.sqrt(2)
assert distance_between((-1, -2), (2, 2)) == 5.0



class Circle:
    def __init__(self, centroid: tuple[float, float], radius: float):
        self._centroid = centroid
        self._radius = radius


    def centroid(self) -> tuple[float, float]:
        return self._centroid


    def radius(self) -> float:
        return self._radius


    def move(self, new_centroid: tuple[float, float]) -> None:
        self._centroid = new_centroid


    def area(self) -> float:
        return self._radius * self._radius * math.pi


    def perimeter(self) -> float:
        return 2 * math.pi * self._radius


    def contains(self, point: tuple[float, float]) -> bool:
        return distance_between(self._centroid, point) <= self._radius


assert Circle((2.0, 3.0), 4.0).centroid() == (2.0, 3.0)
assert Circle((4.0, 6.0), 2.5).radius() == 2.5

def test_moving_circle_changes_centroid() -> None:
    c = Circle((2.0, 3.0), 4.0)
    c.move((5.0, 7.0))
    assert c.centroid() == (5.0, 7.0)

test_moving_circle_changes_centroid()

# The problem with floats is that they aren't precisely real numbers,
# but are approximations of them.  That means when we compare them after
# performing calculations on them, we need to take into account that there
# is imprecision in those calculations.  What I'm using here is called
# math.isclose(), which is a function in Python's math library for comparing
# two floats to, in this case, within 0.001%.
assert math.isclose(Circle((2.0, 3.0), 4.0).area(), 50.2654832, rel_tol = 0.00001)
assert math.isclose(Circle((0.0, 0.0), 4.0).area(), 50.2654832, rel_tol = 0.00001)
assert math.isclose(Circle((1.0, -1.0), 2.5).area(), 19.6349544, rel_tol = 0.00001)

assert math.isclose(Circle((2.0, 3.0), 4.0).perimeter(), 25.1327416, rel_tol = 0.00001)
assert math.isclose(Circle((0.0, 0.0), 4.0).perimeter(), 25.1327416, rel_tol = 0.00001)
assert math.isclose(Circle((1.0, -1.0), 2.5).perimeter(), 15.7079635, rel_tol = 0.00001)

assert Circle((2.0, 3.0), 4.0).contains((2.0, 3.0))
assert Circle((2.0, 3.0), 4.0).contains((-2.0, 3.0))
assert Circle((2.0, 3.0), 4.0).contains((6.0, 3.0))
assert Circle((2.0, 3.0), 4.0).contains((2.0, -1.0))
assert Circle((2.0, 3.0), 4.0).contains((2.0, 7.0))
assert not Circle((1.0, 2.0), 3.0).contains((5.0, 4.0))
assert not Circle((1.0, 2.0), 3.0).contains((2.0, 6.0))
assert not Circle((1.0, 2.0), 3.0).contains((4.0, 5.0))

def test_moving_affects_whether_contains_point() -> None:
    c = Circle((0.0, 0.0), 4.0)
    assert c.contains((0.0, 0.0))
    c.move((5.0, 5.0))
    assert not c.contains((0.0, 0.0))

test_moving_affects_whether_contains_point()



def area_sum(shapes: list['Shape']) -> float:
    total_area = 0.0
    
    for shape in shapes:
        total_area += shape.area()

    return total_area


assert math.isclose(
    area_sum([Circle((2.0, 3.0), 4.0), Circle((5.0, 5.0), 5.0)]),
    128.8053007,
    rel_tol = 0.00001)

assert area_sum([]) == 0.0
