# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Favor `File.(bin)read` convenience methods.
      #
      # @example
      #   ## text mode
      #   # bad
      #   File.open(filename).read
      #   File.open(filename, &:read)
      #   File.open(filename) { |f| f.read }
      #   File.open(filename) do |f|
      #     f.read
      #   end
      #   File.open(filename, 'r').read
      #   File.open(filename, 'r', &:read)
      #   File.open(filename, 'r') do |f|
      #     f.read
      #   end
      #
      #   # good
      #   File.read(filename)
      #
      # @example
      #   ## binary mode
      #   # bad
      #   File.open(filename, 'rb').read
      #   File.open(filename, 'rb', &:read)
      #   File.open(filename, 'rb') do |f|
      #     f.read
      #   end
      #
      #   # good
      #   File.binread(filename)
      #
      class FileRead < Base
        extend AutoCorrector
        include RangeHelp

        MSG = 'Use `File.%<read_method>s`.'

        RESTRICT_ON_SEND = %i[open].freeze

        READ_FILE_START_TO_FINISH_MODES = %w[r rt rb r+ r+t r+b].to_set.freeze

        # @!method file_open?(node)
        def_node_matcher :file_open?, <<~PATTERN
          (send
            (const {nil? cbase} :File)
            :open
            $_
            (str $%READ_FILE_START_TO_FINISH_MODES)?
            $(block-pass (sym :read))?
          )
        PATTERN

        # @!method send_read?(node)
        def_node_matcher :send_read?, <<~PATTERN
          (send _ :read)
        PATTERN

        # @!method block_read?(node)
        def_node_matcher :block_read?, <<~PATTERN
          (block _ (args (arg $_)) (send (lvar $_) :read))
        PATTERN

        def on_send(node)
          evidence(node) do |filename, mode, read_node|
            message = format(MSG, read_method: read_method(mode))

            add_offense(read_node, message: message) do |corrector|
              range = range_between(node.loc.selector.begin_pos, read_node.loc.expression.end_pos)
              replacement = "#{read_method(mode)}(#{filename.source})"

              corrector.replace(range, replacement)
            end
          end
        end

        private

        def evidence(node)
          file_open?(node) do |filename, mode_array, block_pass|
            read_node?(node, block_pass) do |read_node|
              yield(filename, mode_array.first || 'r', read_node)
            end
          end
        end

        def read_node?(node, block_pass)
          if block_pass.any?
            yield(node)
          elsif file_open_read?(node.parent)
            yield(node.parent)
          end
        end

        def file_open_read?(node)
          return true if send_read?(node)

          block_read?(node) { |block_arg, read_lvar| block_arg == read_lvar }
        end

        def read_method(mode)
          mode.end_with?('b') ? :binread : :read
        end
      end
    end
  end
end
