"""multiplying two 32-bit numbers without using the multiply operator in Python
Todo:
    * write unit-tests for all the methods in the module
    * reduce module level variables
"""

import datetime
from random import randint
import numpy as np


def calculate_sign(first_operand: int, second_operand: int, result: int)->(int, int, int):
    """
    calculates the sign when first_operand and second_operand is multiplied,
    and updates result accordingly, it is assumed result is positive when
    passed as argument.
    if both first_operand and second_operand are greater than zero, sign should
    be positive and result should be greater 0.
    if only one of the variables from first_operand and second_operand is greater
    than zero, sign should be negative and result should be less than 0
    @first_operand: 32-bit integer
    @second_operand: 32-bit integer
    @result: result when abs(first_operand) and abs(second_operand) are multiplied
    """
    if first_operand < 0 and second_operand < 0:
        pass
    elif first_operand < 0 or second_operand < 0:
        result = 0 - result
    return result


def find_number_pairs(abs_a: int, abs_b: int)->(int, int):
    """
    check for min(big_a, big_b)
    @big_a: 32-bit positive integer
    @big_b: 32-bit positive integer
    """
    x, y = abs_b, abs_a
    if abs_a < abs_b:
        x = abs_a
        y = abs_b
    return x, y


def multiply_optimized(first_operand: int, second_operand: int)->(int, int):
    big_a, big_b = first_operand, second_operand
    if big_a < 0:
        big_a = 0 - big_a
    if big_b < 0:
        big_b = 0 - big_b
    x, y = find_number_pairs(big_a, big_b)
    iterations = x
    i = 0
    delta = 0
    while iterations is not 0:
        j = 1
        s = y
        while j < iterations:
            s = s << 1
            j = j << 1
        if i % 2 == 0:
            delta = delta + s
        else:
            delta = delta - s
        iterations = j - iterations
        i = i + 1
    return calculate_sign(first_operand, second_operand, y+delta)


def multiply_unoptimized(first_operand: int, second_operand: int)->(int, int):
    """
    for any numbers a and b, just use a for loop for abs(a)
    iterations were we keep adding abs(b) to result
    @a:32-bit integer
    @b:32-bit integer
    """
    big_a, big_b = abs(first_operand), abs(second_operand)
    result = 0
    for _ in range(big_a):
        result = result + big_b
    answer = calculate_sign(first_operand, second_operand, result)
    return answer


def pretty_print(first_operand: int, second_operand: int, value_optimized: int, value_unoptimized: int)->(int, int, int, int):
    """
    compares to see if the expected values are identical to
    the values generated by the optimized and unoptimized functions
    of the multiplier, benchmarks both the functions in terms of latency
    @first_operand: 32-bit integer, first number to multiply
    @second_operand: 32-bit integer, second number to multiply
    @value_optimized: value returned by the function multiply_optimized_version
    @value_unoptimized: value returned by the function multiply_unoptimized_version
    """
    print("first operand: {}".format(first_operand))
    print("second operand: {}".format(second_operand))
    print("expected result for multiplication: {}" .format((first_operand*second_operand)))
    print("result unoptimized version: {}".format(value_unoptimized))
    print("result optimized version: {}\n".format(value_optimized))


if __name__ == "__main__":
    TIMES_OPTIMIZED = []
    TIMES_UNOPTIMIZED = []
    value = 0.0
    for i in range(100):
        FIRST_OPERAND = randint(-5e7, 5e7)
        SECOND_OPERAND = randint(-5e7, 5e7)
        START_TIME = datetime.datetime.now()
        VALUE_UNOPTIMIZED = multiply_unoptimized(FIRST_OPERAND, SECOND_OPERAND)
        TIMES_UNOPTIMIZED.append(float((datetime.datetime.now() - START_TIME).total_seconds()))
        START_TIME = datetime.datetime.now()
        VALUE_OPTIMIZED = multiply_optimized(FIRST_OPERAND, SECOND_OPERAND)
        TIMES_OPTIMIZED.append((datetime.datetime.now() - START_TIME).total_seconds())
        pretty_print(FIRST_OPERAND, SECOND_OPERAND, VALUE_OPTIMIZED, VALUE_OPTIMIZED)
    print("uoptimized version took: {} ms on average".format((1000.0 * np.mean(TIMES_UNOPTIMIZED))))
    print("optimized version took: %.50f ms on average" % \
            ((1000.0 * float(np.mean(TIMES_OPTIMIZED)))))
