### Copyright 2025 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 Jamf

  # Module Variables
  #####################################

  # Module Methods
  #####################################

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

  # A mix-in module that centralizes the code for handling objects which can be
  # assigned a 'category' in the JSS.
  #
  # Objects in the JSS present category data in two different ways:
  #
  # 1) An 'old' style, where the top-level Hash of the API data contains a
  #    :category key which contains a String, being the category name.
  #
  # 2) A 'new' style, where the :category key is a Hash with a :name and :id key.
  #    This Hash is usually in the :general subset, but may be elsewhere.
  #
  # Classes mixing in this module MUST:
  #
  # - Define the constant CATEGORY_SUBSET as a symbol indicating where in
  #   the API data the :category key will be found. The symbol is either
  #   :top for the top-level of the API data, or the name of the subsection
  #   Hash containing :category, e.g. :general.
  #
  # - Define the constant CATEGORY_DATA_TYPE as either String or Hash
  #   (the class names) which indicate if the contents of the :category key
  #   is a String (The category name) or a Hash (containing :name and :id)
  #
  # - call {#add_category_to_xml(xmldoc)} from their #rest_xml method if they are
  #   {Updatable} or {Creatable}
  #
  module Categorizable

    # Module Constants
    #####################################

    CATEGORIZABLE = true

    # When no category has been assigned, this is the 'name'
    NO_CATEGORY_NAME = 'No category assigned'.freeze
    # When no category has been assigned, this is the id
    NO_CATEGORY_ID = -1

    # Setting the category to any of these values will unset the category
    NON_CATEGORIES = [
      nil,
      '',
      0,
      NO_CATEGORY_NAME,
      NO_CATEGORY_ID
    ].freeze

    # Mixed-in Public Instance Methods
    #####################################

    # The name of the category for this object.
    # For backward compatibility, this is aliased to just
    # 'category'
    #
    # @return [String] The name of the category for this object.
    #
    def category_name
      @category_name
    end # cat name
    alias category category_name

    # The id of the category for this object.
    #
    # @return [Integer] The id of the category for this object.
    #
    def category_id
      @category_id
    end # cat id

    # The Jamf::Category instance for this object's category
    #
    # @return [Jamf::Category] The Jamf::Category instance for this object's category
    #
    def category_object
      return nil unless category_assigned?

      Jamf::Category.fetch id: @category_id
    end # cat obj

    # Does this object have a category assigned?
    #
    # @return [Boolean] Does this object have a category assigned?
    #
    def category_assigned?
      !@category_name.nil?
    end # cat assigned?
    alias categorized? category_assigned?

    # Change the category of this object.
    # Any of the NON_CATEGORIES values will
    # unset the category
    #
    # @param new_cat[Integer, String] The new category
    #
    # @return [void]
    #
    def category=(new_cat)
      return nil unless updatable? || creatable?

      # unset the category? Use nil or an empty string
      if NON_CATEGORIES.include? new_cat
        unset_category
        return
      end

      new_name, new_id = evaluate_new_category(new_cat)

      # no change, go home.
      return nil if new_name == @category_name

      raise Jamf::NoSuchItemError, "Category '#{new_cat}' is not known to the JSS" unless Jamf::Category.all_names(:ref, cnx: @cnx).include? new_name

      @category_name = new_name
      @category_id = new_id
      @need_to_update = true
    end # category =

    # Given a category name or id, return the name and id
    # TODO: use APIObject.exist? and/or APIObject.valid_id
    # @param new_cat[String, Integer] The name or id of a possible category
    #
    # @return [Array<String, Integer>] The matching name and id, which may be nil.
    #
    def evaluate_new_category(new_cat)
      # if we were given anything but a string, assume it was an id.
      if new_cat.is_a? String
        new_name = new_cat
        new_id = Jamf::Category.category_id_from_name new_cat, cnx: @cnx
      else
        new_id = new_cat
        new_name = Jamf::Category.map_all_ids_to(:name, cnx: @cnx)[new_id]
      end
      [new_name, new_id]
    end

    # Set the category to nothing
    #
    # @return [void]
    #
    def unset_category
      # no change, go home
      return nil if @category_name.nil?

      @category_name = nil
      @category_id = nil
      @need_to_update = true
    end # unset category

    # Mixed-in Private Instance Methods
    #####################################
    private

    # Parse the category data from any incoming API data
    #
    # @return [void] description_of_returned_object
    #
    def parse_category
      cat =
        if self.class::CATEGORY_SUBSET == :top
          @init_data[:category]
        else
          @init_data[self.class::CATEGORY_SUBSET][:category]
        end

      if cat.is_a? String
        @category_name = cat
        @category_id = Jamf::Category.category_id_from_name @category_name, cnx: @cnx
      else
        @category_name = cat[:name]
        @category_id = cat[:id]
      end
      clean_raw_categories
    end # parse category

    # Set the category name and id to nil, if need be.
    def clean_raw_categories
      @category_name = nil if @category_name && @category_name.to_s.casecmp(NO_CATEGORY_NAME).zero?
      @category_id = nil if @category_id == NO_CATEGORY_ID
    end

    # Add the category to the XML for POSTing or PUTting to the API.
    #
    # @param xmldoc[REXML::Document] The in-construction XML document
    #
    # @return [void]
    #
    def add_category_to_xml(xmldoc)
      return if category_name.to_s.empty?

      cat_elem = REXML::Element.new('category')

      if self.class::CATEGORY_DATA_TYPE == String
        cat_elem.text = @category_name.to_s
      elsif self.class::CATEGORY_DATA_TYPE == Hash
        cat_elem.add_element('name').text = @category_name.to_s
      else
        raise Jamf::InvalidDataError, "Uknown CATEGORY_DATA_TYPE for class #{self.class}"
      end

      root = xmldoc.root

      if self.class::CATEGORY_SUBSET == :top
        root.add_element cat_elem
        return
      end

      parent = root.elements[self.class::CATEGORY_SUBSET.to_s]
      parent ||= root.add_element self.class::CATEGORY_SUBSET.to_s
      parent.add_element cat_elem
    end # add_category_to_xml

  end # module categorizable

end # module
