# Tutorial demonstrating the use of classes in Python

# Note: The subject of classes in Python is both broad and quite complex.
#       This should serve as a simple example of how to use classes,
#       objects created from them, and how to access their attributes
#       and methods. Further research online, and personal experimentation
#       is HIGHLY recommended.

# A class that describes a Person
class Person():
    # This is a class variable. It can be accessed both from the base class
    # i.e. Person.species
    # Or from within an instance object
    # i.e. self.species
    species: str = "Human"

    # This is the method that gets called automatically when an instance of this class (an object) is created
    # Named parameters here passed into the object creation are declared with default values
    def __init__(self, first_name: int = "", last_name: int = "", height: float = 0.0, weight: float = 0.0):
        # We must create instance variables from the passed parameters
        # Instance variables are only available within objects of a particular class type
        self.first_name = first_name
        self.last_name = last_name
        self.height = height
        self.weight = weight
    
    # This is a simple method to return a full name as a concatination of first and last names
    def full_name(self):
        return self.first_name + " " + self.last_name
    
    # __repr__ is a special function called when you (for example) print() an object of this class type
    # It is meant to be used to generate a human readable and machine parseable representation of this class
    def __repr__(self):
        return f"{self.__class__.__name__}(species='{self.species}', first_name='{self.first_name}', last_name='{self.last_name}', height={self.height}, weight={self.weight})"

class Student(Person):
    def __init__(self, school_name: str = "", id: int = 0):
        # We first call the parent's __init__ method to include it's attributes and methods here
        super().__init__()
        self.school_name = school_name
        self.id = id
    
    def __repr__(self):
        return f"{self.__class__.__name__}(species='{self.species}', first_name='{self.first_name}', last_name='{self.last_name}', height={self.height}, weight={self.weight}, school_name='{self.school_name}', id={self.id})"

class Employee(Person):
    def __init__(self, company_name: str = "", id: int = 0, salary: int = 0):
        super().__init__()
        self.company_name = company_name
        self.id = id
        self.salary = salary
    
    def __repr__(self):
        return f"{self.__class__.__name__}(species='{self.species}', first_name='{self.first_name}', last_name='{self.last_name}', height={self.height}, weight={self.weight}, company_name='{self.company_name}', id={self.id}, salary={self.salary})"

# We create a new student with a high school name and ID,
# but the attributes from the parent are all set to defaults
my_student = Student("Beavercreek High School", 99)
print(my_student)
# We'll also reference Person's class variable just for giggles
print(f"my_student is of type {my_student.__class__.__name__} which inherits from {my_student.__class__.__base__.__name__} and is a {Student.species}")
# We create a new employee with all default values
my_employee = Employee()
# We set a bunch of attribute values
my_employee.species = "Anthropomorphic Duck"
my_employee.first_name = "Scrooge"
my_employee.last_name = "McDuck"
my_employee.height = 48
my_employee.weight = 75
my_employee.company_name = "Greed, Inc."
my_employee.id = 1
my_employee.salary = 99000000
# Note that we can call the parent method "full_name()" from the child object
print(f"{my_employee.full_name()} is a {my_employee.species} describe as", my_employee)