class Point():
    """Holds on a point (x,y) in the plane"""
    
    def __init__(self, x=0, y=0):
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))
        self.x = float(x)
        self.y = float(y)

p = Point(1,2)
print("point", p.x, p.y)
origin = Point()
print("origin", origin.x, origin.y)

"""
Explanation: Classes
Point class
We will write a class for a point in a two dimensional Euclidian space ($\mathbb{R}^2$).
We start with the class definition (def) and the constructor (__init__) which defines the creation of a new class instance.
Note:

The first argument to class methods (class functions) is always self, a reference to the instance.
The other arguments to __init__ have a default values 0.
We assert that the __init__ arguments are numbers.
End of explanation
"""


p

"""
Explanation: Notice that when we send a Point to the console we get:
End of explanation
"""


class Point():
    """Holds on a point (x,y) in the plane"""
    def __init__(self, x=0, y=0):
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))
        self.x = float(x)
        self.y = float(y)
    
    def __repr__(self):
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"

Point(1,2)

"""
Explanation: Which is not useful, so we will define how Point is represented in the console using __repr__.
End of explanation
"""


class Point():
    """Holds on a point (x,y) in the plane"""
    def __init__(self, x=0, y=0):
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))
        self.x = float(x)
        self.y = float(y)
    def __repr__(self):
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"
    
    def add(self, other):
        assert isinstance(other, (int, Point))
        if isinstance(other, Point):
            return Point(self.x + other.x , self.y + other.y)
        else: # other is int, taken as (int, int)
            return Point(self.x + other , self.y + other)

Point(1,1).add(Point(2,2))

Point(1,1).add(2)

"""
Explanation: Next up we define a method to add two points. Addition is by elements - $(x_1, y_1) + (x_2, y_2) = (x_1+x_2, y_1+y_2)$.
We also allow to add an int, in which case we add the point to a another point with both coordinates equal to the argument value.
End of explanation
"""


class Point():
    """Holds on a point (x,y) in the plane"""
    def __init__(self, x=0, y=0):
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))
        self.x = float(x)
        self.y = float(y)
    def __repr__(self):
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"
    
    def __add__(self, other):
        assert isinstance(other, (int, Point))
        if isinstance(other, Point):
            return Point(self.x + other.x , self.y + other.y)
        else: # other is int, taken as (int, int)
            return Point(self.x + other , self.y + other)

Point(1,1) + Point(2,2)

Point(1,1) + 2

"""
Explanation: A nicer way to do it is to overload the addition operator + by defining the addition method name to a name Python reserves for addition - __add__ (those are double underscores):
End of explanation
"""


Point(1,2) == Point(2,1)

Point(1,2) == Point(1,2)

p = Point()
p == p

Point(1,2) > Point(2,1)

"""
Explanation: We want to be a able to compare Points:
End of explanation
"""


class Point():
    """Holds on a point (x,y) in the plane"""
    def __init__(self, x=0, y=0):
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))
        self.x = float(x)
        self.y = float(y)
    def __repr__(self):
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"
    def __add__(self, other):
        assert isinstance(other, (int, Point))
        if isinstance(other, Point):
            return Point(self.x + other.x , self.y + other.y)
        else: # other is int, taken as (int, int)
            return Point(self.x + other , self.y + other)
    
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
    
    def __gt__(self, other):
        return (self.x > other.x and self.y > other.y)

"""
Explanation: So == checks by identity and &gt; is not defined. Let us overload both these operators:
End of explanation
"""


Point(1,0) == Point(1,2)

Point(1,0) == Point(1,0)

"""
Explanation: First we check if two points are equal:
End of explanation
"""


Point(1,0) > Point(1,2)

"""
Explanation: Then if one is strictly smaller than the other:
End of explanation
"""


class Point():
    """Holds on a point (x,y) in the plane"""
    def __init__(self, x=0, y=0):
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))
        self.x = float(x)
        self.y = float(y)
    def __repr__(self):
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
    def __gt__(self, other):
        return (self.x > other.x and self.y > other.y)
    def __add__(self, other):
        assert isinstance(other, (int, Point))
        if isinstance(other, Point):
            return Point(self.x + other.x , self.y + other.y)
        else: # other is int, taken as (int, int)
            return Point(self.x + other , self.y + other)
    
    def increment(self, other): 
        '''this method changes self (add "inplace")'''
        assert isinstance(other,Point)
        self.x += other.x
        self.y += other.y

p = Point(6.5, 7)
p + Point(1,2)
print(p)
p.increment(Point(1,2))
print(p)

Point(5,6) > Point(1,2)

"""
Explanation: The addition operator + returns a new instance. 
Next we will write a method that instead of returning a new instance, changes the current instance:
End of explanation
"""


class Point():
    """Holds on a point (x,y) in the plane"""
    def __init__(self, x=0, y=0):
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))
        self.x = float(x)
        self.y = float(y)
    def __repr__(self):
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
    def __lt__(self, other):
        return (self.x < other.x and self.y < other.y)
    def __add__(self, other):
        assert isinstance(other, (int, Point))
        if isinstance(other, Point):
            return Point(self.x + other.x , self.y + other.y)
        else: # other is int, taken as (int, int)
            return Point(self.x + other , self.y + other)
    def increment(self, other): 
        '''this method changes self (add "inplace")'''
        assert isinstance(other,Point)
        self.x += other.x
        self.y += other.y
    
    def is_extreme(self, *points):
        for point in points:
            if not self > point:
                return False
        return True

p = Point(5, 6)
p.is_extreme(Point(1,1))

p.is_extreme(Point(1,1), Point(2,5), Point(6,2))

"""
Explanation: We now write a method that given many points, checks if the current point is more extreme than the other points.
Note that the argument *points means that more than one argument may be given.
End of explanation
"""


Point.is_extreme(Point(7,8), Point(1,1), Point(4,5), Point(2,3))

"""
Explanation: We can also use the method via the class instead of the instance, and give the instance of interest (the one that we want to know if it is the extreme) as the first argument self. Much like this, we can either do 'hi'.upper() or str.upper('hi').
End of explanation
"""


class Rectangle1():
    """
    Holds a parallel-axes rectangle by storing two points
    lower left vertex - llv
    upper right vertex - urv
    """
    def __init__(self, lower_left_vertex, upper_right_vertex):
        assert isinstance(lower_left_vertex, Point)
        assert isinstance(upper_right_vertex, Point)
        assert lower_left_vertex < upper_right_vertex 
        self.llv = lower_left_vertex
        self.urv = upper_right_vertex
        
    def __repr__(self):
        representation = "Rectangle with lower left {0} and upper right {1}"
        return representation.format(self.llv, self.urv)

    def dimensions(self):
        height = self.urv.y - self.llv.y
        width = self.urv.x - self.llv.x
        return height, width
    
    def area(self):
        height, width = self.dimensions()
        area = height * width
        return area
    
    def transpose(self):
        """
        Reflection with regard to the line passing through lower left vertex with angle 315 (-45) degrees
        """
        height, width = self.dimensions()
        self.urv = self.llv
        self.llv = Point(self.urv.x - height, self.urv.y - width)

rec = Rectangle1(Point(), Point(2,1))
print(rec)
print("Area:", rec.area())
print("Dimensions:", rec.dimensions())
rec.transpose()
print("Transposed:", rec)

"""
Explanation: Rectangle class
We will implement two classes for rectangles, and compare the two implementations.
First implementation - two points
The first implementation defines a rectangle by its lower left and upper right vertices.
End of explanation
"""


class Rectangle2():
    """
    Holds a parallel-axes rectangle by storing lower left point, height and width
    """
    def __init__(self, point, height, width):
        assert isinstance(point, Point)
        assert isinstance(height, (int,float))
        assert isinstance(width, (int,float))
        assert height > 0
        assert width > 0        
        self.point = point
        self.height = float(height)
        self.width = float(width)
        
    def __repr__(self):
        representation = "Rectangle with lower left {0} and upper right {1}"
        return representation.format(self.point, Point(self.point.x + self.width, self.point.y + self.height))
    
    def dimensions(self):
        return self.height, self.width

    def area(self):
        area = self.height * self.width
        return area

    def transpose(self):
        self.point = Point(self.point.x - self.height , self.point.y - self.width)
        self.height, self.width = self.width, self.height

rec = Rectangle2(Point(), 1, 2)
print(rec)
print("Area:", rec.area())
print("Dimensions:", rec.dimensions())
rec.transpose()
print("Transposed:", rec)

"""
Explanation: Second implementation - point and dimensions
The second implementation defines a rectangle by the lower left point, the height and the width.
We define the exact same methods as in Rectangle1, with the same input and output, but  different inner representation / implementation.
End of explanation
"""


