#!/usr/bin/env python

# Copyright 2018 Earth Sciences Department, BSC-CNS
#
# This file is part of HERMESv3_GR.
#
# HERMESv3_GR is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# HERMESv3_GR is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with HERMESv3_GR. If not, see <http://www.gnu.org/licenses/>.


import os
import sys
import timeit
import hermesv3_gr.config.settings as settings
import numpy as np


class TemporalDistribution(object):
    """
    TemporalDistribution class that contains all the information for the temporal disaggregation.

    :param starting_date: Date of the first timestep.
    :type starting_date: datetime.datetime

    :param timestep_type: Relation between timesteps. It can be hourly, monthly or yearly.
    :type timestep_type: str

    :param timestep_num: Quantity of timesteps.
    :type timestep_num: int

    :param timestep_freq: Quantity of timestep_type between timesteps.
        eg: If timestep_type = hourly; timestep_freq = 2; The difference between time of each timestep is 2 hours.
    :type timestep_freq: int

    :param monthly_profile_path: Path to the file that contains all the monthly profiles.
    :type monthly_profile_path: str

    :param month_profile_id: ID of the monthly profile to use.
    :type month_profile_id: str

    :param daily_profile_path: Path to the file that contains all the daily profiles.
    :type weekly_profile_path: str

    :param daily_profile_id: ID of the daily profile to use.
    :type weekly_profile_id: str

    :param hourly_profile_path: Path to the file that contains all the hourly profiles.
    :type hourly_profile_path: str

    :param hourly_profile_id: ID of the hourly profile to use.
    :type hourly_profile_id: str

    :param world_info_path: Path to the file that contains the necessary information to do the NetCDF of timezones.
    :type world_info_path: str

    :param auxiliar_files_dir: Path to the directory where will be all the needed auxiliar files like the NetCDf of
        timezones.
    :type auxiliar_files_dir: str
    """
    def __init__(self, starting_date, timestep_type, timestep_num, timestep_freq, monthly_profile_path,
                 month_profile_id, weekly_profile_path, weekly_profile_id, hourly_profile_path, hourly_profile_id,
                 world_info_path, auxiliar_files_dir, grid):
        from timezonefinder import TimezoneFinder

        import pandas as pd

        st_time = timeit.default_timer()
        settings.write_log('\t\tInitializing Temporal.', level=2)

        self.grid = grid

        self.starting_date = starting_date

        self.timestep_type = timestep_type
        self.timestep_num = timestep_num
        self.timestep_freq = timestep_freq

        self.ending_date = self.calculate_ending_date()
        if month_profile_id is not None:
            if len(month_profile_id) > 4:
                if os.path.exists(month_profile_id):
                    self.monthly_profile = self.read_gridded_profile(month_profile_id, 'Fmonth')
                    self.monthly_profile = self.monthly_profile.reshape(
                        (self.monthly_profile.shape[0], self.monthly_profile.shape[1] * self.monthly_profile.shape[2]))

                    self.monthly_profile_path = month_profile_id
                else:
                    settings.write_log('ERROR: Check the .err file to get more info.')
                    if settings.rank == 0:
                        raise IOError('ERROR: Gridded monthly profile file not found: {0}'.format(month_profile_id))
                    sys.exit(1)
            else:
                self.monthly_profile = self.get_temporal_monthly_profile(monthly_profile_path, month_profile_id)
        else:
            self.monthly_profile = None
            settings.write_log("\t\t\tNo temporal monthly profile is set.", level=3)

        self.weekly_profile_id = weekly_profile_id
        self.weekly_profile_path = weekly_profile_path
        if weekly_profile_id is not None:
            settings.write_log("\t\t\tGetting temporal daily profile id '{0}' from {1} .".format(
                weekly_profile_id, weekly_profile_path), level=3)
        else:
            settings.write_log("\t\t\tNo temporal daily profile is set.", level=3)

        self.hourly_profile_path = hourly_profile_path
        if hourly_profile_id is not None and len(hourly_profile_id) is 4:
            self.hourly_profile = self.get_temporal_hourly_profile(hourly_profile_id)
            settings.write_log("\t\t\tGetting temporal hourly profile id '{0}' from {1} .".format(
                hourly_profile_id, hourly_profile_path), level=3)
        else:
            if hourly_profile_id is None:
                settings.write_log("\t\t\tNo temporal hourly profile is set.", level=3)
            else:
                settings.write_log("\t\t\tGetting temporal hourly profile ids {0} from {1} .".format(
                    hourly_profile_id, hourly_profile_path), level=3)
            self.hourly_profile = hourly_profile_id

        self.world_info = world_info_path
        self.netcdf_timezones = os.path.join(auxiliar_files_dir, 'timezones.nc')

        self.hours_since = []

        self.world_info_df = pd.read_csv(self.world_info, sep=';')
        self.tf = TimezoneFinder()

        if not self.is_created_netcdf_timezones():
            settings.write_log("\t\tTimezones netCDF is not created. Lets try to create it.", level=1)
            self.create_netcdf_timezones(grid)
        self.timezones_array = self.calculate_timezones()

        settings.write_time('TemporalDistribution', 'Init', timeit.default_timer() - st_time, level=2)

    def calculate_ending_date(self):
        """
        Calculate the date of the last timestep.

        :return: Date of the last timestep
        :rtype: datetime.datetime
        """
        from datetime import timedelta

        st_time = timeit.default_timer()

        if self.timestep_type == 'hourly':
            end_date = self.starting_date + (self.timestep_num - 1) * timedelta(hours=self.timestep_freq)
        elif self.timestep_type == 'daily':
            end_date = self.starting_date + (self.timestep_num - 1) * timedelta(hours=self.timestep_freq * 24)
        elif self.timestep_type == 'monthly':
            delta_year = (self.timestep_num - 1) * self.timestep_freq // 12
            delta_month = (self.timestep_num - 1) * self.timestep_freq % 12
            end_date = self.starting_date.replace(year=self.starting_date.year + delta_year,
                                                  month=self.starting_date.month + delta_month)
        elif self.timestep_type == 'yearly':
            delta_year = (self.timestep_num - 1) * self.timestep_freq
            end_date = self.starting_date.replace(year=self.starting_date.year + delta_year)
        else:
            end_date = self.starting_date

        settings.write_time('TemporalDistribution', 'calculate_ending_date', timeit.default_timer() - st_time, level=3)

        return end_date

    def calculate_timedelta(self, date):
        """
        Calculate the difference of time to the next timestep.

        :param date: Date of the current timestep.
        :type date: datetime.datetime

        :return: Difference of time to the next timestep.
        :rtype: datetime.timedelta
        """
        from datetime import timedelta
        from calendar import monthrange, isleap

        st_time = timeit.default_timer()

        if self.timestep_type == 'hourly':
            delta = timedelta(hours=self.timestep_freq)
        elif self.timestep_type == 'daily':
            delta = timedelta(hours=self.timestep_freq * 24)
        elif self.timestep_type == 'monthly':
            days = monthrange(date.year, date.month)[1]
            delta = timedelta(hours=days * 24)
        elif self.timestep_type == 'yearly':
            if isleap(date.year):
                delta = timedelta(hours=366 * 24)
            else:
                delta = timedelta(hours=365 * 24)
        else:
            delta = None

        settings.write_time('TemporalDistribution', 'calculate_ending_date', timeit.default_timer() - st_time, level=3)

        return delta

    def get_tz_from_id(self, tz_id):
        """
        Extract the timezone (string format) for the given id (int).

        :param tz_id: ID of the timezone.
        :type tz_id: int

        :return: Timezone
        :rtype: str
        """
        tz = self.world_info_df.time_zone[self.world_info_df.time_zone_code == tz_id].values

        return tz[0]

    def get_id_from_tz(self, tz):
        """
        Extract the id (int) for the given timezone (string format).

        :param tz: Timezone of the ID.
        :type tz: str

        :return: ID
        :rtype: int
        """
        tz_id = self.world_info_df.time_zone_code[self.world_info_df.time_zone == tz].values

        try:
            tz_id = tz_id[0]
        except IndexError:
            settings.write_log('ERROR: Check the .err file to get more info.')
            if settings.rank == 0:
                raise ValueError("ERROR: The timezone '{0}' is not in the {1} file. ".format(tz, self.world_info) +
                                 "Please add it as a new line with an unique time_zone_code " +
                                 "and the corresponding country and country_code.")
            sys.exit(1)

        return tz_id

    @staticmethod
    def parse_tz(timezone):
        """
        Parse the timezone (string format).

        It is needed because some libraries have more timezones than others and it
        tries to simplify setting the strange ones into the nearest common one.
        Examples:
            'America/Punta_Arenas': 'America/Santiago',
            'Europe/Astrakhan': 'Europe/Moscow',
            'Asia/Atyrau': 'Asia/Aqtau',
            'Asia/Barnaul': 'Asia/Almaty',
            'Europe/Saratov': 'Europe/Moscow',
            'Europe/Ulyanovsk': 'Europe/Moscow',
            'Europe/Kirov': 'Europe/Moscow',
            'Asia/Tomsk': 'Asia/Novokuznetsk',
            'America/Fort_Nelson': 'America/Vancouver'

        :param timezone: Not parsed timezone.
        :type timezone: str

        :return: Parsed timezone
        :rtype: str
        """
        tz_dict = {
            'America/Punta_Arenas': 'America/Santiago',
            'Europe/Astrakhan': 'Europe/Moscow',
            'Asia/Atyrau': 'Asia/Aqtau',
            'Asia/Barnaul': 'Asia/Almaty',
            'Europe/Saratov': 'Europe/Moscow',
            'Europe/Ulyanovsk': 'Europe/Moscow',
            'Europe/Kirov': 'Europe/Moscow',
            'Asia/Tomsk': 'Asia/Novokuznetsk',
            'America/Fort_Nelson': 'America/Vancouver',
            'Asia/Famagusta': 'Asia/Nicosia',
        }

        if timezone in tz_dict.iterkeys():
            timezone = tz_dict[timezone]

        return timezone

    def find_closest_timezone(self, latitude, longitude):
        """
        Find the closest timezone for the given coordinates.

        :param latitude: Latitude coordinate to find timezone.
        :type latitude: float

        :param longitude: Longitude coordinate fo find the timezone.
        :type longitude: float

        :return: Nearest timezone of the given coordinates.
        :rtype: str
        """
        st_time = timeit.default_timer()

        degrees = 0
        timezone = None
        while timezone is None:
            timezone = self.tf.closest_timezone_at(lng=longitude, lat=latitude, delta_degree=degrees)
            degrees += 1

        settings.write_time('TemporalDistribution', 'find_closest_timezone', timeit.default_timer() - st_time, level=3)

        return timezone

    def is_created_netcdf_timezones(self):
        """
        Check if the NetCDF of timezones is created

        :return: True if it is already created.
        :rtype: bool
        """
        return os.path.exists(self.netcdf_timezones)

    def create_netcdf_timezones(self, grid):
        """
        Create a NetCDF with the timezones in the resolution of the given grid.

        :param grid: Grid object with the coordinates.
        :type grid: Grid

        :return: True if it is created.
        :rtype: bool
        """
        from hermesv3_gr.tools.netcdf_tools import write_netcdf

        st_time = timeit.default_timer()
        settings.write_log("\t\tCreating {0} file.".format(self.netcdf_timezones), level=2)

        lat, lon = grid.get_coordinates_2d()
        total_lat = settings.comm.gather(lat, root=0)
        total_lon = settings.comm.gather(lon, root=0)

        dst_var = []

        num = 0
        points = zip(lat.flatten(), lon.flatten())

        for lat_aux, lon_aux in points:
            num += 1
            settings.write_log("\t\t\tlat:{0}, lon:{1} ({2}/{3})".format(lat_aux, lon_aux, num, len(points)), level=3)
            timezone = self.find_closest_timezone(lat_aux, lon_aux)
            tz_id = self.get_id_from_tz(timezone)
            dst_var.append(tz_id)
        dst_var = np.array(dst_var)
        dst_var = dst_var.reshape((1,) + lat.shape)
        dst_var = settings.comm.gather(dst_var, root=0)
        if settings.rank == 0:
            total_lat = np.concatenate(total_lat, axis=1)
            total_lon = np.concatenate(total_lon, axis=1)
            dst_var = np.concatenate(dst_var, axis=2)
            data = [{'name': 'timezone_id', 'units': '', 'data': dst_var}]

            write_netcdf(self.netcdf_timezones, total_lat, total_lon, data, regular_latlon=True)
        settings.comm.Barrier()

        settings.write_time('TemporalDistribution', 'create_netcdf_timezones', timeit.default_timer() - st_time,
                            level=2)

        return True

    def read_gridded_profile(self, path, value):
        # TODO Documentation
        """

        :param path:
        :param value:
        :return:
        """
        from netCDF4 import Dataset

        st_time = timeit.default_timer()

        settings.write_log('\t\t\tGetting gridded temporal monthly profile from {0} .'.format(path), level=3)

        nc_in = Dataset(path)
        profile = nc_in.variables[value][:, self.grid.x_lower_bound:self.grid.x_upper_bound,
                                         self.grid.y_lower_bound:self.grid.y_upper_bound]
        nc_in.close()

        profile[profile <= 0] = 1

        settings.write_time('TemporalDistribution', 'read_gridded_profile', timeit.default_timer() - st_time, level=3)

        return profile

    def calculate_timezones(self):
        """
        Calculate the timezones ID's from the NetCDF and convert them to the timezone (str).

        :return: Array with the timezone of each cell.
        :rtype: numpy.chararray
        """
        from netCDF4 import Dataset

        st_time = timeit.default_timer()

        nc_in = Dataset(self.netcdf_timezones)
        timezones = nc_in.variables['timezone_id'][:, self.grid.x_lower_bound:self.grid.x_upper_bound,
                                                   self.grid.y_lower_bound:self.grid.y_upper_bound].astype(int)

        nc_in.close()
        tz_list = np.chararray(timezones.shape, itemsize=32)
        for id_aux in xrange(timezones.min(), timezones.max() + 1):
            try:
                timezone = self.get_tz_from_id(id_aux)
                tz_list[timezones == id_aux] = timezone
            except IndexError:
                pass
        settings.write_time('TemporalDistribution', 'calculate_timezones', timeit.default_timer() - st_time, level=3)

        return tz_list

    def calculate_2d_temporal_factors(self, date):
        """
        Calculate the temporal factor to correct the input data of the given date for each cell.

        :param date: Date of the current timestep.
        :type date: datetime.datetime

        :return: 2D array with the factors to correct the input data to the date of this timestep.
        :rtype: numpy.array
        """
        import pytz
        import pandas as pd

        st_time = timeit.default_timer()

        df = pd.DataFrame(self.timezones_array.flatten(), columns=['tz'])
        df['i'] = df.index

        df['utc'] = pd.to_datetime(date)
        try:
            df['local'] = df.groupby('tz')['utc'].apply(
                lambda x: pd.to_datetime(x).dt.tz_localize(pytz.utc).dt.tz_convert(x.name).dt.tz_localize(None))
        except pytz.exceptions.UnknownTimeZoneError:
            df['local'] = df.groupby('tz')['utc'].apply(
                lambda x: pd.to_datetime(x).dt.tz_localize(pytz.utc).dt.tz_convert(
                    self.parse_tz(x.name)).dt.tz_localize(None))
        df.set_index('local', inplace=True)

        df['month'] = df.index.month
        df['weekday'] = df.index.weekday
        df['hour'] = df.index.hour

        if self.hourly_profile is not None:
            if isinstance(self.hourly_profile, dict):
                df['hour_factor'] = df['hour'].map(self.hourly_profile)
            else:
                profile_ids = self.parse_hourly_profile_id()
                weekday_profile = self.get_temporal_hourly_profile(profile_ids['weekday'])
                saturday_profile = self.get_temporal_hourly_profile(profile_ids['saturday'])
                sunday_profile = self.get_temporal_hourly_profile(profile_ids['sunday'])
                df['weekday_factor'] = df['hour'].map(weekday_profile)
                df['saturday_factor'] = df['hour'].map(saturday_profile)
                df['sunday_factor'] = df['hour'].map(sunday_profile)

                del df['tz'], df['utc']
                df['hour_factor'] = 0

                df.loc[df['weekday'] <= 4, 'hour_factor'] = df['weekday_factor'][df['weekday'] <= 4].values
                df.loc[df['weekday'] == 5, 'hour_factor'] = df['saturday_factor'][df['weekday'] == 5].values
                df.loc[df['weekday'] == 6, 'hour_factor'] = df['sunday_factor'][df['weekday'] == 6].values

                del df['weekday_factor'], df['saturday_factor'], df['sunday_factor']
        else:
            df['hour_factor'] = 1
        del df['hour']

        if self.weekly_profile_id is not None:
            daily_profile = self.get_temporal_daily_profile(date)
            df['week_factor'] = df['weekday'].map(daily_profile)
        else:
            df['week_factor'] = 1
        del df['weekday']

        if self.monthly_profile is None:
            df['month_factor'] = 1
        elif isinstance(self.monthly_profile, dict):
            df['month_factor'] = df['month'].map(self.monthly_profile)
        elif isinstance(self.monthly_profile, np.ndarray):
            for m, df_aux in df.groupby('month'):
                try:
                    df.loc[df['month'] == m, 'month_factor'] = \
                        self.monthly_profile[m - 1, df.loc[df['month'] == m, 'i'].values]
                except IndexError:
                    settings.write_log('ERROR: Check the .err file to get more info.')
                    if settings.rank == 0:
                        raise IOError("The gridded temporal profile {0} ".format(self.monthly_profile_path) +
                                      "is not on the output grid resolution.")
                    sys.exit(1)
        else:
            df['month_factor'] = 1
        del df['month']

        df['factor'] = df['month_factor'] * df['week_factor'] * df['hour_factor']

        # TODO make timezones_aray 2D
        factors = np.array(df['factor'].values).reshape((self.timezones_array.shape[1], self.timezones_array.shape[2]))
        del df

        settings.write_time('TemporalDistribution', 'calculate_2d_temporal_factors', timeit.default_timer() - st_time,
                            level=3)

        return factors

    def calculate_3d_temporal_factors(self):
        """
        Calculate the temporal factor to correct the input data of the given date for each cell.

        :return: 3D array with the factors to correct the input data to the date of this timestep.
        :rtype: numpy.array
        """
        st_time = timeit.default_timer()
        settings.write_log("\tCalculating temporal factors.", level=2)

        factors = []
        date = self.starting_date
        count = 0

        while date <= self.ending_date:
            count += 1
            settings.write_log("\t\t{0} temporal factor ({1}/{2}).".format(
                date.strftime('%Y/%m/%d %H:%M:%S'), count, self.timestep_num), level=3)

            factors.append(self.calculate_2d_temporal_factors(date))

            date_aux = date - self.starting_date
            self.hours_since.append(date_aux.seconds / 3600 + date_aux.days * 24)  # 3600 seconds per hour
            date = date + self.calculate_timedelta(date)

        factors = np.array(factors)

        settings.write_time('TemporalDistribution', 'calculate_3d_temporal_factors', timeit.default_timer() - st_time,
                            level=3)
        return factors

    def parse_hourly_profile_id(self):
        """
        Parse the hourly profile ID to get a dictionary with the ID for "weekday", "saturday" and "sunday"

        :return:
        """
        import re

        dict_aux = {}
        list_aux = list(map(str, re.split(' , | ,|, |,| ', self.hourly_profile)))
        for element in list_aux:
            key_value_list = list(map(str, re.split(':| :|: | : |=| =|= | = ', element)))
            dict_aux[key_value_list[0]] = key_value_list[1]

        return dict_aux

    def get_temporal_hourly_profile(self, profile_id, date=None):
        """
        Extract the hourly profile of the given ID in a dictionary format.

        The hour (0 to 23) is the key (int) and the value (float) is the factor.

        :param profile_id: ID of the hourly profile to use.
        :type profile_id: str

        :param date: Date of the timestep to simulate. Not necessary for a single ID.
        :type date: datetime.datetime

        :return: Hourly profile where the hour (0 to 23) is the key (int) and the value (float) is the factor.
        :rtype: dict
        """
        import pandas as pd

        st_time = timeit.default_timer()
        if date is None:
            df = pd.read_csv(self.hourly_profile_path)
            try:
                profile = df.loc[df[df.TP_H == profile_id].index[0]].to_dict()
            except IndexError:
                settings.write_log('ERROR: Check the .err file to get more info.')
                if settings.rank == 0:
                    raise AttributeError('ERROR: Hourly profile ID {0} is not in the {1} file.'.format(
                        profile_id, self.hourly_profile_path))
                sys.exit(1)
            profile.pop('TP_H', None)
            profile = {int(k): float(v) for k, v in profile.items()}
        else:
            profile = None
        settings.write_time('TemporalDistribution', 'get_temporal_hourly_profile', timeit.default_timer() - st_time,
                            level=3)

        return profile

    def get_temporal_daily_profile(self, date):
        """
        Extract the daily profile of the given ID in a dictionary format.

        The weekday (0 to 6) is the key (int) and the value (float) is the factor.

        :param date: Date of the timestep to simulate.
        :type date: datetime.datetime

        :return: Daily profile where the weekday (0 to 6) is the key (int) and the value (float) is the factor.
        :rtype: dict
        """
        import pandas as pd

        st_time = timeit.default_timer()

        if self.weekly_profile_id is not None:
            df = pd.read_csv(self.weekly_profile_path)
            try:
                profile = df.loc[df[df.TP_W == self.weekly_profile_id].index[0]].to_dict()
            except IndexError:
                settings.write_log('ERROR: Check the .err file to get more info.')
                if settings.rank == 0:
                    raise AttributeError('ERROR: Daily profile ID {0} is not in the {1} file.'.format(
                        self.weekly_profile_id, self.weekly_profile_path))
                sys.exit(1)
            profile.pop('TP_W', None)
            profile_aux = {int(k): float(v) for k, v in profile.items()}
            rebalance_factor = self.calculate_rebalance_factor(profile_aux, date)
            profile = {int(k): float(v) + rebalance_factor for k, v in profile.items()}
        else:
            profile = None

        settings.write_time('TemporalDistribution', 'get_temporal_daily_profile', timeit.default_timer() - st_time,
                            level=3)

        return profile

    def calculate_rebalance_factor(self, profile, date):
        """
        Calculate the necessary factor make consistent the full month data.

        This is needed for the months that if you sum the daily factor of each day of the month it doesn't sum as
        the number of days of the month.

        :param profile: Daily profile.
        :type profile: dict

        :param date: Date of the timestep to simulate.
        :type date: datetime.datetime

        :return: Rebalance factor to be sum to the daily factor.
        :rtype: float
        """
        st_time = timeit.default_timer()

        weekdays = self.calculate_weekdays(date)
        rebalance_factor = self.calculate_weekday_factor_full_month(profile, weekdays)

        settings.write_time('TemporalDistribution', 'calculate_rebalance_factor', timeit.default_timer() - st_time,
                            level=3)

        return rebalance_factor

    @staticmethod
    def calculate_weekday_factor_full_month(profile, weekdays):
        # TODO Documentation
        """
        Operate with all the days of the month to get the sum of daily factors of the full month.

        :param profile:
        :param weekdays:
        :return:
        """
        st_time = timeit.default_timer()

        weekdays_factors = 0
        num_days = 0
        for weekday in xrange(7):
            weekdays_factors += profile[weekday] * weekdays[weekday]
            num_days += weekdays[weekday]

        settings.write_time('TemporalDistribution', 'calculate_weekday_factor_full_month',
                            timeit.default_timer() - st_time, level=3)

        return (num_days - weekdays_factors) / num_days

    @staticmethod
    def calculate_weekdays(date):
        # TODO Documentation
        """

        :param date:
        :return:
        """
        from calendar import monthrange, weekday, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY

        st_time = timeit.default_timer()

        weekdays = [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
        days = [weekday(date.year, date.month, d + 1) for d in xrange(monthrange(date.year, date.month)[1])]
        weekdays_dict = {}
        count = 0
        for day in weekdays:
            weekdays_dict[count] = days.count(day)

            count += 1

        settings.write_time('TemporalDistribution', 'calculate_weekdays', timeit.default_timer() - st_time, level=3)
        return weekdays_dict

    @staticmethod
    def get_temporal_monthly_profile(profile_path, profile_id):
        """
        Extract the monthly profile of the given ID in a dictionary format.

        The month (1 to 12) is the key (int) and the value (float) is the factor.

        :param profile_path: Path to the file that contains all the monthly profiles.
        :type profile_path: str

        :param profile_id: ID of the monthly profile to use.
        :type profile_id: str

        :return: Monthly profile where the month (1 to 12) is the key (int) and the value (float) is the factor.
        :rtype: dict
        """
        import pandas as pd

        st_time = timeit.default_timer()

        settings.write_log("\t\t\tGetting temporal monthly profile id '{0}' from {1} .".format(
            profile_id, profile_path), level=3)

        if profile_id is not None:
            df = pd.read_csv(profile_path)
            try:
                profile = df.loc[df[df.TP_M == profile_id].index[0]].to_dict()
            except IndexError:
                settings.write_log('ERROR: Check the .err file to get more info.')
                if settings.rank == 0:
                    raise AttributeError('ERROR: Monthly profile ID {0} is not in the {1} file.'.format(
                        profile_id, profile_path))
                sys.exit(1)
            profile.pop('TP_M', None)
            profile = {int(k): float(v) for k, v in profile.items()}
        else:
            profile = None

        settings.write_time('TemporalDistribution', 'get_temporal_monthly_profile', timeit.default_timer() - st_time,
                            level=2)

        return profile

    @staticmethod
    def calculate_delta_hours(st_date, time_step_type, time_step_num, time_step_freq):
        # TODO Documentation
        """

        :param st_date:
        :param time_step_type:
        :param time_step_num:
        :param time_step_freq:
        :return:
        """
        from datetime import timedelta
        from calendar import monthrange, isleap

        st_time = timeit.default_timer()

        settings.write_log('Calculating time array of {0} time steps starting from {1}.'.format(
            time_step_num, st_date.strftime('%Y/%m/%d %H:%M:%S')))

        if time_step_type == 'hourly':
            end_date = st_date + (time_step_num - 1) * timedelta(hours=time_step_freq)
        elif time_step_type == 'daily':
            end_date = st_date + (time_step_num - 1) * timedelta(hours=time_step_freq * 24)
        elif time_step_type == 'monthly':
            delta_year = (time_step_num - 1) * time_step_freq // 12
            delta_month = (time_step_num - 1) * time_step_freq % 12
            end_date = st_date.replace(year=st_date.year + delta_year,
                                       month=st_date.month + delta_month)
        elif time_step_type == 'yearly':
            delta_year = (time_step_num - 1) * time_step_freq
            end_date = st_date.replace(year=st_date.year + delta_year)
        else:
            end_date = st_date

        date_aux = st_date
        hours_since = []
        while date_aux <= end_date:
            d = date_aux - st_date
            hours_since.append(d.seconds / 3600 + d.days * 24)  # 3600 seconds per hour

            if time_step_type == 'hourly':
                delta = timedelta(hours=time_step_freq)
            elif time_step_type == 'daily':
                delta = timedelta(hours=time_step_freq * 24)
            elif time_step_type == 'monthly':
                days = monthrange(date_aux.year, date_aux.month)[1]
                delta = timedelta(hours=days * 24)
            elif time_step_type == 'yearly':
                if isleap(date_aux.year):
                    delta = timedelta(hours=366 * 24)
                else:
                    delta = timedelta(hours=365 * 24)
            else:
                delta = None

            date_aux = date_aux + delta

        settings.write_time('TemporalDistribution', 'calculate_delta_hours', timeit.default_timer() - st_time, level=2)

        return hours_since


if __name__ == '__main__':
    pass
