#!/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 pandas import read_csv
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__()
        description_csv_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "arguments_description.csv")
        args_csv = read_csv(description_csv_file, index_col="Parameter", keep_default_na=False, comment="#")
        self.options = self.read_options(args_csv)

    def read_options(self, args_csv):
        """
        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
        for arg_name, csv_row in args_csv.iterrows():
            arg_name=arg_name.strip()
            if csv_row["Choices"]== "None":
                csv_row["Choices"] = None
            else:
                csv_row["Choices"] = self._parse_list(csv_row["Choices"])

            p.add_argument(f"--{arg_name}",
                           dest=arg_name,
                           help=csv_row["Description"],
                           type=eval(csv_row["Data Type"]),
                           choices=csv_row["Choices"],
                           required=csv_row["Required"],
                           default=eval(csv_row["Default"]))

        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] = self._parse_path(path=arguments.__dict__[item],
                                                            data_path=arguments.data_path,
                                                            input_dir=arguments.input_dir,
                                                            version="v" + __version__,
                                                            domain_type=arguments.domain_type)
                self.process_domain_type(arguments, item)

        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)

    def process_domain_type(self,arguments, item):
        domain_type = arguments.domain_type

        if domain_type in ['global', 'regular']:
            if arguments.inc_lat is not None and arguments.inc_lon is not None:
                arguments.__dict__[item] = arguments.__dict__[item].replace('<resolution>',
                                                                    f'{item}_{arguments.inc_lat}_{arguments.inc_lon}')
            else:
                raise RuntimeError(
                    f"Arguments 'inc_lat' and 'inc_lon' are needed for the specified domain_type '{domain_type}'")

        elif domain_type == 'rotated':
            if arguments.inc_rlat is not None and arguments.inc_rlon is not None:
                arguments.__dict__[item] =  arguments.__dict__[item].replace('<resolution>',
                                                                    f'{item}_{arguments.inc_rlat}_{arguments.inc_rlon}')
            else:
                raise RuntimeError(
                    f"Arguments 'inc_rlat' and 'inc_rlon' are needed for the specified domain_type '{domain_type}'")

        elif domain_type == 'rotated_nested':
            if arguments.n_rlat is not None and arguments.n_rlon is not None:
                arguments.__dict__[item] =  arguments.__dict__[item].replace('<resolution>',
                                                                    f'{item}_{arguments.n_rlat}_{arguments.n_rlon}')
            else:
                raise RuntimeError(
                    f"Arguments 'n_rlat' and 'n_rlon' are needed for the specified domain_type '{domain_type}'")

        elif domain_type in ['lcc', 'mercator']:
            if arguments.inc_x is not None and arguments.inc_y is not None:
                arguments.__dict__[item] =  arguments.__dict__[item].replace('<resolution>',
                                                                    f'{item}_{arguments.inc_x}_{arguments.inc_y}')
            else:
                raise RuntimeError(
                    f"Arguments 'inc_x' and 'inc_y' are needed for the specified domain_type '{domain_type}'")

        else:
            raise RuntimeError(f"Unsupported domain_type: '{domain_type}'")

        return None

    @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)

    @staticmethod
    def _parse_list(string: str) -> list:
        """
        Parses a string into a list

        Parameters
        ----------
        string : str, list
            String to convert into a list

        Returns
        -------
        values : list
            Parsed list
        """
        delimiters = [',', ';']

        for delimiter in delimiters:
            if delimiter in string:
                items = string.split(delimiter)
                break
        else:
            # If no delimiter is found, return the string itself
            return [string]
        # Attempt to evaluate each item; if evaluation fails, keep it as a string
        values = []
        for item in items:
            try:
                values.append(eval(item.strip()))
            except Exception:
                values.append(item.strip())
        return values

    @staticmethod
    def _parse_path(path, data_path=None, input_dir=None, version=None, domain_type=None) -> str:
        """
        Parses path using specific patterns.

        This function replaces the following patterns within the input path string with the provided values:
        - '<data_path>'
        - '<input_dir>'
        - '<version>'
        - '<domain_type>'

        Parameters
        ----------
        path : str
            Path with patterns to parse.
        data_path : str, optional
            String to replace the pattern '<data_path>' (default: None).
        input_dir : str, optional
            String to replace the pattern '<input_dir>' (default: None).
        version : str, optional
            String to replace the pattern '<version>' (default: None).
        domain_type : str, optional
            String to replace the pattern '<domain_type>' (default: None).

        Returns
        -------
        path : str
            Parsed path with replaced patterns.
        """

        if data_path is not None:
            path = path.replace("<data_path>", data_path)
        if input_dir is not None:
            path = path.replace("<input_dir>", input_dir)
        if version is not None:
            path = path.replace("<version>", version)
        if domain_type is not None:
            path = path.replace("<domain_type>", domain_type)

        return path

    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)
