"""Contains the executor class."""


import logging
import re
from copy import deepcopy
from pathlib import Path
from typing import Optional

from bout_runners.make.make import Make
from bout_runners.parameters.bout_paths import BoutPaths
from bout_runners.parameters.run_parameters import RunParameters
from bout_runners.submitter.abstract_cluster_submitter import AbstractClusterSubmitter
from bout_runners.submitter.abstract_submitter import AbstractSubmitter
from bout_runners.submitter.submitter_factory import get_submitter


class BoutRunExecutor:
    r"""
    Executes the command for submitting a bout run.

    Attributes
    ----------
    __bout_paths : BoutPaths
        Getter variable for project_path
    __make : Make
        Object for making the project
    __run_parameters : RunParameters
        Object containing the run parameters
    bout_paths : BoutPaths
        Object containing the paths
    exec_name : str
        Name of the executable
    restart_from : None or Path
        Path to copy restart files from prior to the execution
    run_parameters : RunParameters
        Object containing the run parameters
    submitter : AbstractSubmitter
        Object containing the submitter

    Methods
    -------
    get_execute_command()
        Return the execute command string
    execute()
        Execute a BOUT++ run

    Examples
    --------
    The easiest way to use the Executor is to run a script from the root directory of
    the project (i.e. where the `Makefile` and `data` directory are normally
    situated. The script can simply call

    >>> BoutRunExecutor().execute()

    and `Executor` takes care of the rest.

    A more elaborate example where all the dependency objects are built manually:

    Import the dependencies

    >>> from pathlib import Path
    >>> from bout_runners.executor.bout_paths import BoutPaths
    >>> from bout_runners.submitter.local_submitter import LocalSubmitter

    Create the `bout_paths` object

    >>> project_path = Path().joinpath('path', 'to', 'project')
    >>> bout_inp_src_dir = Path().joinpath('path', 'to', 'source', 'BOUT.inp')
    >>> bout_inp_dst_dir = Path().joinpath('path', 'to', 'destination', 'BOUT.inp')
    >>> bout_paths = BoutPaths(project_path=project_path,
    ...                        bout_inp_src_dir=bout_inp_src_dir,
    ...                        bout_inp_dst_dir=bout_inp_dst_dir)

    Create the executor object

    >>> run_parameters = RunParameters({'global': {'nout': 0}})
    >>> executor = BoutRunExecutor(
    ...     bout_paths=bout_paths,
    ...     submitter=LocalSubmitter(bout_paths.project_path),
    ...     run_parameters=run_parameters)

    Execute the run

    >>> executor.execute()
    """

    def __init__(
        self,
        bout_paths: Optional[BoutPaths] = None,
        submitter: Optional[AbstractSubmitter] = None,
        run_parameters: Optional[RunParameters] = None,
        restart_from: Optional[Path] = None,
    ) -> None:
        """
        Set the input parameters.

        Parameters
        ----------
        bout_paths : BoutPaths or None
            Object containing the paths
            If None, default BoutPaths values will be used
        submitter : AbstractSubmitter
            Object containing the submitter
        run_parameters : RunParameters or None
            Object containing the run parameters
            If None, default parameters will be used
        restart_from : Path or None
            The path to copy the restart files from
        """
        # NOTE: We are not setting the default as a keyword argument
        #       as this would mess up the paths
        # NOTE: We are deepcopying bout_paths as it may be altered by for
        #       example the self.restart_from setter
        logging.info("Start: Making an BoutRunExecutor object")
        self.__bout_paths = (
            deepcopy(bout_paths) if bout_paths is not None else BoutPaths()
        )
        self.__run_parameters = (
            run_parameters if run_parameters is not None else RunParameters()
        )
        self.__make = Make(self.__bout_paths.project_path)

        self.submitter = submitter if submitter is not None else get_submitter()
        if isinstance(self.submitter, AbstractClusterSubmitter):
            self.submitter.store_dir = self.__bout_paths.bout_inp_dst_dir

        self.__restart_from = None
        self.restart_from = restart_from
        logging.info("Done: Making an BoutRunExecutor object")

    @property
    def restart_from(self) -> Optional[Path]:
        """
        Set the properties of self.restart from and update bout_inp_dst_dir.

        The bout_inp_dst_dir is updated to reflect that this is a restart run.

        The new bout_inp_dst_dir will be the same as
        bout_run_setup.executor.restart_from with _restart_/d* appended
        /d* will be the next digit based on the number of other restart directories

        Notes
        -----
        This will not copy the restart files as the restart files may not be ready.
        Copying of files can either be done manually using
        bout_runner.utils.file_operations.copy_restart_files or automatically by
        using BoutRunner.__inject_copy_restart_files_node which is called from
        BoutRunner.__prepare_run

        See Also
        --------
        bout_runners.runner.bout_runner.BoutRunner.__inject_copy_restart_files_node
            Search for restart files, make a restart node where needed
        """
        return self.__restart_from

    @restart_from.setter
    def restart_from(self, restart_from: Optional[Path]) -> None:
        self.__restart_from = restart_from
        if restart_from is not None:
            logging.debug(
                "Changing bout_paths.bout_inp_dst_dir as restart_from is not None"
            )
            restart_dir_parent = restart_from.parent
            restart_dir_name = restart_from.name
            restart_dirs = list(restart_dir_parent.glob(f"{restart_dir_name}*"))
            restart_number = 0
            restart_numbers = list()
            pattern = r"_restart_(\d)+$"
            for restart_dir in restart_dirs:
                match = re.search(pattern, restart_dir.name)
                if match is not None:
                    # NOTE: The zeroth group is the matching string
                    restart_numbers.append(int(match.group(1)))
            if len(restart_numbers) != 0:
                restart_numbers.sort()
                restart_number = restart_numbers[-1] + 1
            prev_inp_dst_dir = self.bout_paths.bout_inp_dst_dir
            stripped_restart_dir_name = re.sub(pattern, "", restart_dir_name)
            new_inp_dst_dir = restart_dir_parent.joinpath(
                f"{stripped_restart_dir_name}_restart_{restart_number}"
            )
            self.bout_paths.bout_inp_dst_dir = new_inp_dst_dir
            logging.debug(
                "bout_run_setup.bout_paths.bout_inp_dst_dir set from %s to %s",
                prev_inp_dst_dir,
                new_inp_dst_dir,
            )

    @property
    def bout_paths(self) -> BoutPaths:
        """
        Get the properties of self.bout_paths.

        Returns
        -------
        self.__bout_paths : BoutPaths
            Object containing the paths

        Notes
        -----
        The bout_paths is read only
        """
        return self.__bout_paths

    @property
    def exec_name(self) -> str:
        """
        Set the properties of self.bout_paths.

        Returns
        -------
        self.__bout_paths : BoutPaths
            Object containing the paths

        Notes
        -----
        The exec_name is read only
        """
        return self.__make.exec_name

    @property
    def run_parameters(self) -> RunParameters:
        """
        Get the properties of self.run_parameters.

        Returns
        -------
        self.__run_parameters : RunParameters
            Object containing the run parameters

        Notes
        -----
        The run_parameters is read only
        """
        return self.__run_parameters

    def get_execute_command(self) -> str:
        """
        Return the execute command string.

        Returns
        -------
        command : str
            The terminal command for executing the run
        """
        mpi_cmd = "mpirun -np"

        # NOTE: No spaces if parameters are None
        command = (
            f"{mpi_cmd} "
            f"{self.submitter.processor_split.number_of_processors} "
            f"{self.__bout_paths.project_path.joinpath(self.exec_name)} "
            f"-d {self.__bout_paths.bout_inp_dst_dir} "
            f"{self.__run_parameters.run_parameters_str}"
        )
        return command

    def execute(self, restart: bool = False) -> None:
        """
        Execute a BOUT++ run.

        Parameters
        ----------
        restart : bool
            If True the 'restart' will be appended to the command string
        """
        # Make the project if not already made
        self.__make.run_make()
        # Submit the command
        command = self.get_execute_command()
        if restart:
            command += " restart"
        self.submitter.submit_command(command)
