module Xcodeproj
  class Project
    # This class represents an ordered relationship to many objects.
    #
    # It works in conjunction with the {AbstractObject} class to ensure that
    # the project is not serialized with unreachable objects by updating the
    # with reference count on modifications.
    #
    # @note Concerning the mutations methods it is safe to call only those
    #       which are overridden to inform objects reference count. Ideally all
    #       the array methods should be covered, but this is not done yet.
    #       Moreover it is a moving target because the methods of array
    #       usually are implemented in C
    #
    # @todo Cover all the mutations methods of the {Array} class.
    #
    class ObjectList < Array
      # {Xcodeproj} clients are not expected to create instances of
      # {ObjectList}, it is always initialized empty and automatically by the
      # synthesized methods generated by {AbstractObject.has_many}.
      #
      def initialize(attribute, owner)
        @attribute = attribute
        @owner = owner
      end

      # @return [Array<Class>] the attribute that generated the list.
      #
      attr_reader :attribute

      # @return [Array<Class>] the object that owns the list.
      #
      attr_reader :owner

      # @return [Array<String>]
      #   the UUIDs of all the objects referenced by this list.
      #
      def uuids
        map(&:uuid)
      end

      # @return [Array<AbstractObject>]
      #   a new array generated with the objects contained in the list.
      #
      def objects
        to_a
      end

      public

      # @!group Notification enabled methods
      #------------------------------------------------------------------------#

      # TODO: the overridden methods are incomplete.

      # Adds an array of objects to list and updates their references count.
      #
      # @param [Array<AbstractObject, ObjectDictionary>] objects
      #   an array of objects to add to the list.
      #
      # @return [void]
      #
      def +(other)
        perform_additions_operations(other)
        super
      end

      # Appends an object to list the and updates its references count.
      #
      # @param  [AbstractObject, ObjectDictionary] object
      #         The object to add to the list.
      #
      # @return [void]
      #
      def <<(object)
        perform_additions_operations(object)
        super
      end

      # Adds an object to the given index of the list the and updates its
      # references count.
      #
      # @param  [AbstractObject, ObjectDictionary] object
      #         The object to add to the list.
      #
      # @return [void]
      #
      def insert(index, object)
        perform_additions_operations(object)
        super
      end

      # Prepends an object to the list and updates its references count.
      #
      # @param  [AbstractObject, ObjectDictionary] object
      #         The object to add to the list.
      #
      # @return [void]
      #
      def unshift(object)
        perform_additions_operations(object)
        super
      end

      # Removes an object to list and updates its references count.
      #
      # @param [AbstractObject, ObjectDictionary] object
      #   the object to delete from the list.
      #
      # @return [AbstractObject, ObjectDictionary, Nil] the object if found.
      #
      def delete(object)
        perform_deletion_operations(object)
        super
      end

      # Removes the object at the given index from the list and updates its
      # references count.
      #
      # @param [Fixnum] from
      #        The index of the object.
      #
      # @return [AbstractObject, ObjectDictionary, Nil] the object if found.
      #
      def delete_at(index)
        object = at(index)
        perform_deletion_operations(object)
        super
      end

      # Removes all the objects contained in the list and updates their
      # reference counts.
      #
      # @return [void]
      #
      def clear
        objects.each do |object|
          perform_deletion_operations(object)
        end
        super
      end

      # Moves the given object to the given index.
      #
      # @param [AbstractObject, ObjectDictionary] object
      #        The object to move.
      #
      # @param [Fixnum] to
      #        The new index for the object.
      #
      # @return [void]
      #
      def move(object, new_index)
        return if index(object) == new_index
        if obj = delete(object)
          insert(new_index, obj)
        else
          raise "Attempt to move object `#{object}` not present in the list `#{inspect}`"
        end
      end

      # Moves the object at the given index to the given position.
      #
      # @param [Fixnum] from
      #        The current index of the object.
      #
      # @param [Fixnum] to
      #        The new index for the object.
      #
      # @return [void]
      #
      def move_from(current_index, new_index)
        return if current_index == new_index
        if obj = delete_at(current_index)
          insert(new_index, obj)
        else
          raise "Attempt to move object from index `#{current_index}` which is beyond bounds of the list `#{inspect}`"
        end
      end

      def sort!
        return super if owner.project.dirty?
        previous = to_a
        super
        owner.mark_project_as_dirty! unless previous == to_a
        self
      end

      private

      # @!group Notification Methods
      #------------------------------------------------------------------------#

      # Informs an object that it was added to the list. In practice it adds
      # the owner of the list as referrer to the objects. It also validates the
      # value.
      #
      # @return [void]
      #
      def perform_additions_operations(objects)
        objects = [objects] unless objects.is_a?(Array)
        objects.each do |obj|
          owner.mark_project_as_dirty!
          obj.add_referrer(owner)
          attribute.validate_value(obj) unless obj.is_a?(ObjectDictionary)
        end
      end

      # Informs an object that it was removed from to the list, so it can
      # remove its owner from its referrers and take the appropriate actions.
      #
      # @return [void]
      #
      def perform_deletion_operations(objects)
        objects = [objects] unless objects.is_a?(Array)
        objects.each do |obj|
          owner.mark_project_as_dirty!
          obj.remove_referrer(owner) unless obj.is_a?(ObjectDictionary)
        end
      end
    end
  end
end
