module TaliaCore

  # This class provides an ordering on the related sources. It will load the
  # related elements into an array. All changes made to the OrderedSource will
  # reflect on the Array, and will only be persisted to the store on save.
  #
  # The collection is contained in the ordered_objects - if you ever use that
  # accessor all "ordering" relations for the object will be completely overwritten
  # on saving. (You can still assign the predicates manually as long as the
  # ordered_objects accesssor is not called before saving.)
  class OrderedSource < ActiveSource
    
    attr_reader :current_index
    
    before_save :rewrite_order_relations

    # Initialize SeqContainer
    def self.new(uri)
      @resource = super(uri)
      #      @resource.save!
      #      @resource.types << RDF::Seq.uri
      #      @resource
    end
    
    # Returns all elements (not the relations) in an ordered array. Unlike the
    # ordered_objects accessor this will not include nil elements and the
    # index will not have a 1:1 relation to the position of the elment.
    #
    # The ordering of the elements will be preserved, though.
    def elements
      # execute query
      ordered_objects.compact
    end
    
    # Returns the first element of the collection
    def first
      ordered_objects.find { |el| !el.nil? }
    end

    # return the item at position index.
    # 
    #  * index: int
    #  * return value: TaliaCore::ActiveSource
    def at(index)
      @current_index = index
      ordered_objects.at(index)
    end
    
    # return next item
    # * current_element: int or string. Current element. If nil, the index is the last integer used with at method
    def next(current_element = nil)
      set_current_index_for(current_element)
      
      # if current element is nil, next must return first value
      @current_index ||= 0

      if (@current_index < (size - 1))
        return at(@current_index + 1) # TODO: Current is not increased, is this intentional? Do we need this method at all?
      else
      raise "Last item reached"
    end
    end

    # return previous item
    # * current_element: int or string. Current element. If nil, the index is the last integer used with at method
    def previous(current_element = nil)
      set_current_index_for(current_element)
      
      # if current element is nil, next must return first value
      @current_index = (size + 1) if @current_index.nil?

      if (@current_index > 1) # TODO: This assumes a one-base array, not really useful
        return at(@current_index - 1) # TODO: See above
      else
      raise "First item reached"
    end
    end
    
    # Return the "size" of the collection. This is actually the maximum position
    # index that is used. The value is cached internally and will be increased
    # on insert_at (only if inserting at a value larger at the current size) and
    # on the add operation. If elements are added in another way, the count
    # may be off, but in that case it would be off anyway. Delete will decrease
    # the size by 1, if the index removed equals the size (this is a rough guess
    # which may be off!)
    # The cached counter will be reset on saving.
    def size
      ordered_objects.size
      end
    
    # Inserts an element at the given index.
    def insert_at(index, object)
      write_for_index(ordered_objects, index, object)
    end
    
    # Add new item to ordered source. This will add the object after the last
    # element. ATTENTION: If you add on an empty collection, it will start at
    # index 1 (One), for backwards compatability
    def add(object)
      position = (size > 0) ? size : 1
      insert_at(position, object)
    end
    
    # remove an existing object to ordered source. This will reorder the
    # existing positions if deleting from the middle! If you don't want
    # that use insert_at(index, nil)
    def delete(index)
      ordered_objects.delete_at(index)
    end
  
    # remove all existing object to ordered source
    # TODO: This will not reliably delete all elements, since the size
    #       value is not reliable
    def delete_all
      @ordered_objects = []
    end
    
    # replace item as position index with object
    # * index: int
    # * object: TaliaCore::ActiveSource
    def replace(index, object)
      replace_for_index(ordered_objects, index, object)
    end
    
    # Find the position index of the given object. This will always find the
    # first occurence
    def find_position_by_object(object)
      ordered_objects.index(object)
    end
      
    # return string for index
    def index_to_predicate(index)
      self.class.index_to_predicate(index)
    end
      
    # return index of predicate
    def predicate_to_index(predicate)
      self.class.predicate_to_index(predicate)
    end
    
    # Returns all the objects that are ordered in an array where the array
    # index equals the position of the object in the ordered set. The array
    # is zero-based, position that don't have an object attached will be set to 
    # nil.
    def ordered_objects
      return @ordered_objects if(@ordered_objects)
      relations = query
      # Let's assume the follwing is a sane assumption ;-)
      # Even if a one-base collection comes in, we need to push just one element
      @ordered_objects = Array.new(relations.size)
      # Now add the elements so that the relation property is reflected
      # on the position in the array
      relations.each do |rel|
        index = rel.rel_order
        write_for_index(@ordered_objects, index, rel.object)
      end

      @ordered_objects
    end
    
    # return string for index
    def self.index_to_predicate(index)
      'http://www.w3.org/1999/02/22-rdf-syntax-ns#_' << ("%06d" % index.to_i) 
    end
    
    # return index of predicate
    def self.predicate_to_index(predicate)
      predicate.sub('http://www.w3.org/1999/02/22-rdf-syntax-ns#_', '').to_i
    end

    private

    # Set the current index for the given element, which can be a number, a
    # predicate string or an object contained in the collection. Passing nil
    # causes it to do nothing.
    def set_current_index_for(current_element)
      return unless(current_element)

      case current_element
      when Fixnum   then @current_index = current_element
      when String   then @current_index = predicate_to_index(current_element)
      when TaliaCore::ActiveSource
        # find semantic relation
        pos = find_position_by_object(current_element)
        # if no relations is found
        raise "Object isn't in current OrderedSource" if pos.nil?
        # else we have a position
        @current_index = pos
      else
        raise "Class #{current_element.class} not supported"
      end
    end

    # This will be called before saving and will completely rewrite the relations
    # that make up the ordered store, based on the internal array
    def rewrite_order_relations
      return unless(@ordered_objects) # If this is nil, the relations weren't loaded in the first place
      objects = ordered_objects # Fetch them before deleting
      # Now destroy the existing elements
      SemanticRelation.destroy_all(['subject_id = ? AND rel_order IS NOT NULL', self.id])
      # rewrite from the relations array
      objects.each_index do |index|
        if(obj = objects.at(index)) # Check if there's a value to handle
          # Create a new relation with an order
          self[index_to_predicate(index)].add_with_order(obj, index)
        end
      end
    end

    # Helper to "grow" an array to the given index. Unless replace is true, it
    # will raise an error if the element already exists.
    def write_for_index(arr, index, value, replace = false)
      raise(RuntimeError, "Duplicate element found on index #{index}") if(arr.at(index) && !replace)
      arr[index] = value
    end
    
    def replace_for_index(arr, index, value)
      write_for_index(arr, index, value, true)
    end

    # execute query and return the result
    def query(scope = :all)
      # execute query
      self.semantic_relations.find(scope, :conditions => 'rel_order IS NOT NULL', :order => :rel_order)
    end

  end
end