### Copyright 2018 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

  #####################################
  ### Classes
  #####################################

  ###
  ### A peripheral_type in the JSS.
  ###
  ### A PeripheralType (as opposed to an individual {JSS::Peripheral}) is just an id, a name, and an Array of
  ### Hashes describing the fields of data to be stored for peripherals of this type.
  ###
  ### See {#fields} for a desciption of how field definitions are stored.
  ###
  ### For manipulating the fields, see {#fields=}, {#set_field}, {#append_field}, {#prepend_field}, {#insert_field}, and {#delete_field}
  ###
  ### @see JSS::APIObject
  ###
  class PeripheralType  < JSS::APIObject

    #####################################
    ### MixIns
    #####################################

    include JSS::Creatable
    include JSS::Updatable

    #####################################
    ### Class Methods
    #####################################

    #####################################
    ### Class Constants
    #####################################

    ### The base for REST resources of this class
    RSRC_BASE = "peripheraltypes"

    ### the hash key used for the JSON list output of all objects in the JSS
    RSRC_LIST_KEY = :peripheral_types

    ### The hash key used for the JSON object output.
    ### It's also used in various error messages
    RSRC_OBJECT_KEY = :peripheral_type

    ### these keys, as well as :id and :name,  are present in valid API JSON data for this class
    VALID_DATA_KEYS = [:fields]

    ### field types can be one of these, either String or Symbol
    FIELD_TYPES = [:text, :menu]

    # the object type for this object in
    # the object history table.
    # See {APIObject#add_object_history_entry}
    OBJECT_HISTORY_OBJECT_TYPE = 11

    #####################################
    ### Attributes
    #####################################

    #####################################
    ### Instance Methods
    #####################################

    ###
    ### Initialize
    ###
    def initialize (args = {})

      super

      @fields = []
      if @init_data[:fields]
        @init_data[:fields].each{ |f|  @fields[f[:order]] = f }
      end
    end # initialize

    ### The definitions of the fields stored for this peripheral type.
    ###
    ### Each Hash defines a field of data to store. The keys are:
    ### - :name, String, the name of the field
    ### - :type, String or Symbol, the kind of data to be stored in the field, either :text or :menu
    ### - :choices, Array of Strings - if type is :menu, these are the menu choices.
    ### - :order, the one-based index of this field amongst it's peers.
    ###
    ### Since Arrays are zero-based, and the field order is one-based, keeping
    ### a nil at the front of the Array will keep the :order number in sync with the
    ### Array index of each field definition. This is done automatically by the field-editing
    ### methods: {#fields=}, {#set_field}, {#append_field}, {#prepend_field}, {#insert_field},
    ### and {#delete_field}.
    ###
    ### So the Array from the API comes like this:
    ###   [ {:type=>"text", :order=>1, :choices=>[], :name=>"make"},
    ###     {:type=>"text", :order=>2, :choices=>[], :name=>"model"},
    ###     {:type=>"text", :order=>3, :choices=>[], :name=>"family"},
    ###     {:type=>"text", :order=>4, :choices=>[], :name=>"serialnum"} ]
    ### But will be stored in a PeripheralType instance like this:
    ###   [ nil,
    ###     {:type=>"text", :order=>1, :choices=>[], :name=>"make"},
    ###     {:type=>"text", :order=>2, :choices=>[], :name=>"model"},
    ###     {:type=>"text", :order=>3, :choices=>[], :name=>"family"},
    ###     {:type=>"text", :order=>4, :choices=>[], :name=>"serialnum"} ]
    ###
    ### therefore
    ###   myPerifType.fields[2]
    ### will get you the second field, which has :order => 2.
    ###
    ### @return [Array<Hash>] The field definitions
    ###
    def fields
      @fields
    end

    ###
    ### Replace the entire Array of field definitions.
    ### The :order of each will be set based on the indexes of the
    ### Array provided.
    ###
    ### @param new_fields[Array<Hash>] the new field definitions
    ###
    ### @return [void]
    ###
    def fields= (new_fields)
      unless new_fields.kind_of? Array and  new_fields.reject{|c| c.kind_of? Hash }.empty?
        raise JSS::InvalidDataError, "Argument must be an Array of Hashes."
      end
      raise "A peripheral type can have a maximmum of 20 fields"  if new_fields.count > 20
      new_fields.each{ |f| field_ok? f }
      @fields = new_fields
      order_fields
      @need_to_update = true
    end

    ###
    ### Replace the details of one specific field.
    ###
    ### The order must already exist. Otherwise use
    ### {#append_field}, {#prepend_field}, or {#insert_field}
    ###
    ### @param order[Integer] which field are we replacing?
    ###
    ### @param field[Hash] the new field data
    ###
    ### @return [void]
    ###
    def set_field(order, field = {})
      raise JSS::NoSuchItemError, "No field with number '#{order}'. Use #append_field, #prepend_field, or #insert_field" unless @fields[order]
      field_ok? field
      @fields[order] = field
      @need_to_update = true
    end

    ###
    ### Add a new field to the end of the field list
    ###
    ### @param field[Hash] the new field data
    ###
    ### @return [void]
    ###
    def append_field(field = {})
      field_ok? field
      @fields << field
      order_fields
      @need_to_update = true
    end

    ###
    ### Add a new field to the beginning of the field list
    ###
    ### @param field[Hash] the new field data
    ###
    ### @return [void]
    ###
    def prepend_field(field = {})
      field_ok? field
      @fields.unshift field
      order_fields
      @need_to_update = true
    end

    ###
    ### Add a new field to the middle of the fields Array.
    ###
    ### @param order[Integer] where does the new field go?
    ###
    ### @param field[Hash] the new field data
    ###
    ### @return [void]
    ###
    def insert_field(order,field = {})
      field_ok? field
      @fields.insert((order -1), field)
      order_fields
      @need_to_update = true
    end

    ###
    ### Remove a field from the array of fields.
    ###
    ### @param order[Integer] which field to remove?
    ###
    ### @return [void]
    ###
    def delete_field(order)
      if @fields[order]
        raise JSS::MissingDataError, "Fields can't be empty" if @fields.count == 1
        @fields.delete_at index
        order_fields
        @need_to_update = true
      end
    end



    ##############################
    ### private methods
    ##############################
    private

    ###
    ### is a Hash of field data OK for use in the JSS?
    ### Return true or raise an exception
    ###
    def field_ok?(field)
      raise JSS::InvalidDataError, "Field elements must be hashes with :name, :type, and possibly :choices" unless field.kind_of? Hash
      raise JSS::InvalidDataError, "Fields require names" if field[:name].to_s.empty?
      raise JSS::InvalidDataError, "Fields :type must be one of: :#{FIELD_TYPES.join(', :')}" unless FIELD_TYPES.include? field[:type].to_sym

      if field[:type].to_sym == :menu
        unless field[:choices].kind_of? Array and  field[:choices].reject{|c| c.kind_of? String}.empty?
          raise JSS::InvalidDataError, "Choices for menu fields must be an Array of Strings"
        end # unless
      else
        field[:choices] = []
      end # if type -- menu
      true
    end # def field ok?

    ###
    ### Close up gaps in the field order, and make each field's :order match it's array index
    ###
    def order_fields
      @fields.compact!
      @fields.each_index{|i| @fields[i][:order] = i+1}
      @fields.unshift nil
    end


    ###
    ###
    ###
    def rest_xml
      order_fields
      doc = REXML::Document.new APIConnection::XML_HEADER
      pkg = doc.add_element RSRC_OBJECT_KEY.to_s
      pkg.add_element('id').text = @id
      pkg.add_element('name').text = @name
      fields = pkg.add_element 'fields'

      flds =  @fields.compact
      flds.each_index do |i|
        field = fields.add_element 'field'
        field.add_element('order').text =flds[i][:order]
        field.add_element('name').text = flds[i][:name]
        field.add_element('type').text = flds[i][:type].to_s
        choices = field.add_element('choices')
        unless flds[i][:choices].empty?
          flds[i][:choices].each{|c| choices.add_element('choice').text = c}
        end
      end # each index do i
      return doc.to_s
    end # rest xml

  end # class Peripheral
end # module