# encoding: utf-8

# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# 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 Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File:
#      modules/Initrd.ycp
#
# Module:
#      Bootloader installation and configuration
#
# Summary:
#      functions for initial ramdisk setup and creation
#
# Authors:
#      Jiri Srain <jsrain@suse.cz>
#
# $Id$
#
require "yast"
require "shellwords"

module Yast
  class InitrdClass < Module
    include Yast::Logger

    def main
      Yast.import "UI"

      Yast.import "Arch"
      Yast.import "Label"
      Yast.import "Misc"
      Yast.import "Mode"
      Yast.import "Report"
      Yast.import "Stage"
      Yast.import "Directory"

      textdomain "base"

      # module variables

      # List of modules for Initrd
      @modules = []
      # For each of modules - true if should be inserted to initrd, false
      # otherwise. Used to keep order from first-stage installation
      @modules_to_store = {}
      # List of modules that were in sysconfig file when reading settings
      @read_modules = []
      # map of settings for modules for being contained in initrd
      @modules_settings = {}
      # true if settings were changed and initrd needs to be rebuilt,
      # false otherwise
      @changed = false
      # true if settings were already read, flase otherwise
      @was_read = false
      # parametr for mkinitrd because of splash screen
      # used for choosing right size of splash
      @splash = ""
      # Additional parameters for mkinitrd
      @additional_parameters = ""
      # List of modules which should be not added/removed to/from initrd
      @modules_to_skip = nil

      # List of fallback vga modes to be used when hwinfo --framebuffer
      # doesn't return any value
      @known_modes = [
        { "color" => 8, "height" => 200, "mode" => 816, "width" => 320 },
        { "color" => 16, "height" => 200, "mode" => 782, "width" => 320 },
        { "color" => 24, "height" => 200, "mode" => 783, "width" => 320 },
        { "color" => 8, "height" => 240, "mode" => 820, "width" => 320 },
        { "color" => 16, "height" => 240, "mode" => 821, "width" => 320 },
        { "color" => 24, "height" => 240, "mode" => 822, "width" => 320 },
        { "color" => 8, "height" => 400, "mode" => 817, "width" => 320 },
        { "color" => 16, "height" => 400, "mode" => 818, "width" => 320 },
        { "color" => 24, "height" => 400, "mode" => 819, "width" => 320 },
        { "color" => 8, "height" => 400, "mode" => 768, "width" => 640 },
        { "color" => 16, "height" => 400, "mode" => 829, "width" => 640 },
        { "color" => 24, "height" => 400, "mode" => 830, "width" => 640 },
        { "color" => 8, "height" => 480, "mode" => 769, "width" => 640 },
        { "color" => 16, "height" => 480, "mode" => 785, "width" => 640 },
        { "color" => 24, "height" => 480, "mode" => 786, "width" => 640 },
        { "color" => 8, "height" => 600, "mode" => 771, "width" => 800 },
        { "color" => 16, "height" => 600, "mode" => 788, "width" => 800 },
        { "color" => 24, "height" => 600, "mode" => 789, "width" => 800 },
        { "color" => 8, "height" => 768, "mode" => 773, "width" => 1024 },
        { "color" => 16, "height" => 768, "mode" => 791, "width" => 1024 },
        { "color" => 24, "height" => 768, "mode" => 792, "width" => 1024 },
        { "color" => 8, "height" => 1024, "mode" => 775, "width" => 1280 },
        { "color" => 16, "height" => 1024, "mode" => 794, "width" => 1280 },
        { "color" => 24, "height" => 1024, "mode" => 795, "width" => 1280 },
        { "color" => 8, "height" => 1200, "mode" => 837, "width" => 1600 },
        { "color" => 16, "height" => 1200, "mode" => 838, "width" => 1600 }
      ]
    end

    # module functions

    # Get the list of modules which don't belong to initrd
    # Initialize the list if was not initialized before according to the
    # architecture
    # @return a list of modules
    def getModulesToSkip
      if @modules_to_skip.nil?
        # usb and cdrom modules dont belong to initrd,
        # they're loaded by hotplug
        @modules_to_skip = [
          "input",
          "hid",
          "keybdev",
          "mousedev",
          "cdrom",
          "ide-cd",
          "sr_mod",
          "xfs_support",
          "xfs_dmapi",
          "ide-scsi"
        ]
        # some other modules don't belong to initrd on PPC
        if Arch.ppc
          ppc_modules_to_skip = ["reiserfs", "ext3", "jbd"]
          @modules_to_skip = Convert.convert(
            Builtins.merge(@modules_to_skip, ppc_modules_to_skip),
            from: "list",
            to:   "list <string>"
          )
        end
        # currently no disk controller modules are known to fail in initrd (bnc#719696), list removed
      end
      deep_copy(@modules_to_skip)
    end

    # reset settings to empty list of modules
    def Reset
      Builtins.y2milestone("Reseting initrd settings")
      @was_read = false
      @changed = false
      @modules = []
      @modules_to_store = {}
      @read_modules = []
      @modules_settings = {}

      nil
    end

    # read seettings from sysconfig
    # @return true on success
    def Read
      Reset()
      @was_read = true
      return true if Stage.initial && !Mode.update # nothing to read

      # test for missing files - probably an error - should never occur
      if SCR.Read(path(".target.size"), "/etc/sysconfig/kernel") == -1
        Builtins.y2error("sysconfig/kernel not found")
        return false
      end

      s_modnames = Convert.to_string(
        SCR.Read(path(".sysconfig.kernel.INITRD_MODULES"))
      )
      s_modnames = "" if s_modnames.nil?
      @modules = Builtins.splitstring(s_modnames, " ")
      @modules = Builtins.filter(@modules) { |m| m != "" }
      Builtins.foreach(@modules) do |m|
        Ops.set(@modules_settings, m, {})
        Ops.set(@modules_to_store, m, true)
      end
      @read_modules = deep_copy(@modules)
      true
    end

    # List modules included in initrd
    # @return [Array] of strings with modulenames
    def ListModules
      Read() if !(@was_read || Mode.config)
      Builtins.filter(@modules) { |m| Ops.get(@modules_to_store, m, false) }
    end

    # add module to ramdisk
    # @param [String] modname name of module
    # @param [String] modargs arguments to be passes to module
    def AddModule(modname, modargs)
      log.warn "Initrd.AddModule() is deprecated, do not use (sysconfig.kernel.INITRD_MODULES " \
        "is not written anymore, see bnc#895084)"

      if Stage.initial && Builtins.size(@modules) == 0
        tmp_mods = Convert.to_string(
          SCR.Read(path(".etc.install_inf.InitrdModules"))
        )
        if !tmp_mods.nil? && tmp_mods != ""
          @modules = Builtins.splitstring(tmp_mods, " ")
        end
        @was_read = true
      elsif !(@was_read || Mode.config)
        Read()
      end
      if !Builtins.contains(ListModules(), modname) ||
          modname == "aic7xxx" &&
              !Builtins.contains(ListModules(), "aic7xxx_old") ||
          modname == "aic7xxx_old" &&
              !Builtins.contains(ListModules(), "aic7xxx")
        if !Builtins.contains(getModulesToSkip, modname)
          @changed = true
          Ops.set(@modules_to_store, modname, true)
          Ops.set(@modules_settings, modname, Misc.SplitOptions(modargs, {}))
          if !Builtins.contains(@modules, modname)
            @modules = Builtins.add(@modules, modname)
            Builtins.y2milestone(
              "Module %1 added to initrd, now contains %2",
              modname,
              ListModules()
            )
          else
            Builtins.y2milestone(
              "Module %1 from initial list added to initrd, now contains %2",
              modname,
              ListModules()
            )
          end
        else
          Builtins.y2milestone(
            "Module %1 is in list of modules not to insert to initrd",
            modname
          )
        end
      else
        Builtins.y2milestone("Module %1 already present in initrd", modname)
      end
      nil
    end

    # Export settigs to variable
    # @return [Hash] of initrd settings
    def Export
      Read() if !(@was_read || Mode.config)
      { "list" => Builtins.filter(@modules) do |m|
        Ops.get(@modules_to_store, m, false)
      end, "settings" => @modules_settings }
    end

    # import settings of initrd
    # @param [Hash] settings map of initrd settings
    def Import(settings)
      settings = deep_copy(settings)
      Read() if !Mode.config # to set modules that were read
      # and not add them to the list
      @modules = Ops.get_list(settings, "list", [])
      @modules_settings = Ops.get_map(settings, "settings", {})
      Builtins.foreach(@modules) { |m| Ops.set(@modules_to_store, m, true) }
      @was_read = true
      @changed = true

      nil
    end

    # remove module from list of initrd modules
    # @param [String] modname string name of module to remove
    def RemoveModule(modname)
      Read() if !(@was_read || Mode.config)
      @modules = Builtins.filter(@modules) { |k| k != modname }
      @modules_settings = Builtins.filter(@modules_settings) do |k, _v|
        k != modname
      end
      @changed = true

      nil
    end

    # Update read settings to new version of configuration files
    def Update
      # add other required changes here
      @modules = Builtins.filter(@modules) do |m|
        !Builtins.contains(getModulesToSkip, m)
      end
      @modules_settings = Builtins.filter(@modules_settings) do |k, _v|
        !Builtins.contains(getModulesToSkip, k)
      end
      @changed = true

      nil
    end

    # Display error popup with log
    # FIXME: this is copy-paste from ../routines/popups.ycp
    # @param [String] header string error header
    # @param [String] log string logfile contents
    def errorWithLogPopup(header, log)
      log = "" if log.nil?
      text = RichText(Opt(:plainText), log)
      UI.OpenDialog(
        Opt(:decorated),
        VBox(
          HSpacing(75),
          # heading
          Heading(header),
          text, # e.g. `Richtext()
          ButtonBox(
            PushButton(Id(:ok_help), Opt(:default, :okButton), Label.OKButton)
          )
        )
      )

      UI.SetFocus(Id(:ok_help))
      UI.UserInput
      UI.CloseDialog

      nil
    end

    # write settings to sysconfig, rebuild initrd images
    # @return true on success
    def Write
      Read() if !(@was_read || Mode.config)
      Update() if Mode.update
      Builtins.y2milestone(
        "Initrd::Write called, changed: %1, list: %2",
        @changed,
        ListModules()
      )
      # check whether it is neccessary to write initrd
      return true if !@changed && Mode.normal

      modules_written = false

      Builtins.foreach(@modules_settings) do |modname, optmap|
        next if !Ops.is_map?(optmap)
        if Ops.greater_than(Builtins.size(Convert.to_map(optmap)), 0)
          # write options to /etc/modules.conf
          p = Builtins.add(path(".modules.options"), modname)
          SCR.Write(p, Convert.to_map(optmap))
          modules_written = true
        end
      end

      SCR.Write(path(".modules"), nil) if modules_written

      # check modules that could be added during module's run (bug 26717)
      if SCR.Read(path(".target.size"), "/etc/sysconfig/kernel") != -1
        s_modnames = Convert.to_string(
          SCR.Read(path(".sysconfig.kernel.INITRD_MODULES"))
        )
        s_modnames = "" if s_modnames.nil?
        s_modules = Builtins.splitstring(s_modnames, " ")
        s_modules = Builtins.filter(s_modules) do |m|
          !Builtins.contains(@read_modules, m)
        end
        s_modules = Builtins.filter(s_modules) do |m|
          !Builtins.contains(@modules, m)
        end
        Builtins.y2milestone(
          "Modules %1 were added to initrd not using Initrd module",
          s_modules
        )
        Builtins.foreach(s_modules) { |m| AddModule(m, "") }
      end

      # save sysconfig
      SCR.Execute(
        path(".target.bash"),
        "/usr/bin/touch /etc/sysconfig/bootloader"
      )

      # FIXME: the modules are not written, remove them completely,
      # for now just log them without any change
      mods = Builtins.mergestring(ListModules(), " ")
      log.warn "Ignoring configured kernel modules: #{mods}" unless mods.empty?

      # recreate initrd
      param = ""
      if @splash != "" && !@splash.nil? &&
          Ops.less_than(
            0,
            Convert.to_integer(
              SCR.Read(
                path(".target.size"),
                "/lib/mkinitrd/scripts/setup-splash.sh"
              )
            )
          )
        param = "-s #{@splash.shellescape}"
      end
      if SCR.Execute(
        path(".target.bash"),
        Builtins.sformat(
          "/sbin/mkinitrd %1 %2 >> %3 2>&1",
          param, # escaped already above
          # cannot escape it as it can contain multiple params and
          # shell escape makes it single broken param
          @additional_parameters,
          File.join(Directory.logdir, "y2logmkinitrd").shellescape
        )
      ) != 0
        log = Convert.to_string(
          SCR.Read(
            path(".target.string"),
            Ops.add(Directory.logdir, "/y2logmkinitrd")
          )
        )
        # error report
        errorWithLogPopup(_("An error occurred during initrd creation."), log)
      end
      @changed = false
      true
    end

    def VgaModes
      all_modes = Convert.convert(
        SCR.Read(path(".probe.framebuffer")),
        from: "any",
        to:   "list <map>"
      )
      if all_modes.nil? || Builtins.size(all_modes) == 0
        Builtins.y2warning("Probing VGA modes failed, using fallback list")
        all_modes = deep_copy(@known_modes)
      end
      deep_copy(all_modes)
    end

    # Set the -s parameter of mkinitrd
    # @param [String] vga string the vga kernel parameter
    def setSplash(vga)
      if !Arch.s390
        @changed = true
        # bnc#292013 - Grub-tool does not recreate initrd if the vga-mode changed
        if vga == "normal"
          @splash = "off"
        else
          mode = Builtins.tointeger(vga)
          all_modes = VgaModes()
          Builtins.foreach(all_modes) do |m|
            if Ops.get_integer(m, "mode", 0) == mode &&
                Ops.get_integer(m, "height", 0) != 0 &&
                Ops.get_integer(m, "width", 0) != 0
              @splash = Builtins.sformat(
                "%2x%1",
                Ops.get_integer(m, "height", 0),
                Ops.get_integer(m, "width", 0)
              )
            end
          end
        end
        Builtins.y2milestone("Setting splash resolution to %1", @splash)
      end

      nil
    end

    # Get additional parameters for mkinitrd
    # @return [String] additional mkinitrd parameters
    def AdditionalParameters
      @additional_parameters
    end

    # Set additional parameters for mkinitrd
    # @param [String] params string additional mkinitrd parameters
    def SetAdditionalParameters(params)
      @additional_parameters = params

      nil
    end

    publish variable: :changed, type: "boolean"
    publish function: :getModulesToSkip, type: "list <string> ()"
    publish function: :Reset, type: "void ()"
    publish function: :Read, type: "boolean ()"
    publish function: :ListModules, type: "list <string> ()"
    publish function: :AddModule, type: "void (string, string)"
    publish function: :Export, type: "map ()"
    publish function: :Import, type: "void (map)"
    publish function: :RemoveModule, type: "void (string)"
    publish function: :Update, type: "void ()"
    publish function: :errorWithLogPopup, type: "void (string, string)"
    publish function: :Write, type: "boolean ()"
    publish function: :VgaModes, type: "list <map> ()"
    publish function: :setSplash, type: "void (string)"
    publish function: :AdditionalParameters, type: "string ()"
    publish function: :SetAdditionalParameters, type: "void (string)"
  end

  Initrd = InitrdClass.new
  Initrd.main
end
