### Copyright 2019 Pixar

###
###    Licensed under the Apache License, Version 2.0 (the "Apache License")
###    with the following modification; you may not use this file except in
###    compliance with the Apache License and the following modification to it:
###    Section 6. Trademarks. is deleted and replaced with:
###
###    6. Trademarks. This License does not grant permission to use the trade
###       names, trademarks, service marks, or product names of the Licensor
###       and its affiliates, except as required to comply with Section 4(c) of
###       the License and to reproduce the content of the NOTICE file.
###
###    You may obtain a copy of the Apache License at
###
###        http://www.apache.org/licenses/LICENSE-2.0
###
###    Unless required by applicable law or agreed to in writing, software
###    distributed under the Apache License with the above modification is
###    distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
###    KIND, either express or implied. See the Apache License for the specific
###    language governing permissions and limitations under the Apache License.
###
###

###
module JSS

  #
  class Client

    # Constants
    #####################################

    # The Pathname to the jamfHelper executable
    JAMF_HELPER = SUPPORT_BIN_FOLDER + 'jamfHelper.app/Contents/MacOS/jamfHelper'

    # The window_type options for jamfHelper
    JAMF_HELPER_WINDOW_TYPES = {
      hud: 'hud',
      utility: 'utility',
      util: 'utility',
      full_screen: 'fs',
      fs: 'fs'
    }.freeze

    # The possible window positions for jamfHelper
    JAMF_HELPER_WINDOW_POSITIONS = [nil, :ul, :ll, :ur, :lr].freeze

    # The available buttons in jamfHelper
    JAMF_HELPER_BUTTONS =  [1, 2].freeze

    # The possible alignment positions in jamfHelper
    JAMF_HELPER_ALIGNMENTS = %i[right left center justified natural].freeze

    # class Methods
    #####################################


    # A wrapper for the jamfHelper command, which can display a window on the client machine.
    #
    # The first parameter must be a symbol defining what kind of window to display. The options are
    # - :hud - creates an Apple "Heads Up Display" style window
    # - :utility or :util -  creates an Apple "Utility" style window
    # - :fs or :full_screen or :fullscreen - creates a full screen window that restricts all user input
    #   WARNING: Remote access must be used to unlock machines in this mode
    #
    # The remaining options Hash can contain any of the options listed. See below for descriptions.
    #
    # The value returned is the Integer exitstatus/stdout (both are the same) of the jamfHelper command.
    # The meanings of those integers are:
    #
    # - 0 - Button 1 was clicked
    # - 1 - The Jamf Helper was unable to launch
    # - 2 - Button 2 was clicked
    # - 3 - Process was started as a launchd task
    # - XX1 - Button 1 was clicked with a value of XX seconds selected in the drop-down
    # - XX2 - Button 2 was clicked with a value of XX seconds selected in the drop-down
    # - 239 - The exit button was clicked
    # - 240 - The "ProductVersion" in sw_vers did not return 10.5.X, 10.6.X or 10.7.X
    # - 243 - The window timed-out with no buttons on the screen
    # - 250 - Bad "-windowType"
    # - 254 - Cancel button was select with delay option present
    # - 255 - No "-windowType"
    #
    # If the :abandon_process option is given, the integer returned is the Process ID
    # of the abondoned process running jamfHelper.
    #
    # See also /Library/Application\ Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper -help
    #
    # @note the -startlaunchd and -kill options are not available in this implementation, since
    #   they don't work at the moment (casper 9.4).
    #   -startlaunchd seems to be required to NOT use launchd, and when it's ommited, an error is generated
    #   about the launchd plist permissions being incorrect.
    #
    # @param window_type[Symbol]  The type of window to display
    #
    # @param opts[Hash] the options for the window
    #
    # @option opts :window_position [Symbol,nil] one of [ nil, :ul, :ll. :ur, :lr ]
    #   Positions window in the upper right, upper left, lower right or lower left of the user's screen
    #   If no input is given, the window defaults to the center of the screen
    #
    # @option opts :title [String]
    #   Sets the window's title to the specified string
    #
    # @option opts :heading  [String]
    #   Sets the heading of the window to the specified string
    #
    # @option opts :align_heading [Symbol] one of  [:right, :left, :center, :justified, :natural]
    #   Aligns the heading to the specified alignment
    #
    # @option opts :description [String]
    #   Sets the main contents of the window to the specified string
    #
    # @option opts :align_description [Symbol] one of  [:right, :left, :center, :justified, :natural]
    #   Aligns the description to the specified alignment
    #
    # @option opts :icon [String,Pathname]
    #   Sets the windows image field to the image located at the specified path
    #
    # @option opts :icon_size [Integer]
    #   Changes the image frame to the specified pixel size
    #
    # @option opts :full_screen_icon [any value]
    #   Scales the "icon" to the full size of the window.
    #   Note: Only available in full screen mode
    #
    # @option opts :button1 [String]
    #   Creates a button with the specified label
    #
    # @option opts :button2 [String]
    #   Creates a second button with the specified label
    #
    # @option opts :default_button [Integer]  either 1 or 2
    #   Sets the default button of the window to the specified button. The Default Button will respond to "return"
    #
    # @option opts :cancel_button [Integer]  either 1 or 2
    #   Sets the cancel button of the window to the specified button. The Cancel Button will respond to "escape"
    #
    # @option opts :timeout [Integer]
    #   Causes the window to timeout after the specified amount of seconds
    #   Note: The timeout will cause the default button, button 1 or button 2 to be selected (in that order)
    #
    # @option opts :show_delay_options [String,Array<Integer>] A String of comma-separated Integers, or an Array of Integers.
    #   Enables the "Delay Options Mode". The window will display a dropdown with the values passed through the string
    #
    # @option opts :countdown [any value]
    #   Displays a string notifying the user when the window will time out
    #
    # @option opts :align_countdown [Symbol] one of  [:right, :left, :center, :justified, :natural]
    #   Aligns the countdown to the specified alignment
    #
    # @option opts :lock_hud [Boolean]
    #   Removes the ability to exit the HUD by selecting the close button
    #
    # @option opts :abandon_process [Boolean] Abandon the jamfHelper process so that your code can exit.
    #   This is mostly used so that a policy can finish while a dialog is waiting
    #   (possibly forever) for user response. When true, the returned value is the
    #   process id of the abandoned jamfHelper process.
    #
    # @option opts :output_file [String, Pathname] Save the output of jamfHelper
    #   (the exit code) into this file. This is useful when using abandon_process.
    #   The output file can be examined later to see what happened. If this option
    #   is not provided, no output is saved.
    #
    # @option opts :arg_string [String] The jamfHelper commandline args as a single
    #   String, the way you'd specify them in a shell. This is appended to any
    #   Ruby options provided when calling the method. So calling:
    #      JSS::Client.jamf_helper :hud, title: 'This is a title', arg_string: '-heading "this is a heading"'
    #   will run
    #      jamfHelper -windowType hud -title 'this is a title' -heading "this is a heading"
    #   When using this, be careful not to specify the windowType, since it's generated
    #   by the first, required, parameter of this method.
    #
    # @return [Integer] the exit status of the jamfHelper command. See above.
    #
    def self.jamf_helper(window_type = :hud, opts = {})
      raise JSS::UnmanagedError, 'The jamfHelper app is not installed properly on this computer.' unless JAMF_HELPER.executable?

      unless JAMF_HELPER_WINDOW_TYPES.include? window_type
        raise JSS::InvalidDataError, "The first parameter must be a window type, one of :#{JAMF_HELPER_WINDOW_TYPES.keys.join(', :')}."
      end

      # start building the arg array

      args = ['-startlaunchd', '-windowType', JAMF_HELPER_WINDOW_TYPES[window_type]]

      opts.keys.each do |opt|
        case opt
        when :window_position
          raise JSS::InvalidDataError, ":window_position must be one of :#{JAMF_HELPER_WINDOW_POSITIONS.join(', :')}." unless \
            JAMF_HELPER_WINDOW_POSITIONS.include? opts[opt].to_sym
          args << '-windowPosition'
          args << opts[opt].to_s

        when :title
          args << '-title'
          args << opts[opt].to_s

        when :heading
          args << '-heading'
          args << opts[opt].to_s

        when :align_heading
          raise JSS::InvalidDataError, ":align_heading must be one of :#{JAMF_HELPER_ALIGNMENTS.join(', :')}." unless \
            JAMF_HELPER_ALIGNMENTS.include? opts[opt].to_sym
          args << '-alignHeading'
          args << opts[opt].to_s

        when :description
          args << '-description'
          args << opts[opt].to_s

        when :align_description
          raise JSS::InvalidDataError, ":align_description must be one of :#{JAMF_HELPER_ALIGNMENTS.join(', :')}." unless \
            JAMF_HELPER_ALIGNMENTS.include? opts[opt].to_sym
          args << '-alignDescription'
          args << opts[opt].to_s

        when :icon
          args << '-icon'
          args << opts[opt].to_s

        when :icon_size
          args << '-iconSize'
          args << opts[opt].to_s

        when :full_screen_icon
          args << '-fullScreenIcon'

        when :button1
          args << '-button1'
          args << opts[opt].to_s

        when :button2
          args << '-button2'
          args << opts[opt].to_s

        when :default_button
          raise JSS::InvalidDataError, ":default_button must be one of #{JAMF_HELPER_BUTTONS.join(', ')}." unless \
            JAMF_HELPER_BUTTONS.include? opts[opt]
          args << '-defaultButton'
          args << opts[opt].to_s

        when :cancel_button
          raise JSS::InvalidDataError, ":cancel_button must be one of #{JAMF_HELPER_BUTTONS.join(', ')}." unless \
            JAMF_HELPER_BUTTONS.include? opts[opt]
          args << '-cancelButton'
          args << opts[opt].to_s

        when :timeout
          args << '-timeout'
          args << opts[opt].to_s

        when :show_delay_options
          args << '-showDelayOptions'
          args << JSS.to_s_and_a(opts[opt])[:arrayform].join(', ')

        when :countdown
          args << '-countdown' if opts[opt]

        when :align_countdown
          raise JSS::InvalidDataError, ":align_countdown must be one of :#{JAMF_HELPER_ALIGNMENTS.join(', :')}." unless \
            JAMF_HELPER_ALIGNMENTS.include? opts[opt].to_sym
          args << '-alignCountdown'
          args << opts[opt].to_s

        when :lock_hud
          args << '-lockHUD' if opts[opt]

        end # case opt
      end # each do opt

      cmd = Shellwords.escape JAMF_HELPER.to_s
      args.each { |arg| cmd << " #{Shellwords.escape arg}" }
      cmd << " #{opts[:arg_string]}" if opts[:arg_string]
      cmd << " > #{Shellwords.escape opts[:output_file]}" if opts[:output_file]

      if opts[:abandon_process]
        pid = Process.fork
        if pid.nil?
          # In child
          exec cmd
        else
          # In parent
          Process.detach(pid)
          pid
        end
      else
        system cmd
        $CHILD_STATUS.exitstatus
      end
    end # def self.jamf_helper

  end # class Client

end # module