# coding: utf-8
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
# Distributed under the terms of "New BSD License", see the LICENSE file.

import six
from pyiron_base.database.generic import DatabaseAccess
from pyiron_base.database.filetable import FileTable

"""
The JobStatus class belongs to the GenericJob object.
"""

__author__ = "Jan Janssen"
__copyright__ = (
    "Copyright 2020, Max-Planck-Institut für Eisenforschung GmbH - "
    "Computational Materials Design (CM) Department"
)
__version__ = "1.0"
__maintainer__ = "Jan Janssen"
__email__ = "janssen@mpie.de"
__status__ = "production"
__date__ = "Sep 1, 2017"

job_status_successful_lst = [
    "finished",
    "not_converged",
    "warning"
]

job_status_finished_lst = ["aborted"] + job_status_successful_lst

job_status_lst = [
    "initialized",
    "appended",
    "created",
    "submitted",
    "running",
    "collect",
    "suspended",
    "refresh",
    "busy",
] + job_status_finished_lst


def format_docstring_with_statuses(n_tabs=1):
    """
    Replaces a '{}' in the decorated object's docstring with the documentation for all possible job status.

    The intent here is to have a *single place* where all status-value documentation can be maintained.

    Args:
        n_tabs (int): The number of tabs preceding each status type. (Default is one)
    """

    status_docs = """
    {0}initialized: The object for the corresponding job was just created.
    {0}appended: The job was appended to an master job.
    {0}created: The files required for the simulation were written to the harddisk.
    {0}submitted: The job was submitted to the jobscheduler and is waiting to be executed.
    {0}running: The job is currently executed.
    {0}aborted: The job failed to execute.
    {0}collect: The job finished successfully and the written files are being collected.
    {0}suspended: The job was set to sleep, waiting until other related jobs are finished, before it continous.
    {0}refresh: The job was suspended before and it is currently checking if there are new tasks it can execute.
    {0}busy: The job is refreshing, but during the refresh more related jobs finished so another refresh is necessary.
    {0}finished: The job and all connected sub jobs are finished.
    {0}warning: The job finished but encountered warnings in its output.
    """

    def decorator(obj):
        obj.__doc__ = obj.__doc__.format(status_docs.format('\t' * n_tabs))
        return obj
    return decorator


@format_docstring_with_statuses()
class JobStatus(object):
    """
    The JobStatus object handles the different states a job could have. The available states are: {}

    Args:
        initial_status (str): If no initial status is provided the status is set to 'initialized'
        db (DatabaseAccess): The database which is responsible for this job.
        job_id (int): job ID

    Attributes:

        .. attribute:: database

            the database which is responsible for this job.

        .. attribute:: job_id

            Job ID

        .. attribute:: string

            job status as string
    """

    def __init__(self, initial_status="initialized", db=None, job_id=None):
        super(JobStatus, self).__setattr__("_status_dict", {})
        self._db = None
        self._job_id = None
        self.string = initial_status
        self.database = db
        self.job_id = job_id

    @property
    def database(self):
        """
        Get the database which is responsible for this job. If no database is linked it returns None.
        Returns:
            DatabaseAccess: The database which is responsible for this job.
        """
        return self._db

    @database.setter
    def database(self, db):
        """
        Set the database which is responsible for this job.
        Args:
            db (DatabaseAccess): The database which should be responsible for this job.
        """
        if db is not None and not isinstance(db, (DatabaseAccess, FileTable)):
            raise TypeError("The database has to be an DatabaseAccess object.")
        self._db = db

    @property
    def job_id(self):
        """
        Get the job id of the job this jobstatus is associated to.
        Returns:
            int: job id
        """
        return self._job_id

    @job_id.setter
    def job_id(self, unique_id):
        """
        Get the job id of the job this jobstatus is associated to.
        Args:
            unique_id (int): job id
        """
        if unique_id and not isinstance(unique_id, int):
            raise TypeError("The Job_ID should be an integer.")
        self._job_id = unique_id
        self.refresh_status()

    @format_docstring_with_statuses(n_tabs=2)
    @property
    def string(self):
        """
        Get the current status as string, it can be: {}

        Returns:
            (str): status [initialized, appended, created, submitted, running, aborted, collect, suspended, refresh,
                   busy, finished, warning]
        """
        self.refresh_status()
        return self._get_status_from_dict()

    @format_docstring_with_statuses(n_tabs=2)
    @string.setter
    def string(self, status):
        """
        Set the current status, to one of the following: {}

        Args:
            status (str): status [initialized, appended, created, submitted, running, aborted, collect, suspended,
                          refresh, busy, finished, warning]
        """
        self._reset()
        if isinstance(status, six.string_types) and status in self._status_dict.keys():
            self._status_dict[status] = True
            self._status_write()
        else:
            raise (
                "No valid job status: ",
                status,
                " Instead use [initialized, appended, created, submitted, running,"
                "aborted, collect, suspended, refresh, busy, finished, not_converged].",
            )

    def refresh_status(self):
        """
        Refresh the job status - check if the database and job_id are set and if this is the case load the job status
        from the database.
        """
        if self.database and self.job_id and not any([self._status_dict[i] for i in job_status_finished_lst]):
            try:
                status = self.database.get_job_status(job_id=self.job_id)
            except IndexError:
                raise (
                    "The job with the job ID "
                    + str(self.job_id)
                    + " is not listed in the database anymore."
                )
            self._reset()
            self._status_dict[status] = True

    def _status_write(self):
        """
        Private function: Write the job status to the internal variable _key and store it in the database.
        """
        if self.database and self.job_id:
            current_status = str(self._get_status_from_dict())
            if self.database.get_job_status(job_id=self.job_id) != current_status:
                self.database.set_job_status(job_id=self.job_id, status=current_status)

    def _reset(self):
        """
        internal function to reset the run mode - sets all run modes to false.
        """
        self._status_dict = {status: False for status in job_status_lst}

    @staticmethod
    def _bool_check(boolean):
        """
        Private function: Raise TypeError if boolean is not type bool and raise a ValueError if it is not True.
        Args:
            boolean (bool): True
        """
        if not isinstance(boolean, bool):
            raise TypeError(
                "The JobStatus can only be set to a boolean, more specifically to True only."
            )
        if boolean is False:
            raise ValueError("The JobStatus can only be set to True.")

    def _get_status_from_dict(self):
        return [key for key, val in self._status_dict.items() if val][0]

    def __repr__(self):
        """
        Human readable representation of the job status
        Returns:
            str: human readable string
        """
        return repr(self.string)

    def __str__(self):
        """
        Machine readable representation of the job status
        Returns:
            str: machine readable string
        """
        return str(self.string)

    def __getattr__(self, name):
        if name in self._status_dict.keys():
            self.refresh_status()
            return self._status_dict[name]
        raise AttributeError("'{}' object has no attribute '{}'".format(
                                self.__class__.__name__, name))

    def __setattr__(self, name, value):
        if name in self._status_dict.keys():
            if not isinstance(value, bool):
                raise TypeError("A run mode can only be activated using [True].")
            if value:
                self.string = name
            else:
                raise ValueError("A run mode can only be activated using [True].")
        else:
            super(JobStatus, self).__setattr__(name, value)

    def __dir__(self):
        return list(self._status_dict.keys())

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return other._status_dict == self._status_dict
        elif isinstance(other, str):
            return other == self.string
        else:
            return super(JobStatus, self).__eq__(other)

    def __ne__(self, other):
        if isinstance(other, self.__class__):
            return other._status_dict != self._status_dict
        elif isinstance(other, str):
            return other != self.string
        else:
            return super(JobStatus, self).__ne__(other)
