"""
module with the main trajectories class
"""
# -*- coding:utf-8 -*-
from __future__ import unicode_literals

from datetime import datetime, timedelta
from functools import partial
from functools import wraps
from multiprocessing.pool import Pool
from tempfile import mkdtemp
from warnings import warn

import numpy as np
from path import Path

try:
    from shutil import SameFileError
except ImportError:
    from shutil import Error as SameFileError


from .tools import LagrantoException, run_cmd
from .formats import from_netcdf, to_ascii, from_ascii, to_netcdf

__all__ = ['Tra', 'LagrantoRun']


class Tra(object):
    """Class to work with LAGRANTO output.

    Read trajectories from a LAGRANTO file and return a structured numpy array

    Parameters
    ----------
        filename : string
            File containing lagranto trajectories
        usedatetime: bool
            Read times as datetime objects, default True
        array: structured array
            If defined creates a new Tra object filled with the array

    Returns
    -------
        structured array (Tra): trajs(ntra,ntime) with variables as tuple.

    Examples
    --------

    >>>    filename = 'mylslfile.nc'
    >>>    trajs = Tra()
    >>>    trajs.load_netcdf(filename)
    >>>    trajs['lon'][0,:]  # return the longitudes for the first trajectory.

    >>> trajs = Tra(filename)
    >>> selected_trajs = Tra(array=trajs[[10, 20, 30], :])

    Author : Nicolas Piaget, ETH Zurich , 2014
             Sebastiaan Crezee, ETH Zurich , 2014

    """

    _startdate = None

    def __init__(self, filename='', usedatetime=True, array=None, **kwargs):
        """Initialized a Tra object.

        If filename is given, try to load it directly;
        Arguments to the load function can be passed as key=value argument.
        """
        typefile = kwargs.pop('typefile', None)
        if typefile is not None:
            msg = 'typefile is not used anymore;' \
                  'it will be remove in futur version'
            raise DeprecationWarning(msg)
        if not filename:
            if array is None:
                self._array = None
            else:
                self._array = array
            return

        try:
            self.load_netcdf(filename, usedatetime=usedatetime, **kwargs)
        except (OSError, IOError, RuntimeError):
            try:
                self.load_ascii(filename, usedatetime=usedatetime, **kwargs)
            except Exception:
                raise IOError("Unkown fileformat. Known formats " 
                              "are ascii or netcdf")

    def __len__(self):
        return len(self._array)

    def __getattr__(self, attr):
        if attr in self.__dict__:
            return getattr(self, attr)
        return getattr(self._array, attr)

    def __getitem__(self, key):
        return self._array[key]

    def __setitem__(self, key, item):
        if isinstance(key, slice):
            self._array[key] = item
        elif key in self.dtype.names:
            self._array[key] = item
        else:
            dtypes = self._array.dtype.descr
            dtypes.append((key, item.dtype.descr[0][1]))
            dtypes = [(str(d[0]), d[1]) for d in dtypes]
            newarr = np.zeros(self._array.shape, dtype=dtypes)
            for var in self.variables:
                newarr[var] = self._array[var]
            newarr[key] = item
            self._array = newarr

    def __repr__(self):
        try:
            string = " \
            {} trajectories with {} time steps. \n \
            Available fields: {}\n \
            total duration: {} minutes".format(
                self.ntra, self.ntime,
                "/".join(self.variables),
                self.duration
            )
        except AttributeError:
            # Assume it's an empty Tra()
            string = "\
            Empty trajectories container.\n\
            Hint: use load_ascii() or load_netcdf()\n\
            to load data"
        return string

    @property
    def ntra(self):
        """Return the number of trajectories"""
        if self.ndim < 2:
            warn("\nBe careful with the dimensions, "
                 "you may want to change the shape:\n"
                 "either shape + (1,) or (1,)+shape")
            return None
        return self.shape[0]

    @property
    def ntime(self):
        """Return the number of time steps"""
        if self.ndim < 2:
            warn("\nBe careful with the dimensions, "
                 "you may want to change the shape:\n"
                 "either shape + (1,) or (1,)+shape")
            return None
        return self.shape[1]

    @property
    def variables(self):
        """Return the names of the variables"""
        return list(self.dtype.names)

    @property
    def duration(self):
        """Time duration in minutes."""
        end = self['time'][0, -1]
        start = self['time'][0, 0]
        delta = end - start
        if isinstance(delta, np.timedelta64):
            return delta.astype(timedelta).total_seconds() / 60.
        return delta * 60.

    @property
    def initial(self):
        """Give the initial time of the trajectories."""
        starttime = self['time'][0, 0]
        return starttime.astype(datetime)

    @property
    def startdate(self):
        """Return the starting date of the trajectories"""
        if self._startdate is None:
            time0 = self['time'][0, 0]
            if isinstance(time0, np.datetime64):
                self._startdate = time0.astype(datetime)
            else:
                self._startdate = datetime(1900, 1, 1, 0)
        return self._startdate

    def set_array(self, array):
        """To change the trajectories array."""
        self._array = array

    def get_array(self):
        """Return the trajectories array as numpy object"""
        return self._array

    def concatenate(self, trajs, time=False, inplace=False):
        """To concatenate trajectories together.

        Concatenate trajectories together and return a new object.
        The trajectories should contain the same variables.
        if time=False, the number of timestep in each trajs should be the same
        if time=True, the number of trajectories in each Tra should be the same

        Parameters
        ----------

            trajs: Tra or list of Tra
                Trajectories to concatenate with the current one
            time: bool, default False
                if True concatenate along the time dimension
            inplace: bool, default False
                if True append the trajs to current Tra object and return None

        Returns
        -------
        Tra
            Return a new Tra (trajectories) object

        Examples
        --------

        Create a new Tra object which include trajs and all newtrajs

        >>> files = ['lsl_20001010_00.4', 'lsl_2001010_01.4']
        >>> filename = files[0]
        >>> trajs = Tra(filename)
        >>> newtrajs = [Tra(f) for f in files]
        >>> alltrajs = trajs.concatenate(newtrajs)

        Append newtrajs to trajs

        >>> trajs = Tra(filename)
        >>> newtrajs = [Tra(f) for f in files]
        >>> trajs.concatenate(newtrajs, inplace=True)

        """
        if not isinstance(trajs, (tuple, list)):
            trajs = (trajs,)
        if time:
            trajstuple = (self.T,)
            trajstuple += tuple(tra.T for tra in trajs)
            test = np.concatenate(trajstuple).T
        else:
            trajstuple = (self.get_array(),)
            trajstuple += tuple(tra.get_array() for tra in trajs)
            test = np.concatenate(trajstuple)

        if inplace:
            self._array = test
        else:
            newtrajs = Tra()
            newtrajs.set_array(test)
            return newtrajs

    def append(self, trajs):
        """append trajectories

        Parameters
        ---------
        trajs: single Tra or list of Tra
                Trajectories to concatenate with the current one

        Examples
        --------

        >>> files = ['lsl_20001010_00.4', 'lsl_2001010_01.4']
        >>> filename = files[0]
        >>> trajs = Tra(filename)
        >>> newtrajs = [Tra(f) for f in files]
        >>> trajs.append(newtrajs)
        """
        self.concatenate(trajs, inplace=True)

    def write(self, filename, fileformat='netcdf'):
        """Method to write the trajectories to a file"""
        globals()['_write_{}'.format(fileformat)](self, filename)

    def load_netcdf(self, filename, usedatetime=True, msv=-999, unit='hours',
                    **kwargs):
        """Method to load trajectories from a netcdf file"""
        self._array, self._startdate = from_netcdf(filename,
                                                   usedatetime=usedatetime,
                                                   msv=msv,
                                                   unit=unit,
                                                   **kwargs)
    load_netcdf.__doc__ = from_netcdf.__doc__

    def write_netcdf(self, filename, exclude=None, unit='hours'):
        """Write netcdf"""
        to_netcdf(self, filename, exclude=exclude, unit=unit)
    write_netcdf.__doc__ = to_netcdf.__doc__

    def write_ascii(self, filename, gz=False, digit=3, mode='w'):
        """Write ascii"""
        to_ascii(self, filename, gz=gz, digit=digit, mode=mode)
    write_ascii.__doc__ = to_ascii.__doc__

    def load_ascii(self, filename, usedatetime=True, msv=-999.999, gz=False):
        """Load ascii"""
        self._array, self._startdate = from_ascii(filename,
                                                  usedatetime=usedatetime,
                                                  msv=msv,
                                                  gz=gz)
    load_ascii.__doc__ = from_ascii.__doc__


def intmpdir(afunc):
    """Wrapper to excute a command in a temporary directory

    Require workingdir as first argument of the function wrapped
    """
    @wraps(afunc)
    def wrapper(*args, **kwds):
        """Wrapper itself"""
        workingdir = args[0]
        no_tmp_dir = kwds.get('no_tmp_dir', False)
        if Path(workingdir).parent == '/tmp' or no_tmp_dir:
            return afunc(*args, **kwds)
        tmpdir = Path(mkdtemp())
        lwkds = {key: value for key, value in kwds.items()
                 if key in ['outputdir', 'version', 'sdate']}
        link_files(workingdir, tmpdir, **lwkds)
        nargs = (tmpdir,) + args[1:]
        out = afunc(*nargs, **kwds)
        tmpdir.rmdir_p()
        return out
    return wrapper


@intmpdir
def create_startf(workingdir, date, filename, specifier, version='cosmo',
                  outputdir='', sdate=None, tolist=False, cmd_header=None,
                  options=None):
    """ Create a startfile for LAGRANTO."""
    outputdir = Path(outputdir)
    workingdir = Path(workingdir)
    create = "startf.{version} "
    create += "{date:%Y%m%d_%H} {filename} {specifier}"
    if options is not None:
        if isinstance(options, list):
            for option in options:
                create += ' ' + option
        else:
            raise LagrantoException('options as to be a list of string')
    # if tolist:
    #     filename = Path(filename).splitext()[0] + '.4'
    create = create.format(version=version, date=date,
                           filename=filename, specifier=specifier)
    out = run_cmd(create, workingdir, cmd_header=cmd_header)
    if tolist:
        lslname = filename
        filename = Path(lslname).splitext()[0] + '.startf'
        lsl2list = 'lsl2list.{} {} {}'.format(version, lslname, filename)
        out += run_cmd(lsl2list, workingdir, cmd_header=cmd_header)
    try:
        Path.copy(workingdir / filename, outputdir / filename)
    except SameFileError:
        pass
    if tolist:
        return out, filename
    return out


@intmpdir
def select(workingdir, inpfile, outfile, crit,
           outputdir='', sdate=None, version='cosmo',
           cmd_header=None, **kwargs):
    """Select trajectories."""
    netcdf_format = kwargs.pop('netcdf_format', 'CF')
    select_cmd = 'select.{version} {inpfile} {outfile} "{crit}"'
    select_cmd = select_cmd.format(version=version, inpfile=inpfile,
                                   outfile=outfile, crit=crit)
    out = run_cmd(select_cmd, workingdir,
                  netcdf_format=netcdf_format, cmd_header=cmd_header)
    outputdir = Path(outputdir)
    try:
        Path.copy(workingdir / outfile, outputdir / outfile)
    except SameFileError:
        pass
    return out


@intmpdir
def trace(workingdir, filename, outputdir='', outfile='', tracevars='',
          tracevars_content='', field='', sdate=None, version='cosmo',
          cmd_header=None, **kwargs):
    """Trace variables along a trajectory."""
    netcdf_format = kwargs.pop('netcdf_format', 'CF')
    workingdir = Path(workingdir)
    outputdir = Path(outputdir)
    if not outfile:
        outfile = filename
    trace_cmd = 'trace.{version} {filename} {outfile}'
    tracevars_file = 'tracevars'
    if tracevars:
        tracevars_file = tracevars
        trace_cmd += ' -v ' + tracevars
    if tracevars_content:
        with (Path(workingdir) / tracevars_file).open('w') as fname:
            fname.write(tracevars_content)
    if field:
        trace_cmd += ' -f ' + field
    trace_cmd = trace_cmd.format(filename=filename, outfile=outfile,
                                 version=version)
    out = run_cmd(trace_cmd, workingdir,
                  netcdf_format=netcdf_format, cmd_header=cmd_header)
    try:
        Path.copy(workingdir / outfile, outputdir / outfile)
    except SameFileError:
        pass
    return out


@intmpdir
def caltra(workingdir, startdate, enddate, startfile, filename,
           jump=True, outputdir='', sdate=None, version='cosmo',
           cmd_header=None, withtrace=False, tracevars_content='',
           tracevars='', **kwargs):
    """Calculate trajectories for air parcels.

    starting at positions specified in startfile.
    """
    outputdir = Path(outputdir)
    netcdf_format = kwargs.pop('netcdf_format', 'CF')
    no_tmp_dir = kwargs.pop('no_tmp_dir', False)
    prefix = kwargs.pop('prefix', '')
    if no_tmp_dir:
        filename = outputdir / filename
    if not withtrace:
        caltra_cmd = 'caltra.{version}'
        caltra_cmd += ' {startdate:%Y%m%d_%H} {enddate:%Y%m%d_%H}'
        caltra_cmd += ' {startfile} {filename}'
        if jump:
            caltra_cmd += ' -j'
        for key, value in kwargs.items():
            caltra_cmd += ' -{key} {value}'.format(key=key, value=value)
        caltra_cmd = caltra_cmd.format(startdate=startdate, enddate=enddate,
                                       startfile=startfile, filename=filename,
                                       version=version)
    else:
        tracevars_file = tracevars if tracevars else 'tracevars'
        if tracevars_content:
            with (Path(workingdir) / tracevars_file).open('w') as fname:
                fname.write(tracevars_content)
        delta = (enddate - startdate).total_seconds() / (60 * 60)
        caltra_cmd = 'caltra {startfile} {delta:1.0f} {filename} '
        caltra_cmd += '-ref {startdate:%Y%m%d_%H%M}'
        for key, value in kwargs.items():
            caltra_cmd += ' -{key} {value}'.format(key=key, value=value)
        caltra_cmd = caltra_cmd.format(startdate=startdate, delta=delta,
                                       startfile=startfile, filename=filename)
    out = run_cmd(caltra_cmd, workingdir, netcdf_format=netcdf_format,
                  cmd_header=cmd_header, prefix=prefix)
    if not no_tmp_dir:
        try:
            Path.copy(workingdir / filename, outputdir / filename)
        except SameFileError:
            pass
    return out


@intmpdir
def density(workingdir, inpfile, outfile,
            outputdir='', sdate=None, version='cosmo',
            cmd_header=None, **kwargs):
    """Calculate trajectories density."""
    outputdir = Path(outputdir)
    netcdf_format = kwargs.pop('netcdf_format', 'CF')
    density_cmd = 'density.{version} {inpfile} {outfile}'
    for key, value in kwargs.items():
        density_cmd += ' -{key} {value}'.format(key=key, value=value)
    density_cmd = density_cmd.format(inpfile=inpfile, outfile=outfile,
                                     version=version)
    out = run_cmd(density_cmd, workingdir,
                  netcdf_format=netcdf_format, cmd_header=cmd_header)
    try:
        Path.copy(workingdir / outfile, outputdir / outfile)
    except SameFileError:
        pass
    return out


def link_files(folder, tmpdir, outputdir='', version='cosmo',
               sdate=None, sfiles=True, startdate=None, enddate=None):
    """Link data from `folder`, `outputdir`, to `tmpdir`.

    Transform lff[d,f]*.nc files from `folder` in P and S files.

    Parameters
    ----------
    folder: string,
        Path to the origin folder
    tmpdir: string,
        Path to the destination folder
    outputdir: string,
        Path to the folder where results are saved
    version: string,
        Only cosmo is available for now
    sdate: datetime object,
        Starting date of the cosmo run;
        Useful is the cosmo file are named in forecast mode
    sfiles: boolean,
        If True create link with prefix S as well, pointing to the same files
        as the P files.
    startdate: datetime object,
        First date for which to link files. (inclusive)
    enddate: datetime object,
        Last date for which ti link files (exclusive)
    """
    # the necessary files
    folder = Path(folder).abspath()
    tmpdir = Path(tmpdir)
    files = set(folder.files())
    if version == 'cosmo':
        constant_file = folder.files('l*c.nc')[0]
        if constant_file.isfile():
            constant_file.symlink(tmpdir / 'LMCONSTANTS')
        cosmofiles = set(folder.files('lff[df]*[0-9].nc'))
        for fname in cosmofiles:
            try:
                date = datetime.strptime(fname.name, 'lffd%Y%m%d%H.nc')
            except ValueError:
                sfname = fname.name[4:-3]
                days, hours, minutes, seconds = [int(sfname[i:i + 2])
                                                 for i in range(0, len(sfname),
                                                                2)]
                date = sdate + timedelta(days=days, hours=hours,
                                         minutes=minutes, seconds=seconds)
            if (startdate is not None) and (date < startdate):
                continue
            if (enddate is not None) and (date >= enddate):
                continue
            pfile = 'P{:%Y%m%d_%H}'.format(date)
            fname.symlink(tmpdir / pfile)
            if sfiles:
                sfile = 'S{:%Y%m%d_%H}'.format(date)
                fname.symlink(tmpdir / sfile)
        files = files.difference(cosmofiles)
    for fname in files:
        try:
            fname.symlink(tmpdir / fname.name)
        except FileExistsError:
            pass
    if outputdir:
        outputdir = Path(outputdir).abspath()
        for fname in outputdir.files():
            try:
                fname.symlink(tmpdir / fname.name)
            except FileExistsError:
                pass


class LagrantoRun:
    """Perform Lagranto calculation.

    Parameters
    ----------
    dates: list
        list of (startdate, enddate) tuple
    workingdir: string, optional
        path to the model output directory, default to current
    outputdir: string, optional
        path to the trajectory utput directory, defautl to current
    startf: string, optional
        name of the startf to use (or to create), default to startf.4
        The format parameter date can be used, for ex: startf_{date:%Y%m%d%H}.4
    lslname: string, optional
        name of the lsl file, define its type, default to lsl_{:%Y%m%d%H}.4
    tracevars: string, optional
        name of a tracevars file as used by trace, default to none
    field: string, optional
        name of a single field to trace, default to none
    version: string, optional
        name of the model version to use, currently only cosmo (default)
    linkfiles: function, optional
        function used to overwrite link_files in run.
        Should be used if COSMO output is not standard netcdf
    nprocesses: int, optional
        Number of processes used when running in parallel, default to 10
    sdate: datetime object,
        Starting date of the simulation;
        useful if files are named in forecast mode
    fresh: bool, optional
        Fresh start. Remove output directory first.
    cmd_header: string, optional
        Change the header using for running the command;
        see ``run_cmd`` help for more details.
    """

    def __init__(self, dates, workingdir='.', outputdir='.',
                 startf='startf.4', lslname='lsl_{:%Y%m%d%H}.4',
                 tracevars='', field='', version='cosmo', linkfiles=None,
                 nprocesses=10, sdate=None, fresh=False, cmd_header=None):
        self.dates = dates
        self.workingdir = Path(workingdir).abspath()
        self.outputdir = Path(outputdir).abspath()
        if fresh:
            self.clear()
        self.outputdir.makedirs_p()
        self.startf = Path(startf)
        self.lslname = lslname
        self.tracevars = tracevars
        self.field = field
        self.version = version
        self.link_files = linkfiles if linkfiles else link_files
        self.nprocesses = nprocesses
        self.sdate = sdate
        self.cmd_header = cmd_header
        self.caltra_outfile = None
        self.density_outfile = None
        self.select_outfile = None

    def create_startf(self, date, specifier, filename='', tolist=False,
                      **kwargs):
        """Create a file with the starting position of the trajectories.

        Parameters
        ----------
        date: datetime
            date for which the startf date is produced
        specifier: string
            detailed description of starting positions;
            (see LAGRANTO documentations for more details)
        filename: string
            filename where starting positions are saved;
            the default is defined by `LagrantoRun`
            Can make use of the format parameter date, for example:
            startf_{date:%Y%m%d%H}.4
        tolist: bool
            If the starting position should be saved as a list of points;
             Useful if the same file is used for different starting times
        kwargs: dict
            key, value options, possible values are listed bellow;

            - cmd_header:
              header to add to the shell command;
              (see `run_cmd` for more details)
            - options:
              list of options to pass to the startf function;
              (see the LAGRANTO documentation for more details)


        """
        if filename:
            startf = Path(filename.format(date=date))
            self.startf = startf
        else:
            startf = self.startf.format(date=date)
        out = create_startf(self.workingdir, date, startf, specifier,
                            outputdir=self.outputdir, sdate=self.sdate,
                            version=self.version, tolist=tolist,
                            cmd_header=self.cmd_header, **kwargs)
        if tolist:
            self.startf = out[1]
            return out[0]
        return out

    def caltra(self, startdate, enddate, filename='', outfile='', **kwargs):
        """Compute trajectories for the given startdate"""
        if filename:
            self.lslname = filename
        self.startf = self.startf.format(date=startdate)
        if outfile:
            self.caltra_outfile = outfile.format(date=startdate)
        else:
            self.caltra_outfile = self.lslname.format(startdate)
        return caltra(self.workingdir, startdate, enddate, self.startf,
                      self.caltra_outfile, outputdir=self.outputdir,
                      sdate=self.sdate, version=self.version,
                      cmd_header=self.cmd_header, **kwargs)

    def trace(self, date, filename='', outfile='', tracevars='',
              tracevars_content='', field='', **kwargs):
        r"""Trace variable along a trajectory.

        Trace meteorological fields along trajectories

        Parameters
        ----------
        date: datetime object
            starting date of the trajectories to trace
        filename: string, optional
            If not specified use `LagrantoRun.lslname`
        outfile: string, optional
            If not specified same as `filename`
        tracevars: string, optional
            Name of the file with the field to trace;
            If not specified use `LagrantoRun.tracevars`
        tracevars_content: string, optional
            Content of the tracevars file;

            .. code_block: python

            \"\"\"\n
            QV      1000. P 1\n
            PV      1.    T 1\n
            \"\"\"

        field: string,
            Specify a field to trace as follow: QV 1.
        kwargs
        """
        tracef = tracevars if tracevars else self.tracevars
        fieldv = field if field else self.field
        filename = filename if filename else self.lslname
        filename = filename.format(date)
        return trace(self.workingdir, filename, outfile=outfile,
                     outputdir=self.outputdir, tracevars=tracef,
                     tracevars_content=tracevars_content,
                     field=fieldv, sdate=self.sdate,
                     version=self.version, cmd_header=self.cmd_header, **kwargs)

    def density(self, inpfile=None, outfile=None, **kwargs):
        """Return the density of trajectories"""
        if inpfile is None:
            try:
                inpfile = self.caltra_outfile
            except AttributeError:
                raise ValueError('Provide an input file or run caltra')
        if outfile is None:
            self.density_outfile = 'density.4'
        else:
            self.density_outfile = outfile
        return density(self.workingdir, inpfile=inpfile,
                       outfile=self.density_outfile, outputdir=self.outputdir,
                       sdate=self.sdate, version=self.version,
                       cmd_header=self.cmd_header, **kwargs)

    def select(self, inpfile=None, outfile=None, **kwargs):
        """Perform selection of trajectories"""
        if inpfile is None:
            try:
                inpfile = self.caltra_outfile
            except AttributeError:
                raise ValueError('Provide an input file or run caltra')
        if outfile is None:
            self.select_outfile = 'selected.4'
        else:
            self.select_outfile = outfile
        return select(self.workingdir, inpfile=inpfile,
                      outfile=self.select_outfile, outputdir=self.outputdir,
                      sdate=self.sdate, version=self.version,
                      cmd_header=self.cmd_header, **kwargs)

    def run(self, caltra_kw=None, trace_kw=None, startf_kw=None, **kwargs):
        """Run caltra, trace, and/or create_startf.

        Run single_run for each dates tuple
        See single_run description for more details.

        Parameters
        ----------
        caltra_kw: dictionary
            Arguments to pass to the caltra function
        trace_kw: dictionary
            Arguments to pass to the trace function
        startf_kw: dictionary
            Arguments to pass to the create_startf function
        kwargs:
            Arguments to pass to the single_run function

        Returns
        ------
        A list of single_run result: list
        """
        results = []
        if caltra_kw is None:
            caltra_kw = dict()
        if trace_kw is None:
            trace_kw = dict()
        if startf_kw is None:
            startf_kw = dict()
        for startdate, enddate in self.dates:
            res = self.single_run(startdate, enddate, caltra_kw=caltra_kw,
                                  trace_kw=trace_kw,
                                  startf_kw=startf_kw, **kwargs)
            results.append(res)
        return results

    def run_parallel(self, caltra_kw=None, trace_kw=None, startf_kw=None,
                     nprocesses=None, **kwargs):
        """Run caltra, trace, and/or create_startf in parallel.

        Run single_run for each dates tuple using `multiprocessing.Pool`.
        See single_run description for more details.

        Warning:  If you use run_parallel with create_startf and
        if the starting file is not a list, be sure that every starting file
        has a different name, for example doing as follow:

        >>> LagrantoRun(startf='startf_{date:%Y%m%d_%H.4}', ...)


        Parameters
        ----------
        caltra_kw: dictionary
            Arguments to pass to the caltra function
        trace_kw: dictionary
            Arguments to pass to the trace function
        startf_kw: dictionary
            Arguments to pass to the create_startf function
        nprocesses: int,
            Number of processors to use
        kwargs:
            Arguments to pass to the single_run function

        Returns
        -------
        A list of single_run results: list
        """
        if caltra_kw is None:
            caltra_kw = dict()
        if trace_kw is None:
            trace_kw = dict()
        if startf_kw is None:
            startf_kw = dict()
        if nprocesses is not None:
            self.nprocesses = nprocesses

        single_run = partial(self.single_run, caltra_kw=caltra_kw,
                             trace_kw=trace_kw, startf_kw=startf_kw, **kwargs)
        with Pool(processes=self.nprocesses) as pool:
            results = pool.starmap(single_run, self.dates)
        return results

    def single_run(self, sd, ed, caltra_kw=None, trace_kw=None, startf_kw=None,
                   type='both', debug=False, **kwargs):
        """Run caltra, trace and/or create_startf for a given starting date

        Compute a trajectory set using caltra, trace and/or create_startf in a
        temporary directory. Need a working installation of LAGRANTO.

        If caltra, trace or create_startf return a error, single_run return
        a LagrantoException error with the path of the temporary directory.
        In this case, the temporary directory is not deleted.


        Parameters
        ----------
        sd: datetime
            Starting date of the trajectory
        ed: datetime
            Ending date of the trajectory
        caltra_kw: dictionary
            Arguments to pass to the caltra function
        trace_kw: dictionary
            Arguments to pass to the trace function
        startf_kw: dictionary
            Arguments to pass to the create_startf function
        type: string (default both)
            Define the type a run to perform:
            both: caltra + trace
            caltra: caltra
            trace: trace
            all: create_startf + caltra + trace
            create: create_startf + caltra
        debug: Boolean
            If True raise LagrantoException instead of return it as string
        kwargs: dictionnary
            Arguments to pass to the link_files function

        Returns
        -------
        output of caltra, trace and/or create_startf: str

        """
        if caltra_kw is None:
            caltra_kw = dict()
        if trace_kw is None:
            trace_kw = dict()
        if startf_kw is None:
            startf_kw = dict()

        workingdir = self.workingdir
        tmpdir = Path(mkdtemp())
        self.workingdir = tmpdir
        nkwargs = {'version': self.version,
                   'outputdir': self.outputdir,
                   'sdate': self.sdate}
        nkwargs.update(kwargs)
        self.link_files(workingdir, tmpdir, **nkwargs)
        out = ''
        try:
            if type == 'both':
                out += self.caltra(sd, ed, **caltra_kw)
                out += self.trace(sd, **trace_kw)
            elif type == 'caltra':
                out += self.caltra(sd, ed, **caltra_kw)
            elif type == 'trace':
                out += self.trace(sd, **trace_kw)
            elif (type == 'all') or (type == 'create'):
                if 'specifier' not in startf_kw.keys():
                    raise LagrantoException('specifier need to be specified'
                                            ' in startf_kw for type:"{}", '
                                            'see create_starf function'
                                            ''.format(type))
                out += self.create_startf(sd, **startf_kw)
                out += self.caltra(sd, ed, **caltra_kw)
                if type == 'all':
                    out += self.trace(sd, **trace_kw)
        except LagrantoException as err:
            err.args += (tmpdir,)
            if debug:
                raise
            else:
                return err
        self.workingdir = workingdir
        tmpdir.rmtree_p()
        return out

    def clear(self):
        """Remove the output directory"""
        self.outputdir.rmtree_p()
