"""
cmdline parser
"""
import os
import re
from ast import literal_eval
import numpy as np
import pandas as pd
from argparse import ArgumentParser, Action

from pysd import __version__
from pysd.translators.vensim.vensim_utils import supported_extensions as\
    vensim_extensions
from pysd.translators.xmile.xmile_utils import supported_extensions as\
    xmile_extensions

docs = "https://pysd.readthedocs.io/en/master/command_line_usage.html"

parser = ArgumentParser(
    description='Simulating System Dynamics Models in Python',
    prog='PySD')


#########################
# functions and actions #
#########################

def check_output(string):
    """
    Checks that out put file ends with .tab or .csv

    """
    if not string.endswith(('.tab', '.csv', '.nc')):
        parser.error(
            f'when parsing {string}'
            '\nThe output file name must be .tab, .csv or .nc...')

    return string


def check_model(string):
    """
    Checks that model file ends with .py .mdl or .xmile and that exists.

    """
    suffixes = [".py"] + vensim_extensions + xmile_extensions
    if not any(string.lower().endswith(suffix) for suffix in suffixes):
        parser.error(
            f"when parsing {string} \nThe model file name must be a Vensim"
            f" ({', '.join(vensim_extensions)}), a Xmile "
            f"({', '.join(xmile_extensions)}) or a PySD (.py) model file...")

    if not os.path.isfile(string):
        parser.error(
            f"when parsing {string}"
            "\nThe model file does not exist...")

    return string


def check_data_file(string):
    """
    Check that data file is a tab or csv file and that exists.
    """
    if not string.endswith(('.tab', '.csv', '.nc')):
        parser.error(
            f'when parsing {string}'
            '\nThe data file name must be .tab, .csv or .nc...')
    elif not os.path.isfile(string):
        parser.error(
            f'when parsing {string}'
            '\nThe data file does not exist...')
    else:
        return string


def split_files(string):
    """
    Splits the data files and returns and error if file doesn't exists
    --data 'file1.tab, file2.csv' -> ['file1.tab', 'file2.csv']
    --data file1.tab -> ['file1.tab']

    """
    return [check_data_file(s.strip()) for s in string.split(',')]


def split_columns(string):
    """
    Splits the return-columns argument or reads it from .txt
    --return-columns 'temperature c, "heat$"' -> ['temperature c', '"heat$"']
    --return-columns my_vars.txt -> ['temperature c', '"heat$"']

    """
    if string.endswith('.txt'):
        with open(string, 'r') as file:
            return [col.rstrip('\n').strip() for col in file]

    return [s.strip() for s in string.split(',')]


def split_timestamps(string):
    """
    Splits the return-timestamps argument
    --return-timestamps '1, 5, 15' -> array([1., 5., 15.])

    """
    try:
        return np.array([s.strip() for s in string.split(',')], dtype=float)
    except Exception:
        # error
        raise parser.error(
            f'when parsing {string}'
            '\nThe return time stamps must be separated by commas...\n'
            f'See {docs} for examples.')


def split_vars(string):
    """
    Splits the arguments from new_values.
    'a=5' -> {'a': ('param', 5.)}
    'b=[[1,2],[1,10]]' -> {'b': ('param', pd.Series(index=[1,2], data=[1,10]))}
    'a:5' -> {'a': ('initial', 5.)}

    """
    try:
        if '=' in string:
            # new variable value
            var, value = string.split('=')
            type = 'param'

        if ':' in string:
            # initial time value
            var, value = string.split(':')
            type = 'initial'

        if re.match(r"^[+-]?(\d*\.)?\d+$", value.strip()):
            # value is a number
            return {var.strip(): (type, float(value))}

        # value is series
        assert type == 'param'
        value = literal_eval(value)
        assert len(value) == 2
        assert len(value[0]) == len(value[1])
        return {var.strip(): (type,
                              pd.Series(index=value[0], data=value[1]))}

    except Exception:
        # error
        raise parser.error(
            f'when parsing {string}'
            '\nYou must use variable=new_value to redefine values or '
            'variable:initial_value to define initial value.'
            'variable must be a model component, new_value can be a '
            'float or a list of two list, initial_value must be a float'
            '...\n'
            f'See {docs} for examples.')


class SplitVarsAction(Action):
    """
    Convert the list of split variables from new_values to a dictionary.
    [{'a': 5.}, {'b': pd.Series(index=[1, 2], data=[1, 10])}] ->
        {'a': 5., 'b': pd.Series(index=[1, 2], data=[1, 10])}
    """
    def __init__(self, option_strings, dest, **kwargs):
        super().__init__(option_strings, dest, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        main_dict = {'param': {}, 'initial': {}}
        for var in values:
            for var_name, (type, val) in var.items():
                main_dict[type][var_name] = val
        setattr(namespace, self.dest, main_dict)


###########
# options #
###########

parser.add_argument(
    '-v', '--version',
    action='version', version=f'PySD {__version__}')

parser.add_argument(
    '-o', '--output-file', dest='output_file',
    type=check_output, metavar='FILE',
    help='output file to save run outputs (.tab, .csv or .nc)')

parser.add_argument(
    '-p', '--progress', dest='progress',
    action='store_true', default=False,
    help='show progress bar during model integration')

parser.add_argument(
    '-r', '--return-columns', dest='return_columns',
    action='store', type=split_columns,
    metavar='\'var1, var2, .., varN\' or FILE (.txt)',
    help='provide the return columns separated by commas or a .txt file'
         ' where each row is a variable')

parser.add_argument(
    '-e', '--export', dest='export_file',
    type=str, metavar='FILE',
    help='export to a pickle stateful objects states at the end of the '
         'simulation')

parser.add_argument(
    '-i', '--import-initial', dest='import_file',
    type=str, metavar='FILE',
    help='import stateful objects states from a pickle file,'
         'if given initial conditions from var:value will be ignored')


###################
# Model arguments #
###################

model_arguments = parser.add_argument_group(
    'model arguments',
    'Modify model control variables.')

model_arguments.add_argument(
    '-I', '--initial-time', dest='initial_time',
    action='store', type=float, metavar='VALUE',
    help='modify initial time of the simulation')

model_arguments.add_argument(
    '-F', '--final-time', dest='final_time',
    action='store', type=float, metavar='VALUE',
    help='modify final time of the simulation')

model_arguments.add_argument(
    '-T', '--time-step', dest='time_step',
    action='store', type=float, metavar='VALUE',
    help='modify time step of the simulation')

model_arguments.add_argument(
    '-S', '--saveper', dest='saveper',
    action='store', type=float, metavar='VALUE',
    help='modify time step of the output')

model_arguments.add_argument(
    '-R', '--return-timestamps', dest='return_timestamps',
    action='store', type=split_timestamps,
    metavar='\'value1, value2, .., valueN\'',
    help='provide the return time stamps separated by commas, if given '
         '--saveper will be ignored')

model_arguments.add_argument(
    '-D', '--data', dest='data_files',
    action='store', type=split_files, metavar='\'FILE1, FILE2, .., FILEN\'',
    help='input data file or files to run the model')

#########################
# Translation arguments #
#########################

trans_arguments = parser.add_argument_group(
    'translation arguments',
    'Configure the translation of the original model.')

trans_arguments.add_argument(
    '--translate', dest='run',
    action='store_false', default=True,
    help='only translate the model_file, '
         'it does not run it after translation')

trans_arguments.add_argument(
    '--split-views', dest='split_views',
    action='store_true', default=False,
    help='parse the sketch to detect model elements in each model view,'
         ' and then translate each view in a separate Python file')

trans_arguments.add_argument(
    '--subview-sep', dest='subview_sep',
    action='store', nargs="*", default=[],
    metavar='separator_1 separator_2 ... separator_n',
    help='further division of views into subviews, by identifying the '
         'separator string in the view name, only availabe if --split-views'
         ' is used. Passing positional arguments after this argument will'
         ' not work')


#######################
# Warnings and errors #
#######################

warn_err_arguments = parser.add_argument_group(
    'warning and errors arguments',
    'Modify warning and errors management.')

warn_err_arguments.add_argument(
    '--missing-values', dest='missing_values', default="warning",
    action='store', type=str, choices=['warning', 'raise', 'ignore', 'keep'],
    help='exception with missing values, \'warning\' (default) shows a '
         'warning message and interpolates the values, \'raise\' raises '
         'an error, \'ignore\' interpolates the values without showing '
         'anything, \'keep\' keeps the missing values')


########################
# Positional arguments #
########################

parser.add_argument('model_file', metavar='model_file', type=check_model,
                    help='Vensim, Xmile or PySD model file')

parser.add_argument('new_values',
                    metavar='variable=new_value', type=split_vars,
                    nargs='*', action=SplitVarsAction,
                    help='redefine the value of variable with new value. '
                    'variable must be a model component, new_value may be a '
                    'float or a list of two lists')

# The destination new_values2 will never be used as the previous argument
# is given also with nargs='*'. Nevertheless, the following variable
# is declared for documentation
parser.add_argument('new_values2',
                    metavar='variable:initial_value', type=split_vars,
                    nargs='*', action=SplitVarsAction,
                    help='redefine the initial value of variable. '
                    'variable must be a model stateful element, initial_value'
                    ' must be a float')


#########
# Usage #
#########

parser.usage = parser.format_usage().replace("usage: PySD", "python -m pysd")
