import bisect
import datetime
import math
import time

import dateutil
import ephem
import numpy as np
import numpy.lib.recfunctions as rf
import pandas as pd

from crossnetwork_processing_functions import set_file_permissions_ownership, create_GHOST_stage_logfile
from geographic_functions import get_unique_longitude_latitude_pairs
from time_functions import fractional_minutes_from_reference_to_datetime, utc_to_local


class TemporalCodes:

    '''Class for processing temporal classification codes (i.e. day/night, weekday/weekend, season) per data point for a station.'''

    def __init__(self, process_instance):

        #add passed instance variables to self
        self.station_reference = process_instance.station_reference
        self.station_numpy_filename = process_instance.station_numpy_filename
        self.metadata = process_instance.metadata
        self.data = process_instance.data
        self.run_details = process_instance.run_details
        self.parameter_details = process_instance.parameter_details
        self.network_details = process_instance.network_details

        self.run_details['log_file_str'] += 'TEMPORAL CODES\n'


    def do_work(self):
        '''Function which does executes the functions of the class in a pipeline'''

        #get temporal codes
        self.get_temporal_codes()

        #remove some data variables from dataframe that no longer need (as have been used when creating QA flags)
        self.data = rf.drop_fields(self.data, ['reported_lower_limit_of_detection_per_measurement','reported_upper_limit_of_detection_per_measurement'])

        #get start and end of data ranges
        start_time = self.data['time'][0]
        end_time = self.data['time'][-1]+int(self.data['temporal_resolution'][-1])

        #write out to finalised native file directory
        fname = self.run_details['data_root']+'{}/{}/native/{}/{}_{}_{}.npz'.format(self.run_details['network'],self.run_details['ghost_version'],self.parameter_details['bsc_parameter_name'],self.station_reference,start_time,end_time)
        np.savez_compressed(fname,data=self.data,metadata=self.metadata)
        #give 770 permissions for file and make owner bsc32
        set_file_permissions_ownership(fname)
 
        #return valid process logfile (0)
        self.run_details['log_file_str'] += str((time.time() - self.run_details['stage_process_start_time'])/60.)
        create_GHOST_stage_logfile(0,self.run_details,previous_stage_data_files=self.station_numpy_filename)
        

    def get_temporal_codes(self):

        '''iterate through all stored time windows in data array
           and determine if each window measurement was mostly made in the day/night; weekday/weekend/ spring/summer/autumn/winter
           the determined temporal classifications are stored as coded variables.
       
           day/night classification: 
           - only calculated for data with a measurement resolution finer than 1 day, and station timezone str is valid 
           - calculate the solar elevation angle for a latitude/longitude/measurement altitude at a certain timestamp.
           - the solar elevation angle is the angle between the horizon and the centre of the sun's disc.
           - the distinction between day and night is partially subjective. 
           - there are 3 intermediary states between night and day: 
           - 1. astronomical twilight (where solar elevation angle is between -18 deg. and -12 deg.)
           - 2. nautical twilight (where solar elevation angle is between -12 deg. and -6 deg.)
           - 3. civil twilight (where solar elevation angle is between -6 deg. and 0 deg.)
           - sunrise is classically defined as when these states end (where solar elevation angle is >= 0)
           - sunset is classically defined as when these states start (where solar elevation angle is < 0)
           - in this classification, if the solar elevation angle > 0 degrees, time is classed as daytime, otherwise it is nighttime 
           - as it is centre of measurement window time, if 51% of measurement window is night - the window will be classed as night
           - these classes are then converted to codes (Daytime = 0, Nighttime = 1)

           weekday/weekend classification:
           - only calculated for data with a measurement resolution finer than 1 day, and station timezone str is valid 
           - find out which day the centre local time is in, and subsequently if weekday/weekend
           - Weekday = (Monday-Friday), Weekend = (Saturday, Sunday)
           - as it is centre of measurement window time, if 51% of measurement window is Monday - the window will be classed as weekday
           - these classes are then converted to codes (Weekday = 0, Weekend = 1)  

           spring/summer/autumn/winter:
           - only calculated for data with a measurement resolution finer than 31 days
           - find what month the centre UTC time is in, and subsequently in what hemisphere adjusted season 
           - NH Winter/ SH Summer = (December, January, February), NH Spring/ SH Autumn = (March, April, May), NH Summer/ SH Winter = (June, July, August), NH Autumn/ SH Spring = (September, October, November) 
           - as it is centre of measurement window time, if 51% of measurement window is January - the window will be classed as winter
           - these classes are then converted to codes (Spring=0, Summer=1, Autumn=2, Winter=3)
        '''

        #get unique longitude and latitude pairs across metadata, with associated metadata/data indices of each pair
        unique_pair_dictionary = get_unique_longitude_latitude_pairs(self.metadata, self.data)

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

        #define angular radius of the sun
        sun_angular_radius = (32.7/60.0)/2.0

        #iterate through all unique longitude/latitude pairs
        for unique_pair_ii, unique_pair in unique_pair_dictionary.items():

            #get current unique longitude/latitude
            current_longitude = unique_pair['longitude']
            current_latitude = unique_pair['latitude']

            #get metadata indices associated with current unique longitude/latitude pair
            relevant_metadata_indices = unique_pair['metadata_indices']

            #get current local station timezone (will be unique for longitude/latitude pair as it is derived from these)
            station_timezone = self.metadata['station_timezone'][relevant_metadata_indices[0]]
        
            #check if station timezone is valid
            if not pd.isnull(station_timezone):
                #get local tz object, from local tz string
                station_tz_obj = dateutil.tz.gettz(station_timezone)
            else:
                station_tz_obj = None

            #get current hemisphere of longitude/latitude pair (0 deg latitude taken to be NH)
            if current_latitude < 0:
                hemisphere = 'SH'
            elif current_latitude >= 0:
                hemisphere = 'NH'

            #iterate through all current relevant data indices
            for relevant_data_index in unique_pair['data_indices']:

                #get current start time
                current_start_time = self.data['time'][relevant_data_index]

                #convert current window start time to datetime
                current_start_datetime = fractional_minutes_from_reference_to_datetime(float(current_start_time))

                #get temporal resolution of window (in minutes)
                current_temporal_resolution = self.data['temporal_resolution'][relevant_data_index]
                #convert to timedelta (in centre of window)
                centre_timedelta = datetime.timedelta(minutes=float(current_temporal_resolution)/2.) 

                #get datetime in centre of measurement window (in UTC)
                centre_datetime = current_start_datetime + centre_timedelta 

                #convert centre datetime (in UTC) to local time (if have station tz obj)
                if station_tz_obj is not None:
                    local_centre_datetime = utc_to_local(centre_datetime, station_tz_obj)
            
                #-----------------------------------------#
                #calculate daytime/nightime classification code --> using centre window local datetime
                #daytime = 0, nighttime = 1

                #only do classification if measurement resolution finer than 1 day, and have valid local station timezone
                if (current_temporal_resolution < (60*24)) & (station_tz_obj is not None): 
                    
                    #get current measurement altitude for current data index
                    current_measurement_altitude = self.metadata['measurement_altitude'][bisect.bisect(self.metadata['meta_update_stamp'], current_start_time)-1]
            
                    #define observer coordinates
                    observer.lon = str(current_longitude)
                    observer.lat = str(current_latitude)
                    observer.elevation = current_measurement_altitude

                    #set observer date to be centre window local datetime
                    observer.date = local_centre_datetime

                    #compute solar elevation angle (in degrees) --> this angle relates to the centre of the sun disc 
                    sun.compute(observer)
                    solar_elevation_angle = (sun.alt/math.pi)*180.
                    #adjust solar elevation angle to be from the edge of the sun disc (upper limb), by adding the sun angular radius
                    solar_elevation_angle = solar_elevation_angle + sun_angular_radius

                    #if solar elevation angle > 0 degrees, time is classed as daytime 
                    if solar_elevation_angle > 0:
                        self.data['day_night_code'][relevant_data_index] = 0
                    #otherwise time is classed as nighttime
                    else:
                        self.data['day_night_code'][relevant_data_index] = 1
        
                #-----------------------------------------#
                #calculate weekday/weekend classification code --> using centre window local datetime 
                #weekday = 0, weekend = 1        

                #only do classification if measurement resolution finer than 1 day, and have valid local station timezone
                if (current_temporal_resolution < (60*24)) & (station_tz_obj is not None):  

                    #determine if centre window local datetime is weekday or weekend
                    #get n_day of week, where Monday = 0 and Sunday = 6
                    n_day_of_week = local_centre_datetime.weekday()
                    #weekday
                    if n_day_of_week < 5:
                        #write out weekday code (0)
                        self.data['weekday_weekend_code'][relevant_data_index] = 0
                    #weekend
                    else:
                        #write out weekend code (1)
                        self.data['weekday_weekend_code'][relevant_data_index] = 1

                #-----------------------------------------#
                #calculate season classification code --> using centre window datetime (UTC)
                #Spring=0, Summer=1, Autumn=2, Winter=3

                 #only do classification if measurement resolution finer than 31 days
                if current_temporal_resolution < ((60*24)*31):
            
                    #NH Winter/ SH Summer (December, January, February)
                    if centre_datetime.month in [12,1,2]:
                        if hemisphere == 'NH':
                            self.data['season_code'][relevant_data_index] = 3
                        elif hemisphere == 'SH':
                            self.data['season_code'][relevant_data_index] = 1

                    #NH Spring/ SH Autumn (March, April, May)
                    elif centre_datetime.month in [3,4,5]:
                        if hemisphere == 'NH':
                            self.data['season_code'][relevant_data_index] = 0
                        elif hemisphere == 'SH':
                            self.data['season_code'][relevant_data_index] = 2
            
                    #NH Summer/ SH Winter (June, July, August)
                    elif centre_datetime.month in [6,7,8]:
                        if hemisphere == 'NH':
                            self.data['season_code'][relevant_data_index] = 1
                        elif hemisphere == 'SH':
                            self.data['season_code'][relevant_data_index] = 3
            
                    #NH Autumn/ SH Spring (September, October, November)
                    elif centre_datetime.month in [9,10,11]:
                        if hemisphere == 'NH':
                            self.data['season_code'][relevant_data_index] = 2
                        elif hemisphere == 'SH':
                            self.data['season_code'][relevant_data_index]= 0