# The Binder is responsible for processing layered bindings that can be used to setup an Injector.
#
# An instance should be created and a call to {#define_layers} should be made which will process the layered bindings
# (handle overrides, abstract entries etc.).
# The constructed hash with `key => InjectorEntry` mappings is obtained as {#injector_entries}, and is used to initialize an
# {Puppet::Pops::Binder::Injector Injector}.
#
# @api public
#
class Puppet::Pops::Binder::Binder

  # @api private
  attr_reader :injector_entries

  # @api private
  attr :id_index

  # @api private
  attr_reader :key_factory

  # A parent Binder or nil
  # @api private
  attr_reader :parent

  # The next anonymous key to use
  # @api private
  attr_reader :anonymous_key

  # This binder's precedence
  # @api private
  attr_reader :binder_precedence

  # @api public
  def initialize(layered_bindings, parent_binder=nil)
    @parent = parent_binder
    @id_index = Hash.new() { |k, v| [] }

    @key_factory = Puppet::Pops::Binder::KeyFactory.new()

    # Resulting hash of all key -> binding
    @injector_entries = {}

    if @parent.nil?
      @anonymous_key = 0
      @binder_precedence = 0
    else
      # First anonymous key is the parent's next (non incremented key). (The parent can not change, it is
      # the final, free key).
      @anonymous_key = @parent.anonymous_key
      @binder_precedence = @parent.binder_precedence + 1
    end
    define_layers(layered_bindings)
  end

  # Binds layers from highest to lowest as defined by the given LayeredBindings.
  # @note
  #   The model should have been
  #   validated to get better error messages if the model is invalid. This implementation expects the model
  #   to be valid, and any errors raised will be more technical runtime errors.
  #
  # @param layered_bindings [Puppet::Pops::Binder::Bindings::LayeredBindings] the named and ordered layers
  # @raise ArgumentError if this binder is already configured
  # @raise ArgumentError if bindings with unresolved 'override' surfaces as an effective binding
  # @raise ArgumentError if the given argument has the wrong type, or if model is invalid in some way
  # @return [Puppet::Pops::Binder::Binder] self
  # @api public
  #
  def define_layers(layered_bindings)

    LayerProcessor.new(self, key_factory).bind(layered_bindings)
    contribution_keys = []
    # make one pass over entries to collect contributions, and check overrides
    injector_entries.each do |k,v|
      if key_factory.is_contributions_key?(k)
        contribution_keys << [k,v]
      elsif !v.is_resolved?()
        raise ArgumentError, "Binding with unresolved 'override' detected: #{self.class.format_binding(v.binding)}}"
      else
        # if binding has an id, add it to the index
        add_id_to_index(v.binding)
      end
    end

    # If a lower level binder has contributions for a key also contributed to in this binder
    # they must included in the higher shadowing contribution.
    # If a contribution is made to an id that is defined in a parent
    # contribute to an id that is defined in a lower binder, it must be promoted to this binder (copied) or
    # there is risk of making the lower level injector dirty.
    #
    contribution_keys.each do |kv|
      parent_contribution = lookup_in_parent(kv[0])
      next unless parent_contribution
      injector_entries[kv[0]] = kv[1] + parent_contributions

      # key the multibind_id from the contribution key
      multibind_id = key_factory.multibind_contribution_key_to_id(kv[0])
      promote_matching_bindings(self, @parent, multibind_id)
    end
  end
  private :define_layers

  # @api private
  def next_anonymous_key
    tmp = @anonymous_key
    @anonymous_key += 1
    tmp
  end

  def add_id_to_index(binding)
    return unless binding.is_a?(Puppet::Pops::Binder::Bindings::Multibinding) && !(id = binding.id).nil?
    @id_index[id] = @id_index[id] << binding
  end

  def promote_matching_bindings(to_binder, from_binder, multibind_id)
    return if from_binder.nil?
    from_binder.id_index[ multibind_id ].each do |binding|
      key = key_factory.binding_key(binding)
      entry = lookup(key)
      unless entry.precedence == @binder_precedence
        # it is from a lower layer it must be promoted
        injector_entries[ key ] = Puppet::Pops::Binder::InjectorEntry.new(binding, binder_precedence)
      end
    end
    # recursive "up the parent chain" to promote all
    promote_matching_bindings(to_binder, from_binder.parent, multibind_id)
  end

  def lookup_in_parent(key)
    @parent.nil? ? nil : @parent.lookup(key)
  end

  def lookup(key)
    if x = injector_entries[key]
      return x
    end
    @parent ?  @parent.lookup(key) :  nil
  end

  # @api private
  def self.format_binding(b)
    type_name = Puppet::Pops::Types::TypeCalculator.new().string(b.type)
    layer_name, bindings_name = get_named_binding_layer_and_name(b)
    "binding: '#{type_name}/#{b.name}' in: '#{bindings_name}' in layer: '#{layer_name}'"
  end

  # @api private
  def self.format_contribution_source(b) 
    layer_name, bindings_name = get_named_binding_layer_and_name(b)
    "(layer: #{layer_name}, bindings: #{bindings_name})"
  end

  # @api private
  def self.get_named_binding_layer_and_name(b)
    return ['<unknown>', '<unknown>'] if b.nil?
    return [get_named_layer(b), b.name] if b.is_a?(Puppet::Pops::Binder::Bindings::NamedBindings)
    get_named_binding_layer_and_name(b.eContainer)
  end

  # @api private
  def self.get_named_layer(b)
    return '<unknown>' if b.nil?
    return b.name if b.is_a?(Puppet::Pops::Binder::Bindings::NamedLayer)
    get_named_layer(b.eContainer)
  end

  # Processes the information in a layer, aggregating it to the injector_entries hash in its parent binder.
  # A LayerProcessor holds the intermediate state required while processing one layer.
  #
  # @api private
  #
  class LayerProcessor
    attr :bindings
    attr :binder
    attr :key_factory
    attr :contributions
    attr :binder_precedence

    def initialize(binder, key_factory)
      @binder = binder
      @binder_precedence = binder.binder_precedence
      @key_factory = key_factory
      @bindings = []
      @contributions = []
      @@bind_visitor ||= Puppet::Pops::Visitor.new(nil,"bind",0,0)
    end

    # Add the binding to the list of potentially effective bindings from this layer
    # @api private
    #
    def add(b)
      bindings << Puppet::Pops::Binder::InjectorEntry.new(b, binder_precedence)
    end

    # Add a multibind contribution
    # @api private
    #
    def add_contribution(b)
      contributions << Puppet::Pops::Binder::InjectorEntry.new(b, binder_precedence)
    end

    # Bind given abstract binding
    # @api private
    #
    def bind(binding)
      @@bind_visitor.visit_this(self, binding)
    end

    # @return [Puppet::Pops::Binder::InjectorEntry] the entry with the highest precedence
    # @api private
    def highest(b1, b2)
      if b1.is_abstract? != b2.is_abstract?
        # if one is abstract and the other is not, the non abstract wins
        b1.is_abstract? ? b2 : b1
      else
        case b1.precedence <=> b2.precedence
        when 1
          b1
        when -1
          b2
        when 0
          raise_conflicting_binding(b1, b2)
        end
      end
    end

    # Raises a conflicting bindings error given two InjectorEntry's with same precedence in the same layer
    # (if they are in different layers, something is seriously wrong)
    def raise_conflicting_binding(b1, b2)
      b1_layer_name, b1_bindings_name = binder.class.get_named_binding_layer_and_name(b1.binding)
      b2_layer_name, b2_bindings_name = binder.class.get_named_binding_layer_and_name(b2.binding)

      finality_msg = (b1.is_final? || b2.is_final?) ? ". Override of final binding not allowed" : ''

      # TODO: Use of layer_name is not very good, it is not guaranteed to be unique
      unless b1_layer_name == b2_layer_name
        raise ArgumentError, [
          'Conflicting binding for',
          "'#{b1.binding.name}'",
          'being resolved across layers',
          "'#{b1_layer_name}' and",
          "'#{b2_layer_name}'"
          ].join(' ')+finality_msg
      end

      # Conflicting bindings made from the same source
      if b1_bindings_name == b2_bindings_name
        raise ArgumentError, [
          'Conflicting binding for name:',
          "'#{b1.binding.name}'",
          'in layer:',
          "'#{b1_layer_name}', ",
          'both from:',
          "'#{b1_bindings_name}'"
          ].join(' ')+finality_msg
      end

      # Conflicting bindings from different sources
      raise ArgumentError, [
        'Conflicting binding for name:',
        "'#{b1.binding.name}'",
        'in layer:',
        "'#{b1_layer_name}',",
        'from:',
        "'#{b1_bindings_name}', and",
        "'#{b2_bindings_name}'"
        ].join(' ')+finality_msg
    end


    # Produces the key for the given Binding.
    # @param binding [Puppet::Pops::Binder::Bindings::Binding] the binding to get a key for
    # @return [Object] an opaque key
    # @api private
    #
    def key(binding)
      k = if is_contribution?(binding)
        # contributions get a unique (sequential) key
        binder.next_anonymous_key()
      else
        key_factory.binding_key(binding)
      end
    end

    # @api private
    def is_contribution?(binding)
      ! binding.multibind_id.nil?
    end

    # @api private
    def bind_Binding(o)
      if is_contribution?(o)
        add_contribution(o)
      else
        add(o)
      end
    end

    # @api private
    def bind_Bindings(o)
      o.bindings.each {|b| bind(b) }
    end

    # @api private
    def bind_NamedBindings(o)
      # Name is ignored here, it should be introspected when needed (in case of errors)
      o.bindings.each {|b| bind(b) }
    end

    # Process layered bindings from highest to lowest layer
    # @api private
    #
    def bind_LayeredBindings(o)
      o.layers.each do |layer|
        processor = LayerProcessor.new(binder, key_factory)
        # All except abstract (==error) are transfered to injector_entries

        processor.bind(layer).each do |k, v|
          entry = binder.injector_entries[k]
          unless key_factory.is_contributions_key?(k)
            if v.is_abstract?()
              layer_name, bindings_name = Puppet::Pops::Binder::Binder.get_named_binding_layer_and_name(v.binding)
              type_name = key_factory.type_calculator.string(v.binding.type)
              raise ArgumentError, "The abstract binding '#{type_name}/#{v.binding.name}' in '#{bindings_name}' in layer '#{layer_name}' was not overridden"
            end
            raise ArgumentError, "Internal Error - redefinition of key: #{k}, (should never happen)" if entry
            binder.injector_entries[k] = v
          else
            entry ? entry << v : binder.injector_entries[k] = v
          end
        end
      end
    end

    # Processes one named ("top level") layer consisting of a list of NamedBindings
    # @api private
    #
    def bind_NamedLayer(o)
      o.bindings.each {|b| bind(b) }
      this_layer = {}

      # process regular bindings
      bindings.each do |b|
        bkey = key(b.binding)

        # ignore if a higher layer defined it (unless the lower is final), but ensure override gets resolved
        # (override is not resolved across binders)
        if x = binder.injector_entries[bkey]
          if b.is_final?
            raise_conflicting_binding(x, b)
          end
          x.mark_override_resolved()
          next
        end

        # If a lower (parent) binder exposes a final binding it may not be overridden
        #
        if (x = binder.lookup_in_parent(bkey)) && x.is_final?
          raise_conflicting_binding(x, b)
        end

        # if already found in this layer, one wins (and resolves override), or it is an error
        existing = this_layer[bkey]
        winner = existing ? highest(existing, b) : b
        this_layer[bkey] = winner
        if existing
          winner.mark_override_resolved()
        end
      end

      # Process contributions
      # - organize map multibind_id to bindings with this id
      # - for each id, create an array with the unique anonymous keys to the contributed bindings
      # - bind the index to a special multibind contributions key (these are aggregated)
      #
      c_hash = Hash.new {|hash, key| hash[ key ] = [] }
      contributions.each {|b| c_hash[ b.binding.multibind_id ] << b }
      # - for each id
      c_hash.each do |k, v|
        index = v.collect do |b|
          bkey = key(b.binding)
          this_layer[bkey] = b
          bkey
        end
        contributions_key = key_factory.multibind_contributions(k)
        unless this_layer[contributions_key]
          this_layer[contributions_key] = []
        end
        this_layer[contributions_key] += index
      end
      this_layer
    end
  end
end
