require 'rgen/ecore/ecore'

module RGen
  
module ECore

# ECoreToRuby can turn ECore models into their Ruby metamodel representations
class ECoreToRuby
  
  def initialize
    @modules = {}
    @classifiers = {}
    @features_added = {}
  end

  # Create a Ruby module representing +epackage+.
  # This includes all nested modules/packages, classes and enums.
  #
  # If a parent module is provided with the "under" parameter, 
  # the new module will be nested under the parent module.
  #
  # If the parent module has a non-temporary name,
  # (more precisely: a non-temporary classpath) i.e. if it is reachable
  # via a path of constant names from the root, then the nested
  # modules and classes will also have non-temporary names.
  # In particular, this means that they will keep their names even
  # if they are assigned to new constants.
  # 
  # If no parent module is provided or the parent module has a
  # temporary name by itself, then the nested modules and classes will
  # also have temporary names. This means that their name will stay
  # 'volatile' until they are assigned to constants reachable from
  # the root and the Module#name method is called for the first time.
  #
  # While the second approach is more flexible, it can come with a major
  # performance impact. The reason is that Ruby searches the space of
  # all known non-temporary classes/modules every time the name
  # of a class/module with a temporary name is queried.
  #
  def create_module(epackage, under=Module.new)
    with_empty_constant_order_helper do
      temp = under.to_s.start_with?("#")
      mod = create_module_internal(epackage, under, temp)

      epackage.eAllClassifiers.each do |c| 
        if c.is_a?(RGen::ECore::EClass)
          create_class(c, temp)
        elsif c.is_a?(RGen::ECore::EEnum)
          create_enum(c)
        end
      end

      mod
    end
  end

  private

  def create_module_internal(epackage, under, temp)
    return @modules[epackage] if @modules[epackage]
    
    if temp
      mod = Module.new do
        extend RGen::MetamodelBuilder::ModuleExtension
      end
      under.const_set(epackage.name, mod)
    else
      under.module_eval <<-END
        module #{epackage.name}
          extend RGen::MetamodelBuilder::ModuleExtension
        end
      END
      mod = under.const_get(epackage.name)
    end
    @modules[epackage] = mod

    epackage.eSubpackages.each{|p| create_module_internal(p, mod, temp)}
    mod._set_ecore_internal(epackage)

    mod
  end

  def create_class(eclass, temp)
    return @classifiers[eclass] if @classifiers[eclass]

    mod = @modules[eclass.ePackage]
    if temp
      cls = Class.new(super_class(eclass, temp)) do
        abstract if eclass.abstract
        class << self
          attr_accessor :_ecore_to_ruby
         end
      end
      mod.const_set(eclass.name, cls)
    else
      mod.module_eval <<-END
        class #{eclass.name} < #{super_class(eclass, temp)}
          #{eclass.abstract ? 'abstract' : ''}
          class << self
            attr_accessor :_ecore_to_ruby
          end
        end
      END
      cls = mod.const_get(eclass.name)
    end

    class << eclass
      attr_accessor :instanceClass
      def instanceClassName
        instanceClass.to_s
      end
    end
    eclass.instanceClass = cls

    cls::ClassModule.module_eval do
      alias _method_missing method_missing
      def method_missing(m, *args)
        if self.class._ecore_to_ruby.add_features(self.class.ecore)
          send(m, *args)
        else
          _method_missing(m, *args)
        end
      end
      alias _respond_to respond_to?
      def respond_to?(m, include_all=false)
        self.class._ecore_to_ruby.add_features(self.class.ecore)
        _respond_to(m)
      end
    end
    @classifiers[eclass] = cls
    cls._set_ecore_internal(eclass)
    cls._ecore_to_ruby = self

    cls
  end

  def create_enum(eenum)
    return @classifiers[eenum] if @classifiers[eenum]

    e = RGen::MetamodelBuilder::DataTypes::Enum.new(eenum.eLiterals.collect{|l| l.name.to_sym})
    @classifiers[eenum] = e

    @modules[eenum.ePackage].const_set(eenum.name, e)
    e
  end

  class FeatureWrapper
    def initialize(efeature, classifiers)
      @efeature = efeature
      @classifiers = classifiers
    end
    def value(prop)
      return false if prop == :containment && @efeature.is_a?(RGen::ECore::EAttribute)
      @efeature.send(prop)
    end
    def many?
      @efeature.many
    end
    def reference?
      @efeature.is_a?(RGen::ECore::EReference)
    end
    def opposite
      @efeature.eOpposite
    end
    def impl_type
      etype = @efeature.eType
      if etype.is_a?(RGen::ECore::EClass) || etype.is_a?(RGen::ECore::EEnum)
        @classifiers[etype]
      else
        ic = etype.instanceClass
        if ic
          ic
        else
          raise "unknown type: #{etype.name}" 
        end
      end
    end
  end

  def super_class(eclass, temp)
    super_types = eclass.eSuperTypes
    if temp
      case super_types.size
      when 0
        RGen::MetamodelBuilder::MMBase
      when 1
        create_class(super_types.first, temp)
      else
        RGen::MetamodelBuilder::MMMultiple(*super_types.collect{|t| create_class(t, temp)})
      end
    else
      case super_types.size
      when 0
        "RGen::MetamodelBuilder::MMBase"
      when 1
        create_class(super_types.first, temp).name
      else
        "RGen::MetamodelBuilder::MMMultiple(" + 
          super_types.collect{|t| create_class(t, temp).name}.join(",") + ")"
      end
    end
  end

  class EmptyConstantOrderHelper
    def classCreated(c); end
    def moduleCreated(m); end
    def enumCreated(e); end
  end

  def with_empty_constant_order_helper
    orig_coh = RGen::MetamodelBuilder::ConstantOrderHelper
    RGen::MetamodelBuilder.instance_eval { remove_const(:ConstantOrderHelper) }
    RGen::MetamodelBuilder.const_set(:ConstantOrderHelper, EmptyConstantOrderHelper.new)

    begin
      result = yield
    ensure
      RGen::MetamodelBuilder.instance_eval { remove_const(:ConstantOrderHelper) }
      RGen::MetamodelBuilder.const_set(:ConstantOrderHelper, orig_coh)
    end

    result
  end

  public

  def add_features(eclass)
    return false if @features_added[eclass]
    c = @classifiers[eclass]
    eclass.eStructuralFeatures.each do |f|
      w1 = FeatureWrapper.new(f, @classifiers) 
      w2 = FeatureWrapper.new(f.eOpposite, @classifiers) if f.is_a?(RGen::ECore::EReference) && f.eOpposite
      c.module_eval do
        if w1.many?
          _build_many_methods(w1, w2)
        else
          _build_one_methods(w1, w2)
        end
      end
    end
    @features_added[eclass] = true
    eclass.eSuperTypes.each do |t|
      add_features(t)
    end
    true
  end

end

end

end

