#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++

require 'fileutils'
require 'yaml'

require 'rubygems'
require 'rubygems/installer'
require 'rubygems/source_info_cache'

require 'sources'

module Gem
  class DependencyError < Gem::Exception; end
  class GemNotFoundException < Gem::Exception; end
  class RemoteInstallationCancelled < Gem::Exception; end
  class RemoteInstallationSkipped < Gem::Exception; end

  class RemoteInstaller

    include UserInteraction

    # <tt>options[:http_proxy]</tt>::
    # * [String]: explicit specification of proxy; overrides any
    #   environment variable setting
    # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, HTTP_PROXY_PASS)
    # * <tt>:no_proxy</tt>: ignore environment variables and _don't_
    #   use a proxy
    #
    # * <tt>:cache_dir</tt>: override where downloaded gems are cached.
    def initialize(options={})
      @options = options
      @source_index_hash = nil
    end

    # This method will install package_name onto the local system.  
    #
    # gem_name::
    #   [String] Name of the Gem to install
    #
    # version_requirement::
    #   [default = "> 0.0.0"] Gem version requirement to install
    #
    # Returns::
    #   an array of Gem::Specification objects, one for each gem installed. 
    #
    def install(gem_name, version_requirement = "> 0.0.0", force=false,
                install_dir=Gem.dir, install_stub=true)
      unless version_requirement.respond_to?(:satisfied_by?)
        version_requirement = Version::Requirement.new [version_requirement]
      end
      installed_gems = []
      begin
        spec, source = find_gem_to_install(gem_name, version_requirement)
        dependencies = find_dependencies_not_installed(spec.dependencies)

        installed_gems << install_dependencies(dependencies, force, install_dir)

        cache_dir = @options[:cache_dir] || File.join(install_dir, "cache")
        destination_file = File.join(cache_dir, spec.full_name + ".gem")

        download_gem(destination_file, source, spec)

        installer = new_installer(destination_file)
        installed_gems.unshift installer.install(force, install_dir, install_stub)
      rescue RemoteInstallationSkipped => e
        puts e.message
      end
      installed_gems.flatten
    end

    # Return a hash mapping the available source names to the source
    # index of that source.
    def source_index_hash
      return @source_index_hash if @source_index_hash
      @source_index_hash = {}
      Gem::SourceInfoCache.cache_data.each do |source_uri, sic_entry|
        @source_index_hash[source_uri] = sic_entry.source_index
      end
      @source_index_hash
    end
    
    # Finds the Gem::Specification objects and the corresponding source URI
    # for gems matching +gem_name+ and +version_requirement+
    def specs_n_sources_matching(gem_name, version_requirement)
      specs_n_sources = []

      source_index_hash.each do |source_uri, source_index|
        specs = source_index.search(/^#{Regexp.escape gem_name}$/i,
                                    version_requirement)
        # TODO move to SourceIndex#search?
        ruby_version = Gem::Version.new RUBY_VERSION
        specs = specs.select do |spec|
          spec.required_ruby_version.nil? or
            spec.required_ruby_version.satisfied_by? ruby_version
        end
        specs.each { |spec| specs_n_sources << [spec, source_uri] }
      end

      if specs_n_sources.empty? then
        raise GemNotFoundException, "Could not find #{gem_name} (#{version_requirement}) in any repository"
      end

      specs_n_sources = specs_n_sources.sort_by { |gs,| gs.version }.reverse

      specs_n_sources
    end

    # Find a gem to be installed by interacting with the user.
    def find_gem_to_install(gem_name, version_requirement)
      specs_n_sources = specs_n_sources_matching gem_name, version_requirement

      top_3_versions = specs_n_sources.map{|gs| gs.first.version}.uniq[0..3]
      specs_n_sources.reject!{|gs| !top_3_versions.include?(gs.first.version)}

      binary_gems = specs_n_sources.reject { |item|
        item[0].platform.nil? || item[0].platform==Platform::RUBY
      }

      # only non-binary gems...return latest
      return specs_n_sources.first if binary_gems.empty?

      list = specs_n_sources.collect { |spec, source_uri|
        "#{spec.name} #{spec.version} (#{spec.platform})"
      }

      list << "Skip this gem"
      list << "Cancel installation"

      string, index = choose_from_list(
        "Select which gem to install for your platform (#{RUBY_PLATFORM})",
        list)

      if index.nil? or index == (list.size - 1) then
        raise RemoteInstallationCancelled, "Installation of #{gem_name} cancelled."
      end

      if index == (list.size - 2) then
        raise RemoteInstallationSkipped, "Installation of #{gem_name} skipped."
      end

      specs_n_sources[index]
    end

    def find_dependencies_not_installed(dependencies)
      to_install = []
      dependencies.each do |dependency|
        srcindex = Gem::SourceIndex.from_installed_gems
        matches = srcindex.find_name(dependency.name, dependency.requirement_list)
        to_install.push dependency if matches.empty?
      end
      to_install
    end

    # Install all the given dependencies.  Returns an array of
    # Gem::Specification objects, one for each dependency installed.
    # 
    # TODO: For now, we recursively install, but this is not the right
    # way to do things (e.g.  if a package fails to download, we
    # shouldn't install anything).
    def install_dependencies(dependencies, force, install_dir)
      return if @options[:ignore_dependencies]
      installed_gems = []
      dependencies.each do |dep|
        if @options[:include_dependencies] ||
           ask_yes_no("Install required dependency #{dep.name}?", true)
          remote_installer = RemoteInstaller.new @options
          installed_gems << remote_installer.install(dep.name,
                                                     dep.version_requirements,
                                                     force, install_dir)
        elsif force then
          # ignore
        else
          raise DependencyError, "Required dependency #{dep.name} not installed"
        end
      end
      installed_gems
    end

    def download_gem(destination_file, source, spec)
      return if File.exist? destination_file
      uri = source + "/gems/#{spec.full_name}.gem"
      response = Gem::RemoteFetcher.fetcher.fetch_path uri
      write_gem_to_file response, destination_file
    end

    def write_gem_to_file(body, destination_file)
      FileUtils.mkdir_p(File.dirname(destination_file)) unless File.exist?(destination_file)
      File.open(destination_file, 'wb') do |out|
        out.write(body)
      end
    end
    
    def new_installer(gem)
      return Installer.new(gem, @options)
    end
  end

end
