# frozen_string_literal: true

# rubocop:disable Metrics/ClassLength
module Jazzy
  module SymbolGraph
    # A Graph is the coordinator to import a symbolgraph json file.
    # Deserialize it to Symbols and Relationships, then rebuild
    # the AST shape using SymNodes and ExtNodes and extract SourceKit json.
    class Graph
      attr_accessor :module_name # Our module
      attr_accessor :ext_module_name # Module being extended
      attr_accessor :symbol_nodes # usr -> SymNode
      attr_accessor :relationships # [Relationship]
      attr_accessor :ext_nodes # (usr, constraints) -> ExtNode

      # Parse the JSON into flat tables of data
      def initialize(json, module_name, ext_module_name)
        self.module_name = module_name
        self.ext_module_name = ext_module_name
        graph = JSON.parse(json, symbolize_names: true)

        self.symbol_nodes = {}
        self.ext_nodes = {}

        graph[:symbols].each do |hash|
          symbol = Symbol.new(hash)
          if symbol.extension?
            node = ExtSymNode.new(symbol)
            ext_nodes[node.ext_key] = node
          else
            symbol_nodes[symbol.usr] = SymNode.new(symbol)
          end
        end

        self.relationships =
          graph[:relationships].map { |hash| Relationship.new(hash) }
      end

      # ExtNode index.  ExtKey (type USR, extension constraints) -> ExtNode.
      # This minimizes the number of extensions

      def add_ext_member(type_usr, member_node, constraints)
        key = ExtKey.new(type_usr, constraints.ext)
        if ext_node = ext_nodes[key]
          ext_node.add_child(member_node)
        else
          ext_nodes[key] =
            ExtNode.new_for_member(type_usr, member_node, constraints)
        end
      end

      def add_ext_conformance(type_usr,
                              type_name,
                              protocol,
                              constraints)
        key = ExtKey.new(type_usr, constraints.ext)
        if ext_node = ext_nodes[key]
          ext_node.add_conformance(protocol)
        else
          ext_nodes[key] =
            ExtNode.new_for_conformance(type_usr,
                                        type_name,
                                        protocol,
                                        constraints)
        end
      end

      # Increasingly desparate ways to find the name of the symbol
      # at the target end of a relationship
      def rel_target_name(rel, target_node)
        target_node&.symbol&.name ||
          rel.target_fallback ||
          Jazzy::SymbolGraph.demangle(rel.target_usr)
      end

      # Same for the source end.  Less help from the tool here
      def rel_source_name(rel, source_node)
        source_node&.qualified_name ||
          Jazzy::SymbolGraph.demangle(rel.source_usr)
      end

      # Protocol conformance is redundant if it's unconditional
      # and already expressed in the type's declaration.
      #
      # Skip implementation-detail conformances.
      def redundant_conformance?(rel, type, protocol)
        return false unless type

        (rel.constraints.empty? && type.conformance?(protocol)) ||
          (type.actor? && rel.actor_protocol?)
      end

      # source is a member/protocol requirement of target
      def rebuild_member(rel, source, target)
        return unless source

        source.protocol_requirement = rel.protocol_requirement?
        constraints =
          ExtConstraints.new(target&.constraints,
                             source.unique_context_constraints(target))

        # Add to its parent or invent an extension
        unless target&.try_add_child(source, constraints.ext)
          add_ext_member(rel.target_usr, source, constraints)
        end
      end

      # "source : target" either from type decl or ext decl
      def rebuild_conformance(rel, source, target)
        protocol_name = rel_target_name(rel, target)

        return if redundant_conformance?(rel, source, protocol_name)

        type_constraints = source&.constraints || []
        constraints =
          ExtConstraints.new(type_constraints,
                             rel.constraints - type_constraints)

        # Create an extension or enhance an existing one
        add_ext_conformance(rel.source_usr,
                            rel_source_name(rel, source),
                            protocol_name,
                            constraints)
      end

      # "source is a default implementation of protocol requirement target"
      def rebuild_default_implementation(_rel, source, target)
        return unless source

        unless target &&
               (target_parent = target.parent) &&
               target_parent.is_a?(SymNode)
          # Could probably figure this out with demangle, but...
          warn "Can't resolve membership of default implementation " \
            "#{source.symbol.usr}."
          source.unlisted = true
          return
        end
        constraints =
          ExtConstraints.new(target_parent.constraints,
                             source.unique_context_constraints(target_parent))

        add_ext_member(target_parent.symbol.usr,
                       source,
                       constraints)
      end

      # "source is a class that inherits from target"
      def rebuild_inherits(_rel, source, target)
        if source && target
          source.superclass_name = target.symbol.name
        end
      end

      # "References to fake_usr should be real_usr"
      def unalias_extensions(fake_usr, real_usr)
        ext_nodes.each_pair do |key, ext|
          if key.usr == fake_usr
            ext.real_usr = real_usr
          end
        end
      end

      # Process a structural relationship to link nodes
      def rebuild_rel(rel)
        source = symbol_nodes[rel.source_usr]
        target = symbol_nodes[rel.target_usr]

        case rel.kind
        when :memberOf, :optionalRequirementOf, :requirementOf
          rebuild_member(rel, source, target)

        when :conformsTo
          rebuild_conformance(rel, source, target)

        when :defaultImplementationOf
          rebuild_default_implementation(rel, source, target)

        when :inheritsFrom
          rebuild_inherits(rel, source, target)

        when :extensionTo
          unalias_extensions(rel.source_usr, rel.target_usr)
        end

        # don't seem to care about:
        # - overrides: not bothered, also unimplemented for protocols
      end

      # Rebuild the AST structure  and convert to SourceKit
      def to_sourcekit
        relationships.sort.each { |r| rebuild_rel(r) }

        root_symbol_nodes =
          symbol_nodes.values
            .select(&:top_level_decl?)
            .sort
            .map { |n| n.to_sourcekit(module_name) }

        root_ext_nodes =
          ext_nodes.values
            .sort
            .map { |n| n.to_sourcekit(module_name, ext_module_name) }
        {
          'key.diagnostic_stage' => 'parse',
          'key.substructure' => root_symbol_nodes + root_ext_nodes,
        }
      end
    end
  end
end
# rubocop:enable Metrics/ClassLength