### Copyright 2016 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 D3
  class Package < JSS::Package

    ### General validation checks for the package as a whole,
    ### For individual attribute validation, see the
    ### D3::Package::Validate module.

    ### Check that there's not a newer version of this thing alreay installed
    ### Raise an exception if so.
    ###
    ### return [void]
    ###
    def check_for_newer_version
      if D3::Client::Receipt.basenames.include? @basename
          rcpt = D3::Client::Receipt.all[@basename]
          raise D3::InstallError, "The installed #{rcpt.edition} (#{rcpt.status}) is the same or newer. Use --force if needed." if rcpt.id >= @id
      end
    end # check for newer version

    ### Check that we're not installing a deprecated pkg, and raise an exception if we are.
    ###
    ### return [void]
    ###
    def check_for_deprecated
      raise D3::InstallError, "#{edition} is deprecated. Use --force if needed." if deprecated?
    end

    ### Check that we're not trying to install a skipped pkg, and raise an exception if we are.
    ###
    ### return [void]
    ###
    def check_for_skipped
      raise D3::InstallError, "#{edition} was skipped. Use --force if needed." if skipped?
    end

    ### Check if this machine is in an excluded group.
    ### Raise an exception if so.
    ###
    ### return [void]
    ###
    def check_for_exclusions
      excl_grps = D3::Client.computer_groups & @excluded_groups
      raise D3::InstallError,  "This machine is excluded for #{desired_pkg.edition}. Use --force if needed." unless excl_grps.empty?
      return true
    end # check for exclusions

    ### Check if this machine is OK wrt to the os limitations
    ### Raise an exception if not
    ###
    ### return [void]
    ###
    def check_oses
       my_os = `/usr/bin/sw_vers -productVersion`.chomp
       raise D3::InstallError,  "This machine doesn't have the correct OS to install #{self.edition}." unless JSS.os_ok?  @os_requirements, my_os
       return true
    end

    ### Check if this machine is OK wrt to the processor limitations
    ### Raise an exception if not
    ###
    ### return [void]
    ###
    def check_cpu
       my_cpu = `/usr/bin/uname -p`.chomp
       raise D3::InstallError,  "This machine doesn't have the correct OS to install #{self.edition}." unless  JSS.processor_ok?  @required_processor, my_cpu
    end


    ### This module contains methods for validating attribute
    ### values in d3 Packages
    ###
    ### Each method takes an argument, and either raises an exception
    ### if the argument isn't valid for its destination, or
    ### converts it to the proper type for its destination.
    ###
    ### For example, the {#validate_groups} takes either a comma-seprated String
    ### or an Array of computer group names, converts the String to an Array
    ### if needed, and then confirms that each group exists in the JSS
    ### If they all do, the Array is returned.
    ###
    module Validate
      extend self

      ### Check the existence of a basename in d3.
      ###
      ### @param name[String] the basename to check
      ###
      ### @return [Boolean] does that basename exist in d3?
      ###
      def basename_exist? (name)
        D3::Package.all_basenames.include? name
      end

      ### Check the existence of an edition in d3.
      ###
      ### @param name[String] the edition to check
      ###
      ### @return [Boolean] does that edition exist in d3?
      ###
      def edition_exist? (edition)
        D3::Package.all_editions.include? edition
      end

      ### Check the existence of a filename in the JSS.
      ###
      ### @param name[String] the name to check
      ###
      ### @return [Boolean] does that package filename exist in d3?
      ###
      def filename_exist? (name)
        D3::Package.all_filenames.values.include? name
      end


      ### check that the given package name doesn't already exist
      ###
      ### @see {JSS::Package.validate_package_name}
      ###
      def validate_package_name(name)
        raise JSS::AlreadyExistsError, "There's already a package in the JSS with the name '#{name}'" if JSS::Package.all_names.include? name
        name
      end

      ### check that the given filename doesn't already exist
      ###
      ### @param name[String] the name to check
      ###
      ### @return [String] the valid new file name
      ###
      def validate_filename(name)
        raise JSS::AlreadyExistsError, "There's already a package in the JSS with the filename '#{name}'" if self.filename_exist? name
        name
      end

      ### Check if an edition exists and raise an exception if so
      ### Also check that it contains at least two hyphens
      ###
      ### @param edition[String] the edition to check
      ###
      ### @return [String] the valid, unique edition
      ###
      def validate_edition (edition)
        raise JSS::AlreadyExistsError, "There's already a package in the JSS with the edition '#{edition}'" if edition_exist? edition
        raise JSS::InvalidDataError, "'#{edition}' doesn't seem like a valid edition" unless edition.count("-") >= 2
        edition
      end



      ### Confirm the validity of a version. Raise an exception if invalid.
      ###
      ### @param vers[String] the version to check
      ###
      ### @return [String] An error message, or true if the value is ok
      ###
      def validate_version (vers)
        raise JSS::InvalidDataError, "Version must be a String." unless vers.is_a? String
        raise JSS::InvalidDataError, "Version can't be empty." if vers.empty?
        return vers.gsub(" ", "_")
      end

      ### Confirm the validity of a revision.
      ### Raise an exception if invalid.
      ###
      ### @param rev[Integer] the revision to check
      ###
      ### @return [Integer] the valid revision
      ###
      def validate_revision (rev)
        raise JSS::InvalidDataError, "Revision must be an Integer." unless rev.to_s =~ /^\d+$/
        rev.to_i
      end

      ### Check the validity of a pre_install script
      ###
      ### @see #validate_script
      ###
      def validate_pre_install_script (script)
        validate_script script
      end

      ### Check the validity of a post_install script
      ###
      ### @see #validate_script
      ###
      def validate_post_install_script (script)
        validate_script script
      end

      ### Check the validity of a pre_remove script
      ###
      ### @see #validate_script
      ###
      def validate_pre_remove_script (script)
        validate_script script
      end

      ### Check the validity of a pre_remove script
      ###
      ### @see #validate_script
      ###
      def validate_post_remove_script (script)
        validate_script script
      end

      ### Check the validity of a script, either Pathname, JSS id, or JSS name
      ### Raise an exception if not valid
      ###
      ### @param script[Pathname, Integer, String] the script to check
      ###
      ### @return [Pathname, String] the valid local path or JSS name of the script
      ###
      def validate_script (script)
        script = nil if script.to_s =~ /^n(one)?$/i
        return nil if script.to_s.empty?
        script = script.to_s.strip

        if script =~ /^\d+$/
          script = script.to_i
          return JSS::Script.map_all_ids_to(:name)[script] if JSS::Script.all_ids.include? script
          raise JSS::NoSuchItemError, "No JSS script with id '#{script}'"

        else
          # if its a file path, return it fully expanded
          path = Pathname.new script
          return path.expand_path if path.file?

          # otherwise, its a JSS Script name,return its id
          if JSS::Script.all_names.include? script.to_s
            return script
          end

          raise JSS::NoSuchItemError, "No local file or JSS script named '#{script}'"
        end # if a fixnum
      end

      ### @see #validate_groups
      def validate_auto_groups (groups)
        validate_groups groups, :check_for_std
      end

      ### @see #validate_groups
      def validate_excluded_groups (groups)
        validate_groups groups
      end

      ### Confirm the existence of a list of Computer Group names
      ### (String or Array) and return them as an Array
      ###
      ### If "n", "", "none" or nil are passed, an empty array is returned.
      ###
      ### Raise an exception if any group is not valid.
      ###
      ### @param groups[String, Array<String>]
      ###
      ### @param check_for_std[Boolean] should we check for the D3::STANDARD_AUTO_GROUP?
      ###
      ### @return [Array] The valid groups as an array
      ###
      def validate_groups (groups, check_for_std = false)
        groups = [] if groups.to_s =~ /^n(one)?$/i
        group_array = JSS.to_s_and_a(groups)[:arrayform].compact
        return [] if group_array.empty?
        return [] if group_array.reject{|g| g =~ /^n(one)$/i }.empty? # ['n'], ['None'], case-free
        return [D3::STANDARD_AUTO_GROUP] if check_for_std and group_array.include?(D3::STANDARD_AUTO_GROUP)

        group_array.each do |group|
          raise JSS::NoSuchItemError, "No ComputerGroup named '#{group}' in the JSS" unless JSS::ComputerGroup.all_names.include? group
        end
        group_array
      end

      ### Make sure auto and excluded groups don't have any
      ### members in common, raise an exception if they do.
      ###
      ### @param auto[Array] the array of auto groups
      ###
      ### @param excl[Array] the array of excluded groups
      ###
      ### @return [True] true if there are no groups in common
      ###
      def validate_non_overlapping_groups (auto, excl)
        return nil unless auto and excl
        auto = JSS.to_s_and_a(auto)[:arrayform]
        excl = JSS.to_s_and_a(excl)[:arrayform]
        raise JSS::InvalidDataError, "Auto and Excluded group-lists can't contain groups in common." unless (auto & excl).empty?
        true
      end

      ### Check the validity of a list of OSes
      ### Raise an exception if not valid
      ###
      ### @param [String,Array] Array or comma-separated list of OSes to check
      ###
      ### @return [Array] the valid OS list
      ###
      def validate_oses (os_list)
        os_list = nil if os_list.to_s =~ /^n(one)?$/i
        return [] if os_list.to_s.empty?
         ### if any value starts with >=, expand it
        case os_list
          when String
            os_list = JSS.expand_min_os(os_list) if os_list =~ /^>=/
          when Array
            os_list.map!{|a|  a =~ /^>=/ ? JSS.expand_min_os(a) : a }
            os_list.flatten!
            os_list.uniq!
          else
            raise JSS::InvalidDataError, "os_list must be a String or an Array of strings"
        end
        ### return the array version
        JSS.to_s_and_a(os_list)[:arrayform]
      end

      ### Check the validity of a CPU type
      ### Raise an exception if not valid
      ###
      ### @param [Symbol] the CPU type to check
      ###
      ### @return [Symbol] the valid CPU type
      ###
      def validate_cpu_type (type)
        type = JSS::Package::DEFAULT_CPU_TYPE if type.to_s.empty?
        type = "None" if type =~ /^n(one)?$/i
        type = "x86" if type.casecmp('intel') == 0
        type = "ppc" if type.casecmp('ppc') == 0
        unless JSS::Package::CPU_TYPES.include? type
          raise JSS::InvalidDataError, "CPU type must be one of: #{JSS::Package::CPU_TYPES.join ', '}"
        end
        type
      end

      ### Check the validity of a category name
      ### Raise an exception if not valid.
      ### nil and empty strings are acceptable to unset the category.
      ###
      ### @param cat[String] the category to check
      ###
      ### @return [String] the valid category name
      ###
      def validate_category (cat)
        cat = nil if cat.to_s =~ /^n(one)?$/i
        return "" if  cat.to_s.empty?
        raise JSS::NoSuchItemError, "No category '#{cat}' in the JSS" unless JSS::Category.all_names.include? cat
        cat
      end

      ### Check the offered prohibiting process pattern
      ###
      ### @param match_string[String] the data entered by the user
      ###
      ### @return [String, nil] the regexp used to do the match
      ###
      def validate_prohibiting_process (match_string)
        match_string = nil if match_string.to_s =~ /^n(one)?$/i
        return nil if match_string.nil? or match_string.empty?
        match_string.to_s
      end

      ### check the validity of a yes/no,true/false,1/0 input value
      ###
      ### TrueClass, "true", "y","yes", and 1 are true
      ###
      ### FalseClass, nil, "false", "n", "no", and 0 are false
      ###
      ### (Strings are case-insensitive)
      ### Anything else raises an exception.
      ###
      ### @param type[String,Boolean,Integer] the value to check
      ###
      ### @return [Boolean]
      ###
      def validate_yes_no (yn)
        case yn
        when Fixnum
          return true if yn == 1
          return false if yn == 0
        when String
          return true if yn.strip =~ /^y(es)?$/i
          return false if yn.strip =~ /^no?$/i
        when TrueClass
          return true
        when FalseClass
          return false
        when nil
          return false
        end
        raise JSS::InvalidDataError, "Value must be one of: 'yes', 'y', 'true', '1', 'no', 'n', 'false', '0'"
      end

      ### Confirm the validity of an expiration.
      ### Raise an exception if invalid.
      ###
      ### @param exp[Integer] the expiration to check
      ###
      ### @return [Integer] the valid expiration
      ###
      def validate_expiration (exp)
        exp ||= '0'
        raise JSS::InvalidDataError, "Expiration must be a non-negative Integer." unless exp.to_s =~ /^\d+$/
        exp = 0 if exp.to_i < 0
        exp.to_i
      end

      ### Confirm the validity of an expiration path.
      ### any string that starts with a / is valid.
      ###
      ### @param path[Pathname, String] the path to check
      ###
      ### @return [Pathname, nil] the valid path
      ###
      def validate_expiration_path (path)
        path = path.to_s
        return nil if path.empty?
        raise JSS::InvalidDataError, "Expiration Path must be a full path starting with /." unless path.start_with? "/"
        Pathname.new path
      end


    end # module Validate

  end # class package
end # module D3