# When provided a diff for a specific file, maps old line numbers to new line
# numbers and back, to find out where a specific line in a file was moved by the
# changes.
module Gitlab
  module Diff
    class LineMapper
      attr_accessor :diff_file

      def initialize(diff_file)
        @diff_file = diff_file
      end

      # Find new line number for old line number.
      def old_to_new(old_line)
        map_line_number(old_line, from: :old_line, to: :new_line)
      end

      # Find old line number for new line number.
      def new_to_old(new_line)
        map_line_number(new_line, from: :new_line, to: :old_line)
      end

      private

      def diff_lines
        @diff_lines ||= @diff_file.diff_lines
      end

      # Find old/new line number based on its old/new counterpart line number.
      def map_line_number(from_line, from:, to:)
        # If no diff file could be found, the file wasn't changed, and the
        # mapped line number is the same as the specified line number.
        return from_line unless diff_file

        # To find the mapped line number for the specified line number,
        # we need to find:
        # - The diff line with that exact line number, if it is in the diff context
        # - The first diff line with a higher line number, if it falls between diff contexts
        # - The last known diff line, if it falls after the last diff context
        diff_line = diff_lines.find do |diff_line|
          diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend
          diff_from_line && diff_from_line >= from_line
        end
        diff_line ||= diff_lines.last

        # If no diff line could be found, the file wasn't changed, and the
        # mapped line number is the same as the specified line number.
        return from_line unless diff_line

        diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend
        diff_to_line = diff_line.public_send(to) # rubocop:disable GitlabSecurity/PublicSend

        # If the line was removed, there is no mapped line number.
        return unless diff_to_line

        # Because we may not have the diff line with the exact line number
        # we were looking for, we need to adjust the mapped line number.
        distance = diff_from_line - from_line

        diff_to_line - distance
      end
    end
  end
end
