# frozen_string_literal: true

require 'json'
require 'pathname'
require 'shellwords'
require 'xcinvoke'
require 'cgi'
require 'rexml/document'

require 'jazzy/config'
require 'jazzy/executable'
require 'jazzy/highlighter'
require 'jazzy/source_declaration'
require 'jazzy/source_mark'
require 'jazzy/stats'
require 'jazzy/grouper'
require 'jazzy/doc_index'

ELIDED_AUTOLINK_TOKEN = '36f8f5912051ae747ef441d6511ca4cb'

def autolink_regex(middle_regex, after_highlight)
  start_tag_re, end_tag_re =
    if after_highlight
      [/<span class="(?:n|kt|kd|nc)">/, '</span>']
    else
      ['<code>', '</code>']
    end
  /(#{start_tag_re})[ \t]*(#{middle_regex})[ \t]*(#{end_tag_re})/
end

class String
  def autolink_block(doc_url, middle_regex, after_highlight)
    gsub(autolink_regex(middle_regex, after_highlight)) do
      original = Regexp.last_match(0)
      start_tag, raw_name, end_tag = Regexp.last_match.captures
      link_target, display_name = yield(CGI.unescape_html(raw_name))

      if link_target &&
         !link_target.type.extension? &&
         link_target.url &&
         link_target.url != doc_url.split('#').first && # Don't link to parent
         link_target.url != doc_url # Don't link to self
        "#{start_tag}<a href=\"#{ELIDED_AUTOLINK_TOKEN}#{link_target.url}\">" \
          "#{CGI.escape_html(display_name)}</a>#{end_tag}"
      else
        original
      end
    end
  end

  def unindent(count)
    gsub(/^#{' ' * count}/, '')
  end
end

module Jazzy
  # This module interacts with the sourcekitten command-line executable
  module SourceKitten
    def self.undocumented_abstract
      @undocumented_abstract ||= Markdown.render(
        Config.instance.undocumented_text,
      ).freeze
    end

    #
    # URL assignment
    #

    def self.sanitize_filename(doc)
      unsafe_filename = doc.docs_filename
      sanitzation_enabled = Config.instance.use_safe_filenames
      if sanitzation_enabled && !doc.type.name_controlled_manually?
        CGI.escape(unsafe_filename).gsub('_', '%5F').tr('%', '_')
      else
        unsafe_filename
      end
    end

    # rubocop:disable Metrics/MethodLength
    # Generate doc URL by prepending its parents' URLs
    # @return [Hash] input docs with URLs
    def self.make_doc_urls(docs)
      docs.each do |doc|
        if doc.render_as_page?
          doc.url = (
            subdir_for_doc(doc) +
            [sanitize_filename(doc) + '.html']
          ).map { |path| ERB::Util.url_encode(path) }.join('/')
          doc.children = make_doc_urls(doc.children)
        else
          # Don't create HTML page for this doc if it doesn't have children
          # Instead, make its link a hash-link on its parent's page
          if doc.typename == '<<error type>>'
            warn "A compile error prevented #{doc.fully_qualified_name} " \
              'from receiving a unique USR. Documentation may be ' \
              'incomplete. Please check for compile errors by running ' \
              '`xcodebuild` or `swift build` with arguments ' \
              "`#{Config.instance.build_tool_arguments.shelljoin}`."
          end
          id = doc.usr
          unless id
            id = doc.name || 'unknown'
            warn "`#{id}` has no USR. First make sure all modules used in " \
              'your project have been imported. If all used modules are ' \
              'imported, please report this problem by filing an issue at ' \
              'https://github.com/realm/jazzy/issues along with your ' \
              'Xcode project. If this token is declared in an `#if` block, ' \
              'please ignore this message.'
          end
          doc.url = "#{doc.parent_in_docs.url}#/#{id}"
        end
      end
    end
    # rubocop:enable Metrics/MethodLength

    # Determine the subdirectory in which a doc should be placed.
    # Guides in the root for back-compatibility.
    # Declarations under outer namespace type (Structures, Classes, etc.)
    def self.subdir_for_doc(doc)
      if Config.instance.multiple_modules?
        subdir_for_doc_multi_module(doc)
      else
        # Back-compatibility layout version
        subdir_for_doc_single_module(doc)
      end
    end

    # Pre-multi-module site layout, does not allow for
    # types with the same name.
    def self.subdir_for_doc_single_module(doc)
      # Guides + Groups in the root
      return [] if doc.type.markdown? || doc.type.overview?

      [doc.namespace_path.first.type.plural_url_name] +
        doc.namespace_ancestors.map(&:name)
    end

    # Multi-module site layout, separate each module that
    # is being documented.
    def self.subdir_for_doc_multi_module(doc)
      # Guides + Groups in the root
      return [] if doc.type.markdown? || doc.type.overview?

      root_decl = doc.namespace_path.first

      # Extensions need an extra dir to allow for extending
      # ExternalModule1.TypeName and ExternalModule2.TypeName
      namespace_subdir =
        if root_decl.type.swift_extension?
          ['Extensions', root_decl.module_name]
        else
          [doc.namespace_path.first.type.plural_url_name]
        end

      [root_decl.doc_module_name] +
        namespace_subdir +
        doc.namespace_ancestors.map(&:name)
    end

    #
    # CLI argument calculation
    #

    # returns all subdirectories of specified path
    def self.rec_path(path)
      path.children.collect do |child|
        if child.directory?
          rec_path(child) + [child]
        end
      end.select { |x| x }.flatten(1)
    end

    def self.use_spm?(module_config)
      module_config.swift_build_tool == :spm ||
        (!module_config.swift_build_tool_configured &&
         Dir['*.xcodeproj', '*.xcworkspace'].empty? &&
         !module_config.build_tool_arguments.include?('-project') &&
         !module_config.build_tool_arguments.include?('-workspace'))
    end

    # Builds SourceKitten arguments based on Jazzy options
    def self.arguments_from_options(module_config)
      arguments = ['doc']
      if module_config.objc_mode
        arguments += objc_arguments_from_options(module_config)
      else
        arguments += ['--spm'] if use_spm?(module_config)
        unless module_config.module_name.empty?
          arguments += ['--module-name', module_config.module_name]
        end
        arguments += ['--']
      end

      arguments + module_config.build_tool_arguments
    end

    def self.objc_arguments_from_options(module_config)
      arguments = []
      if module_config.build_tool_arguments.empty?
        arguments += ['--objc', module_config.umbrella_header.to_s, '--', '-x',
                      'objective-c', '-isysroot',
                      `xcrun --show-sdk-path --sdk #{module_config.sdk}`.chomp,
                      '-I', module_config.framework_root.to_s,
                      '-fmodules']
      end
      # add additional -I arguments for each subdirectory of framework_root
      unless module_config.framework_root.nil?
        rec_path(Pathname.new(module_config.framework_root.to_s)).collect do |child|
          if child.directory?
            arguments += ['-I', child.to_s]
          end
        end
      end
      arguments
    end

    # Run sourcekitten with given arguments and return STDOUT
    def self.run_sourcekitten(arguments)
      if swift_version = Config.instance.swift_version
        unless xcode = XCInvoke::Xcode.find_swift_version(swift_version)
          raise "Unable to find an Xcode with swift version #{swift_version}."
        end

        env = xcode.as_env
      else
        env = ENV
      end
      bin_path = Pathname(__FILE__) + '../../../bin/sourcekitten'
      output, = Executable.execute_command(bin_path, arguments, true, env: env)
      output
    end

    #
    # SourceDeclaration generation
    #

    def self.make_default_doc_info(declaration)
      # @todo: Fix these
      declaration.abstract = ''
      declaration.parameters = []
      declaration.children = []
    end

    def self.attribute?(doc, attr_name)
      doc['key.attributes']&.find do |attribute|
        attribute['key.attribute'] == "source.decl.attribute.#{attr_name}"
      end
    end

    def self.availability_attribute?(doc)
      attribute?(doc, 'available')
    end

    def self.spi_attribute?(doc)
      attribute?(doc, '_spi')
    end

    def self.should_document?(doc)
      return false if doc['key.doc.comment'].to_s.include?(':nodoc:')

      type = SourceDeclaration::Type.new(doc['key.kind'])

      # Always document Objective-C declarations.
      return true unless type.swift_type?

      # Don't document Swift types if we are hiding Swift
      return false if Config.instance.hide_swift?

      # Don't document @available declarations with no USR, since it means
      # they're unavailable.
      if availability_attribute?(doc) && !doc['key.usr']
        return false
      end

      # Only document @_spi declarations in some scenarios
      return false unless should_document_spi?(doc)

      # Don't document declarations excluded by the min_acl setting
      if type.swift_extension?
        should_document_swift_extension?(doc)
      else
        should_document_acl?(type, doc)
      end
    end

    # Check visibility: SPI
    def self.should_document_spi?(doc)
      spi_ok = @min_acl < SourceDeclaration::AccessControlLevel.public ||
               Config.instance.include_spi_declarations ||
               (!spi_attribute?(doc) && !doc['key.symgraph_spi'])

      @stats.add_spi_skipped unless spi_ok
      spi_ok
    end

    # Check visibility: access control
    def self.should_document_acl?(type, doc)
      # Include all enum elements for now, can't tell their ACL.
      return true if type.swift_enum_element?

      acl_ok = SourceDeclaration::AccessControlLevel.from_doc(doc) >= @min_acl
      unless acl_ok
        @stats.add_acl_skipped
        @inaccessible_protocols.append(doc['key.name']) if type.swift_protocol?
      end
      acl_ok
    end

    # Document extensions if they add protocol conformances, or have any
    # member that needs to be documented.
    def self.should_document_swift_extension?(doc)
      doc['key.inheritedtypes'] ||
        Array(doc['key.substructure']).any? do |subdoc|
          subtype = SourceDeclaration::Type.new(subdoc['key.kind'])
          !subtype.mark? && should_document?(subdoc)
        end
    end

    def self.process_undocumented_token(doc, declaration)
      make_default_doc_info(declaration)

      if declaration.mark_undocumented?
        @stats.add_undocumented(declaration)
        return nil if @skip_undocumented

        declaration.abstract = undocumented_abstract
      else
        declaration.abstract = Markdown.render(doc['key.doc.comment'] || '',
                                               declaration.highlight_language)
      end

      declaration
    end

    def self.parameters(doc, discovered)
      (doc['key.doc.parameters'] || []).map do |p|
        name = p['name']
        {
          name: name,
          discussion: discovered[name],
        }
      end.reject { |param| param[:discussion].nil? }
    end

    def self.make_doc_info(doc, declaration)
      return unless should_document?(doc)

      highlight_declaration(doc, declaration)
      make_deprecation_info(doc, declaration)

      unless doc['key.doc.full_as_xml']
        return process_undocumented_token(doc, declaration)
      end

      declaration.abstract = Markdown.render(doc['key.doc.comment'] || '',
                                             declaration.highlight_language)
      declaration.discussion = ''
      declaration.return = Markdown.rendered_returns
      declaration.parameters = parameters(doc, Markdown.rendered_parameters)

      @stats.add_documented
    end

    def self.highlight_declaration(doc, declaration)
      if declaration.swift?
        declaration.declaration =
          Highlighter.highlight_swift(make_swift_declaration(doc, declaration))
      else
        declaration.declaration =
          Highlighter.highlight_objc(
            make_objc_declaration(doc['key.parsed_declaration']),
          )
        declaration.other_language_declaration =
          Highlighter.highlight_swift(doc['key.swift_declaration'])
      end
    end

    def self.make_deprecation_info(doc, declaration)
      if declaration.deprecated
        declaration.deprecation_message =
          Markdown.render(doc['key.deprecation_message'] || '')
      end
      if declaration.unavailable
        declaration.unavailable_message =
          Markdown.render(doc['key.unavailable_message'] || '')
      end
    end

    # Strip tags and convert entities
    def self.xml_to_text(xml)
      document = REXML::Document.new(xml)
      REXML::XPath.match(document.root, '//text()').map(&:value).join
    rescue StandardError
      ''
    end

    # Regexp to match an @attribute.  Complex to handle @available().
    def self.attribute_regexp(name)
      qstring = /"(?:[^"\\]*|\\.)*"/
      %r{@#{name}      # @attr name
        (?:\s*\(       # optionally followed by spaces + parens,
          (?:          # containing any number of either..
            [^")]*|    # normal characters or...
            #{qstring} # quoted strings.
          )*           # (end parens content)
        \))?           # (end optional parens)
      }x
    end

    # Get all attributes of some name
    def self.extract_attributes(declaration, name = '\w+')
      attrs = declaration.scan(attribute_regexp(name))
      # Rouge #806 workaround, use unicode lookalike for ')' inside attributes.
      attrs.map { |str| str.gsub(/\)(?!\s*$)/, "\ufe5a") }
    end

    # Keep everything except instructions to us
    def self.extract_documented_attributes(declaration)
      extract_attributes(declaration).reject do |attr|
        attr.start_with?('@_documentation')
      end
    end

    def self.extract_availability(declaration)
      extract_attributes(declaration, 'available')
    end

    # Split leading attributes from a decl, returning both parts.
    def self.split_decl_attributes(declaration)
      declaration =~ /^((?:#{attribute_regexp('\w+')}\s*)*)(.*)$/m
      Regexp.last_match.captures
    end

    def self.prefer_parsed_decl?(parsed, annotated, type)
      return true if annotated.empty?
      return false unless parsed
      return false if type.swift_variable? # prefer { get }-style

      annotated.include?(' = default') || # SR-2608
        (parsed.scan(/@autoclosure|@escaping/).count >
         annotated.scan(/@autoclosure|@escaping/).count) || # SR-6321
        parsed.include?("\n") # user formatting
    end

    # Apply fixes to improve the compiler's declaration
    def self.fix_up_compiler_decl(annotated_decl, declaration)
      annotated_decl.
        # Replace the fully qualified name of a type with its base name
        gsub(declaration.fully_qualified_name_regexp,
             declaration.name).
        # Workaround for SR-9816
        gsub(" {\n  get\n  }", '').
        # Workaround for SR-12139
        gsub(/mutating\s+mutating/, 'mutating')
    end

    # Find the best Swift declaration
    def self.make_swift_declaration(doc, declaration)
      # From compiler 'quick help' style
      annotated_decl_xml = doc['key.annotated_decl']

      return nil unless annotated_decl_xml

      annotated_decl_attrs, annotated_decl_body =
        split_decl_attributes(xml_to_text(annotated_decl_xml))

      # From source code
      parsed_decl = doc['key.parsed_declaration']

      # Don't present type attributes on extensions
      return parsed_decl if declaration.type.extension?

      decl =
        if prefer_parsed_decl?(parsed_decl,
                               annotated_decl_body,
                               declaration.type)
          # Strip any attrs captured by parsed version
          inline_attrs, parsed_decl_body = split_decl_attributes(parsed_decl)
          parsed_decl_body.unindent(inline_attrs.length)
        else
          # Improve the compiler declaration
          fix_up_compiler_decl(annotated_decl_body, declaration)
        end

      # @available attrs only in compiler 'interface' style
      extract_availability(doc['key.doc.declaration'] || '')
        .concat(extract_documented_attributes(annotated_decl_attrs))
        .push(decl)
        .join("\n")
    end

    # Exclude non-async routines that accept async closures
    def self.swift_async?(fully_annotated_decl)
      document = REXML::Document.new(fully_annotated_decl)
      !document.elements['/*/syntaxtype.keyword[text()="async"]'].nil?
    rescue StandardError
      nil
    end

    # Strip default property attributes because libclang
    # adds them all, even if absent in the original source code.
    DEFAULT_ATTRIBUTES = %w[atomic readwrite assign unsafe_unretained].freeze

    def self.make_objc_declaration(declaration)
      return declaration if Config.instance.keep_property_attributes

      declaration =~ /\A@property\s+\((.*?)\)/
      return declaration unless Regexp.last_match

      attrs = Regexp.last_match[1].split(',').map(&:strip) - DEFAULT_ATTRIBUTES
      attrs_text = attrs.empty? ? '' : " (#{attrs.join(', ')})"

      declaration
        .sub(/(?<=@property)\s+\(.*?\)/, attrs_text)
        .gsub(/\s+/, ' ')
    end

    def self.make_substructure(doc, declaration)
      return [] unless subdocs = doc['key.substructure']

      make_source_declarations(subdocs,
                               declaration,
                               declaration.mark_for_children)
    end

    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/PerceivedComplexity
    def self.make_source_declarations(docs, parent = nil, mark = SourceMark.new)
      declarations = []
      current_mark = mark
      Array(docs).each do |doc|
        if doc.key?('key.diagnostic_stage')
          declarations += make_source_declarations(
            doc['key.substructure'], parent
          )
          next
        end
        declaration = SourceDeclaration.new
        declaration.parent_in_code = parent
        declaration.type =
          SourceDeclaration::Type.new(doc['key.kind'],
                                      doc['key.fully_annotated_decl'])
        declaration.typename = doc['key.typename']
        declaration.objc_name = doc['key.name']
        documented_name = if Config.instance.hide_objc? && doc['key.swift_name']
                            doc['key.swift_name']
                          else
                            declaration.objc_name
                          end
        if declaration.type.task_mark?(documented_name)
          current_mark = SourceMark.new(documented_name)
        end
        if declaration.type.swift_enum_case?
          # Enum "cases" are thin wrappers around enum "elements".
          declarations += make_source_declarations(
            doc['key.substructure'], parent, current_mark
          )
          next
        end
        next unless declaration.type.should_document?

        unless declaration.type.name
          raise 'Please file an issue at ' \
            'https://github.com/realm/jazzy/issues about adding support ' \
            "for `#{declaration.type.kind}`."
        end

        unless documented_name
          warn 'Found a declaration without `key.name` that will be ' \
            'ignored.  Documentation may be incomplete.  This is probably ' \
            'caused by unresolved compiler errors: check the sourcekitten ' \
            'output for error messages.'
          next
        end

        declaration.file = Pathname(doc['key.filepath']) if doc['key.filepath']
        declaration.usr = doc['key.usr']
        declaration.type_usr = doc['key.typeusr']
        declaration.module_name =
          if declaration.swift?
            # Filter out Apple sub-framework implementation names
            doc['key.modulename']&.sub(/\..*$/, '')
          else
            # ObjC best effort, category original module is unavailable
            @current_module_name
          end
        declaration.doc_module_name = @current_module_name
        declaration.name = documented_name
        declaration.mark = current_mark
        declaration.access_control_level =
          SourceDeclaration::AccessControlLevel.from_doc(doc)
        declaration.line = doc['key.doc.line'] || doc['key.line']
        declaration.column = doc['key.doc.column'] || doc['key.column']
        declaration.start_line = doc['key.parsed_scope.start']
        declaration.end_line = doc['key.parsed_scope.end']
        declaration.deprecated = doc['key.always_deprecated']
        declaration.unavailable = doc['key.always_unavailable']
        declaration.generic_requirements =
          find_generic_requirements(doc['key.parsed_declaration'])
        inherited_types = doc['key.inheritedtypes'] || []
        declaration.inherited_types =
          inherited_types.map { |type| type['key.name'] }.compact
        declaration.async =
          doc['key.symgraph_async'] ||
          if xml_declaration = doc['key.fully_annotated_decl']
            swift_async?(xml_declaration)
          end

        next unless make_doc_info(doc, declaration)

        declaration.children = make_substructure(doc, declaration)
        next if declaration.type.extension? &&
                declaration.children.empty? &&
                !declaration.inherited_types?

        declarations << declaration
      end
      declarations
    end
    # rubocop:enable Metrics/PerceivedComplexity
    # rubocop:enable Metrics/CyclomaticComplexity
    # rubocop:enable Metrics/MethodLength

    def self.find_generic_requirements(parsed_declaration)
      parsed_declaration =~ /\bwhere\s+(.*)$/m
      return nil unless Regexp.last_match

      Regexp.last_match[1].gsub(/\s+/, ' ')
    end

    #
    # SourceDeclaration generation - extension management
    #

    # Expands extensions of nested types declared at the top level into
    # a tree so they can be deduplicated properly
    def self.expand_extensions(decls)
      decls.map do |decl|
        next decl unless decl.type.extension? && decl.name.include?('.')

        # Don't expand the Swift namespace if we're in ObjC mode.
        # ex: NS_SWIFT_NAME(Foo.Bar) should not create top-level Foo
        next decl if decl.swift_objc_extension? && !Config.instance.hide_objc?

        name_parts = decl.name.split('.')
        decl.name = name_parts.pop
        expand_extension(decl, name_parts, decls)
      end
    end

    def self.expand_extension(extension, name_parts, decls)
      return extension if name_parts.empty?

      name = name_parts.shift
      candidates = decls.select { |decl| decl.name == name }
      SourceDeclaration.new.tap do |decl|
        make_default_doc_info(decl)
        decl.name = name
        decl.module_name = extension.module_name
        decl.doc_module_name = extension.doc_module_name
        decl.type = extension.type
        decl.mark = extension.mark
        decl.usr = candidates.first.usr unless candidates.empty?
        child = expand_extension(extension,
                                 name_parts,
                                 candidates.flat_map(&:children).uniq)
        child.parent_in_code = decl
        decl.children = [child]
      end
    end

    # Merges multiple extensions of the same entity into a single document.
    #
    # Merges extensions into the protocol/class/struct/enum they extend, if it
    # occurs in the same project.
    #
    # Merges redundant declarations when documenting podspecs.
    def self.deduplicate_declarations(declarations)
      duplicate_groups = declarations
        .group_by { |d| deduplication_key(d, declarations) }
        .values

      duplicate_groups.flat_map do |group|
        # Put extended type (if present) before extensions
        merge_declarations(group)
      end.compact
    end

    # Returns true if an Objective-C declaration is mergeable.
    def self.mergeable_objc?(decl, root_decls)
      decl.type.objc_class? ||
        (decl.type.objc_category? &&
          (category_classname = decl.objc_category_name[0]) &&
          root_decls.any? { _1.name == category_classname })
    end

    # Returns if a Swift declaration is mergeable.
    # Start off merging in typealiases to help understand extensions.
    def self.mergeable_swift?(decl)
      decl.type.swift_extensible? ||
        decl.type.swift_extension? ||
        decl.type.swift_typealias?
    end

    # Normally merge all extensions into their types and each other.
    #
    # :none means only merge within a module -- so two extensions to
    #     some type get merged, but an extension to a type from
    #     another documented module does not get merged into that type
    # :extensions means extensions of documented modules get merged,
    #     but if we're documenting ModA and ModB, and they both provide
    #     extensions to Swift.String, then those two extensions still
    #     appear separately.
    #
    # (The USR part of the dedup key means ModA.Foo and ModB.Foo do not
    # get merged.)
    def self.module_deduplication_key(decl)
      if (Config.instance.merge_modules == :none) ||
         (Config.instance.merge_modules == :extensions &&
          decl.extension_of_external_type?)
        decl.doc_module_name
      else
        ''
      end
    end

    # Two declarations get merged if they have the same deduplication key.
    def self.deduplication_key(decl, root_decls)
      mod_key = module_deduplication_key(decl)
      # Swift extension of objc class
      if decl.swift_objc_extension?
        [decl.swift_extension_objc_name, :objc_class_and_categories, mod_key]
      # Swift type or Swift extension of Swift type
      elsif mergeable_swift?(decl)
        [decl.usr, decl.name, mod_key]
      # Objc categories and classes
      elsif mergeable_objc?(decl, root_decls)
        # Using the ObjC name to match swift_objc_extension.
        name, _ = decl.objc_category_name || decl.objc_name
        [name, :objc_class_and_categories, mod_key]
      # Non-mergable declarations (funcs, typedefs etc...)
      else
        [decl.usr, decl.name, decl.type.kind, '']
      end
    end

    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/PerceivedComplexity
    # Merges all of the given types and extensions into a single document.
    def self.merge_declarations(decls)
      extensions, typedecls = decls.partition { |d| d.type.extension? }

      if typedecls.size > 1
        info = typedecls
          .map { |t| "#{t.type.name.downcase} #{t.name}" }
          .join(', ')
        warn 'Found conflicting type declarations with the same name, which ' \
          "may indicate a build issue or a bug in Jazzy: #{info}"
      end
      typedecl = typedecls.first

      extensions = reject_inaccessible_extensions(typedecl, extensions)

      if typedecl
        if typedecl.type.swift_protocol?
          mark_and_merge_protocol_extensions(typedecl, extensions)
          extensions.reject! { |ext| ext.children.empty? }
        end

        merge_objc_declaration_marks(typedecl, extensions)
      end

      # Keep type-aliases separate from any extensions
      if typedecl&.type&.swift_typealias?
        [merge_type_and_extensions(typedecls, []),
         merge_type_and_extensions([], extensions)]
      else
        merge_type_and_extensions(typedecls, extensions)
      end
    end
    # rubocop:enable Metrics/PerceivedComplexity
    # rubocop:enable Metrics/MethodLength

    def self.merge_type_and_extensions(typedecls, extensions)
      # Constrained extensions at the end
      constrained, regular_exts = extensions.partition(&:constrained_extension?)
      decls = typedecls + regular_exts + constrained
      return nil if decls.empty?

      move_merged_extension_marks(decls)
      merge_code_declaration(decls)

      decls.first.tap do |merged|
        merged.children = deduplicate_declarations(
          decls.flat_map(&:children).uniq,
        )
        merged.children.each do |child|
          child.parent_in_code = merged
        end
      end
    end

    # Now we know all the public types and all the private protocols,
    # reject extensions that add public protocols to private types
    # or add private protocols to public types.
    def self.reject_inaccessible_extensions(typedecl, extensions)
      swift_exts, objc_exts = extensions.partition(&:swift?)

      # Reject extensions that are just conformances to private protocols
      unwanted_exts, wanted_exts = swift_exts.partition do |ext|
        ext.children.empty? &&
          !ext.other_inherited_types?(@inaccessible_protocols)
      end

      # Given extensions of a type from this module, without the
      # type itself, the type must be private and the extensions
      # should be rejected.
      if !typedecl &&
         wanted_exts.first &&
         wanted_exts.first.type_from_doc_module?
        unwanted_exts += wanted_exts
        wanted_exts = []
      end

      # Don't tell the user to document them
      unwanted_exts.each { |e| @stats.remove_undocumented(e) }

      objc_exts + wanted_exts
    end

    # Protocol extensions.
    #
    # If any of the extensions provide default implementations for methods in
    # the given protocol, merge those members into the protocol doc instead of
    # keeping them on the extension. These get a “Default implementation”
    # annotation in the generated docs.  Default implementations added by
    # conditional extensions are annotated but listed separately.
    #
    # Protocol methods provided only in an extension and not in the protocol
    # itself are a special beast: they do not use dynamic dispatch. These get an
    # “Extension method” annotation in the generated docs.
    def self.mark_and_merge_protocol_extensions(protocol, extensions)
      extensions.each do |ext|
        ext.children = ext.children.select do |ext_member|
          proto_member = protocol.children.find do |p|
            p.name == ext_member.name &&
              p.type == ext_member.type &&
              p.async == ext_member.async
          end

          # Extension-only method, keep.
          unless proto_member
            ext_member.from_protocol_extension = true
            next true
          end

          # Default impl but constrained, mark and keep.
          if ext.constrained_extension?
            ext_member.default_impl_abstract = ext_member.abstract
            ext_member.abstract = nil
            next true
          end

          # Default impl for all users, merge.
          proto_member.default_impl_abstract = ext_member.abstract
          next false
        end
      end
    end

    # Mark children merged from categories with the name of category
    # (unless they already have a mark)
    def self.merge_objc_declaration_marks(typedecl, extensions)
      return unless typedecl.type.objc_class?

      extensions.each do |ext|
        _, category_name = ext.objc_category_name
        ext.children.each { |c| c.mark.name ||= category_name }
      end
    end

    # For each extension to be merged, move any MARK from the extension
    # declaration down to the extension contents so it still shows up.
    def self.move_merged_extension_marks(decls)
      return unless to_be_merged = decls[1..]

      to_be_merged.each do |ext|
        child = ext.children.first
        if child && child.mark.empty?
          child.mark.copy(ext.mark)
        end
      end
    end

    # Merge useful information added by extensions into the main
    # declaration: public protocol conformances and, for top-level extensions,
    # further conditional extensions of the same type.
    def self.merge_code_declaration(decls)
      declarations = decls[1..].select do |decl|
        decl.type.swift_extension? &&
          (decl.other_inherited_types?(@inaccessible_protocols) ||
            (decls.first.type.swift_extension? && decl.constrained_extension?))
      end.prepend(decls.first)

      html_declaration = ''
      until declarations.empty?
        module_decls, declarations = next_doc_module_group(declarations)
        first = module_decls.first
        if need_doc_module_note?(first, html_declaration)
          html_declaration += "<span class='declaration-note'>From #{first.doc_module_name}:</span>"
        end
        html_declaration += module_decls.map(&:declaration).uniq.join
      end

      # Must preserve `nil` for edge cases
      decls.first.declaration = html_declaration unless html_declaration.empty?
    end

    # Grab all the extensions from the same doc module
    def self.next_doc_module_group(decls)
      decls.partition { _1.doc_module_name == decls.first.doc_module_name }
    end

    # Does this extension/type need a note explaining which doc module it is from?
    # Only for extensions, if there actually are multiple modules.
    # Last condition avoids it for simple 'extension Array'.
    def self.need_doc_module_note?(decl, html_declaration)
      Config.instance.multiple_modules? &&
        decl.type.swift_extension? &&
        !(html_declaration.empty? &&
          !decl.constrained_extension? &&
          !decl.inherited_types?)
    end

    #
    # Autolinking
    #

    # Links recognized top-level declarations within
    # - inlined code within docs
    # - method signatures after they've been processed by the highlighter
    #
    # The `after_highlight` flag is used to differentiate between the two modes.
    #
    # DocC link format - follow Xcode and don't display slash-separated parts.
    def self.autolink_text(text, doc, after_highlight: false)
      text.autolink_block(doc.url, '[^\s]+', after_highlight) do |raw_name|
        sym_name =
          (raw_name[/^<doc:(.*)>$/, 1] || raw_name).sub(/(?<!^)-.+$/, '')

        [@doc_index.lookup(sym_name, doc), sym_name.sub(%r{^.*/}, '')]
      end.autolink_block(doc.url, '[+-]\[\w+(?: ?\(\w+\))? [\w:]+\]',
                         after_highlight) do |raw_name|
        [@doc_index.lookup(raw_name, doc), raw_name]
      end.autolink_block(doc.url, '[+-]\w[\w:]*', after_highlight) do |raw_name|
        [@doc_index.lookup(raw_name, doc), raw_name]
      end
    end

    AUTOLINK_TEXT_FIELDS = %w[return
                              abstract
                              unavailable_message
                              deprecation_message].freeze

    def self.autolink_text_fields(doc)
      AUTOLINK_TEXT_FIELDS.each do |field|
        if text = doc.send(field)
          doc.send(field + '=', autolink_text(text, doc))
        end
      end

      (doc.parameters || []).each do |param|
        param[:discussion] =
          autolink_text(param[:discussion], doc)
      end
    end

    AUTOLINK_HIGHLIGHT_FIELDS = %w[declaration
                                   other_language_declaration].freeze

    def self.autolink_highlight_fields(doc)
      AUTOLINK_HIGHLIGHT_FIELDS.each do |field|
        if text = doc.send(field)
          doc.send(field + '=',
                   autolink_text(text, doc, after_highlight: true))
        end
      end
    end

    def self.autolink(docs)
      docs.each do |doc|
        doc.children = autolink(doc.children)
        autolink_text_fields(doc)
        autolink_highlight_fields(doc)
      end
    end

    # For autolinking external markdown documents
    def self.autolink_document(html, doc)
      autolink_text(html, doc)
    end

    #
    # Entrypoint and misc filtering
    #

    # Apply filtering based on the "included" and "excluded" flags.
    def self.filter_files(json)
      json = filter_included_files(json) if Config.instance.included_files.any?
      json = filter_excluded_files(json) if Config.instance.excluded_files.any?
      json.map do |doc|
        key = doc.keys.first
        doc[key]
      end.compact
    end

    # Filter based on the "included" flag.
    def self.filter_included_files(json)
      included_files = Config.instance.included_files
      json.map do |doc|
        key = doc.keys.first
        doc if included_files.detect do |include|
          File.fnmatch?(include, key)
        end
      end.compact
    end

    # Filter based on the "excluded" flag.
    def self.filter_excluded_files(json)
      excluded_files = Config.instance.excluded_files
      json.map do |doc|
        key = doc.keys.first
        doc unless excluded_files.detect do |exclude|
          File.fnmatch?(exclude, key)
        end
      end.compact
    end

    def self.reject_objc_types(docs)
      enums = docs.map do |doc|
        [doc, doc.children]
      end.flatten.select { |child| child.type.objc_enum? }.map(&:objc_name)
      docs.map do |doc|
        doc.children = doc.children.reject do |child|
          child.type.objc_typedef? && enums.include?(child.name)
        end
        doc
      end.reject do |doc|
        doc.type.objc_unexposed? ||
          (doc.type.objc_typedef? && enums.include?(doc.name))
      end
    end

    # Remove top-level enum cases because it means they have an ACL lower
    # than min_acl
    def self.reject_swift_types(docs)
      docs.reject { _1.type.swift_enum_element? }
    end

    # Spot and mark any categories on classes not declared in these docs
    def self.mark_objc_external_categories(docs)
      class_names = docs.select { _1.type.objc_class? }.to_set(&:name)

      docs.map do |doc|
        if (names = doc.objc_category_name) && !class_names.include?(names.first)
          doc.module_name = '(Imported)'
        end
        doc
      end
    end

    # Parse sourcekitten STDOUT output as JSON
    # @return [Hash] structured docs
    def self.parse(sourcekitten_output, options, inject_docs)
      @min_acl = options.min_acl
      @skip_undocumented = options.skip_undocumented
      @stats = Stats.new
      @inaccessible_protocols = []

      # Process each module separately to inject the source module name
      docs = sourcekitten_output.zip(options.module_names).map do |json, name|
        @current_module_name = name
        sourcekitten_dicts = filter_files(JSON.parse(json).flatten)
        make_source_declarations(sourcekitten_dicts)
      end.flatten + inject_docs

      docs = expand_extensions(docs)
      docs = deduplicate_declarations(docs)
      docs = reject_objc_types(docs)
      docs = reject_swift_types(docs)
      docs = mark_objc_external_categories(docs)

      @doc_index = DocIndex.new(docs)

      docs = Grouper.group_docs(docs, @doc_index)

      make_doc_urls(docs)
      autolink(docs)
      [docs, @stats]
    end
  end
end