#
# base.py
# Base classes for Anaconda modules.
#
# Copyright (C) 2017 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.
#
import os
import warnings
from abc import ABC
from locale import setlocale, LC_ALL

from pykickstart.errors import KickstartError, KickstartParseWarning

from pyanaconda.core.glib import create_main_loop
from pyanaconda.core.timer import Timer
from pyanaconda.core.util import setenv
from pyanaconda.core.dbus import DBus
from pyanaconda.core.signal import Signal
from pyanaconda.core.kickstart.specification import NoKickstartSpecification, \
    KickstartSpecificationHandler, KickstartSpecificationParser
from pyanaconda.modules.common.structures.kickstart import KickstartReport, KickstartMessage

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


class BaseModule(ABC):
    """Implementation of a base module."""

    def __init__(self):
        super().__init__()
        self._module_properties_changed = Signal()

    @property
    def module_properties_changed(self):
        """Signal that module might have changed.

        If someone changes properties of this module
        on the python level (and not from DBus), he
        should emit this signal once he is done.

        Example of the callback:

            def callback():
                pass
        """
        return self._module_properties_changed

    def publish(self):
        """Publish DBus objects and register a DBus service.

        Nothing is published by default.
        """
        pass


class Service(BaseModule):
    """Implementation of a DBus service.

    The service is an owner of the main loop, so it is
    able to start and stop the application.
    """

    def __init__(self):
        super().__init__()
        self._loop = create_main_loop()

    @property
    def loop(self):
        """Return the loop."""
        return self._loop

    def run(self):
        """Run the loop."""
        log.debug("Publish the service.")
        self.publish()
        log.debug("Start the loop.")
        self._loop.run()

    def stop(self):
        """Stop the loop."""
        DBus.disconnect()
        Timer().timeout_sec(1, self.loop.quit)

    def set_locale(self, locale):
        """Set the locale for the module.

        This function modifies the process environment, which is not thread-safe.
        It should be called before any threads are run.

        We cannot get around setting $LANG. Python's gettext implementation
        differs from C in that consults only the environment for the current
        language and not the data set via setlocale. If we want translations
        from python modules to work, something needs to be set in the
        environment when the language changes.

        :param str locale: locale to set
        """
        os.environ["LANG"] = locale  # pylint: disable=environment-modify
        setlocale(LC_ALL, locale)
        # Set locale for child processes
        setenv("LANG", locale)
        log.debug("Locale is set to %s.", locale)


class KickstartBaseModule(BaseModule):
    """Implementation of a base kickstart module."""

    def process_kickstart(self, data):
        """Process the kickstart data.

        Use the given kickstart data to set the module attributes.

        :param data: a kickstart handler
        """
        pass

    def setup_kickstart(self, data):
        """Set the given kickstart data.

        Use the module attributes to set the kickstart data.

        :param data: a kickstart handler
        """
        pass

    def collect_requirements(self):
        """Return installation requirements.

        :return: a list of requirements
        """
        return []


class KickstartService(Service, KickstartBaseModule):
    """Implementation of a DBus service with kickstart support.

    The kickstart service is able to parse and generate the given
    kickstart string based on its kickstart specification.
    """

    def __init__(self):
        super().__init__()
        self.kickstarted_changed = Signal()
        self._kickstarted = False

    @property
    def kickstart_specification(self):
        """Return a kickstart specification.

        Every kickstart module that is interested in processing
        kickstart files, should provide its own specification.

        :return: a subclass of KickstartSpecification
        """
        return NoKickstartSpecification

    @property
    def kickstart_command_names(self):
        """Return a list of kickstart command names."""
        return list(self.kickstart_specification.commands.keys())

    @property
    def kickstart_section_names(self):
        """Return a list of kickstart section names."""
        return list(self.kickstart_specification.sections.keys())

    @property
    def kickstart_addon_names(self):
        """Return a list of kickstart addon names."""
        return list(self.kickstart_specification.addons.keys())

    @property
    def kickstarted(self):
        """Was this module set up by the kickstart?"""
        return self._kickstarted

    @kickstarted.setter
    def kickstarted(self, value):
        self._kickstarted = value
        self.kickstarted_changed.emit()
        log.debug("Kickstarted is set to %s.", value)

    def get_kickstart_handler(self):
        """Return a kickstart handler.

        :return: a kickstart handler
        """
        return KickstartSpecificationHandler(self.kickstart_specification)

    def get_kickstart_parser(self, handler):
        """Return a kickstart parser.

        :param handler: a kickstart handler
        :return: a kickstart parser
        """
        return KickstartSpecificationParser(handler, self.kickstart_specification)

    def read_kickstart(self, s):
        """Read the given kickstart string.

        The kickstart string should contain only commands and
        sections that are defined by the kickstart specification.

        :param s: a kickstart string
        :return: a kickstart report
        """
        log.debug("Reading kickstart...")
        report = KickstartReport()

        try:
            handler = self.get_kickstart_handler()
            parser = self.get_kickstart_parser(handler)

            with warnings.catch_warnings(record=True) as warns:
                warnings.simplefilter(action="always", category=KickstartParseWarning)

                parser.readKickstartFromString(s)
                self.process_kickstart(handler)

                for warn in warns:
                    if issubclass(warn.category, KickstartParseWarning):
                        message = str(warn.message)
                        data = KickstartMessage.for_warning(message)
                        report.warning_messages.append(data)

        except KickstartError as e:
            data = KickstartMessage.for_error(e)
            report.error_messages.append(data)
        else:
            self.kickstarted = True

        return report

    def generate_kickstart(self):
        """Return a kickstart representation of this module.

        The kickstart string should contain only commands and
        sections that are defined by the kickstart specification.

        :return: a kickstart string
        """
        log.debug("Generating kickstart...")
        handler = self.get_kickstart_handler()
        self.setup_kickstart(handler)
        return str(handler)

    def configure_with_tasks(self):
        """Configure the runtime environment.

        Note: Addons should use it instead of the setup method.

        :return: a list of DBus paths of the installation tasks
        """
        return []

    def configure_bootloader_with_tasks(self, kernel_versions):
        """Configure the bootloader after the payload installation with a list of tasks.

        FIXME: This is a temporary workaround. The method might change.

        :param kernel_versions: a list of kernel versions
        :return: a list of tasks
        """
        return []

    def install_with_tasks(self):
        """Return installation tasks of this module.

        Note: Addons should use it instead of the execute method.

        :return: a list of DBus paths of the installation tasks
        """
        return []

    def teardown_with_tasks(self):
        """Returns teardown tasks for this module.

        :return: a list of DBus paths of the installation tasks
        """
        return []
