# frozen_string_literal: true

# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
class Blob < SimpleDelegator
  include GlobalID::Identification
  include Presentable
  include BlobLanguageFromGitAttributes
  include BlobActiveModel

  MODE_SYMLINK = '120000' # The STRING 120000 is the git-reported octal filemode for a symlink
  MODE_EXECUTABLE = '100755' # The STRING 100755 is the git-reported octal filemode for an executable file
  MODE_TREE = '40000'

  CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
  CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour

  # Finding a viewer for a blob happens based only on extension and whether the
  # blob is binary or text, which means 1 blob should only be matched by 1 viewer,
  # and the order of these viewers doesn't really matter.
  #
  # However, when the blob is an LFS pointer, we cannot know for sure whether the
  # file being pointed to is binary or text. In this case, we match only on
  # extension, preferring binary viewers over text ones if both exist, since the
  # large files referred to in "Large File Storage" are much more likely to be
  # binary than text.
  #
  # `.stl` files, for example, exist in both binary and text forms, and are
  # handled by different viewers (`BinarySTL` and `TextSTL`) depending on blob
  # type. LFS pointers to `.stl` files are assumed to always be the binary kind,
  # and use the `BinarySTL` viewer.
  RICH_VIEWERS = [
    BlobViewer::CSV,
    BlobViewer::Markup,
    BlobViewer::Notebook,
    BlobViewer::SVG,
    BlobViewer::OpenApi,
    BlobViewer::GeoJson,
    BlobViewer::Graph,

    BlobViewer::Image,
    BlobViewer::Sketch,

    BlobViewer::Video,
    BlobViewer::Audio,

    BlobViewer::PDF,

    BlobViewer::BinarySTL,
    BlobViewer::TextSTL
  ].sort_by { |v| v.binary? ? 0 : 1 }.freeze

  AUXILIARY_VIEWERS = [
    BlobViewer::RouteMap,

    BlobViewer::Readme,
    BlobViewer::License,
    BlobViewer::Contributing,
    BlobViewer::Changelog,

    BlobViewer::CargoToml,
    BlobViewer::Cartfile,
    BlobViewer::ComposerJson,
    BlobViewer::Gemfile,
    BlobViewer::Gemspec,
    BlobViewer::GodepsJson,
    BlobViewer::GoMod,
    BlobViewer::PackageJson,
    BlobViewer::Podfile,
    BlobViewer::Podspec,
    BlobViewer::PodspecJson,
    BlobViewer::RequirementsTxt,
    BlobViewer::YarnLock
  ].freeze

  attr_reader :container
  attr_accessor :ref_type

  delegate :repository, to: :container, allow_nil: true
  delegate :project, to: :repository, allow_nil: true

  # Wrap a Gitlab::Git::Blob object, or return nil when given nil
  #
  # This method prevents the decorated object from evaluating to "truthy" when
  # given a nil value. For example:
  #
  #     blob = Blob.new(nil)
  #     puts "truthy" if blob # => "truthy"
  #
  #     blob = Blob.decorate(nil)
  #     puts "truthy" if blob # No output
  def self.decorate(blob, container = nil)
    return if blob.nil?

    new(blob, container)
  end

  def self.lazy(repository, commit_id, path, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
    key = [:repository_blobs, repository, blob_size_limit]

    BatchLoader.for([commit_id, path]).batch(key: key) do |items, loader, args|
      args[:key].second.blobs_at(items, blob_size_limit: blob_size_limit).each do |blob|
        loader.call([blob.commit_id, blob.path], blob) if blob
      end
    end
  end

  def initialize(blob, container = nil)
    @container = container

    super(blob)
  end

  def inspect
    "#<#{self.class.name} oid:#{id[0..8]} commit:#{commit_id[0..8]} path:#{path}>"
  end

  # Returns the data of the blob.
  #
  # If the blob is a text based blob the content is converted to UTF-8 and any
  # invalid byte sequences are replaced.
  def data
    if binary_in_repo?
      super
    else
      @data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
    end
  end

  def load_all_data!
    # Endpoint needed: https://gitlab.com/gitlab-org/gitaly/issues/756
    Gitlab::GitalyClient.allow_n_plus_1_calls do
      super(repository) if container
    end
  end

  def empty?
    raw_size == 0
  end

  def external_storage_error?
    if external_storage == :lfs
      !repository.lfs_enabled?
    else
      false
    end
  end

  def stored_externally?
    return @stored_externally if defined?(@stored_externally)

    @stored_externally = external_storage && !external_storage_error?
  end

  # Returns the size of the file that this blob represents. If this blob is an
  # LFS pointer, this is the size of the file stored in LFS. Otherwise, this is
  # the size of the blob itself.
  def raw_size
    if stored_externally?
      external_size
    else
      size
    end
  end

  # Returns whether the file that this blob represents is binary. If this blob is
  # an LFS pointer, we assume the file stored in LFS is binary, unless a
  # text-based rich blob viewer matched on the file's extension. Otherwise, this
  # depends on the type of the blob itself.
  def binary?
    if stored_externally?
      if rich_viewer
        rich_viewer.binary?
      elsif known_extension?
        false
      elsif _mime_type
        _mime_type.binary?
      else
        true
      end
    else
      binary_in_repo?
    end
  end

  def symlink?
    mode == MODE_SYMLINK
  end

  def executable?
    mode == MODE_EXECUTABLE
  end

  def tree?
    mode == MODE_TREE
  end

  def extension
    @extension ||= extname.downcase.delete('.')
  end

  def file_type
    name = File.basename(path)

    Gitlab::FileDetector.type_of(path) || Gitlab::FileDetector.type_of(name)
  end

  def video?
    UploaderHelper::SAFE_VIDEO_EXT.include?(extension)
  end

  def audio?
    UploaderHelper::SAFE_AUDIO_EXT.include?(extension)
  end

  def readable_text?
    text_in_repo? && !stored_externally? && !truncated?
  end

  def simple_viewer
    @simple_viewer ||= simple_viewer_class.new(self)
  end

  def rich_viewer
    return @rich_viewer if defined?(@rich_viewer)

    @rich_viewer = rich_viewer_class&.new(self)
  end

  def auxiliary_viewer
    return @auxiliary_viewer if defined?(@auxiliary_viewer)

    @auxiliary_viewer = auxiliary_viewer_class&.new(self)
  end

  def rendered_as_text?(ignore_errors: true)
    simple_viewer.is_a?(BlobViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
  end

  def show_viewer_switcher?
    rendered_as_text? && rich_viewer
  end

  def expanded?
    !!@expanded
  end

  def expand!
    @expanded = true
  end

  def file_hash
    OpenSSL::Digest::SHA256.hexdigest(path)
  end

  def short_file_hash
    file_hash[0..8]
  end

  private

  def simple_viewer_class
    if empty?
      BlobViewer::Empty
    elsif binary?
      BlobViewer::Download
    else # text
      BlobViewer::Text
    end
  end

  def rich_viewer_class
    viewer_class_from(RICH_VIEWERS)
  end

  def auxiliary_viewer_class
    viewer_class_from(AUXILIARY_VIEWERS)
  end

  def viewer_class_from(classes)
    return if empty? || external_storage_error?

    verify_binary = !stored_externally?

    classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
  end
end
