#
# Base object of all payloads.
#
# Copyright (C) 2019 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 abc import ABCMeta, abstractmethod

from dasbus.server.publishable import Publishable

from pyanaconda.core.signal import Signal
from pyanaconda.modules.common.errors.general import UnavailableValueError
from pyanaconda.modules.common.errors.payload import IncompatibleSourceError, SourceSetupError
from pyanaconda.modules.common.base import KickstartBaseModule
from pyanaconda.modules.payloads.base.initialization import SetUpSourcesTask, TearDownSourcesTask
from pyanaconda.modules.payloads.constants import SourceState

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


class PayloadBase(KickstartBaseModule, Publishable, metaclass=ABCMeta):
    """Base class for all the payload modules.

    This will contain all API specific to payload which will be called
    by the base payload module.
    """
    def __init__(self):
        super().__init__()
        self._sources = []
        self.sources_changed = Signal()
        self._kernel_version_list = None

    @property
    @abstractmethod
    def type(self):
        """Type of this payload.

        :return: value of the payload.base.constants.PayloadType enum
        """
        return None

    @property
    @abstractmethod
    def default_source_type(self):
        """Type of the default source.

        :return SourceType: a default source type
        """
        return None

    @property
    @abstractmethod
    def supported_source_types(self):
        """Get list of supported source types.

        :return [SourceType]: a list of supported source types
        """
        return []

    @property
    def sources(self):
        """Get list of sources attached to this payload.

        :return: list of source objects attached to this payload
        :rtype: [instance of PayloadSourceBase class]
        """
        return self._sources

    def _get_source(self, source_type):
        """Get an attached source object of the specified type.

        :param SourceType source_type: a type of the source
        :return: a source object or None
        """
        for source in self.sources:
            if source.type == source_type:
                return source

        return None

    def set_sources(self, sources):
        """Set a new list of sources to this payload.

        Before setting the sources, please make sure the sources are not initialized otherwise
        the SourceSetupError exception will be raised. Payload have to cleanup after itself.

        ..NOTE:
        The SourceSetupError is a reasonable effort to solve the race condition. However,
        there is still a possibility that the task to initialize sources (`SetupSourcesWithTask()`)
        was created with the old list but not run yet. In that case this check will not work and
        the initialization task will run with the old list.

        :param sources: set a new sources
        :type sources: instance of pyanaconda.modules.payloads.source.source_base.PayloadSourceBase
        :raise: IncompatibleSourceError when source is not a supported type
                SourceSetupError when attached sources are initialized
        """
        for source in sources:
            if source.type not in self.supported_source_types:
                raise IncompatibleSourceError("Source type {} is not supported by this payload."
                                              .format(source.type.value))

        if any(source.get_state() == SourceState.READY for source in self.sources):
            raise SourceSetupError("Can't change list of sources if there is at least one source "
                                   "initialized! Please tear down the sources first.")

        self._sources = sources
        log.debug("New sources %s were added.", sources)
        self.sources_changed.emit()

    def add_source(self, source):
        """Module scope API for easier adding of sources.

        :param source: Source we want to add.
        """
        sources = list(self.sources)
        sources.append(source)
        self.set_sources(sources)

    def is_network_required(self):
        """Do the sources require a network?

        :return: True or False
        """
        for source in self.sources:
            if source.network_required:
                return True

        return False

    def calculate_required_space(self):
        """Calculate space required for the installation.

        :return: required size in bytes
        :rtype: int
        """
        total = 0

        for source in self.sources:
            total += source.required_space

        return total

    def get_kernel_version_list(self):
        """Get the kernel versions list.

        The kernel version list doesn't have to be available
        before the payload installation.

        :return: a list of kernel versions
        :raises UnavailableValueError: if the list is not available
        """
        if self._kernel_version_list is None:
            raise UnavailableValueError("The kernel version list is not available.")

        return self._kernel_version_list

    def set_kernel_version_list(self, kernels):
        """Set the kernel versions list.

        This function should be called by one of the installation tasks.

        :param kernels: a list of kernel versions
        """
        self._kernel_version_list = kernels
        log.debug("The kernel version list is set to: %s", kernels)

    @abstractmethod
    def install_with_tasks(self):
        """Install the payload.

        :return: list of tasks
        """
        pass

    def post_install_with_tasks(self):
        """Execute post installation steps.

        :return: list of tasks
        """
        return []

    def set_up_sources_with_task(self):
        """Set up installation sources."""
        return SetUpSourcesTask(self.sources)

    def tear_down_sources_with_task(self):
        """Tear down installation sources."""
        return TearDownSourcesTask(self.sources)

    def tear_down_with_tasks(self):
        """Returns teardown tasks for this payload.

        Clean up everything after this payload.

        :return: a list of tasks
        """
        tasks = []

        if self.sources:
            tasks.append(self.tear_down_sources_with_task())

        return tasks
