#!/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/>.


from configargparse import ArgParser
from warnings import warn
import os
from shutil import rmtree
from mpi4py import MPI
from hermesv3_gr import __version__


class Config(ArgParser):
    """
    Initialization of the arguments that the parser can handle.
    """
    def __init__(self, comm=None):
        if comm is None:
            comm = MPI.COMM_WORLD
        self.comm = comm

        super(Config, self).__init__()
        self.options = self.read_options()

    def read_options(self):
        """
        Reads all the arguments from command line or from the configuration file.
        The value of an argument given by command line has high priority that the one that appear in the
        configuration file.

        :return: Arguments already parsed.
        :rtype: Namespace
        """
        # p = ArgParser(default_config_files=['/home/Earth/mguevara/HERMES/HERMESv3/IN/conf/hermes.conf'])
        p = ArgParser()
        p.add_argument('-c', '--my-config', required=False, is_config_file=True, help='Path to the configuration file.')
        p.add_argument('--version', '-V', action='version', version="%(prog)s " + __version__)
        # TODO Detallar mas que significan 1, 2  y 3 los log_level
        p.add_argument('--log_level', required=True, help='Level of detail of the running process information.',
                       type=int, choices=[1, 2, 3])

        p.add_argument('--input_dir', required=True, help='Path to the input directory of the model.')
        p.add_argument('--data_path', required=True, help='Path to the data necessary for the model.')
        p.add_argument('--output_dir', required=True, help='Path to the output directory of the model.')
        p.add_argument('--output_name', required=True,
                       help="Name of the output file. You can add the string '<date>' that will be substitute by the " +
                            "starting date of the simulation day.")
        p.add_argument('--start_date', required=True, help='Starting Date to simulate (UTC)')
        p.add_argument('--end_date', required=False, default=None,
                       help='If you want to simulate more than one day you have to specify the ending date of ' +
                            'simulation in this parameter. If it is not set end_date = start_date.')

        p.add_argument('--output_timestep_type', required=True, help='Type of timestep.',
                       type=str, choices=['hourly', 'daily', 'monthly', 'yearly'])
        p.add_argument('--output_timestep_num', required=True, help='Number of timesteps to simulate.', type=int)
        p.add_argument('--output_timestep_freq', required=True, help='Frequency between timesteps.', type=int)
        p.add_argument('--first_time', required=False, default='False', type=str,
                       help='Indicates if you want to run it for first time (only create auxiliary files).')
        p.add_argument('--erase_auxiliary_files', required=False, default='False', type=str,
                       help='Indicates if you want to start from scratch removing the auxiliary files already created.')

        p.add_argument('--output_model', required=True, help='Name of the output model.',
                       choices=['MONARCH', 'CMAQ', 'WRF_CHEM', 'MOCAGE'])
        p.add_argument('--compression_level', required=False, type=int, choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
                       help='Compression level of the NetCDF output (0 for no compressed output).', default=4)

        p.add_argument('--output_attributes', required=False,
                       help='Path to the file that contains the global attributes.')

        p.add_argument('--domain_type', required=True, help='Type of domain to simulate.',
                       choices=['global', 'lcc', 'rotated', 'mercator', 'regular', 'rotated_nested'])
        p.add_argument('--auxiliary_files_path', required=True,
                       help='Path to the directory where the necessary auxiliary files will be created if them are ' +
                            'not created yet.')

        p.add_argument('--vertical_description', required=True,
                       help='Path to the file that contains the vertical description of the desired output.')

        # Global arguments
        p.add_argument('--inc_lat', required=False, help='Latitude resolution for a global domain.', type=float)
        p.add_argument('--inc_lon', required=False, help='Longitude resolution for a global domain.', type=float)

        # Regular lat-lon arguments:
        p.add_argument('--lat_orig', required=False, help='Latitude of the corner of the first cell.', type=float)
        p.add_argument('--lon_orig', required=False, help='Longitude of the corner of the first cell.', type=float)
        p.add_argument('--n_lat', required=False, help='Number of latitude elements.', type=float)
        p.add_argument('--n_lon', required=False, help='Number of longitude elements.', type=float)

        # Rotated arguments
        p.add_argument('--centre_lat', required=False,
                       help='Central geographic latitude of grid (non-rotated degrees). Corresponds to the TPH0D ' +
                            'parameter in NMMB-MONARCH.', type=float)
        p.add_argument('--centre_lon', required=False,
                       help='Central geographic longitude of grid (non-rotated degrees, positive east). Corresponds ' +
                            'to the TLM0D parameter in NMMB-MONARCH.', type=float)
        p.add_argument('--west_boundary', required=False,
                       help="Grid's western boundary from center point (rotated degrees). Corresponds to the WBD " +
                            "parameter in NMMB-MONARCH.", type=float)
        p.add_argument('--south_boundary', required=False,
                       help="Grid's southern boundary from center point (rotated degrees). Corresponds to the SBD " +
                            "parameter in NMMB-MONARCH.", type=float)
        p.add_argument('--inc_rlat', required=False,
                       help='Latitudinal grid resolution (rotated degrees). Corresponds to the DPHD parameter in ' +
                            'NMMB-MONARCH.', type=float)
        p.add_argument('--inc_rlon', required=False,
                       help='Longitudinal grid resolution (rotated degrees). Corresponds to the DLMD parameter  ' +
                            'in NMMB-MONARCH.', type=float)

        # Rotated_nested options
        p.add_argument('--parent_grid_path', required=False, type=str,
                       help='Path to the netCDF that contains the grid definition.')
        p.add_argument('--parent_ratio', required=False, type=int,
                       help='Ratio between the parent and the nested domain.')
        p.add_argument('--i_parent_start', required=False, type=int,
                       help='Location of the I to start the nested.')
        p.add_argument('--j_parent_start', required=False, type=int,
                       help='Location of the J to start the nested.')
        p.add_argument('--n_rlat', required=False, type=int,
                       help='Number of rotated latitude points.')
        p.add_argument('--n_rlon', required=False, type=int,
                       help='Number of rotated longitude points.')

        # Lambert conformal conic arguments
        p.add_argument('--lat_1', required=False,
                       help='Standard parallel 1 (in deg). Corresponds to the P_ALP parameter of the GRIDDESC file.',
                       type=float)
        p.add_argument('--lat_2', required=False,
                       help='Standard parallel 2 (in deg). Corresponds to the P_BET parameter of the GRIDDESC file.',
                       type=float)
        p.add_argument('--lon_0', required=False,
                       help='Longitude of the central meridian (degrees). Corresponds to the P_GAM parameter of ' +
                            'the GRIDDESC file.', type=float)
        p.add_argument('--lat_0', required=False,
                       help='Latitude of the origin of the projection (degrees). Corresponds to the Y_CENT  ' +
                            'parameter of the GRIDDESC file.', type=float)
        p.add_argument('--nx', required=False,
                       help='Number of grid columns. Corresponds to the NCOLS parameter of the GRIDDESC file.',
                       type=float)
        p.add_argument('--ny', required=False,
                       help='Number of grid rows. Corresponds to the NROWS parameter of the GRIDDESC file.',
                       type=float)
        p.add_argument('--inc_x', required=False,
                       help='X-coordinate cell dimension (meters). Corresponds to the XCELL parameter of the ' +
                            'GRIDDESC file.', type=float)
        p.add_argument('--inc_y', required=False,
                       help='Y-coordinate cell dimension (meters). Corresponds to the YCELL parameter of the ' +
                            'GRIDDESC file.', type=float)
        p.add_argument('--x_0', required=False,
                       help='X-coordinate origin of grid (meters). Corresponds to the XORIG parameter of the ' +
                            'GRIDDESC file.', type=float)
        p.add_argument('--y_0', required=False,
                       help='Y-coordinate origin of grid (meters). Corresponds to the YORIG parameter of the ' +
                            'GRIDDESC file.', type=float)

        # Mercator
        p.add_argument('--lat_ts', required=False, help='...', type=float)

        p.add_argument('--cross_table', required=True,
                       help='Path to the file that contains the information of the datasets to use.')
        p.add_argument('--p_vertical', required=True,
                       help='Path to the file that contains all the needed vertical profiles.')
        p.add_argument('--p_month', required=True,
                       help='Path to the file that contains all the needed monthly profiles.')
        p.add_argument('--p_week', required=True, help='Path to the file that contains all the needed weekly profiles.')
        p.add_argument('--p_day', required=True, help='Path to the file that contains all the needed daily profiles.')
        p.add_argument('--p_hour', required=True, help='Path to the file that contains all the needed hourly profiles.')
        p.add_argument('--p_speciation', required=True,
                       help='Path to the file that contains all the needed speciation profiles.')
        p.add_argument('--molecular_weights', required=True,
                       help='Path to the file that contains the molecular weights of the input pollutants.')
        p.add_argument('--world_info', required=True,
                       help='Path to the file that contains the world information like timezones, ISO codes, ...')
        p.add_argument('--countries_shapefile', required=False,
                       help='...')

        arguments, unknown = p.parse_known_args()
        if len(unknown) > 0:
            warn("Unrecognized arguments: {0}".format(unknown))

        for item in vars(arguments):
            is_str = isinstance(arguments.__dict__[item], str)
            if is_str:
                arguments.__dict__[item] = arguments.__dict__[item].replace('<data_path>', arguments.data_path)
                arguments.__dict__[item] = arguments.__dict__[item].replace('<input_dir>', arguments.input_dir)
                arguments.__dict__[item] = arguments.__dict__[item].replace('<version>', "v" + __version__)
                arguments.__dict__[item] = arguments.__dict__[item].replace('<domain_type>', arguments.domain_type)
                if arguments.domain_type in ['global', 'regular']:
                    arguments.__dict__[item] = arguments.__dict__[item].replace('<resolution>', '{1}_{2}'.format(
                        item, arguments.inc_lat, arguments.inc_lon))
                elif arguments.domain_type == 'rotated':
                    arguments.__dict__[item] = arguments.__dict__[item].replace('<resolution>', '{1}_{2}'.format(
                        item, arguments.inc_rlat, arguments.inc_rlon))
                elif arguments.domain_type == 'rotated_nested':
                    arguments.__dict__[item] = arguments.__dict__[item].replace('<resolution>', '{1}_{2}'.format(
                        item, arguments.n_rlat, arguments.n_rlon))
                elif arguments.domain_type == 'lcc' or arguments.domain_type == 'mercator':
                    arguments.__dict__[item] = arguments.__dict__[item].replace('<resolution>', '{1}_{2}'.format(
                        item, arguments.inc_x, arguments.inc_y))

        arguments.start_date = self._parse_start_date(arguments.start_date)
        arguments.end_date = self._parse_end_date(arguments.end_date, arguments.start_date)

        arguments.first_time = self._parse_bool(arguments.first_time)
        arguments.erase_auxiliary_files = self._parse_bool(arguments.erase_auxiliary_files)

        self.create_dir(arguments.output_dir)
        if arguments.erase_auxiliary_files:
            if os.path.exists(arguments.auxiliary_files_path):
                if self.comm.Get_rank() == 0:
                    rmtree(arguments.auxiliary_files_path)
            self.comm.Barrier()
        self.create_dir(arguments.auxiliary_files_path)

        return arguments

    def get_output_name(self, date):
        """
        Generates the full path of the output replacing <date> by YYYYMMDDHH, YYYYMMDD, YYYYMM or YYYY depending on the
        output_timestep_type.

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

        :return: Complete path to the output file.
        :rtype: str
        """
        import os
        if self.options.output_timestep_type == 'hourly':
            file_name = self.options.output_name.replace('<date>', date.strftime('%Y%m%d%H'))
        elif self.options.output_timestep_type == 'daily':
            file_name = self.options.output_name.replace('<date>', date.strftime('%Y%m%d'))
        elif self.options.output_timestep_type == 'monthly':
            file_name = self.options.output_name.replace('<date>', date.strftime('%Y%m'))
        elif self.options.output_timestep_type == 'yearly':
            file_name = self.options.output_name.replace('<date>', date.strftime('%Y'))
        else:
            file_name = self.options.output_name
        full_path = os.path.join(self.options.output_dir, file_name)
        return full_path

    def create_dir(self, path):
        """
        Create the given folder if it is not created yet.

        :param path: Path to create.
        :type path: str
        """
        import os
        from mpi4py import MPI

        if not os.path.exists(path):
            os.makedirs(path, exist_ok=True)

    @staticmethod
    def _parse_bool(str_bool):
        """
        Parse the giving string into a boolean.
        The accepted options for a True value are: 'True', 'true', 'T', 't', 'Yes', 'yes', 'Y', 'y', '1'
        The accepted options for a False value are: 'False', 'false', 'F', 'f', 'No', 'no', 'N', 'n', '0'

        If the sting is not in the options it will release a WARNING and the return value will be False.

        :param str_bool: String to convert to boolean.
        :return: bool
        """
        true_options = ['True', 'true', 'T', 't', 'Yes', 'yes', 'Y', 'y', '1', 1, True]
        false_options = ['False', 'false', 'F', 'f', 'No', 'no', 'N', 'n', '0', 0, False, None]

        if str_bool in true_options:
            return True
        elif str_bool in false_options:
            return False
        else:
            print('WARNING: Boolean value not contemplated use {0} for True values and {1} for the False ones'.format(
                true_options, false_options
            ))
            print('/t Using False as default')
            return False

    @staticmethod
    def _parse_start_date(str_date):
        """
        Parse the date form string to datetime.
        It accepts several ways to introduce the date:
            YYYYMMDD, YYYY/MM/DD, YYYYMMDDhh, YYYYYMMDD.hh, YYYY/MM/DD_hh:mm:ss, YYYY-MM-DD_hh:mm:ss,
            YYYY/MM/DD hh:mm:ss, YYYY-MM-DD hh:mm:ss, YYYY/MM/DD_hh, YYYY-MM-DD_hh.

        :param str_date: Date to the day to simulate in string format.
        :type str_date: str

        :return: Date to the day to simulate in datetime format.
        :rtype: datetime.datetime
        """
        from datetime import datetime
        format_types = ['%Y%m%d', '%Y%m%d%H', '%Y%m%d.%H', '%Y/%m/%d_%H:%M:%S', '%Y-%m-%d_%H:%M:%S',
                        '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y/%m/%d_%H', '%Y-%m-%d_%H', '%Y/%m/%d']

        date = None
        for date_format in format_types:
            try:
                date = datetime.strptime(str_date, date_format)
                break
            except ValueError as e:
                if str(e) == 'day is out of range for month':
                    raise ValueError(e)

        if date is None:
            raise ValueError("Date format '{0}' not contemplated. Use one of this: {1}".format(str_date, format_types))

        return date

    def _parse_end_date(self, end_date, start_date):
        """
        Parse the end date.
        If it's not defined it will be the same date that start_date (to do only one day).

        :param end_date: Date to the last day to simulate in string format.
        :type end_date: str

        :param start_date: Date to the first day to simulate.
        :type start_date: datetime.datetime

        :return: Date to the last day to simulate in datetime format.
        :rtype: datetime.datetime
        """
        if end_date is None:
            return start_date
        else:
            return self._parse_start_date(end_date)

    def set_log_level(self, comm):
        """
        Defines the log_level using the common script settings.
        """
        from . import settings
        settings.define_global_vars(self.options.log_level, comm)


if __name__ == '__main__':
    config = Config()
    print(config.options)
