import copy
import datetime
import os
import re
import sys
import time

import dateutil
import numpy as np
import pandas as pd
import json

from crossnetwork_processing_functions import get_data_files, create_GHOST_stage_logfile
from GHOST_standards import get_standard_data, get_standard_metadata, country_ISO3166_to_name, standard_data_flag_name_to_data_flag_code, standard_QA_name_to_QA_code
from geographic_functions import local_timezone_name
from network_file_standardise import NetworkFileStandardise
from network_parsers import ParseWMOWDCPCFile
from standardisation_mapping_functions import map_network_classifications_to_standard_classifications, map_network_measurement_information_to_standard_measurement_information, network_data_flag_to_standard_data_flag_mapping
from time_functions import round_to_minute, datetime_to_fractional_minutes_from_reference, get_datafile_download_timestamp
import unit_converter

#dictionary defining units per species
unit_dict = {'Ca++_PREC': 'mg l-1', 'Mg++_PREC': 'mg l-1', 'K+_PREC': 'mg l-1', 'Na+_PREC': 'mg l-1', 'NH4+_PREC': 'mg l-1', 'NO3-_PREC': 'mg l-1', 'Cl-_PREC': 'mg l-1', 'SO4--_PREC': 'mg l-1', 'PO4---_PREC': 'mg l-1', 'F-_PREC': 'mg l-1', 'Zn_PREC': 'mg l-1', 'H+_PREC': 'ueq l-1', 'PREC_ACCUM': 'mm'}

class StandardiseNetworkData:

    '''Convert one data provider original file for a specific year into GHOST standard numpy station specific files'''

    def __init__(self,station_reference, run_details, parameter_details, network_details):
        
        #add passed variables to self
        self.run_details = run_details
        self.parameter_details = parameter_details
        self.network_details = network_details
        self.station_reference = station_reference
        self.written_data = False

        if '_PREC' in self.run_details['parameter']: # ions
            if self.run_details['parameter'] == 'H+_PREC':
                self.raw_parameter = 'Acidity'
            else:
                self.raw_parameter = self.run_details['parameter'].replace('+', '').replace('-', '').replace('_PREC', '')
        else: # precipitation amount
            self.raw_parameter = 'Guage_Depth'
        

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

        #gather data files for current year
        data_files = self.gather_data_files()

        #parse data files (writing standard files out per station)
        for data_file in data_files:
            self.parse_file(data_file)

        #if stage has completed successfully, with some valid data, write a valid process log file
        if self.written_data:
            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)
        #else, if no data has been written for station write an error log file to inform station is not continuing in the pipeline
        elif not self.written_data:
            self.run_details['log_file_str'] += 'NO DATA HAS BEEN WRITTEN FOR STATION'
            create_GHOST_stage_logfile(1,self.run_details)


    def gather_data_files(self):

        '''Function to gather data files to parse per station'''

        #get specific data files for current year
        data_files = get_data_files(self.run_details, self.parameter_details, specific_station=self.station_reference)
        #load external metadata into memory 
        with open(self.run_details['data_root']+'WMO_WDCPC/metadata/{}/processed/WMO_WDCPC_META.json'.format(self.run_details['ghost_version']), 'r', encoding='utf-8') as f:
            self.external_metadata = json.loads(f.read())

        #get standard measured unit quantity
        conv_obj = unit_converter.convert_units(self.parameter_details['standard_units'],self.parameter_details['standard_units'],1)
        self.measured_unit_quantity = conv_obj.output_represented_quantity

        return data_files

    def parse_file(self, data_file):

        '''Function to convert a specific original data file to GHOST standard numpy station specific files'''

        self.run_details['log_file_str'] += 'PROCESSING FILE: {}\n'.format(data_file) 
        
        #create object for parsing WMO_WDCPC annual file, and read file
        parsing_obj = ParseWMOWDCPCFile(data_file, self.run_details)
        parsing_obj.read_data()

        #get unique station references
        #unique_station_references = np.unique(parsing_obj.parsed_data['id_station'])

        #get cut data variables for station
        start_dt = parsing_obj.parsed_data['start_dt']
        end_dt = parsing_obj.parsed_data['end_dt']
        cut_measurements = parsing_obj.parsed_data[self.run_details['parameter']]
        raw_flags = parsing_obj.parsed_data[self.run_details['parameter']+'_flag']
        if self.run_details['parameter'] != 'PREC_ACCUM': # no detection limits for precip depth
            raw_dl = parsing_obj.parsed_data[self.run_details['parameter']+'_dl_flag']
        else:
            raw_dl = [''] * len(parsing_obj.parsed_data[self.run_details['parameter']])

        #create site instances of metadata/data dictionaries
        site_data =     get_standard_data(self.parameter_details)
        site_metadata = get_standard_metadata(self.parameter_details)

        #------------------------------------------------------------------------#
        #PARSE METADATA
        #------------------------------------------------------------------------#

        #GHOST version
        site_metadata['GHOST_version']['value'].append(self.run_details['ghost_version'])

        #initialise string which will store all GHOST process warnings as empty            
        site_metadata['process_warnings']['value'].append('')

        #write known metadata  
        site_metadata['network']['value'].append(self.run_details['network'])
        site_metadata['data_level']['value'].append('none')   
        site_metadata['contact_institution']['value'].append('World Data Centre for Precipitation Chemistry')      
        site_metadata['contact_email_address']['value'].append('manager@qasac-americas.org')    
        site_metadata['data_licence']['value'].append('Data accessed via the Global Atmosphere Watch, World Data Centre for Precipitation Chemistry (http://wdcpc.org)') 
        #assume datum information
        site_metadata['ellipsoid']['value'].append('WGS 84') 
        site_metadata['horizontal_datum']['value'].append('World Geodetic System 1984')
        site_metadata['vertical_datum']['value'].append('tidal - mean sea level')
        site_metadata['projection']['value'].append('geographic') 

        #add external metadata
        # station_ID = self.station_reference # ???
        # for unique_station_code in self.external_metadata.keys():
        #     if unique_station_code == station_ID: # find right station
        #         if self.run_details['parameter'] in self.external_metadata[unique_station_code]['MonitorFullName']['values'][-1]: # find right monitor
        #             unique_ID = unique_station_code # should only exist once
        #             break 
            
        #self.external_metadata['station_reference']
        cut_external_metadata = self.external_metadata[self.station_reference]

        # check if parameter exists in metadata
        if self.raw_parameter+'_units' in cut_external_metadata: # continue processing
            site_metadata['longitude']['value'].append(cut_external_metadata['Longitude']['values'][-1])
            site_metadata['latitude']['value'].append(cut_external_metadata['Latitude']['values'][-1])
            site_metadata['altitude']['value'].append(cut_external_metadata['Altitude']['values'][-1])
            site_metadata['station_name']['value'].append(cut_external_metadata['Station_name']['values'][-1])

            #add reported measurement unit
            #convert raw reported units given to standard name
            raw_reported_units = cut_external_metadata[self.raw_parameter+'_units']['values'][-1]
            if self.run_details['parameter'] == 'H+_PREC':
                raw_reported_units = 'ueq l-1'

            conv_obj = unit_converter.convert_units(raw_reported_units, raw_reported_units, 1, measured_species = self.parameter_details['chemical_formula'])
            site_metadata['measuring_instrument_reported_units']['value'].append(conv_obj.output_standard_units)

            #process measurement process information
            site_metadata, measurement_qa_flags, standard_method_abbreviation = map_network_measurement_information_to_standard_measurement_information('', site_metadata, self.run_details, self.parameter_details)
            #set measurement qa codes which are True as joined metadata str
            site_metadata['measurement_qa_flags'] = {'value':[], 'data_type':object, 'string_format':'lower_short'}
            measurement_qa_flag_codes = []
            for standard_flag_name in measurement_qa_flags.keys():
                if measurement_qa_flags[standard_flag_name]:
                    measurement_qa_flag_codes.append(str(standard_QA_name_to_QA_code[standard_flag_name]))
            site_metadata['measurement_qa_flags']['value'].append(','.join(measurement_qa_flag_codes))

            #iterate through metadata variables, if any columns have not been appended to add a NaN
            for column_header in list(site_metadata.keys()):
                n_column = len(site_metadata[column_header]['value'])
                #if column is empty, then append NaN
                if n_column < 1:
                    site_metadata[column_header]['value'].append(np.NaN)

            #set station_reference (provided network ID + standard measurement method abbreviation)
            site_metadata['station_reference']['value'][-1] = '{}_{}'.format(self.station_reference,standard_method_abbreviation) 

            #get data file download timestamp
            site_metadata['data_download_stamp']['value'][-1] = get_datafile_download_timestamp(data_file)

            #add data revision timestamp, set as 0001-01-01 00:00 for all files
            data_revision_timestamp = datetime_to_fractional_minutes_from_reference(datetime.datetime(1,1,1,0,0))
            site_metadata['data_revision_stamp']['value'][-1] = data_revision_timestamp

            #------------------------------------------------------------------------#
            #PARSE TIME
            #------------------------------------------------------------------------#

            #get local timezone offset from UTC
            local_timezone = local_timezone_name(longitude=float(site_metadata['longitude']['value'][-1]), latitude=float(site_metadata['latitude']['value'][-1]))
            tzinfo = dateutil.tz.gettz(local_timezone)        

            #convert timezone to UTC
            start_dt = [round_to_minute(dt.replace(tzinfo=tzinfo).astimezone(dateutil.tz.tzutc())).replace(tzinfo=None) for dt in start_dt]
            end_dt = [round_to_minute(dt.replace(tzinfo=tzinfo).astimezone(dateutil.tz.tzutc())).replace(tzinfo=None) for dt in end_dt]

            #convert each start measurement datetime to minutes from reference time
            frac_time = [datetime_to_fractional_minutes_from_reference(dt) for dt in start_dt]
            
            #get timedeltas between start and end of measurement window
            temporal_resolutions = []
            for start, end in zip(start_dt,end_dt):
                dt_window = end - start
                #convert hours to minutes
                dt_window_minutes = int(dt_window.total_seconds()/60.)
                temporal_resolutions.append(dt_window_minutes)  

            #save meta_update_stamp, set as first sorted time processed (as minutes relative to reference time)
            site_metadata['meta_update_stamp']['value'][-1] = sorted(frac_time)[0]

            #------------------------------------------------------------------------#
            #PARSE QA/FLAGS
            #------------------------------------------------------------------------#

            flags = []
            qa = []

            #get dictionary necessary for mapping of WMO_WDCPC file data flags to standard data flag codes
            data_flag_mapping_dictionary = network_data_flag_to_standard_data_flag_mapping['WMO_WDCPC']                

            #iterate through network flags
            for raw_flag in raw_flags:

                #lists to append qa/data flags per measurement
                flags_per_measurement = []
                qa_per_measurement = []

                # split by /
                qr_flagg = raw_flag.split('/')
                for qr_flag in qr_flagg:

                    #set GHOST decreed validity/network decreed validity variables initially as 'V'
                    ghost_decreed_validity = 'V'
                    network_decreed_validity = 'V'
        
                    # map qr flag and append
                    standard_data_flag_dict = data_flag_mapping_dictionary[qr_flag]
                    #get mapped standard data flags
                    standard_data_flag_names = standard_data_flag_dict['standard_data_flag_name']
                    #iterate through standard data flags 
                    for standard_data_flag_ii, standard_data_flag_name in enumerate(standard_data_flag_names):
                        #check if standard data flag is empty (i.e. unmapped as non-useful)
                        if standard_data_flag_name != '': 
                            #get corresponding data flag code for standard data flag
                            flag_code = standard_data_flag_name_to_data_flag_code[standard_data_flag_name]
                            #append standard data flag (if not already appended)
                            if flag_code not in flags_per_measurement:
                                flags_per_measurement.append(flag_code)
                            #determine if GHOST/network decreed valid
                            if standard_data_flag_dict['GHOST_decreed_validity'][standard_data_flag_ii] == 'I':
                                ghost_decreed_validity = 'I'
                            if standard_data_flag_dict['network_decreed_validity'][standard_data_flag_ii] == 'I':
                                network_decreed_validity = 'I'

                    #if ghost decreed validity, or network decreed validity is Invalid, then set QA flags
                    if ghost_decreed_validity == 'I':
                        qa_per_measurement.append(standard_QA_name_to_QA_code['Invalid Data Provider Flags - GHOST Decreed'])
                    if network_decreed_validity == 'I':
                        qa_per_measurement.append(standard_QA_name_to_QA_code['Invalid Data Provider Flags - Network Decreed'])

                #append qa_per_measurement, flags_per_measurement to lists to write out
                flags.append(flags_per_measurement)
                qa.append(qa_per_measurement)

            #------------------------------------------------------------------------#
            #PARSE MEASUREMENTS
            #------------------------------------------------------------------------#
            #measurement values do only need simple converting or none

            measurements = []
            lod = []

            #initialise qa as all having timezone doubt qa flag, as get timezone from longitude/latitude
            qa = [[standard_QA_name_to_QA_code['Timezone Doubt']] for i in range(len(frac_time))]

            standard_network_gas_volume = False
            unknown_gas_volume = False
            if self.parameter_details['matrix'] == 'gas':
                site_metadata['network_provided_volume_standard_temperature']['value'][-1] = '298.15'
                site_metadata['network_provided_volume_standard_pressure']['value'][-1] = '1013.25'
                standard_network_gas_volume = True
            else:
                site_metadata['network_provided_volume_standard_temperature']['value'][-1] = np.NaN
                site_metadata['network_provided_volume_standard_pressure']['value'][-1] = np.NaN
                unknown_gas_volume = True

            #check if reported standard units, match required standard units
            #take care to deal with multiple units in file
            for reported_units in site_metadata['measuring_instrument_reported_units']['value']:

                #set flags for writing QA flags associated with assumptions in unit conversion
                unit_conversion_standard_network = False
                unit_conversion_educated_guess = False

                #convert raw reported units given to standard name
                conv_obj = unit_converter.convert_units(reported_units, reported_units, 1, measured_species = self.parameter_details['chemical_formula'])
                standard_reported_units = conv_obj.output_standard_units
                measuring_instrument_reported_unit_quantity = conv_obj.output_represented_quantity 

                #check if reported standard units, match required standard units
                if standard_reported_units != site_data[self.parameter_details['bsc_parameter_name']]['units']:

                    #mass density to mixing ratio?
                    if (measuring_instrument_reported_unit_quantity == 'mass_density') & (self.measured_unit_quantity == 'mole_fraction'):
                        #get conversion factor
                        input_units = {'temperature':'K', 'pressure':'hPa', 'molar_mass':'kg mol-1', 'mass_density':standard_reported_units}
                        #do conversion with network provided sample gas temperature/pressure values
                        #if these are not set, then make educated guess (assume network standards for gases)
                        if (not pd.isnull(site_metadata['network_provided_volume_standard_temperature']['value'][-1])) & (not pd.isnull(site_metadata['network_provided_volume_standard_pressure']['value'][-1])):   
                            input_vals = {'temperature':site_metadata['network_provided_volume_standard_temperature']['value'][-1], 'pressure':site_metadata['network_provided_volume_standard_pressure']['value'][-1], 'molar_mass':unit_converter.get_molecular_mass(self.parameter_details['chemical_formula']), 'mass_density':1.0}
                            unit_conversion_standard_network = True
                        else:
                            input_vals = {'temperature':'298.15', 'pressure':'1013.25', 'molar_mass':unit_converter.get_molecular_mass(self.parameter_details['chemical_formula']), 'mass_density':1.0}
                            unit_conversion_educated_guess = True
                        conv_obj = unit_converter.convert_units(input_units,site_data[self.parameter_details['bsc_parameter_name']]['units'],input_vals,measured_species = self.parameter_details['chemical_formula'],conversion_input_quantity = 'mass_density')
            
                    #mixing ratio to mass density?
                    elif (measuring_instrument_reported_unit_quantity == 'mole_fraction') & (self.measured_unit_quantity == 'mass_density'):
                        input_units = {'temperature':'K', 'pressure':'hPa', 'molar_mass':'kg mol-1', 'mole_fraction':standard_reported_units}     
                        #do conversion with network provided sample gas temperature/pressure values
                        #if these are not set, then make educated guess (assume network standards)
                        if (not pd.isnull(site_metadata['network_provided_volume_standard_temperature']['value'][-1])) & (not pd.isnull(site_metadata['network_provided_volume_standard_pressure']['value'][-1])): 
                            input_vals = {'temperature':site_metadata['network_provided_volume_standard_temperature']['value'][-1], 'pressure':site_metadata['network_provided_volume_standard_pressure']['value'][-1], 'molar_mass':unit_converter.get_molecular_mass(self.parameter_details['chemical_formula']), 'mole_fraction':1.0}
                            unit_conversion_standard_network = True
                        else:
                            input_vals = {'temperature':'298.15', 'pressure':'1013.25', 'molar_mass':unit_converter.get_molecular_mass(self.parameter_details['chemical_formula']), 'mole_fraction':1.0}
                            unit_conversion_educated_guess = True
                        conv_obj = unit_converter.convert_units(input_units,site_data[self.parameter_details['bsc_parameter_name']]['units'],input_vals,measured_species = self.parameter_details['chemical_formula'],conversion_input_quantity = 'mole_fraction')
                    
                    # molarity/equivalent concentration to mass density
                    elif (measuring_instrument_reported_unit_quantity == 'molarity') & (self.measured_unit_quantity == 'mass_density'):
                        input_units = {'molar_mass':'kg mol-1', 'molarity':standard_reported_units}     
                        #do conversion 
                        input_vals = {'molar_mass':unit_converter.get_molecular_mass(self.parameter_details['chemical_formula']), 'molarity':1.0}
                        conv_obj = unit_converter.convert_units(input_units,site_data[self.parameter_details['bsc_parameter_name']]['units'],input_vals,measured_species = self.parameter_details['chemical_formula'],conversion_input_quantity = 'molarity')

                    #otherwise simple unit conversion, i.e. pptv --> ppbv, ng/m3 --> ug/m3
                    else:
                        conv_obj = unit_converter.convert_units(standard_reported_units,site_data[self.parameter_details['bsc_parameter_name']]['units'],1.0, measured_species = self.parameter_details['chemical_formula'])

                    #process through each measurement and convert value using derived conversion factor
                    for n, meas in enumerate(cut_measurements):
                        #only do conversion if measurement value is above 0 and not infinite, and do not have data flag stating the measurement is missing 
                        try:
                            float_meas = float(meas)
                        except:
                            float_meas = np.NaN
                        if (float_meas > 0.0) & (np.isfinite(float_meas)):
                            measurements.append(float_meas*conv_obj.conversion_factor)
                        else:
                            measurements.append(float_meas)

                        if 'PREC' in self.run_details['parameter']:
                            #only do conversion if lod value is >= 0 and not infinite
                            lim = raw_dl[n]
                            try:
                                float_lim = float(lim)
                            except:
                                float_lim = np.NaN
                            if (float_lim >= 0.0) & (np.isfinite(float_lim)) & (standard_data_flag_name_to_data_flag_code['Missing Data'] not in flags[n]):
                                lod.append(float_lim*conv_obj.conversion_factor)
                            else:
                                lod.append(float_lim)

                        #append sample gas volume/unit_conversion assumption QA flags if necessary
                        if standard_network_gas_volume:
                            qa[n].append(standard_QA_name_to_QA_code['Sample Gas Volume - Network Standard'])
                        if unknown_gas_volume:
                            qa[n].append(standard_QA_name_to_QA_code['Sample Gas Volume - Unknown'])
                        if unit_conversion_standard_network:
                            qa[n].append(standard_QA_name_to_QA_code['Unit Conversion - Network Standard Sample Gas Volume Assumption'])
                        if unit_conversion_educated_guess:
                            qa[n].append(standard_QA_name_to_QA_code['Unit Conversion - Educated Guess Sample Gas Volume Assumption'])
                
                #otherwise do nothing to measured values
                else:
                    for n, meas in enumerate(cut_measurements):
                        #order_n.append(inds_per_unit[reported_units][n])
                        try:
                            float_meas = float(meas)
                        except:
                            float_meas = np.NaN
                        measurements.append(float_meas)

                        if 'PREC' in self.run_details['parameter']:
                            lim = raw_dl[n]
                            try:
                                float_lim = float(lim)
                            except:
                                float_lim = np.NaN
                            lod.append(float_lim)

            #keep only measurement windows that are >= 0 minute in temporal resolution, as well as all other associated variables across other fields
            valid_temporal_resolution_inds = [ii for ii, res in enumerate(temporal_resolutions) if res >= 0]
            if len(valid_temporal_resolution_inds) != len(frac_time):
                frac_time = [frac_time[x] for x in valid_temporal_resolution_inds]
                temporal_resolutions = [temporal_resolutions[x] for x in valid_temporal_resolution_inds]
                qa = [qa[x] for x in valid_temporal_resolution_inds]
                measurements = [measurements[x] for x in valid_temporal_resolution_inds]
                if 'PREC' in self.run_details['parameter']:
                    lod = [lod[x] for x in valid_temporal_resolution_inds]
                    
            #----------------------------------------------------------------------------#
            #WRITE OUT DATA
            #----------------------------------------------------------------------------# 
            site_data['time']['value'] = frac_time
            site_data[self.parameter_details['bsc_parameter_name']]['value'] = measurements
            site_data['temporal_resolution']['value'] = temporal_resolutions   
            #set data flags as all un-flagged
            site_data['flag']['value'] = [[standard_data_flag_name_to_data_flag_code['Un-Flagged Data']] for i in range(len(site_data['time']['value']))]
            site_data['qa']['value'] = qa
            if 'PREC' in self.run_details['parameter']:
                site_data['reported_lower_limit_of_detection_per_measurement']['value'] = lod 

            #iterate through data variables, if any columns have not been appended to, add a list of NaNs (equal to length of time array)
            for column_header in list(site_data.keys()):
                n_column = len(site_data[column_header]['value'])
                #if column is empty, then append NaN
                if n_column == 0:
                    if site_data[column_header]['data_type'] != np.uint8:
                        site_data[column_header]['value'] = [np.NaN]*len(site_data['time']['value'])
                    else:
                        site_data[column_header]['value'] = [255]*len(site_data['time']['value'])
            #------------------------------------------------------

            #standardise metadata/data formats and write out station data to numpy file
            write_obj = NetworkFileStandardise(site_metadata, site_data, self.run_details, self.parameter_details)
            #update flag to state that have written data for station (if have done so)
            if write_obj.written:
                self.written_data = True
            else:
                self.run_details['log_file_str'] += 'DATA ALL MISSING IN FILE: {}. SKIPPING.\n'.format(data_file)