#
# 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>
#
import logging
log = logging.getLogger("anaconda.storage")

from blivet import arch
from blivet.devicelibs import raid
from blivet.formats import get_device_format_class
from blivet.flags import flags
from blivet.size import Size
from pyanaconda.core.i18n import _, N_
from pyanaconda.storage.partspec import PartSpec


class Platform(object):

    """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."""
    _packages = []

    # requirements for bootloader stage1 devices
    _boot_stage1_device_types = []
    _boot_stage1_format_types = []
    _boot_stage1_mountpoints = []
    _boot_stage1_max_end = None
    _boot_stage1_raid_levels = []
    _boot_stage1_raid_metadata = []
    _boot_stage1_raid_member_types = []
    _boot_stage1_description = N_("boot loader device")
    _boot_stage1_missing_error = ""
    _boot_raid_description = N_("RAID Device")
    _boot_partition_description = N_("First sector of boot partition")
    _boot_descriptions = {}

    _non_linux_format_types = []

    def __init__(self):
        """Creates a new Platform object.  This is basically an abstract class.
           You should instead use one of the platform-specific classes as
           returned by get_platform below.  Not all subclasses need to provide
           all the methods in this class."""

        self.update_from_flags()

    def update_from_flags(self):
        if flags.gpt:
            disklabel_class = get_device_format_class("disklabel")
            disklabel_types = disklabel_class.get_platform_label_types()
            if "gpt" not in disklabel_types:
                log.warning("GPT is not a supported disklabel on this platform. Using default "
                            "disklabel %s instead.", disklabel_types[0])
            else:
                disklabel_class.set_default_label_type("gpt")

    def __call__(self):
        return self

    @property
    def boot_stage1_constraint_dict(self):
        d = {"device_types": self._boot_stage1_device_types,
             "format_types": self._boot_stage1_format_types,
             "mountpoints": self._boot_stage1_mountpoints,
             "max_end": self._boot_stage1_max_end,
             "raid_levels": self._boot_stage1_raid_levels,
             "raid_metadata": self._boot_stage1_raid_metadata,
             "raid_member_types": self._boot_stage1_raid_member_types,
             "descriptions": dict((k, _(v)) for k, v in self._boot_descriptions.items())}
        return d

    @property
    def packages(self):
        _packages = self._packages
        if flags.boot_cmdline.get('fips', None) == '1':
            _packages.append('dracut-fips')
        return _packages

    def set_platform_bootloader_reqs(self):
        """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 []

    def set_platform_boot_partition(self):
        """Return the default /boot partition for this platform."""
        return [PartSpec(mountpoint="/boot", size=Size("1GiB"))]

    def set_default_partitioning(self):
        """Return the default platform-specific partitioning information."""
        return self.set_platform_bootloader_reqs() + self.set_platform_boot_partition()

    @property
    def stage1_missing_error(self):
        """A platform-specific error message to be shown if stage1 target
           selection fails."""
        return self._boot_stage1_missing_error


class X86(Platform):
    _boot_stage1_device_types = ["disk"]
    _boot_mbr_description = N_("Master Boot Record")
    _boot_descriptions = {"disk": _boot_mbr_description,
                          "partition": Platform._boot_partition_description,
                          "mdarray": Platform._boot_raid_description}

    # XXX hpfs, if reported by blkid/udev, will end up with a type of None
    _non_linux_format_types = ["vfat", "ntfs", "hpfs"]
    _boot_stage1_missing_error = N_("You must include at least one MBR- or "
                                    "GPT-formatted disk as an install target.")

    def set_platform_bootloader_reqs(self):
        """Return the default platform-specific partitioning information."""
        ret = super().set_platform_bootloader_reqs()
        ret.append(PartSpec(fstype="biosboot", size=Size("1MiB")))
        return ret


class EFI(Platform):

    _boot_stage1_format_types = ["efi"]
    _boot_stage1_device_types = ["partition", "mdarray"]
    _boot_stage1_mountpoints = ["/boot/efi"]
    _boot_stage1_raid_levels = [raid.RAID1]
    _boot_stage1_raid_metadata = ["1.0"]
    _boot_efi_description = N_("EFI System Partition")
    _boot_descriptions = {"partition": _boot_efi_description,
                          "mdarray": Platform._boot_raid_description}

    # XXX hpfs, if reported by blkid/udev, will end up with a type of None
    _non_linux_format_types = ["vfat", "ntfs", "hpfs"]
    _boot_stage1_missing_error = N_("For a UEFI installation, you must include "
                                    "an EFI System Partition on a GPT-formatted "
                                    "disk, mounted at /boot/efi.")

    def set_platform_bootloader_reqs(self):
        ret = super().set_platform_bootloader_reqs()
        ret.append(PartSpec(mountpoint="/boot/efi", fstype="efi",
                            size=Size("20MiB"), max_size=Size("200MiB"),
                            grow=True))
        return ret


class MacEFI(EFI):
    _boot_stage1_format_types = ["macefi"]
    _boot_efi_description = N_("Apple EFI Boot Partition")
    _non_linux_format_types = ["macefi"]
    _packages = ["mactel-boot"]
    _boot_stage1_missing_error = N_("For a UEFI installation, you must include "
                                    "a Linux HFS+ ESP on a GPT-formatted "
                                    "disk, mounted at /boot/efi.")

    def set_platform_bootloader_reqs(self):
        ret = super().set_platform_bootloader_reqs()
        ret.append(PartSpec(mountpoint="/boot/efi", fstype="macefi",
                            size=Size("20MiB"), max_size=Size("200MiB"),
                            grow=True))
        return ret


class Aarch64EFI(EFI):
    _non_linux_format_types = ["vfat", "ntfs"]


class PPC(Platform):
    _ppc_machine = arch.get_ppc_machine()
    _boot_stage1_device_types = ["partition"]

    @property
    def ppc_machine(self):
        return self._ppc_machine


class IPSeriesPPC(PPC):
    _boot_stage1_format_types = ["prepboot"]
    _boot_stage1_max_end = Size("4 GiB")
    _boot_prep_description = N_("PReP Boot Partition")
    _boot_descriptions = {"partition": _boot_prep_description}
    _boot_stage1_missing_error = N_("You must include a PReP Boot Partition "
                                    "within the first 4GiB of an MBR- "
                                    "or GPT-formatted disk.")

    def set_platform_bootloader_reqs(self):
        ret = PPC.set_platform_bootloader_reqs(self)
        ret.append(PartSpec(fstype="prepboot", size=Size("4MiB")))
        return ret


class NewWorldPPC(PPC):
    _boot_stage1_format_types = ["appleboot"]
    _boot_apple_description = N_("Apple Bootstrap Partition")
    _boot_descriptions = {"partition": _boot_apple_description}
    _non_linux_format_types = ["hfs", "hfs+"]
    _boot_stage1_missing_error = N_("You must include an Apple Bootstrap "
                                    "Partition on an Apple Partition Map-"
                                    "formatted disk.")

    def set_platform_bootloader_reqs(self):
        ret = super().set_platform_bootloader_reqs()
        ret.append(PartSpec(fstype="appleboot", size=Size("1MiB")))
        return ret


class PS3(PPC):
    pass


class S390(Platform):
    _packages = ["s390utils"]
    _boot_stage1_device_types = ["disk", "partition"]
    _boot_dasd_description = N_("DASD")
    _boot_mbr_description = N_("Master Boot Record")
    _boot_zfcp_description = N_("zFCP")
    _boot_descriptions = {"dasd": _boot_dasd_description,
                          "zfcp": _boot_zfcp_description,
                          "disk": _boot_mbr_description,
                          "partition": Platform._boot_partition_description}
    _boot_stage1_missing_error = N_("You must include at least one MBR- or "
                                    "DASD-formatted disk as an install target.")

    def set_platform_boot_partition(self):
        """Return the default platform-specific partitioning information."""
        return [PartSpec(mountpoint="/boot", size=Size("1GiB"), lv=False)]


class ARM(Platform):
    _arm_machine = None
    _boot_stage1_device_types = ["disk"]
    _boot_mbr_description = N_("Master Boot Record")
    _boot_descriptions = {"disk": _boot_mbr_description,
                          "partition": Platform._boot_partition_description}

    _boot_stage1_missing_error = N_("You must include at least one MBR-formatted "
                                    "disk as an install target.")

    @property
    def arm_machine(self):
        if not self._arm_machine:
            self._arm_machine = arch.get_arm_machine()
        return self._arm_machine


class omapARM(ARM):
    _boot_stage1_format_types = ["vfat"]
    _boot_stage1_device_types = ["partition"]
    _boot_stage1_mountpoints = ["/boot/uboot"]
    _boot_uboot_description = N_("U-Boot Partition")
    _boot_descriptions = {"partition": _boot_uboot_description}
    _boot_stage1_missing_error = N_("You must include a U-Boot Partition on a "
                                    "FAT-formatted disk, mounted at /boot/uboot.")

    def set_platform_bootloader_reqs(self):
        """Return the ARM-OMAP platform-specific partitioning information."""
        ret = [PartSpec(mountpoint="/boot/uboot", fstype="vfat",
                        size=Size("20MiB"), max_size=Size("200MiB"),
                        grow=True)]
        return ret

    def set_default_partitioning(self):
        ret = ARM.set_default_partitioning(self)
        ret.append(PartSpec(mountpoint="/", fstype="ext4",
                            size=Size("2GiB"), max_size=Size("3GiB")))
        return ret


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 == "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()
        else:
            return EFI()
    elif arch.is_x86():
        return X86()
    elif arch.is_arm():
        arm_machine = arch.get_arm_machine()
        if arm_machine == "omap":
            return omapARM()
        else:
            return ARM()
    else:
        raise SystemError("Could not determine system architecture.")

platform = get_platform()
