module RGen

module Util

# A PatternMatcher can be used to find, insert and remove patterns on a given model.
#
# A pattern is specified by means of a block passed to the add_pattern method.
# The block must take an Environment as first parameter and at least one model element
# as connection point as further parameter. The pattern matches if it can be found
# in a given environment and connected to the given connection point elements.
#
class PatternMatcher

  Match = Struct.new(:root, :elements, :bound_values)
  attr_accessor :debug

  def initialize
    @patterns = {} 
    @insert_mode = false
    @debug = false
  end

  def add_pattern(name, &block)
    raise "a pattern needs at least 2 block parameters: " + 
      "an RGen environment and a model element as connection point" \
      unless block.arity >= 2
    @patterns[name] = block
  end

  def find_pattern(env, name, *connection_points)
    match = find_pattern_internal(env, name, *connection_points)
  end

  def insert_pattern(env, name, *connection_points)
    @insert_mode = true
    root = evaluate_pattern(name, env, connection_points)
    @insert_mode = false
    root
  end

  def remove_pattern(env, name, *connection_points)
    match = find_pattern_internal(env, name, *connection_points)
    if match
      match.elements.each do |e|
        disconnect_element(e)
        env.delete(e)
      end
      match
    else
      nil
    end
  end

  def lazy(&block)
    if @insert_mode
      block.call
    else
      Lazy.new(&block)
    end
  end

  class Lazy < RGen::MetamodelBuilder::MMGeneric
    def initialize(&block)
      @block = block
    end
    def _eval
      @block.call
    end
  end

  private

  class Proxy < RGen::MetamodelBuilder::MMProxy
    attr_reader :_target
    def initialize(target)
      @_target = target
    end
    def method_missing(m, *args)
      result = @_target.send(m, *args)
      if result.is_a?(Array)
        result.collect do |e|
          if e.is_a?(RGen::MetamodelBuilder::MMBase)
            Proxy.new(e)
          else
            e
          end
        end
      else
        if result.is_a?(RGen::MetamodelBuilder::MMBase)
          Proxy.new(result)
        else
          result 
        end
      end
    end
  end

  class Bindable < RGen::MetamodelBuilder::MMGeneric
    # by being an Enumerable, Bindables can be used for many-features as well
    include Enumerable
    def initialize
      @bound = false
      @value = nil
      @many = false
    end
    def _bound?
      @bound
    end
    def _many?
      @many
    end
    def _bind(value)
      @value = value
      @bound = true
    end
    def _value
      @value
    end
    def to_s
      @value.to_s
    end
    # pretend this is an enumerable which contains itself, so the bindable can be
    # inserted into many-features, when this is done the bindable is marked as a many-bindable
    def each
      @many = true
      yield(self)
    end
    def method_missing(m, *args)
      raise "bindable not bound" unless _bound?
      @value.send(m, *args)
    end
  end

  TempEnv = RGen::Environment.new
  class << TempEnv
    def <<(el); end
  end

  def find_pattern_internal(env, name, *connection_points)
    proxied_args = connection_points.collect{|a| 
      a.is_a?(RGen::MetamodelBuilder::MMBase) ?  Proxy.new(a) : a }
    bindables = create_bindables(name, connection_points)
    pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables)
    candidates = candidates_via_connection_points(pattern_root, connection_points)
    candidates ||= env.find(:class => pattern_root.class)
    candidates.each do |e|
      # create new bindables for every try, otherwise they can could be bound to old values
      bindables = create_bindables(name, connection_points) 
      pattern_root = evaluate_pattern(name, TempEnv, proxied_args+bindables)
      matched = match(pattern_root, e)
      return Match.new(e, matched, bindables.collect{|b| b._value}) if matched 
    end
    nil
  end

  def create_bindables(pattern_name, connection_points)
    (1..(num_pattern_variables(pattern_name) - connection_points.size)).collect{|i| Bindable.new}
  end

  def candidates_via_connection_points(pattern_root, connection_points)
    @candidates_via_connection_points_refs ||= {}
    refs = (@candidates_via_connection_points_refs[pattern_root.class] ||= 
      pattern_root.class.ecore.eAllReferences.reject{|r| r.derived || r.many || !r.eOpposite})
    candidates = nil 
    refs.each do |r|
      t = pattern_root.getGeneric(r.name)
      cp = t.is_a?(Proxy) && connection_points.find{|cp| cp.object_id == t._target.object_id}
      if cp
        elements = cp.getGenericAsArray(r.eOpposite.name)
        candidates = elements if candidates.nil? || elements.size < candidates.size 
      end
    end
    candidates
  end
  
  def match(pat_element, test_element)
    visited = {}
    check_later = []
    return false unless match_internal(pat_element, test_element, visited, check_later)
    while cl = check_later.shift
      pv, tv = cl.lazy._eval, cl.value
      if cl.feature.is_a?(RGen::ECore::EAttribute)
        unless pv == tv
          match_failed(cl.feature, "wrong attribute value (lazy): #{pv} vs. #{tv}")
          return false 
        end
      else
        if pv.is_a?(Proxy)
          unless pv._target.object_id == tv.object_id
            match_failed(f, "wrong target object")
            return false 
          end
        else
          unless (pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later))
            return false 
          end
        end
      end
    end
    visited.keys
  end

  CheckLater = Struct.new(:feature, :lazy, :value)
  def match_internal(pat_element, test_element, visited, check_later)
    return true if visited[test_element]
    visited[test_element] = true
    unless pat_element.class == test_element.class
      match_failed(nil, "wrong class: #{pat_element.class} vs #{test_element.class}")
      return false 
    end
    all_structural_features(pat_element).each do |f|
      pat_values = pat_element.getGeneric(f.name)
      # nil values must be kept to support size check with Bindables
      pat_values = [ pat_values ] unless pat_values.is_a?(Array)
      test_values = test_element.getGeneric(f.name)
      test_values = [ test_values] unless test_values.is_a?(Array)
      if pat_values.size == 1 && pat_values.first.is_a?(Bindable) && pat_values.first._many?
        unless match_many_bindable(pat_values.first, test_values)
          return false
        end
      else
        unless pat_values.size == test_values.size
          match_failed(f, "wrong size #{pat_values.size} vs. #{test_values.size}")
          return false 
        end
        pat_values.each_with_index do |pv,i|
          tv = test_values[i]
          if pv.is_a?(Lazy)
            check_later << CheckLater.new(f, pv, tv)
          elsif pv.is_a?(Bindable)
            if pv._bound?
              unless pv._value == tv
                match_failed(f, "value does not match bound value #{pv._value.class}:#{pv._value.object_id} vs. #{tv.class}:#{tv.object_id}")
                return false 
              end
            else
              pv._bind(tv)
            end
          else
            if f.is_a?(RGen::ECore::EAttribute)
              unless pv == tv 
                match_failed(f, "wrong attribute value")
                return false 
              end
            else
              if pv.is_a?(Proxy)
                unless pv._target.object_id == tv.object_id
                  match_failed(f, "wrong target object")
                  return false 
                end
              else
                unless both_nil_or_match(pv, tv, visited, check_later)
                  return false 
                end
              end
            end
          end
        end
      end
    end
    true
  end

  def match_many_bindable(bindable, test_values)
    if bindable._bound? 
      bindable._value.each_with_index do |pv,i|
        tv = test_values[i]
        if f.is_a?(RGen::ECore::EAttribute)
          unless pv == tv 
            match_failed(f, "wrong attribute value")
            return false 
          end
        else
          unless both_nil_or_match(pv, tv, visited, check_later)
            return false 
          end
        end
      end
    else
      bindable._bind(test_values.dup)
    end
    true
  end

  def both_nil_or_match(pv, tv, visited, check_later)
    (pv.nil? && tv.nil?) || (!pv.nil? && !tv.nil? && match_internal(pv, tv, visited, check_later))
  end

  def match_failed(f, msg)
    puts "match failed #{f&&f.eContainingClass.name}##{f&&f.name}: #{msg}" if @debug
  end

  def num_pattern_variables(name)
    prok = @patterns[name]
    prok.arity - 1
  end

  def evaluate_pattern(name, env, connection_points)
    prok = @patterns[name]
    raise "unknown pattern #{name}" unless prok
    raise "wrong number of arguments, expected #{prok.arity-1} connection points)" \
      unless connection_points.size == prok.arity-1
    prok.call(env, *connection_points)
  end

  def disconnect_element(element)
    return if element.nil?
    all_structural_features(element).each do |f|
      if f.many
        element.setGeneric(f.name, [])
      else
        element.setGeneric(f.name, nil)
      end
    end
  end

  def all_structural_features(element)
    @all_structural_features ||= {}
    return @all_structural_features[element.class] if @all_structural_features[element.class]
    @all_structural_features[element.class] = 
     element.class.ecore.eAllStructuralFeatures.reject{|f| f.derived}
  end

end

end

end

