# frozen_string_literal: true

##
# BasicSpecification is an abstract class which implements some common code
# used by both Specification and StubSpecification.

class Gem::BasicSpecification
  ##
  # Allows installation of extensions for git: gems.

  attr_writer :base_dir # :nodoc:

  ##
  # Sets the directory where extensions for this gem will be installed.

  attr_writer :extension_dir # :nodoc:

  ##
  # Is this specification ignored for activation purposes?

  attr_writer :ignored # :nodoc:

  ##
  # The path this gemspec was loaded from.  This attribute is not persisted.

  attr_accessor :loaded_from

  ##
  # Allows correct activation of git: and path: gems.

  attr_writer :full_gem_path # :nodoc:

  def initialize
    internal_init
  end

  ##
  # The path to the gem.build_complete file within the extension install
  # directory.

  def gem_build_complete_path # :nodoc:
    File.join extension_dir, "gem.build_complete"
  end

  ##
  # True when the gem has been activated

  def activated?
    raise NotImplementedError
  end

  ##
  # Returns the full path to the base gem directory.
  #
  # eg: /usr/local/lib/ruby/gems/1.8

  def base_dir
    raise NotImplementedError
  end

  ##
  # Return true if this spec can require +file+.

  def contains_requirable_file?(file)
    if ignored?
      if platform == Gem::Platform::RUBY || Gem::Platform.local === platform
        warn "Ignoring #{full_name} because its extensions are not built. " \
             "Try: gem pristine #{name} --version #{version}"
      end

      return false
    end

    is_soext = file.end_with?(".so", ".o")

    if is_soext
      have_file? file.delete_suffix(File.extname(file)), Gem.dynamic_library_suffixes
    else
      have_file? file, Gem.suffixes
    end
  end

  ##
  # Return true if this spec should be ignored because it's missing extensions.

  def ignored?
    return @ignored unless @ignored.nil?

    @ignored = missing_extensions?
  end

  def default_gem?
    !loaded_from.nil? &&
      File.dirname(loaded_from) == Gem.default_specifications_dir
  end

  ##
  # Regular gems take precedence over default gems

  def default_gem_priority
    default_gem? ? 1 : -1
  end

  ##
  # Gems higher up in +gem_path+ take precedence

  def base_dir_priority(gem_path)
    gem_path.index(base_dir) || gem_path.size
  end

  ##
  # Returns full path to the directory where gem's extensions are installed.

  def extension_dir
    @extension_dir ||= File.expand_path(File.join(extensions_dir, full_name))
  end

  ##
  # Returns path to the extensions directory.

  def extensions_dir
    Gem.default_ext_dir_for(base_dir) ||
      File.join(base_dir, "extensions", Gem::Platform.local.to_s,
                Gem.extension_api_version)
  end

  def find_full_gem_path # :nodoc:
    File.expand_path File.join(gems_dir, full_name)
  end

  private :find_full_gem_path

  ##
  # The full path to the gem (install path + full name).
  #
  # TODO: This is duplicated with #gem_dir. Eventually either of them should be deprecated.

  def full_gem_path
    @full_gem_path ||= find_full_gem_path
  end

  ##
  # Returns the full name (name-version) of this Gem.  Platform information
  # is included (name-version-platform) if it is specified and not the
  # default Ruby platform.

  def full_name
    if platform == Gem::Platform::RUBY || platform.nil?
      "#{name}-#{version}"
    else
      "#{name}-#{version}-#{platform}"
    end
  end

  ##
  # Returns the full name of this Gem (see `Gem::BasicSpecification#full_name`).
  # Information about where the gem is installed is also included if not
  # installed in the default GEM_HOME.

  def full_name_with_location
    if base_dir != Gem.dir
      "#{full_name} in #{base_dir}"
    else
      full_name
    end
  end

  ##
  # Full paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
  # activated.

  def full_require_paths
    @full_require_paths ||=
      begin
        full_paths = raw_require_paths.map do |path|
          File.join full_gem_path, path
        end

        full_paths << extension_dir if have_extensions?

        full_paths
      end
  end

  ##
  # The path to the data directory for this gem.

  def datadir
    # TODO: drop the extra ", gem_name" which is uselessly redundant
    File.expand_path(File.join(gems_dir, full_name, "data", name))
  end

  extend Gem::Deprecate
  rubygems_deprecate :datadir, :none, "4.1"

  ##
  # Full path of the target library file.
  # If the file is not in this gem, return nil.

  def to_fullpath(path)
    if activated?
      @paths_map ||= {}
      Gem.suffixes.each do |suf|
        full_require_paths.each do |dir|
          fullpath = "#{dir}/#{path}#{suf}"
          next unless File.file?(fullpath)
          @paths_map[path] ||= fullpath
        end
      end
      @paths_map[path]
    end
  end

  ##
  # Returns the full path to this spec's gem directory.
  # eg: /usr/local/lib/ruby/1.8/gems/mygem-1.0
  #
  # TODO: This is duplicated with #full_gem_path. Eventually either of them should be deprecated.

  def gem_dir
    @gem_dir ||= find_full_gem_path
  end

  ##
  # Returns the full path to the gems directory containing this spec's
  # gem directory. eg: /usr/local/lib/ruby/1.8/gems

  def gems_dir
    raise NotImplementedError
  end

  def internal_init # :nodoc:
    @extension_dir = nil
    @full_gem_path = nil
    @gem_dir = nil
    @ignored = nil
  end

  ##
  # Name of the gem

  def name
    raise NotImplementedError
  end

  ##
  # Platform of the gem

  def platform
    raise NotImplementedError
  end

  def installable_on_platform?(target_platform) # :nodoc:
    return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform)
    return true if Gem::Platform.new(platform) === target_platform

    false
  end

  def raw_require_paths # :nodoc:
    raise NotImplementedError
  end

  ##
  # Paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
  # activated.
  #
  # See also #require_paths=
  #
  # If you have an extension you do not need to add <code>"ext"</code> to the
  # require path, the extension build process will copy the extension files
  # into "lib" for you.
  #
  # The default value is <code>"lib"</code>
  #
  # Usage:
  #
  #   # If all library files are in the root directory...
  #   spec.require_path = '.'

  def require_paths
    return raw_require_paths unless have_extensions?

    [extension_dir].concat raw_require_paths
  end

  ##
  # Returns the paths to the source files for use with analysis and
  # documentation tools.  These paths are relative to full_gem_path.

  def source_paths
    paths = raw_require_paths.dup

    if have_extensions?
      ext_dirs = extensions.map do |extension|
        extension.split(File::SEPARATOR, 2).first
      end.uniq

      paths.concat ext_dirs
    end

    paths.uniq
  end

  ##
  # Return all files in this gem that match for +glob+.

  def matches_for_glob(glob) # TODO: rename?
    glob = File.join(lib_dirs_glob, glob)

    Dir[glob]
  end

  ##
  # Returns the list of plugins in this spec.

  def plugins
    matches_for_glob("rubygems#{Gem.plugin_suffix_pattern}")
  end

  ##
  # Returns a string usable in Dir.glob to match all requirable paths
  # for this spec.

  def lib_dirs_glob
    dirs = if raw_require_paths
      if raw_require_paths.size > 1
        "{#{raw_require_paths.join(",")}}"
      else
        raw_require_paths.first
      end
    else
      "lib" # default value for require_paths for bundler/inline
    end

    "#{full_gem_path}/#{dirs}"
  end

  ##
  # Return a Gem::Specification from this gem

  def to_spec
    raise NotImplementedError
  end

  ##
  # Version of the gem

  def version
    raise NotImplementedError
  end

  ##
  # Whether this specification is stubbed - i.e. we have information
  # about the gem from a stub line, without having to evaluate the
  # entire gemspec file.
  def stubbed?
    raise NotImplementedError
  end

  def this
    self
  end

  private

  def have_extensions?
    !extensions.empty?
  end

  def have_file?(file, suffixes)
    return true if raw_require_paths.any? do |path|
      base = File.join(gems_dir, full_name, path, file)
      suffixes.any? {|suf| File.file? base + suf }
    end

    if have_extensions?
      base = File.join extension_dir, file
      suffixes.any? {|suf| File.file? base + suf }
    else
      false
    end
  end
end
