# Reusing Code with Inheritance
# parent class, child class, super(), method overriding
# redhorndev.com

# ─────────────────────────────────────────────
# Parent class
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species, age):
        self.name    = name
        self.species = species
        self.age     = age
        self.status  = "available"

    def describe(self):
        print(f"{self.name} is a {self.age} year old {self.species}.")

    def adopt(self):
        self.status = "adopted"
        print(f"{self.name} has been adopted!")

# ─────────────────────────────────────────────
# Child class — inherits from Animal
# ─────────────────────────────────────────────

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, "dog", age)  # runs Animal.__init__ first
        self.breed = breed                   # Dog's own addition

    def fetch(self):
        print(f"{self.name} fetches the ball!")

class Cat(Animal):
    def __init__(self, name, age, indoor):
        super().__init__(name, "cat", age)
        self.indoor = indoor                 # True or False

    def describe(self):                      # overrides Animal.describe()
        location = "indoor" if self.indoor else "outdoor"
        print(f"{self.name} is a {self.age} year old {location} cat.")

# ─────────────────────────────────────────────
# Using the child classes
# ─────────────────────────────────────────────

lassie   = Dog("Lassie",   4, "Collie")
whiskers = Cat("Whiskers", 2, True)

# inherited attributes
print(lassie.name)          # Lassie
print(lassie.status)        # available
print(whiskers.status)      # available

# own attributes
print(lassie.breed)         # Collie
print(whiskers.indoor)      # True

# inherited methods
lassie.adopt()              # Lassie has been adopted!
whiskers.adopt()            # Whiskers has been adopted!

# own methods
lassie.fetch()              # Lassie fetches the ball!

# overridden method
lassie.describe()           # Lassie is a 4 year old dog.       — Animal.describe()
whiskers.describe()         # Whiskers is a 2 year old indoor cat. — Cat.describe()

# ─────────────────────────────────────────────
# isinstance() — checking inheritance
# ─────────────────────────────────────────────

print(isinstance(lassie, Dog))      # True
print(isinstance(lassie, Animal))   # True  — Dog is an Animal
print(isinstance(lassie, Cat))      # False

print(isinstance(whiskers, Cat))    # True
print(isinstance(whiskers, Animal)) # True  — Cat is an Animal
print(isinstance(whiskers, Dog))    # False

# ─────────────────────────────────────────────
# What happens without super()
# ─────────────────────────────────────────────

class BrokenDog(Animal):
    def __init__(self, name, age, breed):
        # super().__init__() not called
        self.breed = breed

# broken = BrokenDog("Rex", 3, "Husky")
# broken.describe()     # AttributeError — name, age, species not set

# ─────────────────────────────────────────────
# Quick reference
# ─────────────────────────────────────────────

# class Child(Parent):              — inherit from Parent
# super().__init__(args)            — call parent's constructor
# child inherits all parent attrs and methods
# child can add new attrs and methods
# child can override parent methods by redefining them
# parent class is never modified by inheritance
#
# isinstance(obj, Class)            — True if obj is instance of Class or its parents
