from orangescape.model.engine.exception.Error import FormulaArgumentError
from orangescape.model.engine.exception.ErrorMessage import *
from orangescape.model.engine.cpython.randutil import randint,random
from exFunctions import getNumber

import math
import cmath
import operator

def SUM(argList):
    #REF : http://aima.cs.berkeley.edu/python/utils.html
    return reduce(operator.add, argList, 0)

def PRODUCT(argList):
    #REF : http://aima.cs.berkeley.edu/python/utils.html
    return reduce(operator.mul, argList, 1)

def ABS(argList):
    return abs(argList[0])

def COMBIN(argList):
    number = int(argList[0])
    number_choosen = int(argList[1])
    if argList[0] < 0 or argList[1] < 0 :
        raise FormulaArgumentError, Engine2007 %('COMBIN','lesser',0)
    elif number < number_choosen:
        raise FormulaArgumentError, Engine2007 %('COMBIN','lesser',number_choosen)
    else:
        top = reduce(operator.mul,[number - i for i in xrange(0,number_choosen)],1)
        bottom = reduce(operator.mul, [number_choosen - i for i in xrange(0,number_choosen)],1)
        return getNumber(top) / bottom


def CEILING(argList):
    number=argList[0]
    significance=argList[1]
    if number == 0 or significance == 0:
        return 0
    modulus = number % significance
    if((number < 0 and significance >0)or (number > 0 and significance < 0)):
        raise FormulaArgumentError, Engine2006 %('CEILING')
    else:
        if abs(modulus) > 0:
            return (number - modulus) + significance
        else:
            return number

def EVEN(argList):
    number=argList[0]
    if number%2==0:
        return int(number)
    result =  CEILING([abs(number),2])
    if number<0:
        return result * -1
    return result

def EXP(argList):
    if argList[0] > 709.7:
        raise FormulaArgumentError,Engine2007 % ('EXP','greater',710)
    elif argList[0] < -710:
        return 0
    number = getNumber(argList[0])
    return math.exp(number)

def FACT(argList):
    if argList[0] < 0 :
        raise FormulaArgumentError,Engine2007 % ('FACT','lesser',0)

    number= int(argList[0])
    if number>=171.0:
        raise FormulaArgumentError,Engine2007 % ('FACT','greater',170)

    return reduce(operator.mul, [i for i in xrange(1,number+1)],1)

def FLOOR(argList):
    # Excel have 2 arguments for floor function, rather python has only one argument.
    if (argList[0] < 0 and argList[1] >= 0) or (argList[0] > 0 and argList[1] <= 0):
        raise FormulaArgumentError,Engine2006 % ('FLOOR')
    number=getNumber(argList[0])
    significance=getNumber(argList[1])
    modulus = number - abs((number // significance)*significance)
    if argList[0] < 0 and argList[1] <0 :
        return (number - modulus)* -1
    return (number - modulus)

def INT(argList):
    number=getNumber(argList[0])
    intNumber = int(number)
    if number==intNumber:
        return number
    elif number < 0:
        return intNumber-1
    else:
        return intNumber

def ROUND(argList):
    number=argList[0]
    num_digits=argList[1]
    return round(number,int(num_digits))

def TRUNC(argList):
    number=str(argList[0])
    num_digits=argList[1]
    length=number.find('.') + 1
    if len(argList)== 2:
        if num_digits < 0 :
            deviser = math.pow( 10,abs(num_digits))
            result = int(argList[0]) % deviser
            if result < 0:
                return 0
            return getNumber(int(argList[0]) - result)
        return getNumber(number[0:int(length+num_digits)])
    else:
        return getNumber(number[0:length])

def RAND(argList):
    return random()

def RANDBETWEEN(argList):
    bottom=getNumber(argList[0])
    top=getNumber(argList[1])
    if top < bottom :
        raise FormulaArgumentError,Engine2007 % ('RANDBETWEEN','lesser',bottom)
    return getNumber(randint(int(bottom),int(top)))


def QUOTIENT(argList):
    numerator=argList[0]
    denominator=argList[1]
    if denominator == 0:
        raise FormulaArgumentError,Engine2017 % (2,'QUOTIENT' , 0)
    return int(numerator/denominator)

def PI(argList):
    return math.pi

def ODD(argList):
    number=argList[0]
    if number<0:
        number=number*(-1)
    intNumber=int(number)
    if intNumber%2==0:
        result=intNumber+1
    else:
        if number == intNumber:
            result = number
        elif number>intNumber:
            result=intNumber+2
    if argList[0]<0:
        result=result*(-1)
    return result

def MOD(argList):
    number=argList[0]
    divisor=argList[1]
    if divisor == 0:
        raise FormulaArgumentError,Engine2017%(2,'MOD',0)
    return divmod(number,divisor)[1]    # Remainder

def SIN(argList):
    return math.sin(argList[0])

def ASIN(argList):
    number=argList[0]
    if number>=-1 and number<=1:
        return round(cmath.asin(number).real,12)
    elif number < -1:
        raise FormulaArgumentError,Engine2007 % ('ASIN','greater',-1)
    elif number > 1:
        raise FormulaArgumentError,Engine2007 % ('ASIN','lesser',1)

def SINH(argList):
    if argList[0] > 709 :
        raise FormulaArgumentError,Engine2007 % ('SINH','greater',709)
    elif argList[0] < -709 :
        raise FormulaArgumentError,Engine2007 % ('SINH','lesser',-709)
    return round(math.sinh(argList[0]),12)

def ASINH(argList):
    return round(cmath.asinh(argList[0]).real,12)

def COS(argList):
    return math.cos(argList[0])

def ACOS(argList):
    number=getNumber(argList[0])
    if number>=-1 and number<=1:
        return round(cmath.acos(number).real,12)
    elif number < -1:
        raise FormulaArgumentError,Engine2007 % ('ACOS','greater',-1)
    elif number > 1:
        raise FormulaArgumentError,Engine2007 % ('ACOS','lesser',1)

def COSH(argList):
    if argList[0] > 709.5:
        raise FormulaArgumentError,Engine2007 % ('COSH','greater',710)
    elif argList[0] < -709 :
        raise FormulaArgumentError,Engine2007 % ('COSH','lesser',-709)
    return cmath.cosh(argList[0]).real

def ACOSH(argList):
    if argList[0] < 1:
        raise FormulaArgumentError,Engine2007 % ('ACOSH','lesser',1)
    return round(cmath.acosh(argList[0]).real,12)

def TAN(argList):
    return math.tan(argList[0])

def ATAN(argList):
    return round(math.atan(argList[0]),12)

def TANH(argList):
    if argList[0] > 11:
        return 1
    elif argList[0] < -11:
        return -1
    return round(math.tanh(argList[0]),12)

def ATANH(argList):
    if argList[0] > -1 and argList[0] < 1 :
       return round(cmath.atanh(argList[0]).real,12)
    elif argList[0] >= 1:
        raise FormulaArgumentError,Engine2007%('ATANH','greater',1)
    else:
        raise FormulaArgumentError,Engine2007%('ATANH','lesser',-1)

def ATAN2(argList):
    x_num=argList[0]
    y_num=argList[1]
    if x_num == 0 and  y_num == 0:
        raise FormulaArgumentError,Engine2019 % (2,'ATAN2',0)
    return round(math.atan2(y_num,x_num),12)    # Since, atan2(y, x)

def DEGREES(argList):
    angle=argList[0]
    return round(math.degrees(angle),12)

def LN(argList):
    if argList[0] < 0:
        raise FormulaArgumentError,Engine2007 % ('LN','lesser',0)
    return math.log(argList[0])

def LOG(argList):
    if argList[0] <= 0 or argList[1] <= 0:
        raise FormulaArgumentError,Engine2007 % ('LOG','lesser',1)
    return math.log(argList[0],argList[1])

def LOG10(argList):
    if argList[0] < 0 :
        raise FormulaArgumentError,Engine2007 % ('LOG10','lesser',0)
    return math.log10(argList[0])

def POWER(argList):
    number=argList[0]
    power=argList[1]
    arg_str = str()
    try:
        return math.pow(number, power)
    except OverflowError:
        arg_str = arg_str.join(str(argList))
        raise FormulaArgumentError,Engine2018 % (arg_str)

def SQRT(argList):
    if argList[0] < 0 :
        raise FormulaArgumentError,Engine2007 % ('SQRT','lesser',0)
    return math.sqrt(argList[0])

def SQRTPI(argList):
    if argList[0] < 0 :
        raise FormulaArgumentError,Engine2007 % ('SQRTPI','lesser',0)
    return math.sqrt((math.pi*argList[0]))

def RADIANS(argList):
    number=argList[0]
    return math.radians(number)

def SUMSQ(argList):
    result = 0.0
    for arg in argList:
        result = result + pow(arg,2)
    return result

def ROUNDUP(argList):
    number = argList[0]
    num_digits = int(argList[1])
    if number < 0 :
        number = argList[0] * -1

    if num_digits > 0:
        temp = round(number,num_digits)
        if number > temp:
            to_add = 1.0/pow(10,num_digits)
            number = temp + to_add
        else:
            number = temp

    if num_digits == 0:
        if((number- int(number)) > 0):
            number = int(number) + 1

    if num_digits < 0 :
        devider = pow(10,(num_digits*-1))
        if int(number) == 0 :number =1
        while int(number) % devider != 0 :
            number += 1
        number = int(number)

    if argList[0] < 0 :
        number = number * -1
    return number

def ROUNDDOWN(argList):
    value = argList[0]
    if argList[0] < 0 :
        value = argList[0] * -1

    if argList[1] > 0:
        value = TRUNC([value,argList[1]])

    if argList[1] == 0:
            value = int(value)

    if argList[1] < 0 :
        devider = pow(10,(argList[1]*-1))
        while int(value) % devider != 0 :
            value -= 1
        value = int(value)

    if argList[0] < 0 :
        value = value * -1
    return value

def MULTINOMIAL(argList):
    top = 0
    bottom = 1
    for arg in argList:
        if arg<0:
            raise FormulaArgumentError ,Engine2007 % ('MULTINOMIAL','greater',0)
        top += int(arg)
        bottom *= FACT([arg])
    return FACT([top])/bottom

def GCD(argList):
    # Nested Function
    def find_gcd(a,b):
        """Return greatest common divisor for two Numbers."""
        if (b==0):
            return a
        else:
            return find_gcd(b, a % b)

    if len(argList)==1:
        if argList[0]<0:
            raise FormulaArgumentError,Engine2007 % ('GCD','lesser',0)
        return int(argList[0])
    else:
        gcd = int(argList[0]);
        for arg in argList:
            if arg is None: pass
            if arg<0:
                raise FormulaArgumentError,Engine2007 % ('GCD','lesser',0)
            arg=int(arg)
            gcd = find_gcd(gcd, arg)
        return gcd;

def LCM(argList):
    # Nested Function
    def find_gcd(a,b):
        """Return greatest common divisor for two Numbers."""
        if (b==0):
            return a
        else:
            return find_gcd(b, a % b)

    def find_lcm(a, b):
        """Return lowest common multiple for two Numbers."""
        return a * b // find_gcd(a, b)

    arg_len = len(argList)
    if arg_len==1:
        if argList[0]<0.0:
            raise FormulaArgumentError,Engine2007 % ('LCM','lesser',0)          # #NUM! Excel Error
        return int(argList[0])
    elif arg_len>1:
        isZero = False
        for arg in argList:
            if arg<0.0:
                raise FormulaArgumentError,Engine2007 % ('LCM','lesser',0) # #NUM! Excel Error
            if int(arg)==0.0:
                isZero = True
        if isZero:
            return 0.0
        lcm = int(argList[0]);
        for arg in argList:
            if arg is None: pass
            arg=int(arg)
            lcm = find_lcm(lcm, arg)
        return lcm;

def FACTDOUBLE(argList):
    if argList[0] < 0:
       raise FormulaArgumentError,Engine2007 %('FACTDOUBLE','lesser',0)
    elif argList[0] > 300.9 :
        raise FormulaArgumentError,Engine2007 % ('FACTDOUBLE','greater',300)
    number = int(argList[0])
    fact=1
    for val in xrange(number, 1, -2):
        fact = fact * val
    return fact

def SIGN(argList):
    if argList[0] < 0:
        return -1
    elif argList[0] > 0:
        return 1
    else:
        return 0

def ROMAN(argList):
    input = argList[0]
    if input < 0 or input > 3999:
        raise FormulaArgumentError,Engine2017 % (1,'ROMAN',input)
    if len(argList) > 1 :type = argList[1]
    else: type = 0

    if type == 0:
        ints = (1000, 900,  500, 400, 100,  90, 50,  40, 10,  9,   5,  4,   1)
        nums = ('M',  'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I')
    elif type == 1:
        ints = (1000.0, 950.0, 900.0, 500.0, 450.0, 400.0,  100.0, 95.0,  90.0, 50.0,  45.0, 40.0, 10.0,  9.0, 5.0,  4.0, 1.0)
        nums = ('M'   ,  'LM',  'CM',   'D',  'LD',  'CD',    'C', 'VC', 'XC' ,  'L',  'VL', 'XL',  'X', 'IX', 'V', 'IV', 'I')
    elif type == 2:
        ints = (1000.0, 990.0, 950.0, 900.0, 500.0, 490.0, 450.0, 400.0,  100.0, 99.0, 95.0,  90.0, 50.0, 49.0,  45.0, 40.0, 10.0,  9.0, 5.0,  4.0, 1.0)
        nums = ('M'   ,  'XM',  'LM',  'CM',   'D',  'XD',  'LD',  'CD',    'C', 'IC', 'VC', 'XC' ,  'L', 'IL', 'VL', 'XL',  'X', 'IX', 'V', 'IV', 'I')
    elif type == 3:
        ints = (1000.0, 995.0, 990.0, 950.0, 900.0, 500.0, 495.0, 490.0, 450.0, 400.0,  100.0, 99.0, 95.0,  90.0, 50.0, 49.0,  45.0, 40.0, 10.0,  9.0, 5.0,  4.0, 1.0)
        nums = ('M'   ,  'VM',  'XM',  'LM',  'CM',   'D',  'VD',  'XD',  'LD',  'CD',    'C', 'IC', 'VC', 'XC' ,  'L', 'IL', 'VL', 'XL',  'X', 'IX', 'V', 'IV', 'I')
    elif type == 4:
        ints = (1000.0, 999.0, 995.0, 990.0, 950.0, 900.0, 500.0, 499.0, 495.0, 490.0, 450.0, 400.0,  100.0, 99.0, 95.0,  90.0, 50.0, 49.0,  45.0, 40.0, 10.0,  9.0, 5.0,  4.0, 1.0)
        nums = ('M'   ,  'IM',  'VM',  'XM',  'LM',  'CM',   'D',  'ID',  'VD',  'XD',  'LD',  'CD',    'C', 'IC', 'VC', 'XC' ,  'L', 'IL', 'VL', 'XL',  'X', 'IX', 'V', 'IV', 'I')

    result = ""
    for i in range(len(ints)):
      count = int(input / ints[i])
      result += nums[i] * count
      input -= ints[i] * count
    return result

def SUMPRODUCT(argList):
    #Ref: Excel
    n = len(argList)
    if len(filter(lambda arg: len(arg) != n, argList)) > 0: raise FormulaArgumentError,Engine2010 % ('Arrays have different length' , 'SUMPRODUCT')
    array = list()
    for i in xrange(0,len(argList[0])):
        res = 1
        for j in xrange(0,n):
            res *= argList[j][i]
        array.append(res)
    return reduce(operator.add,array)