# Copyright 1999-2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import threading

from portage.util.futures import asyncio
from portage.util._async.SchedulerInterface import SchedulerInterface
from portage.util._eventloop.global_event_loop import global_event_loop

from _emerge.getloadavg import getloadavg


class PollScheduler:
    # max time between loadavg checks (seconds)
    _loadavg_latency = None

    def __init__(self, main=False, event_loop=None):
        """
        @param main: If True then use global_event_loop(), otherwise use
                a local EventLoop instance (default is False, for safe use in
                a non-main thread)
        @type main: bool
        """
        self._term_rlock = threading.RLock()
        self._terminated = threading.Event()
        self._terminated_tasks = False
        self._term_check_handle = None
        self._max_jobs = 1
        self._max_load = None
        self._scheduling = False
        self._background = False
        if event_loop is not None:
            self._event_loop = event_loop
        elif main:
            self._event_loop = global_event_loop()
        else:
            self._event_loop = asyncio._safe_loop()
        self._sched_iface = SchedulerInterface(
            self._event_loop, is_background=self._is_background
        )

    @property
    def _loop(self):
        """
        Returns the real underlying asyncio loop.
        """
        return self._event_loop._loop

    def _is_background(self):
        return self._background

    def _cleanup(self):
        """
        Cleanup any callbacks that have been registered with the global
        event loop.
        """
        # The self._term_check_handle attribute requires locking
        # since it's modified by the thread safe terminate method.
        with self._term_rlock:
            if self._term_check_handle not in (None, False):
                self._term_check_handle.cancel()
            # This prevents the terminate method from scheduling
            # any more callbacks (since _cleanup must eliminate all
            # callbacks in order to ensure complete cleanup).
            self._term_check_handle = False

    def terminate(self):
        """
        Schedules asynchronous, graceful termination of the scheduler
        at the earliest opportunity.

        This method is thread-safe (and safe for signal handlers).
        """
        with self._term_rlock:
            if self._term_check_handle is None:
                self._terminated.set()
                self._term_check_handle = self._event_loop.call_soon_threadsafe(
                    self._termination_check, True
                )

    def _termination_check(self, retry=False):
        """
        Calls _terminate_tasks() if appropriate. It's guaranteed not to
        call it while _schedule_tasks() is being called. This method must
        only be called via the event loop thread.

        @param retry: If True then reschedule if scheduling state prevents
                immediate termination.
        @type retry: bool
        """
        if self._terminated.is_set() and not self._terminated_tasks:
            if not self._scheduling:
                self._scheduling = True
                try:
                    self._terminated_tasks = True
                    self._terminate_tasks()
                finally:
                    self._scheduling = False

            elif retry:
                with self._term_rlock:
                    self._term_check_handle = self._event_loop.call_soon(
                        self._termination_check, True
                    )

    def _terminate_tasks(self):
        """
        Send signals to terminate all tasks. This is called once
        from _keep_scheduling() or _is_work_scheduled() in the event
        dispatching thread. It will not be called while the _schedule_tasks()
        implementation is running, in order to avoid potential
        interference. All tasks should be cleaned up at the earliest
        opportunity, but not necessarily before this method returns.
        Typically, this method will send kill signals and return without
        waiting for exit status. This allows basic cleanup to occur, such as
        flushing of buffered output to logs.
        """
        raise NotImplementedError()

    def _keep_scheduling(self):
        """
        @rtype: bool
        @return: True if there may be remaining tasks to schedule,
                False otherwise.
        """
        return False

    def _schedule_tasks(self):
        """
        This is called from inside the _schedule() method, which
        guarantees the following:

        1) It will not be called recursively.
        2) _terminate_tasks() will not be called while it is running.
        3) The state of the boolean _terminated_tasks variable will
           not change while it is running.

        Unless this method is used to perform user interface updates,
        or something like that, the first thing it should do is check
        the state of _terminated_tasks and if that is True then it
        should return immediately (since there's no need to
        schedule anything after _terminate_tasks() has been called).
        """
        pass

    def _schedule(self):
        """
        Calls _schedule_tasks() and automatically returns early from
        any recursive calls to this method that the _schedule_tasks()
        call might trigger. This makes _schedule() safe to call from
        inside exit listeners. This method always returns True, so that
        it may be scheduled continuously via EventLoop.timeout_add().
        """
        if self._scheduling:
            return True
        self._scheduling = True
        try:
            self._schedule_tasks()
        finally:
            self._scheduling = False
        return True

    def _is_work_scheduled(self):
        return bool(self._running_job_count())

    def _running_job_count(self):
        raise NotImplementedError(self)

    def _can_add_job(self):
        if self._terminated_tasks:
            return False

        max_jobs = self._max_jobs
        max_load = self._max_load

        if self._max_jobs is not True and self._running_job_count() >= self._max_jobs:
            return False

        if (
            max_load is not None
            and (max_jobs is True or max_jobs > 1)
            and self._running_job_count() >= 1
        ):
            try:
                avg1, avg5, avg15 = getloadavg()
            except OSError:
                return False

            if avg1 >= max_load:
                return False

        return True
