from abc import ABC, abstractmethod
from time import perf_counter, sleep
from typing import Optional

from cerulean.job_description import JobDescription
from cerulean.job_status import JobStatus


class Scheduler(ABC):
    """Interface for job schedulers.

    To run jobs using a scheduler, you will want to use \
    :class:`SlurmScheduler` or :class:`TorqueScheduler`.
    """

    @abstractmethod
    def submit(self, job_description: JobDescription) -> str:
        """Submit a job for execution.

        Args:
            job_description: A description of the job to run.

        Returns:
            A job id that can be used to keep track of it.
        """
        pass

    @abstractmethod
    def get_status(self, job_id: str) -> JobStatus:
        """Look up the status of a job.

        This method is used to check if a job is still in the queue, \
        running, or done.

        Args:
            job_id: A job id string obtained from :meth:`submit`.

        Returns:
            The status of the job as a :class:`JobStatus`
        """
        pass

    @abstractmethod
    def get_exit_code(self, job_id: str) -> Optional[int]:
        """Get the exit code of a finished job.

        Once a job is done, its exit code may be requested using this \
        method. If the job is still running or failed to start, then \
        there is no exit code, and None will be returned.

        Args:
            job_id: A job id string obtained from :meth:`submit`.

        Returns:
            The exit code, or None if there is none.
        """
        pass

    @abstractmethod
    def cancel(self, job_id: str) -> None:
        """Cancel a running job.

        Submits a cancellation request for a job to the scheduler.

        Args:
            job_id: Id of the job to be cancelled.
        """
        pass

    def wait(self, job_id: str, time_out: float = -1.0) -> Optional[int]:
        """Wait until the job is done.

        Will wait approximately time_out seconds for the job to finish.
        Returns the exit code if the job finished, otherwise None.
        Resolution is 0.1s, or time_out / 20, whichever is larger.

        Args:
            job_id: The job to wait for.
            time_out: Time to wait in seconds. If negative, wait \
                    forever.

        Returns:
            The exit code of the job.
        """
        if time_out < 0.0:
            time_end = None
        else:
            time_end = perf_counter() + time_out

        sleep_time = max(0.1, time_out / 20.0)

        status = self.get_status(job_id)
        while status != JobStatus.DONE:
            sleep(sleep_time)
            status = self.get_status(job_id)

            if time_end is not None and time_end < perf_counter():
                break

        return self.get_exit_code(job_id)
