# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Enforces using `def self.method_name` or `class << self` to define class methods.
      #
      # @example EnforcedStyle: def_self (default)
      #   # bad
      #   class SomeClass
      #     class << self
      #       attr_accessor :class_accessor
      #
      #       def class_method
      #         # ...
      #       end
      #     end
      #   end
      #
      #   # good
      #   class SomeClass
      #     def self.class_method
      #       # ...
      #     end
      #
      #     class << self
      #       attr_accessor :class_accessor
      #     end
      #   end
      #
      #   # good - contains private method
      #   class SomeClass
      #     class << self
      #       attr_accessor :class_accessor
      #
      #       private
      #
      #       def private_class_method
      #         # ...
      #       end
      #     end
      #   end
      #
      # @example EnforcedStyle: self_class
      #   # bad
      #   class SomeClass
      #     def self.class_method
      #       # ...
      #     end
      #   end
      #
      #   # good
      #   class SomeClass
      #     class << self
      #       def class_method
      #         # ...
      #       end
      #     end
      #   end
      #
      class ClassMethodsDefinitions < Base
        include ConfigurableEnforcedStyle
        include CommentsHelp
        include VisibilityHelp
        include RangeHelp
        extend AutoCorrector

        MSG = 'Use `%<preferred>s` to define a class method.'
        MSG_SCLASS = 'Do not define public methods within class << self.'

        def on_sclass(node)
          return unless def_self_style?
          return unless node.identifier.self_type?
          return unless all_methods_public?(node)

          add_offense(node, message: MSG_SCLASS) do |corrector|
            autocorrect_sclass(node, corrector)
          end
        end

        def on_defs(node)
          return if def_self_style?
          return unless node.receiver.self_type?

          message = format(MSG, preferred: 'class << self')
          add_offense(node, message: message)
        end

        private

        def def_self_style?
          style == :def_self
        end

        def all_methods_public?(sclass_node)
          def_nodes = def_nodes(sclass_node)
          return false if def_nodes.empty?

          def_nodes.all? { |def_node| node_visibility(def_node) == :public }
        end

        def def_nodes(sclass_node)
          sclass_def = sclass_node.body
          return [] unless sclass_def

          if sclass_def.def_type?
            [sclass_def]
          elsif sclass_def.begin_type?
            sclass_def.each_child_node(:def).to_a
          else
            []
          end
        end

        def autocorrect_sclass(node, corrector)
          rewritten_defs = []

          def_nodes(node).each do |def_node|
            next unless node_visibility(def_node) == :public

            range, source = extract_def_from_sclass(def_node, node)

            corrector.remove(range)
            rewritten_defs << source
          end

          if sclass_only_has_methods?(node)
            corrector.remove(node)
            rewritten_defs.first&.strip!
          else
            corrector.insert_after(node, "\n")
          end

          corrector.insert_after(node, rewritten_defs.join("\n"))
        end

        def sclass_only_has_methods?(node)
          node.body.def_type? || node.body.each_child_node.all?(&:def_type?)
        end

        def extract_def_from_sclass(def_node, sclass_node)
          range = source_range_with_comment(def_node)
          source = prefix_def_with_self(range, def_node)
          source = source.gsub(/^ {#{indentation_diff(def_node, sclass_node)}}/, '')
          [range, source.chomp]
        end

        # Splice in `self.` at the actual `def` keyword rather than substituting the
        # first textual `def <name>`, which may appear inside a preceding comment.
        def prefix_def_with_self(range, def_node)
          keyword_offset = def_node.loc.keyword.begin_pos - range.begin_pos
          name_end_offset = def_node.loc.name.end_pos - range.begin_pos
          source = range.source.dup
          source[keyword_offset...name_end_offset] = "def self.#{def_node.method_name}"
          source
        end

        def indentation_diff(node1, node2)
          node1.loc.column - node2.loc.column
        end
      end
    end
  end
end
