class MyTime:

    def __init__(self, hrs=0, mins=0, secs=0):
        """ Create a MyTime object initialized to hrs, mins, secs """
        self.hours = hrs
        self.minutes = mins
        self.seconds = secs
        
    def __str__(self):
        return "{h}:{m}:{s}".format(h=self.hours, m=self.minutes, s=self.seconds)
        


tim1 = MyTime(11, 59, 30)
tim2 = MyTime()

print(tim1)
print(tim2)


"""
Explanation: wk4.0
Even more OOP
End of explanation
"""


def add_time(t1, t2):
    h = t1.hours + t2.hours
    m = t1.minutes + t2.minutes
    s = t1.seconds + t2.seconds
    sum_t = MyTime(h, m, s)
    return sum_t

"""
Explanation: Pure functions
End of explanation
"""


current_time = MyTime(1, 10, 0)

bread_time = MyTime(0, 60, 0)

done_time = add_time(current_time, bread_time)

print(done_time)

def add_time(t1, t2): # are we good now?

    h = t1.hours + t2.hours
    m = t1.minutes + t2.minutes
    s = t1.seconds + t2.seconds

    if s >= 60:
        s -= 60
        m += 1

    if m >= 60:
        m -= 60
        h += 1

    sum_t = MyTime(h, m, s)
    return sum_t

current_time = MyTime(1, 10, 0)

bread_time = MyTime(0, 120, 0)

done_time = add_time(current_time, bread_time)

print(done_time)

"""
Explanation: The function creates a new MyTime object and returns a reference to the new object. This is called a pure function because it does not modify any of the objects passed to it as parameters and it has no side effects, such as updating global variables, displaying a value, or getting user input.
```



current_time = MyTime(9, 14, 30)
bread_time = MyTime(3, 35, 0)
done_time = add_time(current_time, bread_time) # When does this break?
print(done_time)
12:49:30
```
End of explanation
"""


def increment(t, secs): # Is this good?
    t.seconds += secs

    if t.seconds >= 60:
        t.seconds -= 60
        t.minutes += 1

    if t.minutes >= 60:
        t.minutes -= 60
        t.hours += 1

def increment(t, seconds): # How about now?
    t.seconds += seconds

    while t.seconds >= 60:
        t.seconds -= 60
        t.minutes += 1

    while t.minutes >= 60:
        t.minutes -= 60
        t.hours += 1

"""
Explanation: Modifiers
There are times when it is useful for a function to modify one or more of the objects it gets as parameters. Usually, the caller keeps a reference to the objects it passes, so any changes the function makes are visible to the caller. Functions that work this way are called modifiers.
End of explanation
"""


class MyTime:
    # Previous method definitions here...

    def increment(self, seconds):
        self.seconds += seconds

        while self.seconds >= 60:
            self.seconds -= 60
            self.minutes += 1

        while self.minutes >= 60:
            self.minutes -= 60
            self.hours += 1

"""
Explanation: # Converting increment to a method
End of explanation
"""


class MyTime:
    # ...

    def to_seconds(self):
        """ Return the number of seconds represented
            by this instance
        """
        return self.hours * 3600 + self.minutes * 60 + self.seconds

## ---- Core time conversion logic ----- #
hrs = tsecs // 3600
leftoversecs = tsecs % 3600
mins = leftoversecs // 60
secs = leftoversecs % 60

"""
Explanation: An “Aha!” insight
Often a high-level insight into the problem can make the programming much easier.
In this case, the insight is that a MyTime object is really a three-digit number in base 60! The second component is the ones column, the minute component is the sixties column, and the hour component is the thirty-six hundreds column.
When we wrote add_time and increment, we were effectively doing addition in base 60, which is why we had to carry from one column to the next.
End of explanation
"""


class MyTime:
    # ...

    def __init__(self, hrs=0, mins=0, secs=0):
        """ Create a new MyTime object initialized to hrs, mins, secs.
           The values of mins and secs may be outside the range 0-59,
           but the resulting MyTime object will be normalized.
        """

        # Calculate total seconds to represent
        totalsecs = hrs*3600 + mins*60 + secs
        self.hours = totalsecs // 3600        # Split in h, m, s
        leftoversecs = totalsecs % 3600
        self.minutes = leftoversecs // 60
        self.seconds = leftoversecs % 60

def add_time(t1, t2):
    secs = t1.to_seconds() + t2.to_seconds()
    return MyTime(0, 0, secs)

"""
Explanation: In OOP we’re really trying to wrap together the data and the operations that apply to it. So we’d like to have this logic inside the MyTime class. A good solution is to rewrite the class initializer so that it can cope with initial values of seconds or minutes that are outside the normalized values. (A normalized time would be something like 3 hours 12 minutes and 20 seconds. The same time, but unnormalized could be 2 hours 70 minutes and 140 seconds.)
End of explanation
"""


class MyTime:
    # Previous method definitions here...

    def after(self, time2):
        """ Return True if I am strictly greater than time2 """
        if self.hours > time2.hours:
            return True
        if self.hours < time2.hours:
            return False

        if self.minutes > time2.minutes:
            return True
        if self.minutes < time2.minutes:
            return False
        if self.seconds > time2.seconds:
            return True

        return False

"""
Explanation: Generalization vs. Specification
Computer scientists vs. mathematicians
Often it may help to try to think about the problem from both points of view — “What would happen if I tried to reduce everything to very few primitive types?”, versus “What would happen if this thing had its own specialized type?”
Example
The after function should compare two times, and tell us whether the first time is strictly after the second, e.g.
```



t1 = MyTime(10, 55, 12)
t2 = MyTime(10, 48, 22)
after(t1, t2)             # Is t1 after t2?
True
```
End of explanation
"""


if current_time.after(done_time):
    print("The bread will be done before it starts!")

"""
Explanation: We invoke this method on one object and pass the other as an argument:
End of explanation
"""


class MyTime:
    # Previous method definitions here...

    def after(self, time2):
        """ Return True if I am strictly greater than time2 """
        return self.to_seconds() > time2.to_seconds()

"""
Explanation: The logic of the if statements deserve special attention here. Lines 11-18 will only be reached if the two hour fields are the same. Similarly, the test at line 16 is only executed if both times have the same hours and the same minutes.
Could we make this easier? Yes!
End of explanation
"""


class MyTime:
    """This makes the clock.
    
    >>> timer = MyTime(5, 4, 3) # Makes a timer with time 5 hr ...
    """

    def __init__(self, hrs=0, mins=0, secs=0):
        """ Create a new MyTime object initialized to hrs, mins, secs.
           The values of mins and secs may be outside the range 0-59,
           but the resulting MyTime object will be normalized.
        """

        # Calculate total seconds to represent
        totalsecs = hrs*3600 + mins*60 + secs
        self.hours = totalsecs // 3600        # Split in h, m, s
        leftoversecs = totalsecs % 3600
        self.minutes = leftoversecs // 60
        self.seconds = leftoversecs % 60

    def __str__(self):
        return "{h}:{m}:{s}".format(h=self.hours, m=self.minutes, s=self.seconds)

    def __add__(self, other):
        return MyTime(0, 0, self.to_seconds() + other.to_seconds())
    
    def to_seconds(self):
        """ Return the number of seconds represented
            by this instance
        """
        return self.hours * 3600 + self.minutes * 60 + self.seconds

t1 = MyTime(0, 0, 42000)
t2 = MyTime(3, 50, 30)

t3 = t1 + t2

print(t3)

help(MyTime())

"""
Explanation: Operator overloading
Some languages, including Python, make it possible to have different meanings for the same operator when applied to different types. For example, + in Python means quite different things for integers and for strings. This feature is called operator overloading.
It is especially useful when programmers can also overload the operators for their own user-defined types.
For example, to override the addition operator +, we can provide a method named __add__:
End of explanation
"""


def front_and_back(front):
    import copy
    back = copy.copy(front)
    back.reverse()
    print(str(front) + str(back))

"""
Explanation: ```



t1 = MyTime(1, 15, 42)
t2 = MyTime(3, 50, 30)
t3 = t1 + t2
print(t3)
05:06:12
```



Polymorphism
End of explanation
"""


