import random


############################################################
# every class inherits from the base class `object`
############################################################


def demo_builtin_inheritance():
    print()
    print(isinstance(123, object))
    print(isinstance(123.0, object))
    print(isinstance("123", object))
    print(isinstance([1, 2, 3], object))
    print(isinstance({1: "a", 2: "b", 3: "c"}, object))
    print(isinstance(lambda x: x**2, object))
    print(isinstance(int, object))

    print()
    print(issubclass(int, object))
    print(issubclass(float, object))
    print(issubclass(str, object))
    print(issubclass(list, object))
    print(issubclass(dict, object))
    print(issubclass(type(lambda x: x**2), object))
    print(issubclass(type, object))


# demo_builtin_inheritance()


# empty Student class inherits dunder methods from object class
class Student:
    pass


def demo_custom_type_inheritance():
    print()
    tony = Student()
    print(isinstance(tony, object))
    print(issubclass(Student, object))

    # inheritance of __str__() and __eq__()
    print()
    riri = Student()
    print(tony)
    print(riri)
    print(tony == riri)

    print()
    print(Student.__init__)
    print(Student.__str__)
    print(Student.__eq__)
    print(Student.__init__ == object.__init__)


# demo_custom_type_inheritance()


############################################################
# more double underscore methods
############################################################


class Fraction:
    """A representation of a fraction, i.e., a rational number."""

    def __init__(self, num, denom):
        assert isinstance(num, int)
        assert isinstance(denom, int) and denom != 0
        self.num = num
        self.denom = denom

    def __str__(self):
        return f"<Fraction {self.num} / {self.denom}>"

    def __float__(self):
        return self.num / self.denom

    def __mul__(self, other):
        top = self.num * other.num
        bottom = self.denom * other.denom
        return Fraction(top, bottom)

    def __truediv__(self, other):
        # straightforward but duplication
        top = self.num * other.denom
        bottom = self.denom * other.num
        return Fraction(top, bottom)

        # better code
        return self * Fraction(other.denom, other.num)

    # FINGER EXERCISE: implement __add__(), __sub__(), __eq__()


def demo_fractions():
    a = Fraction(num=2, denom=5)
    b = Fraction(num=-1, denom=4)
    c = Fraction(num=3, denom=-12)

    print()
    print(a)
    print(b)
    print(c)

    # print()
    # print(Fraction.__float__(a))
    # print(a.__float__())
    # print(float(a))
    # print(float(b))
    # print(float(c))

    # print()
    # print(Fraction.__mul__(a, b))
    # print(a.__mul__(b))
    # print(a * b)
    # print(a / b)

    # print()
    # print(a + b)
    # print(a - b)

    # print()
    # print(b == c)


# demo_fractions()


############################################################
# inheritance: method reuse and overriding/shadowing
############################################################


class Animal:

    def __init__(self, age, name=None):
        self.age = age
        self.name = name

    def __str__(self):
        return f"<Animal {self.name} {self.age} years>"

    def get_age_diff(self, other):
        """
        Return the magnitude of the age difference between two Animals.
        """
        assert isinstance(other, Animal)
        return abs(self.age - other.age)

    def speak(self):
        """Print to the terminal some text that self would say."""
        raise NotImplementedError(
            f"Animal {self} does not have speak()ing ability yet"
        )


def demo_animal():
    print()
    creature = Animal(5, name="Morg")
    critter = Animal(2, name="Florg")
    print(creature)
    print(critter)

    # avoid creating new data attributes on-the-fly
    # print()
    # critter.size = "tiny"
    # print(critter.size)
    # print(creature.size)  # raises AttributeError

    # these are not identical method calls!
    # they just happen to give the same result
    # print()
    # print(creature.get_age_diff(critter))
    # print(critter.get_age_diff(creature))

    # print()
    # creature.speak()  # raises NotImplementedERror


# demo_animal()


class Cat(Animal):
    """A Cat is an Animal that says "Meow" or something like that."""

    def __str__(self):
        return f"<Cat {self.name} {self.age} years>"

    def speak(self):
        """Print something a cat would say."""
        print("Meow")

    def confuse(self, other):
        """
        Given two Cats, make them both speak, swap their names, and
        return a string of the form:
            "<name> and <name> are <#> years apart."
        """
        self.speak()
        other.speak()
        self.name, other.name = other.name, self.name
        print(
            f"{self.name} and {other.name} are "
            f"{self.get_age_diff(other)} years apart."
        )


def demo_cats():
    print()
    lion = Cat(5, "Fluffy")
    tiger = Cat(8, "Furry")
    print(lion)
    print(tiger)

    # print()
    # lion.confuse(tiger)
    # print(lion)
    # print(tiger)


# demo_cats()


############################################################
# inheritance chains and super()
############################################################


class Person(Animal):
    """A Person is an Animal with other Person friends."""

    def __init__(self, name, age):  # note the order of the parameters!
        # use Animal to set common attributes
        Animal.__init__(self, age, name)
        # super().__init__(age, name)  # equivalent to line above

        # store Person-specific info
        self.friends = set()

        # super() reinterprets self as an object of type Animal
        # print()
        # print(Animal.__init__)
        # print(self.__init__)
        # print(super().__init__)

    def __str__(self):
        return f"<Person {self.name!r} {self.age} years old>"

    # enables str()-like printing of objects within collections
    # __repr__ = __str__

    def speak(self):
        print(f"Hello. My name is {self.name}.")

    def add_friend(self, other):
        """Record other as a friend of self and vice versa."""
        assert isinstance(other, Person)
        self.friends.add(other)
        other.friends.add(self)


def demo_person_friends():
    inigo = Person("Inigo Montoya", 35)
    fezzik = Person("Fezzik", 41)
    westley = Person("Dread Pirate Roberts", 25)
    max = Person("Miracle Max", 39)

    print()
    inigo.speak()
    inigo.add_friend(fezzik)
    inigo.add_friend(westley)
    print(inigo.friends)
    print(fezzik.friends)

    # use add_friend() to ensure mutual friendship
    # print()
    # inigo.friends.add(max)
    # print(inigo.friends)
    # print(max.friends)


# demo_person_friends()


class Student(Person):

    def __init__(self, name, age, major=None):
        super().__init__(name, age)
        self.major = major

    def __str__(self):
        name = f"{self.name!r}"
        age = f"{self.age} years old"
        major = f"Course {self.major}"
        return f"<Student {", ".join([name, age, major])}>"

    def speak(self):
        """Introduce yourself and then a friend a random."""
        super().speak()
        friend = random.choice(list(self.friends))
        if isinstance(friend, Student):
            print(f"My friend {friend.name} is Course {friend.major}.")
        else:
            print(f"My friend {friend.name} isn't a student.")


def demo_student_speak():
    alex = Student("Alex", 20, "6-4")
    kelly = Student("Kelly", 22, "CMS")
    sam = Student("Sam", 18, "2-A")
    max = Person("Miracle Max", 39)

    print()
    print(alex)
    alex.add_friend(kelly)
    alex.add_friend(sam)
    alex.add_friend(max)
    for _ in range(6):
        print()
        alex.speak()


# demo_student_speak()
