# Defines a specific location, identified by paths and line numbers,
# within a specific diff, identified by start, head and base commit ids.
module Gitlab
  module Diff
    class Position
      attr_reader :old_path
      attr_reader :new_path
      attr_reader :old_line
      attr_reader :new_line
      attr_reader :base_sha
      attr_reader :start_sha
      attr_reader :head_sha

      def initialize(attrs = {})
        @old_path = attrs[:old_path]
        @new_path = attrs[:new_path]
        @old_line = attrs[:old_line]
        @new_line = attrs[:new_line]

        if attrs[:diff_refs]
          @base_sha  = attrs[:diff_refs].base_sha
          @start_sha = attrs[:diff_refs].start_sha
          @head_sha  = attrs[:diff_refs].head_sha
        else
          @base_sha  = attrs[:base_sha]
          @start_sha = attrs[:start_sha]
          @head_sha  = attrs[:head_sha]
        end
      end

      # `Gitlab::Diff::Position` objects are stored as serialized attributes in
      # `DiffNote`, which use YAML to encode and decode objects.
      # `#init_with` and `#encode_with` can be used to customize the en/decoding
      # behavior. In this case, we override these to prevent memoized instance
      # variables like `@diff_file` and `@diff_line` from being serialized.
      def init_with(coder)
        initialize(coder['attributes'])

        self
      end

      def encode_with(coder)
        coder['attributes'] = self.to_h
      end

      def key
        @key ||= [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || ""), old_line, new_line]
      end

      def ==(other)
        other.is_a?(self.class) && key == other.key
      end

      def to_h
        {
          old_path: old_path,
          new_path: new_path,
          old_line: old_line,
          new_line: new_line,
          base_sha:  base_sha,
          start_sha: start_sha,
          head_sha:  head_sha
        }
      end

      def inspect
        %(#<#{self.class}:#{object_id} #{to_h}>)
      end

      def complete?
        file_path.present? &&
          (old_line || new_line) &&
          diff_refs.complete?
      end

      def to_json(opts = nil)
        JSON.generate(self.to_h, opts)
      end

      def type
        if old_line && new_line
          nil
        elsif new_line
          'new'
        else
          'old'
        end
      end

      def unchanged?
        type.nil?
      end

      def added?
        type == 'new'
      end

      def removed?
        type == 'old'
      end

      def paths
        [old_path, new_path].compact.uniq
      end

      def file_path
        new_path.presence || old_path
      end

      def diff_refs
        @diff_refs ||= DiffRefs.new(base_sha: base_sha, start_sha: start_sha, head_sha: head_sha)
      end

      def diff_file(repository)
        @diff_file ||= begin
          if RequestStore.active?
            key = {
              project_id: repository.project.id,
              start_sha: start_sha,
              head_sha: head_sha,
              path: file_path
            }

            RequestStore.fetch(key) { find_diff_file(repository) }
          else
            find_diff_file(repository)
          end
        end
      end

      def diff_line(repository)
        @diff_line ||= diff_file(repository).line_for_position(self)
      end

      def line_code(repository)
        @line_code ||= diff_file(repository).line_code_for_position(self)
      end

      private

      def find_diff_file(repository)
        # We're at the initial commit, so just get that as we can't compare to anything.
        compare =
          if Gitlab::Git.blank_ref?(start_sha)
            Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
          else
            Gitlab::Git::Compare.new(
              repository.raw_repository,
              start_sha,
              head_sha
            )
          end

        diff = compare.diffs(paths: paths).first

        return unless diff

        Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
      end
    end
  end
end
