# encoding: utf-8

# Copyright (c) [2015] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# 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, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require "y2storage/boot_requirements_strategies/base"
require "y2storage/partition_id"

module Y2Storage
  module BootRequirementsStrategies
    # Strategy to calculate the boot requirements in a legacy system (x86 without EFI)
    class Legacy < Base
      def initialize(*args)
        super
        textdomain "storage"
      end

      # @see Base#needed_partitions
      def needed_partitions(target)
        planned_partitions = super
        planned_partitions << grub_partition(target) if grub_partition_needed? && grub_partition_missing?
        planned_partitions
      end

      # Boot warnings in the current setup
      #
      # Note that all are just warnings and we don't trigger real errors so far.
      #
      # @return [Array<SetupError>]
      def warnings
        res = super

        if boot_ptable_type?(:gpt)
          res.concat(errors_on_gpt)
        elsif boot_ptable_type?(:msdos)
          res.concat(errors_on_msdos)
        else
          res.concat(errors_on_plain_disk)
        end

        res
      end

    protected

      # Whether a BIOS GRUB partition will be needed.
      #
      # Note: this is a bit tricky.
      #
      # This function is intended to be used in #needed_partitions while
      # creating a partition proposal. The catch is that when there is no
      # partition table (yet) it is implicitly assumed that there will be a
      # gpt created finally - so a grub partition is needed also in this
      # case.
      #
      # @return [Boolean]
      def grub_partition_needed?
        future_boot_ptable_type?(:gpt) && grub_part_needed_in_gpt?
      end

      # Whether the partition table that will finally be created matches the
      # given type.
      #
      # This is the same as the partition table type if one already exists. Else
      # the check will be against #preferred_ptable_type.
      #
      # FIXME
      #   It seems that a setup with xen virtual partitions (that are in
      #   fact disks) also ends up here. In that case no partition table will
      #   be created (below this case is indicated by boot_disk = nil).
      #   This looks weird.
      #
      # @return [Boolean] true if the partition table matches.
      def future_boot_ptable_type?(type)
        return false if boot_disk.nil?
        if boot_ptable_type?(nil)
          boot_disk.preferred_ptable_type.is?(type)
        else
          boot_ptable_type?(type)
        end
      end

      # Given the fact we are trying to boot from a GPT disk, whether a BIOS
      # BOOT partition is needed in the current setup
      #
      # This always returns true because the usage of such partition is the only
      # method encouraged and documented for Grub2 in a legacy boot environment.
      # https://www.gnu.org/software/grub/manual/grub/grub.html#BIOS-installation
      #
      # In theory, the bootloader could work properly without BIOS BOOT if Grub2
      # is installed in a formatted partition. For that to work, the filesystem
      # must leave space for Grub at the beginning of the partition (like ExtX
      # does) or must support embedding Grub in the filesystem (like Btrfs).
      # But that's a fragile approach that is discouraged by the Grub2
      # developers. In any case, it will not work with XFS since it leaves
      # no space at the beginning of the partition. It wouldn't work for LVM,
      # encryption or RAID either.
      #
      # @return [Boolean] always true, rationale in the method documentation
      def grub_part_needed_in_gpt?
        true
      end

      def grub_partition_missing?
        # We don't check if the planned partition is in the boot disk,
        # whoever created it is in control of the details
        current_devices = analyzer.planned_devices
        current_devices += boot_disk.partitions if boot_disk
        current_devices.none? { |d| d.match_volume?(grub_volume) }
      end

      # Whether /boot is a plain partition and can embed grub
      #
      # @return [Boolean] true if /boot is a plain partition and can embed grub, else false
      def boot_can_embed_grub?
        boot_fs_can_embed_grub? && !(boot_in_lvm? || boot_in_software_raid? || encrypted_boot?)
      end

      # Whether / is a plain partition and can embed grub
      #
      # @return [Boolean] true if / is a plain partition and can embed grub, else false
      def root_can_embed_grub?
        root_fs_can_embed_grub? && !(root_in_lvm? || root_in_software_raid? || encrypted_root?)
      end

      # Whether the MBR gap is big enough for grub
      #
      # @return [Boolean] true if the MBR gap is big enough for grub, else false
      def mbr_gap_for_grub?
        boot_disk.mbr_gap_for_grub?
      end

      # A separate boot partition is needed if
      #   - partition table is msdos and
      #   - the mbr gap is too small for grub and
      #   - grub can't be embedded into the root file system directly
      #
      # Note: this is *not* the bios grub partition.
      #
      # @return [Boolean] true if a separate boot partition is needed, else false
      def boot_partition_needed?
        boot_ptable_type?(:msdos) && !mbr_gap_for_grub? && !root_can_embed_grub?
      end

      # @return [VolumeSpecification]
      def grub_volume
        @grub_volume ||= volume_specification_for("grub")
      end

      # @return [Planned::Partition]
      def grub_partition(target)
        planned_partition = create_planned_partition(grub_volume, target)
        planned_partition.bootable = false
        planned_partition
      end

      # Boot errors when partition table is gpt
      #
      # @return [Array<SetupError>]
      def errors_on_gpt
        errors = []

        if grub_part_needed_in_gpt? && missing_partition_for?(grub_volume)
          errors << bios_boot_missing_error
          errors << grub_embedding_error
        end

        errors
      end

      # Boot errors when partition table is msdos
      #
      # @return [Array<SetupError>]
      def errors_on_msdos
        errors = []

        if !mbr_gap_for_grub?
          errors << mbr_gap_error
          errors << grub_embedding_error
        end

        errors
      end

      # Boot errors when there's no partition table
      #
      # @return [Array<SetupError>]
      def errors_on_plain_disk
        errors = []

        errors << no_boot_partition_table_error
        errors << grub_embedding_error

        errors
      end

      # Check if boot disk can embed grub and return appropriate message
      #
      # @return [SetupError]
      def grub_embedding_error
        if boot_can_embed_grub?
          bad_config_warning
        else
          bad_config_error
        end
      end

      # Specific error when the boot disk has no partition table
      #
      # @return [SetupError]
      def no_boot_partition_table_error
        # TRANSLATORS: error message
        error_message = _(
          "Boot disk has no partition table."
        )
        SetupError.new(message: error_message)
      end

      # Specific error when the MBR gap is small
      #
      # @return [SetupError]
      def mbr_gap_error
        # TRANSLATORS: error message; %s is a human readable disk size like 256 KiB
        error_message = format(
          _(
            "Not enough space before the first partition to install the bootloader. " \
            "Leave at least %s."
          ),
          PartitionTables::Msdos::MBR_GAP_GRUB_LIMIT.to_human_string
        )
        SetupError.new(message: error_message)
      end

      # Specific error when BIOS GRUB partition is missing
      #
      # @return [SetupError]
      def bios_boot_missing_error
        # TRANSLATORS: %s is a partition type, e.g. "BIOS Boot"
        message = format(
          _("A partition of type %s is needed to install the bootloader."),
          PartitionId::BIOS_BOOT.to_human_string
        )
        SetupError.new(message: message)
      end

      # Specific warning when the current setup is not supported
      #
      # @return [SetupError]
      def bad_config_warning
        message = _(
          "Such a setup is not supported and may cause problems " \
          "with the bootloader now or in the future."
        )
        SetupError.new(message: message)
      end

      # Specific error when we are quite sure the current setup will not work
      #
      # @return [SetupError]
      def bad_config_error
        message = _(
          "It will not be possible to install the bootloader."
        )
        SetupError.new(message: message)
      end
    end
  end
end
