# The relationship graph is the final form of a puppet catalog in
# which all dependency edges are explicitly in the graph. This form of the
# catalog is used to traverse the graph in the order in which resources are
# managed.
#
# @api private
class Puppet::Graph::RelationshipGraph < Puppet::Graph::SimpleGraph
  attr_reader :blockers

  def initialize(prioritizer)
    super()

    @prioritizer = prioritizer

    @ready = Puppet::Graph::RbTreeMap.new
    @generated = {}
    @done = {}
    @blockers = {}
    @providerless_types = []
  end

  def populate_from(catalog)
    add_all_resources_as_vertices(catalog)
    build_manual_dependencies
    build_autorelation_dependencies(catalog)

    write_graph(:relationships) if catalog.host_config?

    replace_containers_with_anchors(catalog)

    write_graph(:expanded_relationships) if catalog.host_config?
  end

  def add_vertex(vertex, priority = nil)
    super(vertex)

    if priority
      @prioritizer.record_priority_for(vertex, priority)
    else
      @prioritizer.generate_priority_for(vertex)
    end
  end

  def add_relationship(f, t, label=nil)
    super(f, t, label)
    @ready.delete(@prioritizer.priority_of(t))
  end

  def remove_vertex!(vertex)
    super
    @prioritizer.forget(vertex)
  end

  def resource_priority(resource)
    @prioritizer.priority_of(resource)
  end

  # Enqueue the initial set of resources, those with no dependencies.
  def enqueue_roots
    vertices.each do |v|
      @blockers[v] = direct_dependencies_of(v).length
      enqueue(v) if @blockers[v] == 0
    end
  end

  # Decrement the blocker count for the resource by 1. If the number of
  # blockers is unknown, count them and THEN decrement by 1.
  def unblock(resource)
    @blockers[resource] ||= direct_dependencies_of(resource).select { |r2| !@done[r2] }.length
    if @blockers[resource] > 0
      @blockers[resource] -= 1
    else
      resource.warning "appears to have a negative number of dependencies"
    end
    @blockers[resource] <= 0
  end

  def clear_blockers
    @blockers.clear
  end

  def enqueue(*resources)
    resources.each do |resource|
      @ready[@prioritizer.priority_of(resource)] = resource
    end
  end

  def finish(resource)
    direct_dependents_of(resource).each do |v|
      enqueue(v) if unblock(v)
    end
    @done[resource] = true
  end

  def next_resource
    @ready.delete_min
  end

  def traverse(options = {}, &block)
    continue_while = options[:while] || lambda { true }
    pre_process = options[:pre_process] || lambda { |resource| }
    overly_deferred_resource_handler = options[:overly_deferred_resource_handler] || lambda { |resource| }
    canceled_resource_handler = options[:canceled_resource_handler] || lambda { |resource| }
    teardown = options[:teardown] || lambda {}

    report_cycles_in_graph

    enqueue_roots

    deferred_resources = []

    while continue_while.call() && (resource = next_resource)
      if resource.suitable?
        made_progress = true

        pre_process.call(resource)

        yield resource

        finish(resource)
      else
        deferred_resources << resource
      end

      if @ready.empty? and deferred_resources.any?
        if made_progress
          enqueue(*deferred_resources)
        else
          deferred_resources.each do |resource|
            overly_deferred_resource_handler.call(resource)
            finish(resource)
          end
        end

        made_progress = false
        deferred_resources = []
      end
    end

    if !continue_while.call()
      while (resource = next_resource)
        canceled_resource_handler.call(resource)
        finish(resource)
      end
    end

    teardown.call()
  end

  private

  def add_all_resources_as_vertices(catalog)
    catalog.resources.each do |vertex|
      add_vertex(vertex)
    end
  end

  def build_manual_dependencies
    vertices.each do |vertex|
      vertex.builddepends.each do |edge|
        add_edge(edge)
      end
    end
  end

  def build_autorelation_dependencies(catalog)
    vertices.each do |vertex|
      [:require,:subscribe].each do |rel_type|
        vertex.send("auto#{rel_type}".to_sym, catalog).each do |edge|
          # don't let automatic relationships conflict with manual ones.
          next if edge?(edge.source, edge.target)

          if edge?(edge.target, edge.source)
            vertex.debug "Skipping automatic relationship with #{edge.source}"
          else
            vertex.debug "Forming automation relationship of type #{rel_type} with #{edge.source}"
            if rel_type == :require
              edge.event = :NONE
            else
              edge.callback = :refresh
              edge.event = :ALL_EVENTS
            end
            add_edge(edge)
          end
        end
      end

      [:before,:notify].each do |rel_type|
        vertex.send("auto#{rel_type}".to_sym, catalog).each do |edge|
          # don't let automatic relationships conflict with manual ones.
          next if edge?(edge.target, edge.source)

          if edge?(edge.source, edge.target)
            vertex.debug "Skipping automatic relationship with #{edge.target}"
          else
            vertex.debug "Forming automation relationship of type #{rel_type} with #{edge.target}"
            if rel_type == :before
              edge.event = :NONE
            else
              edge.callback = :refresh
              edge.event = :ALL_EVENTS
            end
            add_edge(edge)
          end
        end
      end
    end
  end

  # Impose our container information on another graph by using it
  # to replace any container vertices X with a pair of vertices
  # { admissible_X and completed_X } such that
  #
  #    0) completed_X depends on admissible_X
  #    1) contents of X each depend on admissible_X
  #    2) completed_X depends on each on the contents of X
  #    3) everything which depended on X depends on completed_X
  #    4) admissible_X depends on everything X depended on
  #    5) the containers and their edges must be removed
  #
  # Note that this requires attention to the possible case of containers
  # which contain or depend on other containers, but has the advantage
  # that the number of new edges created scales linearly with the number
  # of contained vertices regardless of how containers are related;
  # alternatives such as replacing container-edges with content-edges
  # scale as the product of the number of external dependencies, which is
  # to say geometrically in the case of nested / chained containers.
  #
  Default_label = { :callback => :refresh, :event => :ALL_EVENTS }
  def replace_containers_with_anchors(catalog)
    stage_class      = Puppet::Type.type(:stage)
    whit_class       = Puppet::Type.type(:whit)
    component_class  = Puppet::Type.type(:component)
    containers = catalog.resources.find_all { |v| (v.is_a?(component_class) or v.is_a?(stage_class)) and vertex?(v) }
    #
    # These two hashes comprise the aforementioned attention to the possible
    #   case of containers that contain / depend on other containers; they map
    #   containers to their sentinels but pass other vertices through.  Thus we
    #   can "do the right thing" for references to other vertices that may or
    #   may not be containers.
    #
    admissible = Hash.new { |h,k| k }
    completed  = Hash.new { |h,k| k }
    containers.each { |x|
      admissible[x] = whit_class.new(:name => "admissible_#{x.ref}", :catalog => catalog)
      completed[x]  = whit_class.new(:name => "completed_#{x.ref}",  :catalog => catalog)
      priority = @prioritizer.priority_of(x)
      add_vertex(admissible[x], priority)
      add_vertex(completed[x], priority)
    }
    #
    # Implement the six requirements listed above
    #
    containers.each { |x|
      contents = catalog.adjacent(x, :direction => :out)
      add_edge(admissible[x],completed[x]) if contents.empty? # (0)
      contents.each { |v|
        add_edge(admissible[x],admissible[v],Default_label) # (1)
        add_edge(completed[v], completed[x], Default_label) # (2)
      }
      # (3) & (5)
      adjacent(x,:direction => :in,:type => :edges).each { |e|
        add_edge(completed[e.source],admissible[x],e.label)
        remove_edge! e
      }
      # (4) & (5)
      adjacent(x,:direction => :out,:type => :edges).each { |e|
        add_edge(completed[x],admissible[e.target],e.label)
        remove_edge! e
      }
    }
    containers.each { |x| remove_vertex! x } # (5)
  end
end
