import random


############################################################
# Ball class definition
############################################################


class Ball:

    COLORS = "red", "orange", "yellow", "green", "blue", "purple"

    def __init__(self, radius, color=None):
        self._radius = radius
        self._color = color if color else random.choice(Ball.COLORS)

    def __str__(self):
        return ", ".join([
            f"Ball Type: {self.__class__.__name__}",
            f"Radius: {self._radius}",
            f"Color: {self._color}",
        ])


def demo_ball():
    ball1 = Ball(5)
    ball2 = Ball(3)
    ball3 = Ball(4, "green")
    print(ball1)
    print(ball2)
    print(ball3)


# demo_ball()


############################################################
# subclass definitions
############################################################


class SportsBall(Ball):

    def __init__(self, radius):
        super().__init__(radius, self.DEFAULT_COLOR)


class Basketball(SportsBall):

    DEFAULT_COLOR = "orange"


class Baseball(SportsBall):

    DEFAULT_COLOR = "white"


class Beachball(Ball):

    additional_colors = ["red", "blue", "green"]

    def __init__(self, radius, color=None):
        super().__init__(radius, color)

    def __str__(self):
        all_colors = [self._color] + list(self.additional_colors)
        all_colors = list(dict.fromkeys(all_colors))
        colors_str = ", ".join(all_colors)
        return ", ".join([
            f"Ball Type: {Beachball.__name__}",
            f"Radius: {self._radius}",
            f"Colors: {colors_str}",
        ])


def demo_ball_subclass():
    basketball1 = Basketball(5)
    basketball2 = Basketball(4.5)
    baseball1 = Baseball(2)
    beachball1 = Beachball(6, "red")
    beachball2 = Beachball(4, "yellow")

    print(basketball1)
    print(basketball2)
    print(baseball1)
    print(beachball1)
    print(beachball2)

    print(type(basketball1) == type(basketball2))
    print(type(basketball1) == type(baseball1))


# demo_ball_subclass()


# EXERCISE:
# Draw out the environment diagram for this hierarchy for the following code:

# basketball1 = Basketball(5)
# baseball1 = Baseball(2)
# beachball1 = Beachball(6, "red")


############################################################
# invalid ball sizes, limited edition balls, ball counts
############################################################


# EXERCISE:
# We now want to add some functionality to our subclasses.
# Specifically, we want to do the following three things:
#
# 1. Ensure that our sports balls meet some minimum standardized sizing.
#    If they don't, then an InvalidBallSizeError exception should be
#    raised. We can do this directly in our SportsBall parent class.
#
# 2. Allow for the creation of limited_edition sports balls that have
#    a different color than our standard balls. This can also be done
#    directly in our SportsBall parent class.
#
# 3. Keep track of Basketball and Baseball ball counts, including how
#    many standard and limited balls of each kind there are. These ball
#    counts should be accessible to the user. This will require
#    modifying SportsBall and its respective subclasses.
#
# Some class attributes have been defined for you. You should use these
# while implementing the above functionality. You can run the provided
# manual tests sequentially to test your code.

# TODO: modify SportsBall, Basketball, Baseball below to incorporate
# the desired functionality


class InvalidBallSizeError(Exception):
    pass


class SportsBall(Ball):

    def __init__(self, radius):
        super().__init__(radius, self.DEFAULT_COLOR)


class Basketball(SportsBall):

    MIN_RADIUS = 4.7
    DEFAULT_COLOR = "orange"
    LIMITED_COLOR = "green"
    ball_counts = {"standard": 0, "limited": 0}


class Baseball(SportsBall):

    MIN_RADIUS = 1.4
    DEFAULT_COLOR = "white"
    LIMITED_COLOR = "blue"
    ball_counts = {"standard": 0, "limited": 0}


########################################


def test_ball_size_exception(ball_type):
    if ball_type == Basketball:
        try:
            print(Basketball(4))
        except InvalidBallSizeError as e:
            print(f"Error: {e}")
    elif ball_type == Baseball:
        try:
            print(Baseball(1))
        except InvalidBallSizeError as e:
            print(f"Error: {e}")


# test_ball_size_exception(Basketball) # should catch InvalidBallSizeError
# test_ball_size_exception(Baseball) # should catch InvalidBallSizeError


def test_limited():
    basketball_limited = Basketball(5, True)
    baseball_limited = Baseball(2, True)
    print(basketball_limited)
    print(baseball_limited)


# test_limited() # ball colors should be their limited edition colors


def test_ball_counts():
    basketball_limited = Basketball(5, True)
    print(basketball_limited.get_ball_counts()) # 1, 0
    basketball_standard = Basketball(5)
    print(basketball_standard.get_ball_counts()) # 1, 1

    baseball_limited = Baseball(2, True)
    print(baseball_limited.get_ball_counts()) # 1, 0
    baseball_standard = Baseball(2)
    print(baseball_standard.get_ball_counts()) # 1, 1


# test_ball_counts()


############################################################
# SportsFactory class definition
############################################################


class BallTypeError(Exception):
    pass


class LimitExceededError(Exception):
    pass


class SportsFactory:

    ball_type_counts = {Basketball: 0, Baseball: 0}
    limited_edition_counts = {Basketball: 0, Baseball: 0}
    limited_edition_max = {Basketball: 2, Baseball: 1}

    def get_limited_balls_created(self):
        return SportsFactory.limited_edition_counts

    def get_total_balls_created(self):
        print(", ".join(
            f"{ball_type.__name__}: {count}"
            for ball_type, count in SportsFactory.ball_type_counts.items()
        ))
        return sum(SportsFactory.ball_type_counts.values())

    def create_new_ball(self, ball_type, radius, limited_edition=False):
        if ball_type not in SportsFactory.ball_type_counts:
            raise BallTypeError(f"{ball_type} is not a sports ball!")

        if limited_edition:
            limit_reached = (
                SportsFactory.limited_edition_counts[ball_type]
                >= SportsFactory.limited_edition_max[ball_type]
            )
            if limit_reached:
                raise LimitExceededError(
                    f"No more limited edition {ball_type.__name__}s allowed!"
                )
            SportsFactory.limited_edition_counts[ball_type] += 1

        ball = ball_type(radius, limited_edition)
        SportsFactory.ball_type_counts[ball_type] += 1
        return ball


def demo_sports_factory(limited_error, ball_type_error):
    my_sports_factory = SportsFactory()
    basketball1 = my_sports_factory.create_new_ball(Basketball, 5, False)
    basketball2 = my_sports_factory.create_new_ball(Basketball, 5, True)
    baseball1 = my_sports_factory.create_new_ball(Baseball, 1.5, True)

    if limited_error:
        baseball2 = my_sports_factory.create_new_ball(Baseball, 1.5, True)
    elif ball_type_error:
        beachball1 = my_sports_factory.create_new_ball(Beachball, 5, "red")

    print(basketball1)
    print(basketball2)
    print(baseball1)


# demo_sports_factory(False, False)
# demo_sports_factory(True, False) # limited edition error
# demo_sports_factory(False, True) # ball type error


############################################################
# fulfilling a list of ball orders
############################################################


# EXERCISE:
# Add a function fulfill_orders() to the SportsFactory class as per its
# provided docstring. This function is responsible for processing orders
# and for handling the BallTypeError and LimitExceededError exceptions
# raised by create_new_ball().


class SportsFactory:

    ball_type_counts = {Basketball: 0, Baseball: 0}
    limited_edition_counts = {Basketball: 0, Baseball: 0}
    limited_edition_max = {Basketball: 2, Baseball: 1}

    def get_limited_balls_created(self):
        return SportsFactory.limited_edition_counts

    def get_total_balls_created(self):
        print(", ".join(
            f"{ball_type.__name__}: {count}"
            for ball_type, count in SportsFactory.ball_type_counts.items()
        ))
        return sum(SportsFactory.ball_type_counts.values())

    def create_new_ball(self, ball_type, radius, limited_edition=False):
        if ball_type not in SportsFactory.ball_type_counts:
            raise BallTypeError(f"{ball_type} is not a sports ball!")

        if limited_edition:
            limit_reached = (
                SportsFactory.limited_edition_counts[ball_type]
                >= SportsFactory.limited_edition_max[ball_type]
            )
            if limit_reached:
                raise LimitExceededError(
                    f"No more limited edition {ball_type.__name__}s allowed!"
                )
            SportsFactory.limited_edition_counts[ball_type] += 1

        ball = ball_type(radius, limited_edition)
        SportsFactory.ball_type_counts[ball_type] += 1
        return ball

    def fulfill_orders(self, orders):
        """
        Processes a list of ball orders and creates valid Ball objects.

        If LimitExceededError or BallTypeError is raised when attemping
        to create a Ball, skip that order, and continue processing the
        remaining orders.

        Parameters:
            orders (list): A sequence of ball orders, each a tuple that
                contains the following:
                + ball_type (Ball or subtype)
                + radius (float)
                + limited_edition (bool, optional)

        Return a list of successfully created Ball objects.
        """
        pass
        # TODO: implement the 'fulfill_orders()' function


def demo_fulfill_orders():
    my_sports_factory = SportsFactory()
    orders = [
        (Basketball, 5, False),
        (Baseball, 2, True),
        (Beachball, 6, False),
        (Baseball, 2, True),
        (Baseball, 2, False),
    ]
    created_balls = my_sports_factory.fulfill_orders(orders)
    for ball in created_balls:
        print("Created", ball)


# demo_fulfill_orders()
