# frozen_string_literal: true

# Skip reloading when an identical copy (e.g. the one shipped inside the Bundler
# gem) was already required from a different path, to avoid redefinition warnings.
return if defined?(Gem::CompactIndexClient::INFO_REQS)

##
# The CompactIndexClient fetches and parses the compact index files
# (names, versions and info/[gem]) served by a gem server, keeping a
# local cache so subsequent fetches only transfer what changed.
#
# This is the single shared client: Bundler ships a copy of it and uses
# it underneath its own fetcher orchestration, injecting its fetcher and
# filesystem access hook.

class Gem::CompactIndexClient
  SUPPORTED_DIGESTS = { "sha-256" => :SHA256 }.freeze
  DEBUG_MUTEX = Thread::Mutex.new

  # info returns an Array of INFO Arrays. Each INFO Array has the following indices:
  INFO_NAME = 0
  INFO_VERSION = 1
  INFO_PLATFORM = 2
  INFO_DEPS = 3
  INFO_REQS = 4

  def self.debug
    return unless ENV["DEBUG_COMPACT_INDEX"]
    DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") }
  end

  class Error < StandardError; end

  # Filesystem reads and writes funnel through this hook so that a host
  # (e.g. Bundler) can translate low-level Errno exceptions into its own
  # friendlier errors. The default just yields the path.
  def self.filesystem_access(path, action = :write, &block)
    if @filesystem_access
      @filesystem_access.call(path, action, &block)
    else
      yield path
    end
  end

  def self.filesystem_access=(hook)
    @filesystem_access = hook
  end

  require_relative "compact_index_client/cache"
  require_relative "compact_index_client/cache_file"
  require_relative "compact_index_client/http_fetcher"
  require_relative "compact_index_client/parser"
  require_relative "compact_index_client/updater"

  # The client is instantiated with:
  # - `directory`: the root directory where the cache files are stored.
  # - `fetcher`: (optional) an object that responds to #call(uri_path, headers)
  #   and returns a Gem::Net::HTTP response. When the fetcher is not provided,
  #   the client only reads cached files from disk.
  def initialize(directory, fetcher = nil)
    @cache = Cache.new(directory, fetcher)
    @parser = Parser.new(@cache)
  end

  def names
    Gem::CompactIndexClient.debug { "names" }
    @parser.names
  end

  def versions
    Gem::CompactIndexClient.debug { "versions" }
    @parser.versions
  end

  def dependencies(names)
    Gem::CompactIndexClient.debug { "dependencies(#{names})" }
    names.map {|name| info(name) }
  end

  def info(name)
    Gem::CompactIndexClient.debug { "info(#{name})" }
    @parser.info(name)
  end

  # Fetches a single gem's info without consulting the versions index,
  # using a conditional request to refresh the cached file. Useful when
  # only a few gems are needed and the versions index download would
  # dominate, as in gem install.
  def fetch_info(name)
    Gem::CompactIndexClient.debug { "fetch_info(#{name})" }
    @parser.parse_info(@cache.fetch_info(name), name)
  end

  def latest_version(name)
    Gem::CompactIndexClient.debug { "latest_version(#{name})" }
    @parser.info(name).map {|d| Gem::Version.new(d[INFO_VERSION]) }.max
  end

  def available?
    Gem::CompactIndexClient.debug { "available?" }
    @parser.available?
  end

  def reset!
    Gem::CompactIndexClient.debug { "reset!" }
    @cache.reset!
  end
end
