# frozen_string_literal: true

require "rubygems" unless defined?(Gem)

# We can't let `Gem::Source` be autoloaded in the `Gem::Specification#source`
# redefinition below, so we need to load it upfront. The reason is that if
# Bundler monkeypatches are loaded before RubyGems activates an executable (for
# example, through `ruby -rbundler -S irb`), gem activation might end up calling
# the redefined `Gem::Specification#source` and triggering the `Gem::Source`
# autoload. That would result in requiring "rubygems/source" inside another
# require, which would trigger a monitor error and cause the `autoload` to
# eventually fail. A better solution is probably to completely avoid autoloading
# `Gem::Source` from the redefined `Gem::Specification#source`.
require "rubygems/source"

module Gem
  # Can be removed once RubyGems 3.5.11 support is dropped
  unless Gem.respond_to?(:freebsd_platform?)
    def self.freebsd_platform?
      RbConfig::CONFIG["host_os"].to_s.include?("bsd")
    end
  end

  # Can be removed once RubyGems 3.5.18 support is dropped
  unless Gem.respond_to?(:open_file_with_lock)
    class << self
      remove_method :open_file_with_flock if Gem.respond_to?(:open_file_with_flock)

      def open_file_with_flock(path, &block)
        # read-write mode is used rather than read-only in order to support NFS
        mode = IO::RDWR | IO::APPEND | IO::CREAT | IO::BINARY
        mode |= IO::SHARE_DELETE if IO.const_defined?(:SHARE_DELETE)

        File.open(path, mode) do |io|
          begin
            io.flock(File::LOCK_EX)
          rescue Errno::ENOSYS, Errno::ENOTSUP
          end
          yield io
        end
      end

      def open_file_with_lock(path, &block)
        file_lock = "#{path}.lock"
        open_file_with_flock(file_lock, &block)
      ensure
        FileUtils.rm_f file_lock
      end
    end
  end

  require "rubygems/platform"

  class Platform
    # Can be removed once RubyGems 3.6.9 support is dropped
    unless respond_to?(:generic)
      JAVA  = Gem::Platform.new("java") # :nodoc:
      MSWIN = Gem::Platform.new("mswin32") # :nodoc:
      MSWIN64 = Gem::Platform.new("mswin64") # :nodoc:
      MINGW = Gem::Platform.new("x86-mingw32") # :nodoc:
      X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc:
      X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc:
      UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc:
      WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc:
      X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc:
      X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc:

      GENERICS = [JAVA, *WINDOWS].freeze # :nodoc:
      private_constant :GENERICS

      GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc:
      private_constant :GENERIC_CACHE

      class << self
        ##
        # Returns the generic platform for the given platform.

        def generic(platform)
          return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY

          GENERIC_CACHE[platform] ||= begin
            found = GENERICS.find do |match|
              platform === match
            end
            found || Gem::Platform::RUBY
          end
        end

        ##
        # Returns the platform specificity match for the given spec platform and user platform.

        def platform_specificity_match(spec_platform, user_platform)
          return -1 if spec_platform == user_platform
          return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY

          os_match(spec_platform, user_platform) +
            cpu_match(spec_platform, user_platform) * 10 +
            version_match(spec_platform, user_platform) * 100
        end

        ##
        # Sorts and filters the best platform match for the given matching specs and platform.

        def sort_and_filter_best_platform_match(matching, platform)
          return matching if matching.one?

          exact = matching.select {|spec| spec.platform == platform }
          return exact if exact.any?

          sorted_matching = sort_best_platform_match(matching, platform)
          exemplary_spec = sorted_matching.first

          sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) }
        end

        ##
        # Sorts the best platform match for the given matching specs and platform.

        def sort_best_platform_match(matching, platform)
          matching.sort_by.with_index do |spec, i|
            [
              platform_specificity_match(spec.platform, platform),
              i, # for stable sort
            ]
          end
        end

        private

        def same_specificity?(platform, spec, exemplary_spec)
          platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform)
        end

        def same_deps?(spec, exemplary_spec)
          spec.required_ruby_version == exemplary_spec.required_ruby_version &&
            spec.required_rubygems_version == exemplary_spec.required_rubygems_version &&
            spec.dependencies.sort == exemplary_spec.dependencies.sort
        end

        def os_match(spec_platform, user_platform)
          if spec_platform.os == user_platform.os
            0
          else
            1
          end
        end

        def cpu_match(spec_platform, user_platform)
          if spec_platform.cpu == user_platform.cpu
            0
          elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm")
            0
          elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal"
            1
          else
            2
          end
        end

        def version_match(spec_platform, user_platform)
          if spec_platform.version == user_platform.version
            0
          elsif spec_platform.version.nil?
            1
          else
            2
          end
        end
      end

    end
  end

  require "rubygems/specification"

  # Can be removed once RubyGems 3.5.14 support is dropped
  VALIDATES_FOR_RESOLUTION = Specification.new.respond_to?(:validate_for_resolution).freeze

  class Specification
    # Can be removed once RubyGems 3.5.15 support is dropped
    correct_array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys
    unless @@array_attributes == correct_array_attributes
      @@array_attributes = correct_array_attributes # rubocop:disable Style/ClassVars
    end

    require_relative "match_metadata"
    require_relative "match_platform"

    include ::Bundler::MatchMetadata

    attr_accessor :remote, :relative_loaded_from

    module AllowSettingSource
      attr_writer :source

      def source
        (defined?(@source) && @source) || super
      end
    end

    prepend AllowSettingSource

    alias_method :rg_full_gem_path, :full_gem_path
    alias_method :rg_loaded_from,   :loaded_from

    def full_gem_path
      if source.respond_to?(:root)
        File.expand_path(File.dirname(loaded_from), source.root)
      else
        rg_full_gem_path
      end
    end

    def loaded_from
      if relative_loaded_from
        source.path.join(relative_loaded_from).to_s
      else
        rg_loaded_from
      end
    end

    def load_paths
      full_require_paths
    end

    alias_method :rg_extension_dir, :extension_dir
    def extension_dir
      # following instance variable is already used in original method
      # and that is the reason to prefix it with bundler_ and add rubocop exception
      @bundler_extension_dir ||= if source.respond_to?(:extension_dir_name) # rubocop:disable Naming/MemoizedInstanceVariableName
        unique_extension_dir = [source.extension_dir_name, File.basename(full_gem_path)].uniq.join("-")
        File.expand_path(File.join(extensions_dir, unique_extension_dir))
      else
        rg_extension_dir
      end
    end

    # Can be removed once RubyGems 3.5.21 support is dropped
    remove_method :gem_dir if method_defined?(:gem_dir, false)

    def gem_dir
      full_gem_path
    end

    def insecurely_materialized?
      false
    end

    def groups
      @groups ||= []
    end

    def git_version
      return unless loaded_from && source.is_a?(Bundler::Source::Git)
      " #{source.revision[0..6]}"
    end

    def to_gemfile(path = nil)
      gemfile = String.new("source 'https://rubygems.org'\n")
      gemfile << dependencies_to_gemfile(nondevelopment_dependencies)
      unless development_dependencies.empty?
        gemfile << "\n"
        gemfile << dependencies_to_gemfile(development_dependencies, :development)
      end
      gemfile
    end

    def nondevelopment_dependencies
      dependencies - development_dependencies
    end

    def installation_missing?
      !default_gem? && !File.directory?(full_gem_path)
    end

    def lock_name
      @lock_name ||= name_tuple.lock_name
    end

    unless VALIDATES_FOR_RESOLUTION
      def validate_for_resolution
        SpecificationPolicy.new(self).validate_for_resolution
      end
    end

    if Gem.rubygems_version < Gem::Version.new("3.5.22")
      module FixPathSourceMissingExtensions
        def missing_extensions?
          return false if %w[Bundler::Source::Path Bundler::Source::Gemspec].include?(source.class.name)

          super
        end
      end

      prepend FixPathSourceMissingExtensions
    end

    private

    def dependencies_to_gemfile(dependencies, group = nil)
      gemfile = String.new
      if dependencies.any?
        gemfile << "group :#{group} do\n" if group
        dependencies.each do |dependency|
          gemfile << "  " if group
          gemfile << %(gem "#{dependency.name}")
          req = dependency.requirements_list.first
          gemfile << %(, "#{req}") if req
          gemfile << "\n"
        end
        gemfile << "end\n" if group
      end
      gemfile
    end
  end

  unless VALIDATES_FOR_RESOLUTION
    class SpecificationPolicy
      def validate_for_resolution
        validate_required!
      end
    end
  end

  module BetterPermissionError
    def data
      super
    rescue Errno::EACCES
      raise Bundler::PermissionError.new(loaded_from, :read)
    end
  end

  require "rubygems/stub_specification"

  class StubSpecification
    prepend BetterPermissionError
  end

  class Dependency
    require_relative "force_platform"

    include ::Bundler::ForcePlatform

    attr_reader :force_ruby_platform

    attr_accessor :source, :groups

    alias_method :eql?, :==

    unless method_defined?(:encode_with, false)
      def encode_with(coder)
        [:@name, :@requirement, :@type, :@prerelease, :@version_requirements].each do |ivar|
          coder[ivar.to_s.sub(/^@/, "")] = instance_variable_get(ivar)
        end
      end
    end

    def to_lock
      out = String.new("  #{name}")
      unless requirement.none?
        reqs = requirement.requirements.map {|o, v| "#{o} #{v}" }.sort.reverse
        out << " (#{reqs.join(", ")})"
      end
      out
    end

    if Gem.rubygems_version < Gem::Version.new("3.5.22")
      module FilterIgnoredSpecs
        def matching_specs(platform_only = false)
          super.reject(&:ignored?)
        end
      end

      prepend FilterIgnoredSpecs
    end
  end

  # On universal Rubies, resolve the "universal" arch to the real CPU arch, without changing the extension directory.
  class BasicSpecification
    if /^universal\.(?<arch>.*?)-/ =~ (CROSS_COMPILING || RUBY_PLATFORM)
      local_platform = Platform.local
      if local_platform.cpu == "universal"
        ORIGINAL_LOCAL_PLATFORM = local_platform.to_s.freeze

        local_platform.cpu = if arch == "arm64e" # arm64e is only permitted for Apple system binaries
          "arm64"
        else
          arch
        end

        def extensions_dir
          @extensions_dir ||=
            Gem.default_ext_dir_for(base_dir) || File.join(base_dir, "extensions", ORIGINAL_LOCAL_PLATFORM, Gem.extension_api_version)
        end
      end
    end

    # Can be removed once RubyGems 3.5.22 support is dropped
    unless new.respond_to?(:ignored?)
      def ignored?
        return @ignored unless @ignored.nil?

        @ignored = missing_extensions?
      end
    end

    # Can be removed once RubyGems 3.6.9 support is dropped
    unless new.respond_to?(:installable_on_platform?)
      include(::Bundler::MatchPlatform)
    end
  end

  require "rubygems/name_tuple"

  class NameTuple
    # Versions of RubyGems before about 3.5.0 don't to_s the platform.
    unless Gem::NameTuple.new("a", Gem::Version.new("1"), Gem::Platform.new("x86_64-linux")).platform.is_a?(String)
      alias_method :initialize_with_platform, :initialize

      def initialize(name, version, platform = Gem::Platform::RUBY)
        if Gem::Platform === platform
          initialize_with_platform(name, version, platform.to_s)
        else
          initialize_with_platform(name, version, platform)
        end
      end
    end

    def lock_name
      if platform == Gem::Platform::RUBY
        "#{name} (#{version})"
      else
        "#{name} (#{version}-#{platform})"
      end
    end
  end

  unless Gem.rubygems_version >= Gem::Version.new("3.5.19")
    class Resolver::ActivationRequest
      remove_method :installed?

      def installed?
        case @spec
        when Gem::Resolver::VendorSpecification then
          true
        else
          this_spec = full_spec

          Gem::Specification.any? do |s|
            s == this_spec && s.base_dir == this_spec.base_dir
          end
        end
      end
    end
  end

  unless Gem.rubygems_version >= Gem::Version.new("3.6.7")
    module UnfreezeCompactIndexParsedResponse
      def parse(line)
        version, platform, dependencies, requirements = super
        [version, platform, dependencies.frozen? ? dependencies.dup : dependencies, requirements.frozen? ? requirements.dup : requirements]
      end
    end

    Resolver::APISet::GemParser.prepend(UnfreezeCompactIndexParsedResponse)
  end

  # RubyGems before 4.0.13 split compact index dependency/requirement entries
  # on every colon, which mangles metadata values that contain colons such as
  # the `created_at` timestamps the cooldown feature relies on. Split only on
  # the first colon so those values survive on older RubyGems.
  #
  # The module is defined unconditionally so it stays testable on any RubyGems,
  # but only prepended when the host RubyGems still has the buggy behavior.
  module SplitCompactIndexEntryOnFirstColon
    private

    def parse_dependency(string)
      dependency = string.split(":", 2)
      dependency[-1] = dependency[-1].split("&") if dependency.size > 1
      dependency[0] = -dependency[0]
      dependency
    end
  end

  unless Gem.rubygems_version >= Gem::Version.new("4.0.13")
    Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon)
  end

  if Gem.rubygems_version < Gem::Version.new("3.6.0")
    class Package; end
    require "rubygems/package/tar_reader"
    require "rubygems/package/tar_reader/entry"

    module FixFullNameEncoding
      def full_name
        super.force_encoding(Encoding::UTF_8)
      end
    end

    Package::TarReader::Entry.prepend(FixFullNameEncoding)
  end
end
