require 'fileutils'
require 'open-uri'

##
# multiruby_setup is a script to help you manage multiruby.
#
# usage: multiruby_setup [-h|cmd|spec...]
#
#   cmds:
#
#     -h, --help, help = show this help.
#     build            = build and install everything. used internally.
#     clean            = clean scm build dirs and remove non-scm build dirs.
#     list             = print installed versions.
#     rm:$version      = remove a particular version.
#     rubygems:merge   = symlink all rubygem dirs to one dir.
#     tags             = list all tags from svn.
#     update           = update svn builds.
#     update:rubygems  = update rubygems and nuke install dirs.
#
#   specs:
#
#     the_usual              = alias for latest versions from tar + rubygems
#     mri:svn:current        = alias for mri:svn:releases and mri:svn:branches.
#     mri:svn:releases       = alias for supported releases of mri ruby.
#     mri:svn:branches       = alias for active branches of mri ruby.
#     mri:svn:branch:$branch = install a specific $branch of mri from svn.
#     mri:svn:tag:$tag       = install a specific $tag of mri from svn.
#     mri:tar:$version       = install a specific $version of mri from tarball.
#
#   environment variables:
#
#     GEM_URL  = url for rubygems tarballs
#     MRI_SVN  = url for MRI SVN
#     RUBY_URL = url for MRI tarballs
#     VERSIONS = what versions to install
#
#     RUBYOPT is cleared on installs.
#
# NOTES:
#
# * you can add a symlink to your rubinius build into ~/.multiruby/install
# * I need patches/maintainers for other implementations.
#
module Multiruby
  def self.env name, fallback; ENV[name] || fallback; end # :nodoc:

  TAGS     = %w(    1_8_7 1_9_1 1_9_2)
  BRANCHES = %w(1_8 1_8_7 1_9 trunk)

  VERSIONS = env('VERSIONS', TAGS.join(":").gsub(/_/, '.')).split(/:/)
  MRI_SVN  = env 'MRI_SVN',  'http://svn.ruby-lang.org/repos/ruby'
  RUBY_URL = env 'RUBY_URL', 'http://ftp.ruby-lang.org/pub/ruby'
  GEM_URL  = env 'GEM_URL',  'http://files.rubyforge.vm.bytemark.co.uk/rubygems'

  HELP = []

  File.readlines(__FILE__).each do |line|
    next unless line =~ /^#( |$)/
    HELP << line.sub(/^# ?/, '')
  end

  def self.build_and_install
    ENV.delete 'RUBYOPT'

    root_dir = self.root_dir
    versions = []

    Dir.chdir root_dir do
      self.setup_dirs

      rubygems = Dir["versions/rubygems*.tgz"]
      abort "You should delete all but one rubygem tarball" if rubygems.size > 1
      rubygem_tarball = File.expand_path rubygems.last rescue nil

      Dir.chdir "build" do
        Dir["../versions/*"].sort.each do |tarball|
          next if tarball =~ /rubygems/

          build_dir = File.basename tarball, ".tar.gz"
          version = build_dir.sub(/^ruby-?/, '')
          inst_dir = "#{root_dir}/install/#{version}"

          unless test ?d, inst_dir then
            unless test ?d, build_dir then
              if test ?d, tarball then
                dir = File.basename tarball
                FileUtils.ln_sf "../versions/#{dir}", "../build/#{dir}"
              else
                puts "creating #{inst_dir}"
                Dir.mkdir inst_dir
                run "tar zxf #{tarball}"
              end
            end
            Dir.chdir build_dir do
              puts "building and installing #{version}"
              if test ?f, "configure.in" then
                gnu_utils_build inst_dir
              elsif test ?f, "Rakefile" then
                rake_build inst_dir
              else
                raise "dunno how to build"
              end

              if rubygem_tarball and version !~ /1[._-]9|mri_trunk|rubinius/ then
                rubygems = File.basename rubygem_tarball, ".tgz"
                run "tar zxf #{rubygem_tarball}" unless test ?d, rubygems

                Dir.chdir rubygems do
                  run "../ruby ./setup.rb --no-rdoc --no-ri", "../log.rubygems"
                end
              end
            end
          end
        end
      end

      versions = Dir["install/*"].map { |path| File.basename path }
    end

    versions
  end

  def self.clean
    self.each_scm_build_dir do |style|
      case style
      when :svn then
        if File.exist? "Rakefile" then
          run "rake clean"
        elsif File.exist? "Makefile" then
          run "make clean"
        end
      else
        FileUtils.rm_rf Dir.pwd
      end
    end
  end

  def self.each_scm_build_dir
    Multiruby.in_build_dir do
      Dir["*"].each do |dir|
        next unless File.directory? dir
        Dir.chdir dir do
          if File.exist?(".svn") then
            yield :svn
          else
            yield :none
          end
        end
      end
    end
  end

  def self.matching_versions url, matching=nil
    file = URI.parse(url).read

    map = {
      "preview" => "beta",
      "rc"      => "beta2",
      "p"       => "release",
      "tar"     => "aargh",
      "gz"      => "aargh",
    }

    versions = file.scan(/href="(ruby.*tar.gz)"/).flatten.sort_by { |s|
      s.scan(/\d+|[a-z]+/).map { |a| Integer(a) rescue map[a] || a }
    }

    versions = versions.grep(/#{Regexp.escape(matching)}/) if matching

    versions
  end

  def self.fetch_tar v
    in_versions_dir do
      warn "  Determining latest version for #{v}"
      ver = v[/\d+\.\d+/]
      base = matching_versions("#{RUBY_URL}/#{ver}/", v).last
      abort "Could not determine release for #{v}" unless base
      url = File.join RUBY_URL, ver, base
      unless File.file? base then
        warn "    Fetching #{base} via HTTP... this might take a while."
        open(url) do |f|
          File.open base, 'w' do |out|
            out.write f.read
          end
        end
      end
    end
  end

  def self.gnu_utils_build inst_dir
    run "autoconf" unless test ?f, "configure"
    run "./configure --enable-shared --prefix #{inst_dir}", "log.configure" unless
      test ?f, "Makefile"
    run "(nice make -j4 main; nice make main)", "log.build"
    run "make install-nodoc", "log.install"
  end

  def self.help
    puts HELP.join
  end

  def self.in_build_dir
    in_root_dir "build" do
      yield
    end
  end

  def self.in_install_dir
    in_root_dir "install" do
      yield
    end
  end

  def self.in_root_dir subdir = ""
    Dir.chdir File.join(self.root_dir, subdir) do
      yield
    end
  end

  def self.in_tmp_dir
    in_root_dir "tmp" do
      yield
    end
  end

  def self.in_versions_dir
    in_root_dir "versions" do
      yield
    end
  end

  def self.list
    puts "Known versions:"
    in_install_dir do
      Dir["*"].sort.each do |d|
        puts "  #{d}"
      end
    end
  end

  def self.merge_rubygems
    in_install_dir do
      gems = Dir["*/lib/ruby/gems"]

      unless test ?d, "../gems" then
        FileUtils.mv gems.first, ".."
      end

      gems.each do |d|
        FileUtils.rm_rf d
        FileUtils.ln_sf "../../../../gems", d
      end
    end
  end

  def self.mri_latest_tag v
    Multiruby.tags.grep(/#{v}/).last
  end

  def self.rake_build inst_dir
    run "rake", "log.build"
    FileUtils.ln_sf "../build/#{File.basename Dir.pwd}", inst_dir
  end

  def self.rm name
    Multiruby.in_root_dir do
      FileUtils.rm_rf Dir["*/#{name}"]
      f = "versions/ruby-#{name}.tar.gz"
      File.unlink f if test ?f, f
    end
  end

  def self.root_dir
    root_dir = File.expand_path(ENV['MULTIRUBY'] ||
                                File.join(ENV['HOME'], ".multiruby"))

    unless test ?d, root_dir then
      puts "creating #{root_dir}"
      Dir.mkdir root_dir, 0700
    end

    root_dir
  end

  def self.run base_cmd, log = nil
    cmd = base_cmd
    cmd += " > #{log} 2>&1" if log
    puts "Running command: #{cmd}"
    raise "ERROR: Command failed with exit code #{$?}" unless system cmd
  end

  def self.setup_dirs download = true
    %w(build install versions tmp).each do |dir|
      unless test ?d, dir then
        puts "creating #{dir}"
        Dir.mkdir dir
        if dir == "versions" && download then
          warn "  Downloading initial ruby tarballs to ~/.multiruby/versions:"
          VERSIONS.each do |v|
            self.fetch_tar v
          end
          warn "  ...done"
          warn "  Put other ruby tarballs in ~/.multiruby/versions to use them."
        end
      end
    end
  end

  def self.svn_co url, dir
    Multiruby.in_versions_dir do
      Multiruby.run "svn co #{url} #{dir}" unless File.directory? dir
      FileUtils.ln_sf "../versions/#{dir}", "../build/#{dir}"
    end
  end

  def self.tags
    tags = nil
    Multiruby.in_tmp_dir do
      cache = "svn.tag.cache"
      File.unlink cache if Time.now - File.mtime(cache) > 3600 rescue nil

      File.open cache, "w" do |f|
        f.write `svn ls #{MRI_SVN}/tags/`
      end unless File.exist? cache

      tags = File.read(cache).split(/\n/).grep(/^v/).reject {|s| s =~ /preview/}
    end

    tags = tags.sort_by { |t| t.scan(/\d+/).map { |s| s.to_i } }
  end

  def self.update
    # TODO:
    # update will look at the dir name and act accordingly rel_.* will
    # figure out latest tag on that name and svn sw to it trunk and
    # others will just svn update

    clean = []

    self.each_scm_build_dir do |style|
      dir = File.basename(Dir.pwd)
      warn dir

      case style
      when :svn then
        case dir
        when /mri_\d/ then
          system "svn cleanup" # just in case
          svn_up = `svn up`
          in_build_dir do
            if svn_up =~ /^[ADUCG] / then
              clean << dir
            else
              warn "  no update"
            end
            FileUtils.ln_sf "../build/#{dir}", "../versions/#{dir}"
          end
        when /mri_rel_(.+)/ then
          ver = $1
          url = `svn info`[/^URL: (.*)/, 1]
          latest = self.mri_latest_tag(ver).chomp('/')
          new_url = File.join(File.dirname(url), latest)
          if new_url != url then
            run "svn sw #{new_url}"
            clean << dir
          else
            warn "  no update"
          end
        else
          warn "  update in this svn dir not supported yet: #{dir}"
        end
      else
        warn "  update in non-svn dir not supported yet: #{dir}"
      end
    end

    in_install_dir do
      clean.each do |dir|
        warn "removing install/#{dir}"
        FileUtils.rm_rf dir
      end
    end
  end

  def self.update_rubygems
    warn "  Determining latest version for rubygems"
    html = URI.parse(GEM_URL).read

    versions = html.scan(/href="rubygems-update-(\d+(?:\.\d+)+).gem/i).flatten
    latest = versions.sort_by { |v| v.scan(/\d+/).map { |s| s.to_i } }.last

    Multiruby.in_versions_dir do
      file = "rubygems-#{latest}.tgz"
      unless File.file? file then
        warn "    Fetching rubygems-#{latest}.tgz via HTTP."
        File.unlink(*Dir["rubygems*"])
        File.open file, 'w' do |f|
          f.write URI.parse(GEM_URL+"/"+file).read
        end
      end
    end

    Multiruby.in_build_dir do
      FileUtils.rm_rf Dir["rubygems*"]
    end

    Multiruby.in_install_dir do
      FileUtils.rm_rf Dir["*"]
    end
  end
end
