#
# platform.py:  Architecture-specific information
#
# Copyright (C) 2009-2011
# Red Hat, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>.
#
# Authors: Chris Lumens <clumens@redhat.com>
#
from blivet import arch
from blivet.devicelibs import raid
from blivet.size import Size

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.i18n import _, N_
from pyanaconda.modules.storage.partitioning.specification import PartSpec

log = get_module_logger(__name__)

# Names of stage1 constrains.
PLATFORM_DEVICE_TYPES = "device_types"
PLATFORM_FORMAT_TYPES = "format_types"
PLATFORM_MOUNT_POINTS = "mountpoints"
PLATFORM_MAX_END = "max_end"
PLATFORM_RAID_LEVELS = "raid_levels"
PLATFORM_RAID_METADATA = "raid_metadata"

# Descriptions of stage1 bootloader devices.
PARTITION_DESCRIPTION = N_("First sector of boot partition")
RAID_DESCRIPTION = N_("RAID Device")
MBR_DESCRIPTION = N_("Master Boot Record")
EFI_DESCRIPTION = N_("EFI System Partition")
PREP_BOOT_DESCRIPTION = N_("PReP Boot Partition")
APPLE_EFI_DESCRIPTION = N_("Apple EFI Boot Partition")
APPLE_BOOTSTRAP_DESCRIPTION = N_("Apple Bootstrap Partition")
DASD_DESCRIPTION = N_("DASD")
ZFCP_DESCRIPTION = N_("zFCP")


class Platform(object):
    """A base class for a platform.

    A class containing platform-specific information and methods for use
    during installation.  The intent is to eventually encapsulate all the
    architecture quirks in one place to avoid lots of platform checks
    throughout anaconda.
    """

    @property
    def packages(self):
        """Packages required for this platform.

        :return: a list of package names
        """
        return []

    @property
    def non_linux_format_types(self):
        """Format types of devices with non-linux operating systems."""
        return []

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device.

        :return: a string
        """
        return _("You must include at least one disk as an install target.")

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device.

        :return: a dictionary of device types and their descriptions
        """
        return {}

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device.

        :return: a dictionary of constraints
        """
        return {
            PLATFORM_DEVICE_TYPES: [],
            PLATFORM_FORMAT_TYPES: [],
            PLATFORM_MOUNT_POINTS: [],
            PLATFORM_MAX_END: None,
            PLATFORM_RAID_LEVELS: [],
            PLATFORM_RAID_METADATA: [],
        }

    @property
    def partitions(self):
        """The default platform-specific partitions.

        :return: a list of specifications
        """
        partitions = [
            self._bootloader_partition,
            self._boot_partition
        ]
        return list(filter(None, partitions))

    @property
    def _bootloader_partition(self):
        """The default bootloader partition for this platform.

        Return the required platform-specific bootloader partition
        information. These are typically partitions that do not get
        mounted, like biosboot or prepboot, but may also include
        the /boot/efi partition.

        :return: a specification or None
        """
        return None

    @property
    def _boot_partition(self):
        """The default /boot partition for this platform.

        :return: a specification or None
        """
        return PartSpec(
            mountpoint="/boot",
            size=Size("1GiB")
        )


class X86(Platform):

    @property
    def non_linux_format_types(self):
        """Format types of devices with non-linux operating systems."""
        # XXX hpfs, if reported by blkid/udev, will end up with a type of None
        return ["vfat", "ntfs", "hpfs"]

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _(
            "You must include at least one MBR- or "
            "GPT-formatted disk as an install target."
        )

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {
            "disk": _(MBR_DESCRIPTION),
            "partition": _(PARTITION_DESCRIPTION),
            "mdarray": _(RAID_DESCRIPTION)
        }

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_DEVICE_TYPES: ["disk"]
        }
        return dict(super().stage1_constraints, **constraints)

    @property
    def _bootloader_partition(self):
        """The default bootloader partition for this platform."""
        return PartSpec(
            fstype="biosboot",
            size=Size("1MiB")
        )


class EFI(Platform):

    @property
    def non_linux_format_types(self):
        """Format types of devices with non-linux operating systems."""
        # XXX hpfs, if reported by blkid/udev, will end up with a type of None
        return ["vfat", "ntfs", "hpfs"]

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _(
            "For a UEFI installation, you must include "
            "an EFI System Partition on a GPT-formatted "
            "disk, mounted at /boot/efi."
        )

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {
            "partition": _(EFI_DESCRIPTION),
            "mdarray": _(RAID_DESCRIPTION)
        }

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_FORMAT_TYPES: ["efi"],
            PLATFORM_DEVICE_TYPES: ["partition", "mdarray"],
            PLATFORM_MOUNT_POINTS: ["/boot/efi"],
            PLATFORM_RAID_LEVELS: [raid.RAID1],
            PLATFORM_RAID_METADATA: ["1.0"],
        }
        return dict(super().stage1_constraints, **constraints)

    @property
    def _bootloader_partition(self):
        """The default bootloader partition for this platform."""
        return PartSpec(
            mountpoint="/boot/efi",
            fstype="efi",
            size=Size("200MiB"),
            max_size=Size("600MiB"),
            grow=True
        )


class MacEFI(EFI):

    @property
    def packages(self):
        """Packages required for this platform."""
        return ["mactel-boot"]

    @property
    def non_linux_format_types(self):
        """Format types of devices with non-linux operating systems."""
        return ["macefi"]

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _(
            "For a UEFI installation, you must include "
            "a Linux HFS+ ESP on a GPT-formatted "
            "disk, mounted at /boot/efi."
        )

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {
            "partition": _(APPLE_EFI_DESCRIPTION),
            "mdarray": _(RAID_DESCRIPTION)
        }

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_FORMAT_TYPES: ["macefi"]
        }
        return dict(super().stage1_constraints, **constraints)

    @property
    def _bootloader_partition(self):
        """The default bootloader partition for this platform."""
        return PartSpec(
            mountpoint="/boot/efi",
            fstype="macefi",
            size=Size("200MiB"),
            max_size=Size("600MiB"),
            grow=True,
        )


class Aarch64EFI(EFI):

    @property
    def non_linux_format_types(self):
        """Format types of devices with non-linux operating systems."""
        return ["vfat", "ntfs"]


class ArmEFI(EFI):

    @property
    def non_linux_format_types(self):
        """Format types of devices with non-linux operating systems."""
        return ["vfat", "ntfs"]


class PPC(Platform):

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_DEVICE_TYPES: ["partition"]
        }
        return dict(super().stage1_constraints, **constraints)


class IPSeriesPPC(PPC):

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _(
            "You must include a PReP Boot Partition "
            "within the first 4GiB of an MBR- "
            "or GPT-formatted disk."
        )

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {"partition": _(PREP_BOOT_DESCRIPTION)}

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_FORMAT_TYPES: ["prepboot"],
            PLATFORM_MAX_END: Size("4 GiB")
        }
        return dict(super().stage1_constraints, **constraints)

    @property
    def _bootloader_partition(self):
        """The default bootloader partition for this platform."""
        return PartSpec(
            fstype="prepboot",
            size=Size("4MiB")
        )


class NewWorldPPC(PPC):

    @property
    def non_linux_format_types(self):
        """Format types of devices with non-linux operating systems."""
        return ["hfs", "hfs+"]

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _(
            "You must include an Apple Bootstrap "
            "Partition on an Apple Partition Map-"
            "formatted disk."
        )

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {"partition": _(APPLE_BOOTSTRAP_DESCRIPTION)}

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_FORMAT_TYPES: ["appleboot"]
        }
        return dict(super().stage1_constraints, **constraints)

    @property
    def _bootloader_partition(self):
        """The default bootloader partition for this platform."""
        return PartSpec(
            fstype="appleboot",
            size=Size("1MiB")
        )


class PowerNV(PPC):

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _("You must include at least one disk as an install target.")

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {"partition": _(PARTITION_DESCRIPTION)}


class PS3(PPC):
    pass


class S390(Platform):

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _(
            "You must include at least one MBR- or "
            "DASD-formatted disk as an install target."
        )

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {
            "dasd": _(DASD_DESCRIPTION),
            "zfcp": _(ZFCP_DESCRIPTION),
            "disk": _(MBR_DESCRIPTION),
            "partition": _(PARTITION_DESCRIPTION)
        }

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_DEVICE_TYPES: ["disk", "partition"]
        }
        return dict(super().stage1_constraints, **constraints)

    @property
    def _boot_partition(self):
        """The default /boot partition for this platform."""
        return PartSpec(
            mountpoint="/boot",
            size=Size("1GiB"),
            lv=False
        )


class ARM(Platform):

    @property
    def stage1_suggestion(self):
        """The platform-specific suggestion about the stage1 device."""
        return _(
            "You must include at least one MBR-formatted "
            "disk as an install target."
        )

    @property
    def stage1_descriptions(self):
        """The platform-specific descriptions of the stage1 device."""
        return {
            "disk": _(MBR_DESCRIPTION),
            "partition": _(PARTITION_DESCRIPTION)
        }

    @property
    def stage1_constraints(self):
        """The platform-specific constraints for the stage1 device."""
        constraints = {
            PLATFORM_DEVICE_TYPES: ["disk"]
        }
        return dict(super().stage1_constraints, **constraints)


def get_platform():
    """Check the architecture of the system and return an instance of a
       Platform subclass to match.  If the architecture could not be determined,
       raise an exception."""
    if arch.is_ppc():
        ppc_machine = arch.get_ppc_machine()

        if ppc_machine == "PMac" and arch.get_ppc_mac_gen() == "NewWorld":
            return NewWorldPPC()
        elif ppc_machine in ["iSeries", "pSeries"]:
            return IPSeriesPPC()
        elif ppc_machine == "PowerNV":
            return PowerNV()
        elif ppc_machine == "PS3":
            return PS3()
        else:
            raise SystemError("Unsupported PPC machine type: %s" % ppc_machine)
    elif arch.is_s390():
        return S390()
    elif arch.is_efi():
        if arch.is_mactel():
            return MacEFI()
        elif arch.is_aarch64():
            return Aarch64EFI()
        elif arch.is_arm():
            return ArmEFI()
        else:
            return EFI()
    elif arch.is_x86():
        return X86()
    elif arch.is_arm():
        return ARM()
    else:
        raise SystemError("Could not determine system architecture.")


platform = get_platform()
