require 'rubygems'
require 'gemika/errors'

module Gemika
  ##
  # Version switches to write code that works with different versions of
  # Ruby and gem dependencies.
  #
  module Env

    VERSION_PATTERN = /(?:\d+\.)*\d+/

    ##
    # Returns the path to the gemfile for the current Ruby process.
    #
    def gemfile
      if @gemfile_changed
        @process_gemfile
      else
        ENV['BUNDLE_GEMFILE']
      end
    end

    ##
    # Changes the gemfile to the given `path`, runs the given `block`, then resets
    # the gemfile to its original path.
    #
    # @example
    #   Gemika::Env.with_gemfile('gemfiles/Gemfile.rails3') do
    #     system('rspec spec') or raise 'RSpec failed'
    #   end
    #
    def with_gemfile(path, *args, &block)
      # Make sure that if block calls  #gemfile we still return the gemfile for this
      # process, regardless of what's in ENV temporarily
      @gemfile_changed = true
      @process_gemfile = ENV['BUNDLE_GEMFILE']

      # .with_clean_env is deprecated since Bundler ~> 2.
      bundler_method = if Gemika::Env.gem?('bundler', '< 2')
        :with_clean_env
      else
        :with_unbundled_env
      end

      Bundler.send(bundler_method) do
        ENV['BUNDLE_GEMFILE'] = path
        block.call(*args)
      end
    ensure
      @gemfile_changed = false
      ENV['BUNDLE_GEMFILE'] = @process_gemfile
    end

    ##
    # Check if the given gem was activated by the current gemfile.
    # It might or might not have been `require`d yet.
    #
    # @example
    #   Gemika::Env.gem?('activerecord')
    #   Gemika::Env.gem?('activerecord', '= 5.0.0')
    #   Gemika::Env.gem?('activerecord', '~> 4.2.0')
    #
    def gem?(*args)
      options = args.last.is_a?(Hash) ? args.pop : {}
      name, requirement_string = args
      if options[:gemfile] && !process_gemfile?(options[:gemfile])
        gem_in_gemfile?(options[:gemfile], name, requirement_string)
      else
        gem_activated?(name, requirement_string)
      end
    end

    ##
    # Returns the current version of Ruby.
    #
    def ruby
      RUBY_VERSION
    end

    ##
    # Check if the current version of Ruby satisfies the given requirements.
    #
    # @example
    #   Gemika::Env.ruby?('>= 2.1.0')
    #
    def ruby?(requirement)
      requirement_satisfied?(requirement, ruby)
    end

    ##
    # Return whether this process is running within a Github Actions build.
    def github?
      ENV.key?('GITHUB_WORKFLOW')
    end

    ##
    # Creates an hash that enumerates entries in order of insertion.
    #
    # @!visibility private
    #
    def new_ordered_hash
      # We use it when ActiveSupport is activated
      if ruby?('>= 1.9')
        {}
      elsif gem?('activesupport')
        require 'active_support/ordered_hash'
        ActiveSupport::OrderedHash.new
      else
        # We give up
        {}
      end
    end

    private

    def bundler?
      !gemfile.nil? && gemfile != ''
    end

    def process_gemfile?(given_gemfile)
      bundler? && File.expand_path(gemfile) == File.expand_path(given_gemfile)
    end

    def gem_activated?(name, requirement)
      gem = Gem.loaded_specs[name]
      if gem
        if requirement
          version = gem.version
          requirement_satisfied?(requirement, version)
        else
          true
        end
      else
        false
      end
    end

    def gem_in_gemfile?(gemfile, name, requirement = nil)
      lockfile = lockfile_contents(gemfile)
      if lockfile =~ /\b#{Regexp.escape(name)}\s*\((#{VERSION_PATTERN})\)/
        version = $1
        if requirement
          requirement_satisfied?(requirement, version)
        else
          true
        end
      else
        false
      end
    end

    def requirement_satisfied?(requirement, version)
      requirement = Gem::Requirement.new(requirement) if requirement.is_a?(String)
      version = Gem::Version.new(version) if version.is_a?(String)
      if requirement.respond_to?(:satisfied_by?) # Ruby 1.9.3+
        requirement.satisfied_by?(version)
      else
        ops = Gem::Requirement::OPS
        requirement.requirements.all? { |op, rv| (ops[op] || ops["="]).call version, rv }
      end
    end

    def lockfile_contents(gemfile)
      lockfile = "#{gemfile}.lock"
      File.exist?(lockfile) or raise MissingLockfile, "Lockfile not found: #{lockfile}"
      File.read(lockfile)
    end

    # Make all methods available as static module methods
    extend self

  end
end
