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, interval: Optional[float] = None
            ) -> 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.

        The state of the job will be checked every `interval` seconds.  If `interval` is
        None, or not specified, then the interval will be 1s, or time_out / 50,
        whichever is larger. If time_out is not given, the interval will start at 1s,
        then increase gradually to about 30s.

        Args:
            job_id: The job to wait for.
            time_out: Time to wait in seconds. If negative, wait forever.
            interval: Time to wait between checks. See above.

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

        if interval is None:
            # this is correct for negative time-outs as well
            interval = max(1.0, time_out / 50.0)

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

            if not fixed_interval and interval < 30:
                interval += 2

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

        return self.get_exit_code(job_id)
