# adjacency.rb
#
require 'rgl/mutable'
require 'set'

module RGL

  # The +DirectedAdjacencyGraph+ class implements a generalized adjacency list
  # graph structure. An AdjacencyGraph is basically a two-dimensional structure
  # (ie, a list of lists). Each element of the first dimension represents a
  # vertex. Each of the vertices contains a one-dimensional structure that is
  # the list of all adjacent vertices.
  #
  # The class for representing the adjacency list of a vertex is, by default, a
  # +Set+. This can be configured by the client, however, when an AdjacencyGraph
  # is created.
  class DirectedAdjacencyGraph

    include MutableGraph

    # Shortcut for creating a DirectedAdjacencyGraph
    # @example
    #   RGL::DirectedAdjacencyGraph[1,2, 2,3, 2,4, 4,5].edges.to_a.to_s =>
    #     "(1-2)(2-3)(2-4)(4-5)"
    #
    def self.[](*a)
      result = new
      0.step(a.size - 1, 2) { |i| result.add_edge(a[i], a[i + 1]) }
      result
    end

    # The new empty graph has as its edgelist class the given class. The default
    # edgelist class is Set, to ensure set semantics for edges and vertices.
    #
    # If other graphs are passed as parameters their vertices and edges are
    # added to the new graph.
    # @param [Class] edgelist_class
    # @param [Array[Graph]] other_graphs
    def initialize(edgelist_class = Set, *other_graphs)
      @edgelist_class = edgelist_class
      @vertices_dict = Hash.new
      other_graphs.each do |g|
        g.each_vertex { |v| add_vertex v }
        g.each_edge { |v, w| add_edge v, w }
      end
    end

    # Copy internal vertices_dict
    #
    def initialize_copy(orig)
      @vertices_dict = orig.instance_eval { @vertices_dict }.dup
      @vertices_dict.keys.each do |v|
        @vertices_dict[v] = @vertices_dict[v].dup
      end
    end

    # Iterator for the keys of the vertices list hash.
    # @see Graph#each_vertex
    def each_vertex(&b)
      @vertices_dict.each_key(&b)
    end

    # @see Graph#each_adjacent
    def each_adjacent(v, &b)
      adjacency_list = (@vertices_dict[v] or raise NoVertexError, "No vertex #{v}.")
      adjacency_list.each(&b)
    end

    # @return true.
    #
    def directed?
      true
    end

    # Complexity is O(1), because the vertices are kept in a +Hash+ containing
    # as values the lists of adjacent vertices of _v_.
    #
    # @see Graph#has_vertex
    def has_vertex?(v)
      @vertices_dict.has_key?(v)
    end

    # Complexity is O(1), if a Set is used as adjacency list. Otherwise,
    # complexity is O(out_degree(v)).
    # @see Graph#has_edge?
    def has_edge?(u, v)
      has_vertex?(u) && @vertices_dict[u].include?(v)
    end

    # If the vertex is already in the graph (using +eql?+), the method does
    # nothing.
    # @see MutableGraph#add_vertex
    def add_vertex(v)
      @vertices_dict[v] ||= @edgelist_class.new
    end

    # @see MutableGraph#add_edge.
    def add_edge(u, v)
      add_vertex(u) # ensure key
      add_vertex(v) # ensure key
      basic_add_edge(u, v)
    end

    # @see MutableGraph#remove_vertex.
    def remove_vertex(v)
      @vertices_dict.delete(v)

      # remove v from all adjacency lists
      @vertices_dict.each_value { |adjList| adjList.delete(v) }
    end

    # @see MutableGraph::remove_edge.
    def remove_edge(u, v)
      @vertices_dict[u].delete(v) unless @vertices_dict[u].nil?
    end

    # Converts the adjacency list of each vertex to be of type +klass+. The
    # class is expected to have a new constructor which accepts an enumerable as
    # parameter.
    # @param [Class] klass
    def edgelist_class=(klass)
      @vertices_dict.keys.each do |v|
        @vertices_dict[v] = klass.new @vertices_dict[v].to_a
      end
    end

    protected

    def basic_add_edge(u, v)
      @vertices_dict[u].add(v)
    end

  end # class DirectedAdjacencyGraph

  # AdjacencyGraph is an undirected Graph. The methods
  # {DirectedAdjacencyGraph#add_edge} and {DirectedAdjacencyGraph#remove_edge}
  # are reimplemented: if an edge (u,v) is added or removed, then the reverse
  # edge (v,u) is also added or removed.
  #
  class AdjacencyGraph < DirectedAdjacencyGraph

    # @return false.
    #
    def directed?
      false
    end

    # Also removes (v,u)
    # @see DirectedAdjacencyGraph#remove_edge
    def remove_edge(u, v)
      super
      @vertices_dict[v].delete(u) unless @vertices_dict[v].nil?
    end

    protected

    def basic_add_edge(u, v)
      super
      @vertices_dict[v].add(u) # Insert backwards edge
    end

  end # class AdjacencyGraph


  module Graph

    # Convert a general graph to an AdjacencyGraph. If the graph is directed,
    # returns a {DirectedAdjacencyGraph}; otherwise, returns an {AdjacencyGraph}.
    # @return {DirectedAdjacencyGraph} or {AdjacencyGraph}
    def to_adjacency
      result = (directed? ? DirectedAdjacencyGraph : AdjacencyGraph).new
      each_vertex { |v| result.add_vertex(v) }
      each_edge { |u, v| result.add_edge(u, v) }
      result
    end

    # Return a new DirectedAdjacencyGraph which has the same set of vertices.
    # If (u,v) is an edge of the graph, then (v,u) is an edge of the result.
    #
    # If the graph is undirected, the result is self.
    # @return [DirectedAdjacencyGraph]
    def reverse
      return self unless directed?
      result = DirectedAdjacencyGraph.new
      each_vertex { |v| result.add_vertex v }
      each_edge { |u, v| result.add_edge(v, u) }
      result
    end

    # Return a new {AdjacencyGraph} which has the same set of vertices. If (u,v)
    # is an edge of the graph, then (u,v) and (v,u) (which are the same edges)
    # are edges of the result.
    #
    # If the graph is undirected, the result is +self+.
    # @return [AdjacencyGraph]
    def to_undirected
      return self unless directed?
      AdjacencyGraph.new(Set, self)
    end

  end # module Graph

end # module RGL
