# frozen_string_literal: true

module RuboCop
  module Formatter
    # This formatter displays a YAML configuration file where all cops that
    # detected any offenses are configured to not detect the offense.
    class DisabledConfigFormatter < BaseFormatter
      HEADING =
        ['# This configuration was generated by',
         '# `%s`',
         "# on #{Time.now} using RuboCop version #{Version.version}.",
         '# The point is for the user to remove these configuration records',
         '# one by one as the offenses are removed from the code base.',
         '# Note that changes in the inspected code, or installation of new',
         '# versions of RuboCop, may require this file to be generated again.']
        .join("\n")

      @config_to_allow_offenses = {}
      @detected_styles = {}

      COPS = Cop::Cop.all.group_by(&:cop_name)

      class << self
        attr_accessor :config_to_allow_offenses, :detected_styles
      end

      def initialize(output, options = {})
        super
        @cops_with_offenses ||= Hash.new(0)
        @files_with_offenses ||= {}
      end

      def file_started(_file, _file_info)
        @exclude_limit_option = @options[:exclude_limit]
        @exclude_limit = (
          @exclude_limit_option ||
          RuboCop::Options::DEFAULT_MAXIMUM_EXCLUSION_ITEMS).to_i
        @show_offense_counts = !@options[:no_offense_counts]
      end

      def file_finished(file, offenses)
        offenses.each do |o|
          @cops_with_offenses[o.cop_name] += 1
          @files_with_offenses[o.cop_name] ||= []
          @files_with_offenses[o.cop_name] << file
        end
      end

      def finished(_inspected_files)
        output.puts HEADING % command

        # Syntax isn't a real cop and it can't be disabled.
        @cops_with_offenses.delete('Syntax')

        output_offenses

        puts "Created #{output.path}."
        puts "Run `rubocop --config #{output.path}`, or add `inherit_from: " \
             "#{output.path}` in a .rubocop.yml file."
      end

      private

      def command
        command = 'rubocop --auto-gen-config'
        if @exclude_limit_option
          command += format(' --exclude-limit %d', @exclude_limit_option.to_i)
        end

        command
      end

      def output_offenses
        @cops_with_offenses.sort.each do |cop_name, offense_count|
          output_cop(cop_name, offense_count)
        end
      end

      def output_cop(cop_name, offense_count)
        output.puts
        cfg = self.class.config_to_allow_offenses[cop_name] || {}

        output_cop_comments(output, cfg, cop_name, offense_count)
        output_cop_config(output, cfg, cop_name)
      end

      def output_cop_comments(output, cfg, cop_name, offense_count)
        output.puts "# Offense count: #{offense_count}" if @show_offense_counts
        if COPS[cop_name] && COPS[cop_name].first.new.support_autocorrect?
          output.puts '# Cop supports --auto-correct.'
        end

        default_cfg = default_config(cop_name)
        return unless default_cfg

        params = cop_config_params(default_cfg, cfg)
        return if params.empty?

        output_cop_param_comments(params, default_cfg)
      end

      def cop_config_params(default_cfg, cfg)
        default_cfg.keys -
          %w(Description StyleGuide Reference Enabled Exclude) -
          cfg.keys
      end

      def output_cop_param_comments(params, default_cfg)
        output.puts "# Configuration parameters: #{params.join(', ')}."

        params.each do |param|
          value = default_cfg[param]
          if value.is_a?(Array)
            next if value.empty?
            output.puts "# #{param}: #{value.join(', ')}"
          end
        end
      end

      def default_config(cop_name)
        RuboCop::ConfigLoader.default_configuration[cop_name]
      end

      def output_cop_config(output, cfg, cop_name)
        # 'Enabled' option will be put into file only if exclude
        # limit is exceeded.
        cfg_without_enabled = cfg.reject { |key| key == 'Enabled' }

        output.puts "#{cop_name}:"
        cfg_without_enabled.each do |key, value|
          value = value[0] if value.is_a?(Array)
          output.puts "  #{key}: #{value}"
        end

        output_offending_files(output, cfg_without_enabled, cop_name)
      end

      def output_offending_files(output, cfg, cop_name)
        return unless cfg.empty?

        offending_files = @files_with_offenses[cop_name].uniq.sort
        if offending_files.count > @exclude_limit
          output.puts '  Enabled: false'
        else
          output_exclude_list(output, offending_files, cop_name)
        end
      end

      def output_exclude_list(output, offending_files, cop_name)
        require 'pathname'
        parent = Pathname.new(Dir.pwd)

        output.puts '  Exclude:'
        excludes(offending_files, cop_name, parent).each do |file|
          output_exclude_path(output, file, parent)
        end
      end

      def excludes(offending_files, cop_name, parent)
        # Exclude properties in .rubocop_todo.yml override default ones, as well
        # as any custom excludes in .rubocop.yml, so in order to retain those
        # excludes we must copy them.
        # There can be multiple .rubocop.yml files in subdirectories, but we
        # just look at the current working directory
        config = ConfigStore.new.for(parent)
        cfg = config[cop_name] || {}

        ((cfg['Exclude'] || []) + offending_files).uniq
      end

      def output_exclude_path(output, file, parent)
        file_path = Pathname.new(file)
        relative = file_path.relative_path_from(parent)
        output.puts "    - '#{relative}'"
      rescue ArgumentError
        output.puts "    - '#{file}'"
      end
    end
  end
end
