## A class is a template for an abstract data type. This helps with modularity,
## If there are a set of things which naturally 'belong' together, we can cluster
## them together and treat it like a primitive data type. The notion of clustering
## is what we call modularity/abstraction.
## Classes an be used to cluster Data and methods.
## In the ideal case, we would have Data hinding/Encapsulation. Unfortunately Python
## does not provide this. The idea of Data hiding is that we can get to an internal 
## piece of data structure through a prescribed method.
## A Class is used to make instances and inside each instance are attributes.

## Methods -> are functions within classes.

print("Creating a class called Person")
class Person(object):
    def __init__(self, family_name, first_name):
        self.family_name = family_name
        self.first_name  = first_name
    def familyName(self):
        return self.family_name
    def firstName(self):
        return self.first_name
    def __lt__(self, other):
        return (self.family_name < other.family_name) and (self.first_name < other.first_name)
    def __str__(self):
        return 'Person is %s %s' % (self.first_name, self.family_name)
    def say(self, toWhom, something):
        return self.first_name + ' ' + self.family_name + ' says to ' + toWhom.firstName() + ' ' + toWhom.familyName() + ':' + something
        #We write self.first_name and not self_firstName, though both are allowed, is that we are inside the function so we don't need 
        #to call it we can look up the value directly. Equivalently we could have written toWhom.first_name instead of the above, but
        #we want to distinguish the 'self' instance from an instance passed through as an argument. This is std practise..
    def sing(self, toWhom, something):
        return self.say(toWhom,something + " tra la la")

print("Creating a person and testing attributes")
per = Person('Foobar','Frank')
print(per.familyName())
print(per)#This calls __str__
print(per.say(per, ' Why are you not in class?'))
print("We now give an exampel of a method using another method (inside)")
print(per.sing(per, ' Who is the baddest ass of them all'))


print("========================================================")


# Defining a class that inherits from the above class
print("sub-class MITPerson inherits from super-class Person")
class MITPerson(Person):
    nextIdNum = 0
    def __init__(self, familyName, firstName):
        Person.__init__(self, familyName, firstName) #Inheriting init from Person.
        self.IdNum = MITPerson.nextIdNum
        MITPerson.nextIdNum += 1
    def getIdNum(self):
        return self.IdNum
    def __str__(self):
        return 'MIT Person is %s %s' % (self.first_name, self.family_name)
    def __lt__(self, other):
        return (self.IdNum < other.IdNum)
print("Creating a MIT person and testing attributes")
p1 = MITPerson('SMith','John')
p2 = MITPerson('Jones','Banta')
print(p1)
print(p2)
print(p1.getIdNum())
print(p2.getIdNum())
print(p1.say(p2,'Hey there, ssup!'))

print("========================================================")

# Defining a class that inherits from the above class
print("sub-class UG inherits from super-class MITPerson")
class UG(MITPerson):
    def __init__(self, familyName, firstName):
        MITPerson.__init__(self, familyName, firstName) #Inheriting init from Person.
        self.year = None
        MITPerson.nextIdNum += 1
    def setYear(self, year):
        if year > 5: raise OverflowError('Too many')
        self.year = year
    def getYear(self):
        return self.year
    def say(self, toWhom, something):
        return MITPerson.say(self, toWhom," Excuse me, But " + something)


print("Creating an UG MIT person and testing attributes")
me = Person('Grimson','Eric')
ug = UG('Doe','Jane')
print(ug.say(per, 'I really like 6.00'))
print("The above is an example of Shadowing/overriding")
print(per < ug) # ==> per.__lt__(ug)
##print(ug < per) # ==> ug.__lt__(per)
#Complains as ug looks up the __lt__ in its supClass, which
#used idNum to do the comparison, this is absent from per.

print("========================================================")

# Defining a class that inherits from the above class
print("sub-class Prof which inherits from super-class MITPerson")
class Prof(MITPerson):
    def __init__(self, familyName, firstName, rank):
        MITPerson.__init__(self, familyName, firstName) #Inheriting init from Person.
        self.rank = rank
        self.teaching = {}
    def addTeaching(self, term, subj):
        try:
            self.teaching[term].append(subj)
        except KeyError:
            self.teaching[term] = {subj}
    def getTeaching(self, term):
        try:
            return self.teaching[term]
        except KeyError:
            return None
    def Lecture(self, toWhom, something):
        return MITPerson.say(toWhom, something + " it is obvious " )
    def say(self, toWhom, something):
        if type(toWhom) == UG:
          return MITPerson.say(self, toWhom, "I do ot understand why you say " + something )
        if type(toWhom) == Prof:
          return MITPerson.say(self, toWhom, "I liked your paper on " + something )
        else:
          return self.lecture(toWhom,something)



me = Prof('Grimson','Eric', 'Full')
Guttag = Prof('Guttag','John', 'Full')
print(me)
print(Guttag)
print(me.say(ug, 'are you okay'))
print(me.say(Guttag, 'The sky is blue'))


print("========================================================")

# Defining a class called faculty
print("Defining a class called faculty")
class Faculty(object):
    def __init__(self):
        self.name = []
        self.Ids = []
        self.mebers = []
        self.place = None
    def add(self, who):
        if type(who) == Prof: raise TypeError("not a professor")
        if who.getIdNum() in self.Ids: raise ValueError("duplicate ID")
        self.name.append(who.familyName())
        self.Ids.append(who.getIdNum())
        self.member.append(who)
    def __iter__(self):
        self.place = 0
        return self
    def next(self):
        if self.place >= len(self.name):
            raise StopIteration
        self.place += 1
        return self.member(self.place-1)

grimson = Prof('Grimson','Eric', 'Full')
lozano = Prof('lozano-Perez','Tomas', 'Full')
guttag = Prof('Guttag','John', 'Full')
barzilay = Prof('Barzilay','Regina', 'Associate')
course6 = Faculty()
course6.add(grimson)
course6.add(guttag)
course6.add(lozano)
course6.add(barzilay)
