require 'rgen/instantiator/resolution_helper'

module RGen

module Instantiator

# The ReferenceResolver can be used to resolve unresolved references, i.e. instances
# of class UnresolvedReference
#
# There are two ways how this can be used:
#  1. the identifiers and associated model elements are added upfront using +add_identifier+
#  2. register an :identifier_resolver with the constructor, which will be invoked 
#     for every unresolved identifier
#
class ReferenceResolver
 
  # Instances of this class represent information about not yet resolved references.
  # This consists of the +element+ and metamodel +feature_name+ which hold/is to hold the 
  # reference and the +proxy+ object which is the placeholder for the reference.
  # If the reference could not be resolved because the target type does not match the
  # feature type, the flag +target_type_error+ will be set.
  #
  class UnresolvedReference 
    attr_reader :feature_name, :proxy
    attr_accessor :element, :target_type_error
    def initialize(element, feature_name, proxy)
      @element = element
      @feature_name = feature_name
      @proxy = proxy
    end
  end

  # Create a reference resolver, options:
  #
  #  :identifier_resolver:
  #    a proc which is called with an identifier and which should return the associated element
  #    in case the identifier is not uniq, the proc may return multiple values
  #    default: lookup element in internal map
  #
  def initialize(options={})
    @identifier_resolver = options[:identifier_resolver]
    @identifier_map = {}
  end

  # Add an +identifer+ / +element+ pair which will be used for looking up unresolved identifers
  def add_identifier(ident, element)
    map_entry = @identifier_map[ident]
    if map_entry 
      if map_entry.is_a?(Array)
        map_entry << element
      else
        @identifier_map[ident] = [map_entry, element]
      end
    else 
      @identifier_map[ident] = element
    end
  end

  # Tries to resolve the given +unresolved_refs+. If resolution is successful, the proxy object
  # will be removed, otherwise there will be an error description in the problems array.
  # In case the resolved target element's type is not valid for the given feature, the 
  # +target_type_error+ flag will be set on the unresolved reference.
  # Returns an array of the references which are still unresolved. Options:
  # 
  #  :problems
  #    an array to which problems will be appended
  #
  #  :on_resolve
  #    a proc which will be called for every sucessful resolution, receives the unresolved
  #    reference as well as to new target element
  #
  #  :use_target_type
  #    use the expected target type to narrow the set of possible targets 
  #    (i.e. ignore targets with wrong type)
  #
  #  :failed_resolutions
  #    a Hash which will receive an entry for each failed resolution for which at least one
  #    target element was found (wrong target type, or target not unique).
  #    hash key is the uref, hash value is the target element or the Array of target elements
  #
  def resolve(unresolved_refs, options={})
    problems = options[:problems] || []
    still_unresolved_refs = []
    failed_resolutions = options[:failed_resolutions] || {}
    unresolved_refs.each do |ur|
      if @identifier_resolver
        target = @identifier_resolver.call(ur.proxy.targetIdentifier)
      else
        target = @identifier_map[ur.proxy.targetIdentifier]
      end
      target = [target].compact unless target.is_a?(Array)
      if options[:use_target_type] 
        feature = ur.element.class.ecore.eAllReferences.find{|r| r.name == ur.feature_name}
        target = target.select{|e| e.is_a?(feature.eType.instanceClass)}
      end
      if target.size == 1
        status = ResolutionHelper.set_uref_target(ur, target[0])
        if status == :success
          options[:on_resolve] && options[:on_resolve].call(ur, target[0])
        elsif status == :type_error
          ur.target_type_error = true
          problems << type_error_message(target[0])
          still_unresolved_refs << ur
          failed_resolutions[ur] = target[0]
        end
      elsif target.size > 1
        problems << "identifier #{ur.proxy.targetIdentifier} not uniq"
        still_unresolved_refs << ur
        failed_resolutions[ur] = target
      else
        problems << "identifier #{ur.proxy.targetIdentifier} not found"
        still_unresolved_refs << ur
      end
    end
    still_unresolved_refs
  end   

  private

  def type_error_message(target)
    "invalid target type #{target.class}"
  end

end

end

end
