import calendar
import datetime
import os.path

import numpy as np
import overrides

import util.math.sort
import util.cache.file
import util.cache.memory
import util.logging

import measurements.universal.data
import measurements.universal.constants
import measurements.constants
import measurements.dop.constants
import measurements.dop.pw.constants

logger = util.logging.logger


## data load functions

def prepare_ladolfi_data(measurement_file, start_date, end_date, valid_data_flag):

    # convert missing values to - Inf
    def convert_missing_values_to_nan(value_string):
        try:
            value = float(value_string)
        except ValueError:
            value = - float('nan')

        return value

    # load data, columns: lat, long, depth, dop, flag
    data = np.loadtxt(measurement_file, converters={3: convert_missing_values_to_nan})
    if data.shape[1] != 5:
        raise ValueError('The data in {} has not 5 columns. Its shape is {}'.format(measurement_file, data.shape))

    # skip invalid data and flag
    data = data[data[:,4]==valid_data_flag]
    data = data[:,:4]
    data = data[data[:,3]>=0]
    data = data[np.logical_not(np.isnan(data[:,3]))]

    # move columns in order long, lat, depth, dop
    data[:,[0, 1]] = data[:,[1, 0]]

    # sort data in priority by long, lat, depth
    data = data[np.lexsort((data[:,2], data[:,1], data[:,0]))]

    # interpolate time
    def convert_date_to_year_float(date):
        number_of_days_in_year = date.timetuple().tm_yday
        number_of_total_days_in_year = calendar.isleap(date.year) + 365
        year_frac = number_of_days_in_year / number_of_total_days_in_year
        year = date.year
        time = year + year_frac
        return time

    start_year_float = convert_date_to_year_float(start_date)
    end_year_float = convert_date_to_year_float(end_date)

    n = data.shape[0]
    t = np.arange(n) / (n-1) * (end_year_float - start_year_float) + start_year_float
    t = t.reshape([n, 1])
    data = np.concatenate((t, data), axis=1)

    logger.debug('{} DOP data sets loaded from {}.'.format(data.shape[0], measurement_file))

    return data


@util.cache.file.decorator(cache_file_function=lambda: os.path.join(measurements.dop.pw.constants.LADOLFI_2002_DIR, measurements.dop.pw.constants.DATA_FILENAME))
def load_ladolfi_2002():
    from measurements.dop.pw.constants import LADOLFI_2002_MEASUREMENT_FILE, LADOLFI_2002_START_DATE, LADOLFI_2002_END_DATE, LADOLFI_2002_VALID_DATA_FLAG
    return prepare_ladolfi_data(LADOLFI_2002_MEASUREMENT_FILE, LADOLFI_2002_START_DATE, LADOLFI_2002_END_DATE, LADOLFI_2002_VALID_DATA_FLAG)


@util.cache.file.decorator(cache_file_function=lambda: os.path.join(measurements.dop.pw.constants.LADOLFI_2004_DIR, measurements.dop.pw.constants.DATA_FILENAME))
def load_ladolfi_2004():
    from measurements.dop.pw.constants import LADOLFI_2004_MEASUREMENT_FILE, LADOLFI_2004_START_DATE, LADOLFI_2004_END_DATE, LADOLFI_2004_VALID_DATA_FLAG
    return prepare_ladolfi_data(LADOLFI_2004_MEASUREMENT_FILE, LADOLFI_2004_START_DATE, LADOLFI_2004_END_DATE, LADOLFI_2004_VALID_DATA_FLAG)



def prepare_yoshimura_data(measurement_file):
    ## columns: lat, long, date, time, dop

    ## calculate time
    def convert_date_to_year(value_bytes):
        value_string = value_bytes.decode()
        d = datetime.datetime.strptime(value_string, '%d-%B-%Y').date()
        year = d.year
        logger.debug('Convering: "{}" is in the year {}.'.format(value_bytes, year))
        return year

    def convert_date_to_number_of_days_in_year(value_bytes):
        value_string = value_bytes.decode()
        d = datetime.datetime.strptime(value_string, '%d-%B-%Y').date()
        number_of_days = d.timetuple().tm_yday
        logger.debug('Convering: "{}" is the {}. day in a year.'.format(value_bytes, number_of_days))
        return number_of_days

    def convert_date_to_number_of_all_days_in_year(value_bytes):
        value_string = value_bytes.decode()
        d = datetime.datetime.strptime(value_string, '%d-%B-%Y').date()
        number_of_days_in_year = calendar.isleap(d.year) + 365
        logger.debug('Convering: The year of "{}" has {} days.'.format(value_bytes, number_of_days_in_year))
        return number_of_days_in_year

    def convert_time_to_day_frac(value_bytes):
        value_string = value_bytes.decode()
        dt = datetime.datetime.strptime(value_string, '%H:%M')
        hour_frac = dt.hour + dt.minute / 60
        day_frac = hour_frac / 24
        logger.debug('Convering: The time "{}" is {} of a day.'.format(value_bytes, day_frac))
        return day_frac

    year = np.loadtxt(measurement_file, usecols=(2,), converters={2: convert_date_to_year})

    number_of_days_in_year = np.loadtxt(measurement_file, usecols=(2,), converters={2: convert_date_to_number_of_days_in_year})
    number_of_all_days_in_year = np.loadtxt(measurement_file, usecols=(2,), converters={2: convert_date_to_number_of_all_days_in_year})
    day_frac = np.loadtxt(measurement_file, usecols=(3,), converters={3: convert_time_to_day_frac})
    year_frac = (number_of_days_in_year + day_frac) / number_of_all_days_in_year

    time = year + year_frac


    ## load lat, long and dop
    lat = np.loadtxt(measurement_file, usecols=(0,))
    long = np.loadtxt(measurement_file, usecols=(1,))
    dop = np.loadtxt(measurement_file, usecols=(4,))
    depth = dop * 0


    ## concatenate columns in order long, lat, depth, dop
    data = np.concatenate((time[:,np.newaxis], long[:,np.newaxis], lat[:,np.newaxis], depth[:,np.newaxis], dop[:,np.newaxis]), axis=1)

    logger.debug('{} DOP data sets loaded from {}.'.format(data.shape[0], measurement_file))

    return data


@util.cache.file.decorator(cache_file_function=lambda: os.path.join(measurements.dop.pw.constants.YOSHIMURA_2007_DIR, measurements.dop.pw.constants.DATA_FILENAME))
def load_yoshimura_2007():
    from measurements.dop.pw.constants import YOSHIMURA_2007_MEASUREMENT_FILE
    return prepare_yoshimura_data(YOSHIMURA_2007_MEASUREMENT_FILE)



## measurement classes

class MeasurementsSingleBase(measurements.universal.data.MeasurementsAnnualPeriodicCache):
    
    def __init__(self, data_set_name, load_data_function, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        
        tracer = 'dop'        
        data_set_name = data_set_name       
        
        sample_lsm = measurements.dop.pw.constants.SAMPLE_LSM
        min_standard_deviation = measurements.dop.constants.DEVIATION_MIN_VALUE
        
        super().__init__(sample_lsm, tracer=tracer, data_set_name=data_set_name, min_standard_deviation=min_standard_deviation, min_measurements_correlation=min_measurements_correlation)
        
        self._load_data_function = load_data_function


    @property
    def points_and_results(self):
        values = self._load_data_function()
        sorted_indices = util.math.sort.lex_sorted_indices(values)
        values = values[sorted_indices]
        return values

    @property
    @util.cache.memory.method_decorator()
    @util.cache.file.decorator()
    @overrides.overrides
    def points(self):
        return self.points_and_results[:, :-1]

    @property
    @util.cache.memory.method_decorator()
    @util.cache.file.decorator()
    @overrides.overrides
    def values(self):
        return self.points_and_results[:, -1]
    



class MeasurementsLadolfi2002(MeasurementsSingleBase):
    
    def __init__(self, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        super().__init__('ladolfi_2002', load_ladolfi_2002, min_measurements_correlation=min_measurements_correlation)


class MeasurementsLadolfi2004(MeasurementsSingleBase):
    
    def __init__(self, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        super().__init__('ladolfi_2004', load_ladolfi_2004, min_measurements_correlation=min_measurements_correlation)


class MeasurementsYoshimura2007(MeasurementsSingleBase):
    
    def __init__(self, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        super().__init__('yoshimura_2007', load_yoshimura_2007, min_measurements_correlation=min_measurements_correlation)


class MeasurementsBase(measurements.universal.data.MeasurementsAnnualPeriodicUnionCache):
    
    def __init__(self, *measurement_list):
        super().__init__(*measurement_list)
        self.standard_deviation_concentration_noise_ratio = measurements.dop.pw.constants.DEVIATION_CONCENTRATION_NOISE_RATIO
        self.fill_strategy = 'point_average'
    
    
    ## standard_deviation_concentration_noise_ratio 
    
    @property
    @util.cache.memory.method_decorator(dependency=('self.fill_strategy', 'self.min_measurements_standard_deviation'))
    @util.cache.file.decorator()
    @overrides.overrides
    def concentration_standard_deviations_for_sample_lsm(self):
        return self.standard_deviation_concentration_noise_ratio * self.average_noise_standard_deviations_for_sample_lsm
    
    @property
    @util.cache.memory.method_decorator(dependency=('self.fill_strategy', 'self.min_measurements_standard_deviation'))
    @util.cache.file.decorator()
    @overrides.overrides
    def concentration_standard_deviations(self):
        return self.standard_deviation_concentration_noise_ratio * self.average_noise_standard_deviations
    

    def _fill_strategy_for_kind(self, kind):
        if kind == 'concentration_standard_deviations':
            fill_strategy = self._fill_strategy_for_kind('average_noise_standard_deviations') + '_ratio_{:g}'.format(self.standard_deviation_concentration_noise_ratio)
        else:
            fill_strategy = super()._fill_strategy_for_kind(kind)
        return fill_strategy


class Measurements(MeasurementsBase):
    
    def __init__(self, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        measurement_list = [ measurement_class(min_measurements_correlation=min_measurements_correlation) for measurement_class in [MeasurementsLadolfi2002, MeasurementsLadolfi2004, MeasurementsYoshimura2007] ]
        super().__init__(*measurement_list)



class MeasurementsNearWaterLadolfi2002(measurements.universal.data.MeasurementsAnnualPeriodicNearWaterCache):
    
    def __init__(self, water_lsm=None, max_box_distance_to_water=0, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        measurements = MeasurementsLadolfi2002(min_measurements_correlation=min_measurements_correlation)
        super().__init__(measurements, water_lsm=water_lsm, max_box_distance_to_water=max_box_distance_to_water)


class MeasurementsNearWaterLadolfi2004(measurements.universal.data.MeasurementsAnnualPeriodicNearWaterCache):
    
    def __init__(self, water_lsm=None, max_box_distance_to_water=0, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        measurements = MeasurementsLadolfi2004(min_measurements_correlation=min_measurements_correlation)
        super().__init__(measurements, water_lsm=water_lsm, max_box_distance_to_water=max_box_distance_to_water)


class MeasurementsNearWaterYoshimura2007(measurements.universal.data.MeasurementsAnnualPeriodicNearWaterCache):
    
    def __init__(self, water_lsm=None, max_box_distance_to_water=0, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        measurements = MeasurementsYoshimura2007(min_measurements_correlation=min_measurements_correlation)
        super().__init__(measurements, water_lsm=water_lsm, max_box_distance_to_water=max_box_distance_to_water)


class MeasurementsNearWater(MeasurementsBase):
    
    def __init__(self, water_lsm=None, max_box_distance_to_water=0, min_measurements_correlation=measurements.universal.constants.CORRELATION_MIN_MEASUREMENTS):
        measurement_list = [ measurement_class(water_lsm=water_lsm, max_box_distance_to_water=max_box_distance_to_water, min_measurements_correlation=min_measurements_correlation) for measurement_class in [MeasurementsNearWaterLadolfi2002, MeasurementsNearWaterLadolfi2004, MeasurementsNearWaterYoshimura2007] ]
        super().__init__(*measurement_list)

