# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require "rgl/adjacency"
require "rgl/traversal"
require "rgl/dijkstra"
require "rgl/topsort"

module VagrantPlugins
  module CommandServe
    class Mappers
      module Internal
        class Graph < RGL::DirectedAdjacencyGraph

          # This iterator is used for detecting and breaking cycles
          # discovered within the graph using the DFS graph visitor
          # concept
          class CycleDeletorIterator < RGL::DFSVisitor
            # Create a new iterator instance. Store the graph
            # for later inspection use
            def initialize(graph, *_)
              @graph = graph
              super
            end

            # Examines the vertex to detect any cycles. If an
            # adjacent edge has already been visited then we
            # remove the edge to break the cycle.
            def handle_examine_vertex(u)
              graph.each_adjacent(u) do |v|
                if v.is_a?(Vertex::Output) && color_map[v] != :WHITE
                  graph.remove_edge(u, v)
                end
              end
            end
          end

          autoload :Mappers, Vagrant.source_root.join("plugins/commands/serve/mappers/internal/graph/mappers").to_s
          autoload :Search, Vagrant.source_root.join("plugins/commands/serve/mappers/internal/graph/search").to_s
          autoload :Vertex, Vagrant.source_root.join("plugins/commands/serve/mappers/internal/graph/vertex").to_s
          autoload :WeightedVertex, Vagrant.source_root.join("plugins/commands/serve/mappers/internal/graph/weighted_vertex").to_s

          include Util::HasLogger

          # Default weight used for weighted vetices
          # when no weight is provided
          DEFAULT_WEIGHT = 1000

          def initialize(*_)
            @vertex_map = {}
            super
          end

          def initialize_copy(orig)
            super
            @vertex_map = orig.instance_eval { @vertex_map }.dup
            @vertices_dict = @vertices_dict.dup
            @vertices_dict.keys.each do |v|
              edges = @vertices_dict[v]
              @vertices_dict[v] = {in: edges[:in].dup, out: edges[:out].dup}
            end
          end

          def to_a
            @vertices_dict.keys
          end

          def each_out_vertex(v, &block)
            v = vertex_for(v)
            if !has_vertex?(v)
              raise "No vertex `#{v}' found in graph"
            end
            @vertices_dict[v][:out].each(&block)
          end
          alias :each_adjacent :each_out_vertex

          def out_vertices(v)
            v = vertex_for(v)
            if !has_vertex?(v)
              raise "No vertex `#{v}' found in graph"
            end
            @vertices_dict[v][:out].to_a
          end
          alias :adjacent_vertices :out_vertices

          def out_degree(v)
            v = vertex_for(v)
            if !has_vertex?(v)
              raise "No vertex `#{v}' found in graph"
            end
            @vertices_dict[v][:out].size
          end

          def each_in_vertex(v, &block)
            v = vertex_for(v)
            if !has_vertex?(v)
              raise "No vertex `#{v}' found in graph"
            end
            @vertices_dict[v][:in].each(&block)
          end

          def in_vertices(v)
            v = vertex_for(v)
            if !has_vertex?(v)
              raise "No vertex `#{v}' found in graph"
            end
            @vertices_dict[v][:in].to_a
          end

          def in_degree(v)
            v = vertex_for(v)
            if !has_vertex?(v)
              raise "No vertex `#{v}' found in graph"
            end
            @vertices[v][:in].size
          end

          def vertices
            @vertices_dict.keys
          end

          def each_vertex(&block)
            vertices.each(&block)
          end

          def num_vertices
            @vertices_dict.size
          end

          def num_edges
            @vertices_dict.each_value.inject(0) do |count, edges|
              count + edges[:out].size
            end
          end

          def has_edge?(u, v)
            u = vertex_for(u)
            v = vertex_for(v)
            has_vertex?(u) && @vertices_dict[u][0].include?(v)
          end

          def add_vertex(v)
            if !v.is_a?(Vertex)
              raise TypeError,
                "Expected type `Vertex', got `#{v.class}'"
            end
            if @vertex_map.key?(v.hash_code)
              v = @vertex_map[v.hash_code]
            else
              if !v.is_a?(WeightedVertex)
                v = WeightedVertex.new(v, weight: DEFAULT_WEIGHT)
              end
              @vertex_map[v.hash_code] = v
            end

            @vertices_dict[v] ||= {
              in: @edgelist_class.new,
              out: @edgelist_class.new,
            }
            v
          end

          def add_edge(u, v)
            u = add_vertex(u) # ensure key
            v = add_vertex(v) # ensure key
            basic_add_edge(u, v)
          end

          def remove_vertex(v)
            v = vertex_for(v)
            if has_vertex?(v)
              edges = @vertices_dict[v]
              @vertices_dict.delete(v)
              edges[:in].each do |parent|
                @vertices_dict[parent][:out].delete(v)
              end
              edges[:out].each do |child|
                @vertices_dict[child][:in].delete(v)
              end
              @vertex_map.delete(v.hash_code)
            end
            v
          end

          def remove_edge(u, v)
            u = vertex_for(u)
            v = vertex_for(v)
            if has_vertex?(u) && has_vertex?(v)
              if @vertices_dict[u][:out].delete?(v)
                @vertices_dict[v][:in].delete(u)
              end
            end
            self
          end

          def edgelist_class=(klass)
            @vertices_dict.keys.each do |v|
              edges = @vertices_dict[v]
              @vertices_dict[v] = {
                in: klass.new(edges[:in].to_a),
                out: klass.new(edges[:out].to_a),
              }
            end
            self
          end

          def reverse
            result = clone
            result.reverse!
          end

          def reverse!
            @vertices_dict.keys.each do |v|
              edges = @vertices_dict[v]
              @vertices_dict[v] = {
                in: edges[:out],
                out: edges[:in],
              }
            end
            self
          end

          def break_cycles!(src)
            depth_first_visit(src, CycleDeletorIterator.new(self)) { true }
          end

          def shortest_path(source, target)
            dijkstra_shortest_path(edge_weights_map, source, target)
          end

          def shortest_paths(source, target)
            dijkstra_shortest_paths(edge_weights_map, source, target)
          end

          def edge_weights_map
            Hash.new.tap do |edge_map|
              each_edge do |*edges|
                w = edges.map(&:weight).inject(&:+)
                if edges.first.respond_to?(:type) && edges.last.respond_to?(:type)
                  if edges.first.type != edges.last.type
                    w += 200
                    extra = edges.first.type.ancestors.index(edges.last.type)
                    if extra.nil?
                      extra = edges.last.type.ancestors.index(edges.first.type)
                    end
                    w += extra.to_i
                  end
                end
                edge_map[edges] = w
              end
            end
          end

          def vertex_for(v)
            @vertex_map[v.hash_code]
          end

          def to_s
            "<#{self.class.name}:#{object_id} num_vertices=#{vertices.size}>"
          end

          def inspect
            vinfo = vertices.map do |v|
              ins = in_vertices(v).map do |iv|
                "    -> #{iv}"
              end.join("\n")
              outs = out_vertices(v).map do |ov|
                "    <- #{ov}"
              end.join("\n")
              "  " + [v].tap { |content|
                content << ins if !ins.empty?
                content << outs if !outs.empty?
              }.join("\n")
            end.join("\n")
            "<#{self.class.name}:#{object_id} num_vertices=#{vertices.size} vertices=\n#{vinfo}\n>"
          end

          protected

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