require 'rgen/ecore/ecore'
require 'rgen/instantiator/abstract_xml_instantiator'
require 'rgen/array_extensions'

class ECoreXMLInstantiator < AbstractXMLInstantiator
  
  include RGen::ECore
  
  INFO = 0
  WARN = 1
  ERROR = 2
  
  def initialize(env, loglevel=ERROR)
    @env = env
    @rolestack = []
    @elementstack = []
    @element_by_id = {}
    @loglevel = loglevel
  end
  
  def start_tag(prefix, tag, namespaces, attributes)
    eRef = nil
    if @elementstack.last
      eRef = eAllReferences(@elementstack.last).find{|r|r.name == tag}
      if eRef
        if attributes["xsi:type"] && attributes["xsi:type"] =~ /ecore:(\w+)/
          class_name = $1
          attributes.delete("xsi:type")
        else 
          class_name = eRef.eType.name
        end
      else
        raise "Reference not found: #{tag} on #{@elementstack.last}"
      end
    else
      class_name = tag
    end
    
    eClass = RGen::ECore.ecore.eClassifiers.find{|c| c.name == class_name}
    if eClass
      obj = RGen::ECore.const_get(class_name).new
      if attributes["xmi:id"]
        @element_by_id[attributes["xmi:id"]] = obj
        attributes.delete("xmi:id")
      end
      if eRef
        if eRef.many
          @elementstack.last.addGeneric(eRef.name, obj)
        else
          @elementstack.last.setGeneric(eRef.name, obj)
        end
      end
      @env << obj
      @elementstack.push obj
    else
      log WARN, "Class not found: #{class_name}"
      @elementstack.push nil
    end

    attributes.each_pair do |attr, value|
      set_attribute_internal(attr, value)
    end
  end
  
  def end_tag(prefix, tag)
    @elementstack.pop
  end
  
  ResolverDescription = Struct.new(:object, :attribute, :value)
  
  def set_attribute(attr, value)
    # do nothing, already handled by start_tag/set_attribute_internal
  end

  def set_attribute_internal(attr, value)
    return unless @elementstack.last
    eFeat = eAllStructuralFeatures(@elementstack.last).find{|a| a.name == attr}
    if eFeat.is_a?(EReference)
      rd = ResolverDescription.new
      rd.object = @elementstack.last
      rd.attribute = attr
      rd.value = value
      @resolver_descs << rd
    elsif eFeat
      value = true if value == "true" && eFeat.eType == EBoolean
      value = false if value == "false" && eFeat.eType == EBoolean
      value = value.to_i if eFeat.eType == EInt || eFeat.eType == ELong
      @elementstack.last.setGeneric(attr, value)
    else
      log WARN, "Feature not found: #{attr} on #{@elementstack.last}"
    end
  end
  
  def instantiate(str)
    @resolver_descs = []
#    puts "Instantiating ..."
    super(str, 1000)
    rootpackage = @env.find(:class => EPackage).first
#    puts "Resolving ..."
    @resolver_descs.each do |rd|
      refed = find_referenced(rootpackage, rd.value)
      feature = eAllStructuralFeatures(rd.object).find{|f| f.name == rd.attribute}
      raise StandardError.new("StructuralFeature not found: #{rd.attribute}") unless feature
      if feature.many
        rd.object.setGeneric(feature.name, refed)
      else
        rd.object.setGeneric(feature.name, refed.first)
      end
    end
  end
  
  def eAllReferences(element)
    @eAllReferences ||= {}
    @eAllReferences[element.class] ||= element.class.ecore.eAllReferences
  end
  
  def eAllAttributes(element)
    @eAllAttributes ||= {}
    @eAllAttributes[element.class] ||= element.class.ecore.eAllAttributes
  end
  
  def eAllStructuralFeatures(element)
    @eAllStructuralFeatures ||= {}
    @eAllStructuralFeatures[element.class] ||= element.class.ecore.eAllStructuralFeatures
  end
  
  def find_referenced(context, desc)
    desc.split(/\s+/).collect do |r|
      if r =~ /^#([^\/]+)$/
        @element_by_id[$1]
      elsif r =~ /^#\/\d*\/([\w\/]+)/
        find_in_context(context, $1.split('/'))
      elsif r =~ /#\/\/(\w+)$/
        case $1
          when "EString";     RGen::ECore::EString
          when "EInt";        RGen::ECore::EInt
          when "ELong";       RGen::ECore::ELong
          when "EBoolean";    RGen::ECore::EBoolean
          when "EFloat";      RGen::ECore::EFloat
          when "EJavaObject"; RGen::ECore::EJavaObject
          when "EJavaClass";  RGen::ECore::EJavaClass
        end
      end
    end.compact
  end
  
  def find_in_context(context, desc_elements)
    if context.is_a?(EPackage)
      r = (context.eClassifiers + context.eSubpackages).find{|c| c.name == desc_elements.first}
    elsif context.is_a?(EClass)
      r = context.eStructuralFeatures.find{|s| s.name == desc_elements.first}
    else
      raise StandardError.new("Don't know how to find #{desc_elements.join('/')} in context #{context}")
    end
    if r
      if desc_elements.size > 1
        find_in_context(r, desc_elements[1..-1])
      else
        r
      end
    else
      log WARN, "Can not follow path, element #{desc_elements.first} not found within #{context}(#{context.name})"
    end
  end
  
  def log(level, msg)
    puts %w(INFO WARN ERROR)[level] + ": " + msg if level >= @loglevel
  end
end
