import logging
import pandas as pd 
import numpy as np 
from datetime import datetime as dt

def weekDay(df):
    '''
    Add weekday information about the timestamp to the dataframe

    Given a dataframe, if this dataframe contains a column called 
    timeStamp, then this will add another column called dayOfWeek 
    that will  represent the day of the week to that dataframe 

    Args:
    -----
        pandas.df: 
            dataframe containing a column called timeStamp
            that contains timestamp informamtion. In case 
            the columns dont contain timeStamp, or in the
            case that the con
    Returns:
    --------
        pandas DataFrame
            The DataFrame with the weekday information appended
            to the DataFrame. In the case of an error, the same
            DataFrame is returned. 
    '''

    logger    = logging.getLogger('monthlyReportNew.calculations.weekDay')
    logger.info('Appending Weekday to the data column')
    
    try:
        dOfWk = {
            0 : 'Sunday',
            1 : 'Monday',
            2 : 'Tuesday',
            3 : 'Wednesday',
            4 : 'Thursday',
            5 : 'Friday',
            6 : 'Saturday', 
        }

        if 'timeStamp' in list( df.columns ):
            logger.info('Trying to append the Weekday inormation into the dataframe')
            df['Weekday'] = df.timeStamp.astype(dt).map(lambda m: dOfWk[m.weekday()])
        else:
            logger.error('Unable to find the timeStamp column.')
            logger.error('  Available columns are: [%s]'%( ', '.join( df.columns ) ))
            
        return df
    except:
        logger.error('Error while computing the Weekday information.')
        cols = df.columns + ['Weekday']
        return pd.DataFrame([], columns = cols)

def timeClass(df):
    '''
    Add time-of-day information about the timestamp to the dataframe

    Given a dataframe, if this dataframe contains a column called 
    timeStamp, then this will add another column called 'time of day' 
    that will  represent the day of the week to that dataframe 

    Args:
    -----
        pandas.df: 
            dataframe containing a column called timeStamp
            that contains timestamp informamtion. In case 
            the columns dont contain timeStamp, or in the
            case that the con
    Returns:
    --------
        pandas DataFrame
            The DataFrame with the time of day information appended
            to the DataFrame. In the case of an error, the same
            DataFrame is returned. 
    '''

    logger    = logging.getLogger('monthlyReportNew.calculations.timeClass')
    logger.info('Appending the time of day to the data column')

    try:
        if len(df) == 0:
            return pd.DataFrame([], columns = list(df.columns )+ ['time of day'])

        if 'timeStamp' not in list( df.columns ):
            logger.error('Unable to find timeStamp information. Available columns are: [%s]'%(','.join(df.columns)) )
            return df 
    except:
        logger.error('The data sent in is probably not a pandas DataFrame.')

    compares = [
        lambda m: m < 6,
        lambda m: m >= 6  and m <12,
        lambda m: m >= 12 and m <18,
        lambda m: m >= 18,
    ]

    tags = [
        'early morning',
        'morning',
        'afternoon',
        'night',
    ]
    tags = np.array(tags)

    try:
        df['time of day'] = list(df.timeStamp.map(lambda m: tags[np.array([f(m.hour) for f in compares])][0]))
    except:
        logger.error('Unable to add the time of day column. Please check the DataFrame for the person.')

    return df

def getMonthsTuples(startDate):
    '''
    This function takes a `startDate` and returns a list 
    of tuples by dividing up the list into 28 day segments
    In case there is an error, this will just return an
    empty array, and log the error
    '''

    logger    = logging.getLogger('monthlyReportNew.calculations.getMonthsTuples')
    logger.info('Calculating month tuples')

    try:
        currentDate = np.datetime64(dt.now())
        numDays     = int((currentDate - startDate)/( np.timedelta64(1, 'D') ))

        result = []
        for n in range(  (numDays/28) ):
            d1, d2 = startDate+np.timedelta64( n*28 , 'D'), startDate+np.timedelta64( (n+1)*28 , 'D')
            result.append((d1, d2))
    except:
        logger.error('Unable to generate the month tuples. Returning an empty array')
        return []

    logger.info('[%d] tuples found'%len(result))

    return result

def prePostGlucose(glucose, food):
    '''
    Tags the glucose reading as either premeal or postmeal

    This function takes the table for the 
    glucose and the food, and augments the 
    value of information about whether a 
    particular glucose level preMeal or 
    postMeal

    Args:
    -----
        DataFrame: glucose
            The dataframe containing informamation about the glycometer
            readings of the person
        DataFrame: food
            The dataFrame containing information about the time a particular
            food was rated.
    '''

    logger    = logging.getLogger('monthlyReportNew.calculations.prePostGlucose')
    logger.info('Tagging values for the pre-glucose and post-glucose levels')

    allData = pd.merge( glucose[['timeStamp', 'glucose']], food[['timeStamp', 'food']],
                                on='timeStamp', how='outer' )
    allData = allData.sort_values('timeStamp')

    allData['day']          = allData.timeStamp.map(lambda m: m.day)
    allData['glucoseTag']   = 'unknown'
    allData['glucoseSpike'] = np.nan

    for d, df in allData.groupby('day'):

        if df.food.isnull().all()    : continue
        if df.glucose.isnull().all() : continue

        for ts in list(df.ix[ df.glucose.notnull(), 'timeStamp']):
        
            # Check if this is pre meal
            temp = df.ix[ df.timeStamp >= ts, ['timeStamp', 'food']].dropna()
            if len(temp) == 0: 
                # print 'No food ratings after this ...'
                pass
            else:
                preTimeDelta = temp.iloc[0].timeStamp - ts
                if preTimeDelta < np.timedelta64(1, 'h'):
                    allData.ix[ allData.timeStamp == ts, 'glucoseTag' ] = 'preMeal'

            # Check if it is post meal ...
            temp = df.ix[ df.timeStamp <= ts, ['timeStamp', 'food']].dropna()
            if len(temp) == 0: 
                pass
            else:
                postTimeDelta = ts - temp.iloc[-1].timeStamp
                if postTimeDelta < np.timedelta64(3, 'h'):
                    allData.ix[ allData.timeStamp == ts, 'glucoseTag' ] = 'postMeal'

    # Spikes are differences between the premeal 
    # and postmeal timestamps ...
    for d, df in allData.groupby('day'):

        if df.food.isnull().all()    : continue
        if df.glucose.isnull().all() : continue

        if 'preMeal' not in list(df.glucoseTag): continue
        if 'postMeal' not in list(df.glucoseTag): continue

        postMeals = np.where( np.array(df.glucoseTag) == 'postMeal')[0]
        preMeals  = np.where( np.array(df.glucoseTag) == 'preMeal')[0]

        for postMeal in postMeals:
            pos = np.where(preMeals + 2 == postMeal)[0]
            if len(pos) > 0:
                spike   = df.iloc[preMeals[pos[0]]+2].glucose - df.iloc[preMeals[pos[0]]].glucose
                spikeTs = df.iloc[preMeals[pos[0]]].timeStamp
                allData.ix[ allData.timeStamp == spikeTs  , 'glucoseSpike'] = spike
                # print allData

    return pd.merge(glucose, allData[['timeStamp', 'glucoseTag', 'glucoseSpike']], on='timeStamp'   )

def toDateStr(d1, d2):
    '''
    Returns a string that denotes the beginning and ending dates

    Given the beginning and ending dates, for an event, this function
    returns a string that nicely parses the beginning and ending times
    and returns a string that can be used in a report. 

    In case there is an error, this function returns an string that does 
    not contain the information of the starting and ending dates.

    Args
    ----
    numpy.datetime, datetime.datetime: d1
        starting date and time
    numpy.datetime, datetime.datetime: d2
        ending date and time

    Returns
    -------
    str: 
        String mentioning the progress report condition
    '''

    logger    = logging.getLogger('monthlyReportNew.calculations.toDateStr')
    
    months = {
        1  : "January",
        2  : "February",
        3  : "March", 
        4  : "April",
        5  : "May",
        6  : "June",
        7  : "July",
        8  : "August",
        9  : "September",
        10 : "October",
        11 : "November",
        12 : "December",
    }

    try:
        d1 = pd.Timestamp(d1)
        d2 = pd.Timestamp(d2)
    except:
        logger.error('Unable to convert the dates to a datetime string ...')
        return 'Your progress report'

    if d1.year == d2.year:
        toReturn = '%s %d'%( months[d1.month] , d1.day )
    else:
        toReturn = '%s %d, %d'%( months[d1.month] , d1.day, d1.year )

    toReturn += ' - %s %d, %d'%( months[d2.month] , d2.day, d2.year )

    return 'Your progress report for ' + toReturn
