require 'rgen/instantiator/reference_resolver'

module RGen

module Fragment

# A FragmentedModel represents a model which consists of fragments (ModelFragment).
# 
# The main purpose of this class is to resolve references across fragments and
# to keep the references consistent while fragments are added or removed.
# This way it also plays an important role in keeping the model fragments consistent
# and thus ModelFragment objects should only be accessed via this interface.
# Overall unresolved references after the resolution step are also maintained.
#
# A FragmentedModel can also  keep an RGen::Environment object up to date while fragments
# are added or removed. The environment must be registered with the constructor.
#
# Reference resolution is based on arbitrary identifiers. The identifiers must be
# provided in the fragments' indices. The FragmentedModel takes care to maintain
# the overall index.
#
class FragmentedModel
  attr_reader :fragments
  attr_reader :environment

  # Creates a fragmented model. Options:
  #
  #  :env 
  #    environment which will be updated as model elements are added and removed
  #
  def initialize(options={})
    @environment = options[:env]
    @fragments = []
    @index = nil
    @fragment_change_listeners = []
    @fragment_index = {}
  end

  # Adds a proc which is called when a fragment is added or removed
  # The proc receives the fragment and one of :added, :removed
  #
  def add_fragment_change_listener(listener)
    @fragment_change_listeners << listener
  end

  def remove_fragment_change_listener(listener)
    @fragment_change_listeners.delete(listener)
  end

  # Add a fragment.
  #
  def add_fragment(fragment)
    invalidate_cache
    @fragments << fragment
    fragment.elements.each{|e| @environment << e} if @environment
    @fragment_change_listeners.each{|l| l.call(fragment, :added)}
  end

  # Removes the fragment. The fragment will be unresolved using unresolve_fragment.
  #
  def remove_fragment(fragment)
    raise "fragment not part of model" unless @fragments.include?(fragment)
    invalidate_cache
    @fragments.delete(fragment)
    @fragment_index.delete(fragment)
    unresolve_fragment(fragment)
    fragment.elements.each{|e| @environment.delete(e)} if @environment
    @fragment_change_listeners.each{|l| l.call(fragment, :removed)}
  end

  # Resolve references between fragments. 
  # It is assumed that references within fragments have already been resolved.
  # This method can be called several times. It will update the overall unresolved references.
  #
  # Options:
  #
  #  :fragment_provider:
  #    Only if a +fragment_provider+ is given, the resolve step can be reverted later on
  #    by a call to unresolve_fragment. The fragment provider is a proc which receives a model
  #    element and must return the fragment in which the element is contained.
  #
  #  :use_target_type:
  #    reference resolver uses the expected target type to narrow the set of possible targets 
  #
  def resolve(options={})
    local_index = index
    @fragments.each do |f|
      f.resolve_external(local_index, options)
    end
  end

  # Remove all references between this fragment and all other fragments.
  # The references will be replaced with unresolved references (MMProxy objects).
  #
  def unresolve_fragment(fragment)
    fragment.unresolve_external
    @fragments.each do |f|
      if f != fragment
        f.unresolve_external_fragment(fragment)
      end
    end
  end

  # Returns the overall unresolved references.
  #
  def unresolved_refs
    @fragments.collect{|f| f.unresolved_refs}.flatten
  end

  # Returns the overall index. 
  # This is a Hash mapping identifiers to model elements accessible via the identifier. 
  #
  def index
    fragments.each do |f|
      if !@fragment_index[f] || (@fragment_index[f].object_id != f.index.object_id)
        @fragment_index[f] = f.index
        invalidate_cache
      end
    end
    return @index if @index
    @index = {}
    fragments.each do |f|
      f.index.each do |i| 
        (@index[i[0]] ||= []) << i[1]
      end
    end
    @index
  end

  private

  def invalidate_cache
    @index = nil
  end

end

end

end
