# Run this cell using Ctrl+Enter (⌘+Enter on Mac).
from testing import exercise
from typing import Tuple

import math

Complex = Tuple[float, float]
Polar = Tuple[float, float]

@exercise
def imaginary_power(n : int) -> int:
    # If n is divisible by 4
    if n % 4 == 0:
        return 1
    else:
        return -1

@exercise
def complex_add(x : Complex, y : Complex) -> Complex:
    # You can extract elements from a tuple like this
    a = x[0]
    b = x[1]
    
    c = y[0]
    d = y[1]
    
    # This creates a new variable and stores the real component into it
    real = a + c
    # Replace the ... with code to calculate the imaginary component
    imaginary = b + d
    
    # You can create a tuple like this, with the real and imaginary part of the number
    ans = (real, imaginary)
    
    return ans

@exercise
def complex_mult(x : Complex, y : Complex) -> Complex:
    
    # Here is another, more compact way of extracting the parts of the complex number:
    (a, b) = x
    (c, d) = y
    
    real = (a * c) - (b * d)
    imaginary = (a * d) + (c * b)
    
    # Instead of creating a separate tuple with the result, we return the tuple directly
    return (real, imaginary)

@exercise
def conjugate(x : Complex) -> Complex:
    
    real = x[0]

    # The sign can be easily flipped with the unary minus operator
    imaginary = - x[1]
    
    return (real, imaginary)

@exercise
def complex_div(x : Complex, y : Complex) -> Complex:
    
    (a, b) = x
    (c, d) = y
    
    # We will use the denominator multiple times, so we'll store the value in a separate variable, 
    # to make the code easier to use and less error prone.
    # Here we use ** to raise c and d to the second power.
    denominator = (c ** 2) + (d ** 2)
    
    real = ((a * c) + (b * d)) / denominator
    imaginary = ((a * (-d) ) + (c * b)) / denominator
    
    return (real, imaginary)

@exercise
def modulus(x : Complex) -> float:
    return math.sqrt(x[0] ** 2 + x[1] ** 2)

@exercise
def complex_exp(x : Complex) -> Complex:
    
    (a, b) = x
    
    expa = math.e ** a
    
    real = expa * math.cos(b)
    imaginary = expa * math.sin(b)
    
    return (real, imaginary)

@exercise
def complex_exp_real(r : float, x : Complex) -> Complex:
    
    # Since ln(r) is only defined for positive numbers, we check for this special case.
    # If r = 0, raising it to any power will give 0.
    # Calling return before the end of the function will not execute the rest of the function.
    if (r == 0):
        return (0,0)
    
    (a, b) = x
    
    # Raise r to the power of a
    ra = r ** a
    
    # Natural logarithm of r
    lnr = math.log(r)
    
    real = ra * math.cos(b * lnr)
    imaginary = ra * math.sin(b * lnr)
    
    return (real, imaginary)

@exercise
def polar_convert(x : Complex) -> Polar:
    
    (a, b) = x
    
    r = math.sqrt(a**2 + b **2)
    theta = math.atan2(b, a)
    
    return (r, theta)

@exercise
def cartesian_convert(x : Polar) -> Complex:
    
    (r, theta) = x
    
    real = r * math.cos(theta)
    imaginary = r * math.sin(theta)
    
    return (real, imaginary)

@exercise
def polar_mult(x : Polar, y : Polar) -> Polar:
    
    (r1, theta1) = x
    (r2, theta2) = y
    
    radius = r1 * r2
    
    angle = theta1 + theta2
    
    # If the calculated angle is larger then pi, we will subtract 2pi
    if (angle > math.pi):
        # Reassign the value for angle
        angle = angle - 2.0 * math.pi
        
    # If the calculated angle is smaller then -pi, we will add 2 pi
    elif (angle <= -math.pi):
        angle = angle + 2.0 * math.pi
    
    return (radius, angle)

@exercise
def complex_exp_arbitrary(x : Complex, y : Complex) -> Complex:
    
    (a, b) = x
    (c, d) = y

    # Convert x to polar form
    r = math.sqrt(a ** 2 + b ** 2)
    theta = math.atan2(b, a)
    
    # Special case for r = 0
    if (r == 0):
        return (0, 0)

    lnr = math.log(r)
    
    exponent = math.exp(lnr * c - d * theta)
    
    real = exponent * (math.cos(lnr * d + theta * c ))
    imaginary = exponent * (math.sin(lnr * d + theta * c))
    
    return (real, imaginary)