# controller.py
# Anaconda module lifecycle controller.
#
# Copyright (C) 2016  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#

from threading import RLock
from pyanaconda.core.signal import Signal
from pyanaconda.core.util import synchronized

from pyanaconda.anaconda_loggers import get_module_logger
log = get_module_logger(__name__)

_controllers = {}
_controller_categories_map = {}


def get_controller_by_category(category_name):
    """Return the controller instance that "owns" the corresponding category.

    This is more or less a workaround to the fact that spokes
    need to know what's their controller, but the controllers
    are indexed by hub name and spokes don't have a direct reference
    to the hub they are displayed on.

    Spokes know their category and hubs know what categories the and
    categories  should not be on two hubs. So we match the spoke category
    to get the controller corresponding to the spoke.

    :param str category_name: a spoke category name
    :returns: a Controller instance "owning" the category or None if no matching instance is found
    :rtype: str or None

    """
    for controller_name, controller_categories in _controller_categories_map.items():
        if category_name in controller_categories:
            return _controllers[controller_name]
    # no controller has been found for the given spoke based on it's category
    return None


def get_controller_by_name(controller_name):
    """Return controller instance by name.

    This function wraps the internal controller dictionary, so that it is not directly exposed.

    The controller names currently correspond to hub class name, eq.:
    SummaryHub, ProgressHub, etc.

    :param str controller_name: a name of a controller name
    """
    return _controllers.get(controller_name)


def add_controller(controller_name, controller_categories):
    # The controller name is currently based on Hub name
    # and None indicates that the Hub subclass has not
    # set the hub_name property to a non-default value.
    if controller_name is None:
        log.warning("Controller name is None.")
    else:
        log.info("Adding controller: %s", controller_name)
    controller = Controller()
    _controllers[controller_name] = controller
    _controller_categories_map[controller_name] = controller_categories
    return controller


class Controller(object):
    """A singleton that track initialization of Anaconda modules."""
    def __init__(self):
        self._lock = RLock()
        self._modules = set()
        self._all_modules_added = False
        self.init_done = Signal()
        self._init_done_triggered = False
        self._added_module_count = 0

    @synchronized
    def module_init_start(self, module):
        """Tell the controller that a module has started initialization.

        :param module: a module which has started initialization
        """
        if self._all_modules_added:
            log.warning("Late module_init_start() from: %s", self)
        elif module in self._modules:
            log.warning("Module already marked as initializing: %s", module)
        else:
            self._added_module_count += 1
            self._modules.add(module)

    def all_modules_added(self):
        """Tell the controller that all expected modules have started initialization.

        Tell the controller that all expected modules have been registered
        for initialization tracking (or have already been initialized)
        and no more are expected to be added.

        This is needed so that we don't prematurely trigger the init_done signal
        when all known modules finish initialization while other modules have not
        yet been added.
        """
        init_done = False
        with self._lock:
            log.info("Initialization of all modules (%d) has been started.", self._added_module_count)
            self._all_modules_added = True

            # if all modules finished initialization before this was added then
            # trigger the init_done signal at once
            if not self._modules and not self._init_done_triggered:
                self._init_done_triggered = True
                init_done = True

        # we should emit the signal out of the main lock as it doesn't make sense
        # to hold the controller-state lock once we decide to the trigger init_done signal
        # (and any callbacks registered on it)
        if init_done:
            self._trigger_init_done()

    def module_init_done(self, module):
        """Tell the controller that a module has finished initialization.

        And if no more modules are being initialized trigger the init_done signal.

        :param module: a module that has finished initialization
        """
        init_done = False
        with self._lock:
            # prevent the init_done signal from
            # being triggered more than once
            if self._init_done_triggered:
                log.warning("Late module_init_done from module %s.", module)
            else:
                if module in self._modules:
                    log.info("Module initialized: %s", module)
                    self._modules.discard(module)
                else:
                    log.warning("Unknown module reported as initialized: %s", module)
                # don't trigger the signal if all modules have not yet been added
                if self._all_modules_added and not self._modules:
                    init_done = True
                    self._init_done_triggered = True

        # we should emit the signal out of the main lock as it doesn't make sense
        # to hold the controller-state lock once we decide to the trigger init_done signal
        # (and any callbacks registered on it)
        if init_done:
            self._trigger_init_done()

    def _trigger_init_done(self):
        log.info("All modules have been initialized.")
        self.init_done.emit()
