require 'rgen/array_extensions'

module RGen
  
module ModelBuilder

class ReferenceResolver
  ResolverJob = Struct.new(:receiver, :reference, :namespace, :string)
  
  class ResolverException < Exception
  end

  class ToplevelNamespace
    def initialize(ns)
      raise "Namespace must be an Enumerable" unless ns.is_a?(Enumerable)
      @ns = ns
    end
    def elements
      @ns
    end
  end
  
  def initialize
    @jobs = []
    @elementName = {}
  end

  def addJob(job)
    @jobs << job
  end
  
  def setElementName(element, name)
    @elementName[element] = name
  end

  def resolve(ns=[])
    @toplevelNamespace = ToplevelNamespace.new(ns)
    (@jobs || []).each_with_index do |job, i|
      target = resolveReference(job.namespace || @toplevelNamespace, job.string.split("."))
      raise ResolverException.new("Can not resolve reference #{job.string}") unless target
      if job.reference.many
        job.receiver.addGeneric(job.reference.name, target)
      else
        job.receiver.setGeneric(job.reference.name, target)
      end
    end
  end
  
  private
  
  # TODO: if a reference can not be fully resolved, but a prefix can be found,
  # the exception reported is that its first path element can not be found on
  # toplevel
  def resolveReference(namespace, nameParts)
    element = resolveReferenceDownwards(namespace, nameParts)
    if element.nil? && parentNamespace(namespace)
      element = resolveReference(parentNamespace(namespace), nameParts)
    end
    element
  end
  
  def resolveReferenceDownwards(namespace, nameParts)
    firstPart, *restParts = nameParts
    element = namespaceElementByName(namespace, firstPart)
    return nil unless element
    if restParts.size > 0
      resolveReferenceDownwards(element, restParts)
    else
      element
    end
  end
  
  def namespaceElementByName(namespace, name)
    @namespaceElementsByName ||= {}
    return @namespaceElementsByName[namespace][name] if @namespaceElementsByName[namespace]
    hash = {}
    namespaceElements(namespace).each do |e|
      raise ResolverException.new("Multiple elements named #{elementName(e)} found in #{nsToS(namespace)}") if hash[elementName(e)]
      hash[elementName(e)] = e if elementName(e)
    end
    @namespaceElementsByName[namespace] = hash
    hash[name]
  end
  
  def parentNamespace(namespace)
    if namespace.class.respond_to?(:ecore)
      parents = elementParents(namespace)
      raise ResolverException.new("Element #{nsToS(namespace)} has multiple parents") \
        if parents.size > 1
      parents.first || @toplevelNamespace
    else
      nil
    end
  end
  
  def namespaceElements(namespace)
    if namespace.is_a?(ToplevelNamespace)
      namespace.elements
    elsif namespace.class.respond_to?(:ecore)
      elementChildren(namespace)
    else
      raise ResolverException.new("Element #{nsToS(namespace)} can not be used as a namespace")
    end
  end
  
  def nsToS(namespace)
    if namespace.is_a?(ToplevelNamespace)
      "toplevel namespace"
    else
      result = namespace.class.name    
      result += ":\"#{elementName(namespace)}\"" if elementName(namespace)
      result
    end
  end
  
  def elementName(element)
    @elementName[element]
  end
  
  def elementChildren(element)
    @elementChildren ||= {}
    return @elementChildren[element] if @elementChildren[element]
    children = containmentRefs(element).collect do |r|
      element.getGeneric(r.name)
    end.flatten.compact
    @elementChildren[element] = children
  end
  
  def elementParents(element)
    @elementParents ||= {}
    return @elementParents[element] if @elementParents[element]
    parents = parentRefs(element).collect do |r|
      element.getGeneric(r.name)
    end.flatten.compact
    @elementParents[element] = parents
  end  
  
  def containmentRefs(element)
    @containmentRefs ||= {}
    @containmentRefs[element.class] ||= eAllReferences(element).select{|r| r.containment}
  end
  
  def parentRefs(element)
    @parentRefs ||= {}
    @parentRefs[element.class] ||= eAllReferences(element).select{|r| r.eOpposite && r.eOpposite.containment}
  end

  def eAllReferences(element)
    @eAllReferences ||= {}
    @eAllReferences[element.class] ||= element.class.ecore.eAllReferences
  end
end

end

end