import datetime
import os
import time

import numpy as np


#time rounding functions
def round_to_minute(dt):

    '''rounds datetime object to the nearest minute'''

    dt_start_of_minute = dt.replace(second=0, microsecond=0)
    dt_half_minute = dt.replace(second=30, microsecond=0)

    if dt >= dt_half_minute:
        # round up
        dt = dt_start_of_minute + datetime.timedelta(minutes=1)
    else:
        # round down
        dt = dt_start_of_minute

    return dt

def round_to_hour(dt):

    '''rounds datetime object to the nearest hour'''

    dt_start_of_hour = dt.replace(minute=0, second=0, microsecond=0)
    dt_half_hour = dt.replace(minute=30, second=0, microsecond=0)

    if dt >= dt_half_hour:
        # round up
        dt = dt_start_of_hour + datetime.timedelta(hours=1)
    else:
        # round down
        dt = dt_start_of_hour

    return dt


#functions to interconvert between datetimes and fractional times
def datetime_to_fractional_days_from_reference(dateFuture):

    '''get reference start datetime'''

    datePast = datetime.datetime(1,1,1,0,0)
    difference = dateFuture - datePast
    return difference.total_seconds() / datetime.timedelta(days=1).total_seconds()

def datetime_to_fractional_minutes_from_reference(dateFuture):
    '''get reference start datetime'''

    datePast = datetime.datetime(1,1,1,0,0)
    difference = dateFuture - datePast
    return difference.total_seconds() / datetime.timedelta(minutes=1).total_seconds()

def fractional_days_from_reference_to_datetime(dateFuture):
    '''get reference start datetime'''

    datePast = datetime.datetime(1,1,1,0,0)
    future_timedelta = datetime.timedelta(days=dateFuture)
    return datePast + future_timedelta

def fractional_minutes_from_reference_to_datetime(dateFuture):
    '''get reference start datetime'''

    datePast = datetime.datetime(1,1,1,0,0)
    future_timedelta = datetime.timedelta(minutes=dateFuture)
    return datePast + future_timedelta


def get_datafile_download_timestamp(data_file):
    
    '''get data download timestamp of data file rounded to the nearest minute'''

    data_download_datetime = datetime.datetime.strptime(time.ctime(os.path.getmtime(data_file)), "%a %b %d %H:%M:%S %Y")
    return datetime_to_fractional_minutes_from_reference(round_to_minute(data_download_datetime))


def utc_to_local(utc_dt, station_tz_obj):

    '''convert a UTC datetime to a local datetime, given a dateutil local timezone obj'''
    
    import dateutil

    return utc_dt.replace(tzinfo=dateutil.tz.tzutc()).astimezone(station_tz_obj)


def utc_to_solar(observer, sun):

    '''convert a UTC datetime to a mean solar datetime, given a station longitude
       sidereal time from the ephem module is used in the calculation

       Sidereal time is "the hour angle of the vernal equinox"
       Solar time is "the hour angle of the sun, + 12 hours"

       Apparent solar time is the time based on the current position of the sun.
       It assumes noon to be when the sun is at its highest.
       This seems to be the most obvious way to keep time, but in fact the actual time varies by up to 32 minutes in the course of a year

       Mean solar time makes up for this difference. The sun is no longer always at its highest at noon, but on average (mean) it is.
    '''
    import ephem

    sun.compute(observer)
    hour_angle = observer.sidereal_time() - sun.ra
    return ephem.hours(hour_angle + ephem.hours('12:00')).norm


def create_local_time(station_timezone, utc_times):
  
    '''creates local time variable for station in desired output resolution
       this is done by taking the local timezone string for the station and transforming the UTC times'''
    
    import dateutil
    import pandas as pd

    #check if station timezone is valid --> if not return NaN array
    if pd.isnull(station_timezone):
        return [np.NaN]*len(utc_time)
    else:
        #get local tz obj, from local tz string
        station_tz_obj = dateutil.tz.gettz(station_timezone)
    
    #create list for storing all local datetimes
    local_datetimes = [] 

    #otherwise, iterate through all UTC times
    for utc_time in utc_times:
    
        #convert UTC time to datetime
        utc_datetime = fractional_minutes_from_reference_to_datetime(float(utc_time))

        #convert UTC datetime to local datetime (and append to list)
        local_datetimes.append(utc_to_local(utc_datetime, station_tz_obj).replace(tzinfo=None))

    #return local datetimes converted to minutes since reference time
    return [datetime_to_fractional_minutes_from_reference(local_datetime) for local_datetime in local_datetimes]


def roundTime(dt=None, roundTo=60):
   '''Round a datetime object to any time lapse in seconds
      dt : datetime.datetime object, default now.
      roundTo : Closest number of seconds to round to, default 1 minute.
   '''
   if dt is None : dt = datetime.datetime.now()
   seconds = (dt.replace(tzinfo=None) - dt.min).seconds
   rounding = (seconds+roundTo/2) // roundTo * roundTo
   return dt + datetime.timedelta(0,rounding-seconds,-dt.microsecond)


def create_solar_time(longitude, utc_times):

    '''creates mean solar time variable for station in desired output resolution.
       this is done using sidereal time from the ephem package, and a given longitude, transforming the UTC times
  '''

    import ephem

    #create list for storing all solar datetimes
    solar_datetimes = [] 

    #set ephem location instance
    sun = ephem.Sun()
    observer = ephem.Observer()
    #set observer longitude
    observer.lon = str(longitude)

    #iterate through all UTC times
    for utc_time in utc_times:
    
        #convert UTC time to datetime
        utc_datetime = fractional_minutes_from_reference_to_datetime(float(utc_time))
    
        #set observer date
        observer.date = utc_datetime

        #convert UTC datetime to solar time
        solar_time = utc_to_solar(observer, sun)

        #if solar_time == '24:00:00.00' datetime.strptime will not work, so create solar datetime manually 
        if str(solar_time) == '24:00:00.00':
            solar_time = datetime.datetime(1900, 1, 1, 0, 0)
        #else, convert solar time in ephem.angle format to datetime (ephem.angle has syntax: hh:mm:ss.ff)
        else:
            solar_time = datetime.datetime.strptime(str(solar_time), "%H:%M:%S.%f")
            #round solar time to the nearest minute
            solar_time = roundTime(solar_time)

        #get timedelta between UTC time and solar time (in hours)
        timedelta = (datetime.datetime(1900, 1, 1, solar_time.hour, solar_time.minute) - datetime.datetime(1900, 1, 1, utc_datetime.hour, utc_datetime.minute)).total_seconds()/60./60.

        #assign date to solar time (handling wrapping of time)

        #handle extreme cases near the dateline
        #in some cases the timedelta can be </> 12 hours
        if longitude <= -150:
            #timedelta < -12 hours
            if (timedelta >= 0) & (timedelta < 12.0):
                mod_timedelta = -12-(12.0-timedelta)
            #negative timedelta (needs to be wrapped)
            elif timedelta >= 12:
                mod_timedelta = -(24.0 - timedelta)
            #negative timedelta (no wrapping required)
            else:
                mod_timedelta = timedelta
        elif longitude >= 150:
            #timedelta > +12 hours
            if (timedelta > -12.0) & (timedelta <= 0):
                mod_timedelta = 12+(12.0-np.abs(timedelta))
            #positive timedelta (needs to be wrapped)
            elif timedelta <= -12:
                mod_timedelta = 24.0 - np.abs(timedelta)
            #positive timedelta (no wrapping required)
            else:
                mod_timedelta = timedelta

        #deal with standard wrapping cases
        #negative timedelta (needs to be wrapped)
        elif timedelta >= 12:
            mod_timedelta = -(24.0 - timedelta)
        #positive timedelta (needs to be wrapped)
        elif timedelta <= -12:
            mod_timedelta = 24.0 - np.abs(timedelta)

        #no wrapping required
        else:
            mod_timedelta = timedelta

        #get mean solar datetime (append to list)
        solar_datetimes.append(utc_datetime + datetime.timedelta(hours=mod_timedelta))

    #return solar datetimes converted to minutes since reference time
    return [datetime_to_fractional_minutes_from_reference(solar_datetime) for solar_datetime in solar_datetimes]