# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Module to handle glib main loop and provide asynchronous utilities.
"""

import logging
import random
import threading

from plinth import dbus, network
from plinth.utils import import_from_gi

from . import cfg

glib = import_from_gi('GLib', '2.0')

_thread = None
_main_loop = None

logger = logging.getLogger(__name__)


def run():
    """Run a glib main loop forever in a thread."""
    global _thread
    _thread = threading.Thread(target=_run)
    _thread.start()


def stop():
    """Exit glib main loop and end the thread."""
    if _main_loop:
        logger.info('Exiting glib main loop')
        _main_loop.quit()


def _run():
    """Connect to D-Bus and run main loop."""
    logger.info('Started new thread for glib main loop.')

    # Initialize all modules that use glib main loop
    dbus.init()
    network.init()

    global _main_loop
    _main_loop = glib.MainLoop()
    _main_loop.run()
    _main_loop = None

    logger.info('Glib main loop thread exited.')


def schedule(interval, method, data=None, in_thread=True, repeat=True,
             add_jitter=True, develop_interval=None):
    """Schedule a recurring call to a method with fixed interval.

    develop_interval is number of seconds to schedule the task after when
    running in development mode. A value of 180 seconds assumed if the value is
    set to None or 0.
    """

    def _runner():
        """Run the target method and log and exceptions."""
        try:
            return method(data)
        except Exception as exception:  # pylint: disable=broad-except
            logger.exception('Exception in running scheduled method - %s',
                             exception)

    def _run_bare_or_thread(_user_data):
        """Run the target method in thread or directly (if async)."""
        if not in_thread:
            _runner()
            return repeat

        thread = threading.Thread(target=_runner)
        thread.start()
        return repeat

    # When running in development mode, reduce the interval for tasks so that
    # they are triggered quickly and frequently to facilitate debugging.
    if cfg.develop:
        interval = min(interval, develop_interval or 180)

    if add_jitter:
        # Add or subtract 5% random jitter to given interval to avoid many
        # tasks running at exactly the same time (and competing for DB, disk,
        # network, etc.).
        interval *= 0.95 + (random.random() * 0.1)

    glib.timeout_add(int(interval * 1000), _run_bare_or_thread, None)
