#!/usr/bin/env ruby

require 'rbconfig'
require 'tempfile'
require 'fileutils'

root = File.expand_path File.dirname(__FILE__)

require File.join(root, "kernel", "delta", "options")

class Configure

  def initialize(root)
    @defines = []
    @config = File.join(root, "config.rb")

    # LLVM settings
    @llvm           = :no
    @llvm_path      = nil
    @llvm_configure = nil
    @llvm_svn_dir   = File.join(root, "vm", "external_libs", "llvm")

    # File system paths
    @bindir       = root + "/bin"
    @includedir   = root + "/vm/capi"
    @libdir       = root
    @runtime      = @libdir + "/runtime"
    @lib_path     = @libdir + "/lib"
    @ext_path     = @libdir + "/lib/ext"
    @mandir       = root + "/man"
    @gemsdir      = root + "/gems"

    # Some simple defaults for when running directly out of the build dir
    @sitedir      = @lib_path + "/site"
    @vendordir    = @lib_path + "/vendor"

    @program_name = "rbx"

    # Essential settings (modify these for creating releases)
    @libversion = "1.0"
    @version = "#{@libversion}.0"
    @release_date = "2010-05-14"
    @config_version = 7

    if !ENV['RELEASE'] and File.directory?(root + "/.git")
      @buildrev = `git rev-list --all | head -n1`.chomp
    else
      @buildrev = "release"
    end

    # TODO: conditionalize for Windows
    @host = `./rakelib/config.guess`.chomp
    /([^-]+)-([^-]+)-(.*)/ =~ @host
    @cpu, @vendor, @os = $1, $2, $3

    # TODO: add conditionals for platforms
    if Config::CONFIG["build_os"] =~ /darwin/
      @ldshared = "cc -dynamic -bundle -undefined suppress -flat_namespace"
    else
      @ldshared = "cc -shared"
    end
  end

  def expand(path)
    File.expand_path(path)
  end

  def options
    o = Rubinius::Options.new "Usage: configure [options]", 30
    o.left_align

    o.doc " LLVM settings"

    @use_llvm = true

    o.on "--disable-llvm", "Don't build with LLVM" do
      @use_llvm = false
    end

    o.on "--enable-llvm", "Enable llvm (default)" do
      # noop
    end

    @skip_system = false
    o.on "--skip-system", "Don't consider a system LLVM installation" do
      @skip_system = true
    end

    o.on "--llvm-path", "PATH", "Where to find LLVM" do |dir|
      @llvm_path = dir
    end

    o.on "--update-prebuilt", "Update prebuilt LLVM packages from the internet" do
      update_prebuilt
    end

    o.doc "\n File system paths for installing Rubinius"

    o.on "-P", "--prefix", "PATH", "Install Rubinius in subdirectories of PATH" do |dir|
      dir = File.expand_path dir

      if !ENV['RELEASE'] and File.directory? dir and dir !~ /(rubinius|rbx)\/?$/
        old = dir
        dir += "/rubinius/#{@libversion}"
        puts "The directory #{old} already exists, installing to #{dir}"
      end

      @bindir       = dir + "/bin"
      @includedir   = dir + "/include"
      @libdir       = dir
      @runtime      = @libdir + "/runtime"
      @lib_path     = @libdir + "/lib"
      @ext_path     = @libdir + "/lib/ext"
      @mandir       = dir + "/man"
      @gemsdir      = dir + "/gems"

      @sitedir      = dir + "/site"
      @vendordir    = dir + "/vendor"
    end

    o.on "-B", "--bindir", "PATH", "Install Rubinius executable in PATH" do |dir|
      @bindir = expand dir
    end

    o.on "-I", "--includedir", "PATH", "Install Rubinius C-API include files in PATH" do |dir|
      @includedir = expand dir
    end

    o.on "-L", "--libdir", "PATH", "Install Ruby library in PATH" do |dir|
      @libdir   = expand(dir) + "/rubinius/#{@libversion}"
      @runtime  = @libdir + "/runtime"
      @lib_path = @libdir + "/lib"
      @ext_path = @libdir + "/lib/ext"

      @sitedir   = @libdir + "/site"
      @vendordir = @libdir + "/vendor"
    end

    o.on "-M", "--mandir", "PATH", "Install man pages in PATH" do |dir|
      @mandir = expand dir
    end

    o.on "-G", "--gemsdir", "PATH", "Install gems in PATH" do |dir|
      @gemsdir = expand dir
    end

    o.on "--sitedir", "PATH", "Where site specific ruby code goes" do |dir|
      @sitedir = expand dir
    end

    o.on "--vendordir", "PATH", "Where vendor specific ruby code goes" do |dir|
      @vendordir = expand dir
    end

    o.doc "\n Optional features"

    @options = o

    @features = {}

    feature "execinfo"

    o.doc "\n Help!"

    o.on "--show", "Print the current configuration and exit" do
      print_debug
      exit 0
    end

    o.on "-V", "--verbose", "Print additional info" do
      @verbose = true
    end

    o.help

    o.doc ""

  end

  def feature(name, on_by_default=true)
    @features[name] = on_by_default

    @options.on "--with-#{name}", "Enable #{name}" do
      @features[name] = true
    end

    @options.on "--without-#{name}", "Disable #{name}" do
      @features[name] = false
    end
  end

  def parse(ary)
    @options.parse ary
  end

  require 'digest/md5'

  def md5_checksum(md5_path, full_path)
    md5 = Digest::MD5.new

    File.open full_path do |file|
      until file.eof?
        md5 << file.read(1024)
      end
    end

    return md5.hexdigest == File.read(md5_path).split(" ").first
  end

  require 'net/http'

  def download(url, full_path)
    begin

     if ENV['http_proxy']
        protocol, userinfo, host, port  = URI::split(ENV['http_proxy'])
        proxy_user, proxy_pass = userinfo.split(/:/) if userinfo
        http = Net::HTTP::Proxy(host, port, proxy_user, proxy_pass)
      else
        http = Net::HTTP
      end

      http.get_response(URI(url)) do |res|
        return false if res.is_a?(Net::HTTPClientError)
        size, total = 0, res.header['Content-Length'].to_i
        File.open full_path, "w" do |f|
          res.read_body do |chunk|
            f << chunk
            size += chunk.size
            print "\r    [ %d%% (%d of %d) ]" % [(size * 100) / total, size, total]
          end
        end
        puts ": done!"
      end
    rescue Exception => e
      File.unlink full_path if File.exists?(full_path)
      puts " ERROR"
      return false
    end

    return true
  end

  # Downloads a pre-built LLVM library for a platform if the file exists.  If
  # an MD5 checksum file exists for the library, the checksum of the library
  # is compared and the update fails if it does not match.  If no MD5 checksum
  # file exists, the library is used without check.
  def update_prebuilt
    file = "llvm-#{@host}.tar.bz2"
    full_path = "vm/external_libs/prebuilt/#{file}"
    md5_path = "#{full_path}.md5"

    dir = File.dirname full_path

    Dir.mkdir dir unless File.exists? dir

    url = "http://asset.rubini.us/prebuilt/#{file}"

    unless File.exists? full_path
      download url, full_path

      unless File.exists? full_path
        puts "ERROR. No #{file} available on server."
        return false
      end
    end

    md5_url = "#{url}.md5"
    download md5_url, md5_path

    if File.exists? md5_path
      unless md5_checksum md5_path, full_path
        puts "ERROR. #{file} was corrupted or MD5 checksum is outdated."
        return false
      else
        puts "    MD5 checksum for prebuilt LLVM verified."
      end
    else
      puts "   No MD5 checksum for #{file} available on server."
      puts "   Using LLVM library without checksum validation."
    end

    puts "    Prebuilt packages updated."
  end

  def setup_source
    url = "http://asset.rubini.us/prebuilt/llvm-2.6-source.tar.bz2"
    path = "vm/external_libs/prebuilt/llvm-2.6-source.tar.bz2"

    puts "  Downloading #{url}..."
    return false unless download(url, path)

    if File.exists?(path)
      print "  Unpacking prebuilt LLVM source: "
      system "cd vm/external_libs; tar xjf prebuilt/llvm-2.6-source.tar.bz2"
      puts "done!"

      if File.exists?("vm/external_libs/llvm/include")
        puts "  Code appears to be proper svn tree."
      else
        puts "  Code doesn't appear to be proper LLVM tree!"
        return false
      end

      @llvm = :svn
      return true
    end

  end

  def setup_prebuilt
    puts "  Checking for prebuilt LLVM build..."
    file = "vm/external_libs/prebuilt/llvm-#{@host}.tar.bz2"

    update_prebuilt unless File.exists?(file)

    if File.exists?(file)
      print "  Unpacking prebuilt LLVM for #{@host}: "
      system "cd vm/external_libs; mkdir llvm; cd llvm; tar xjf ../prebuilt/llvm-#{@host}.tar.bz2"
      puts "done!"
      @llvm = :prebuilt
      return true
    end

    return false
  end

  def setup_path
    print "Validating '#{@llvm_path}': "
    if File.directory? @llvm_path
      ["Release", "Debug"].each do |which|
        sub = File.join(@llvm_path, which, "bin")
        if File.directory? sub
          puts "Ok! Using #{which}"
          @llvm_configure = File.join(@llvm_path, which, "bin", "llvm-config")
          @llvm = :config
          return true
        end
      end

      puts "ERROR. Doesn't appear to be built already!"
      return false
    end

    puts "ERROR. Path doesn't exist."
    return false
  end

  def setup_auto
    print "  Checking for existing LLVM tree: "
    if File.directory?(@llvm_svn_dir)
      puts "found!"
      if File.exists?(File.join(@llvm_svn_dir, "Makefile.common"))
        @llvm = :svn
      else
        @llvm = :prebuilt
      end

      return
    else
      puts "not found."
    end

    # If they explicitly said where LLVM is, use that and fail hard.
    if @llvm_path
      unless setup_path
        puts "ABORT: Path '#{@llvm_path}' not a proper LLVM path"
        exit 1
      end

      return
    end

    unless @skip_system
      return if setup_config
    end

    return if setup_prebuilt
    return if setup_source

    puts "WARNING: Unable to configure for LLVM, disabling support."
    @use_llvm = false
  end

  def setup_config
    print "  Checking for 'llvm-config': "
    which = ENV['PATH'].split(":").find do |path|
      File.exists? File.join(path, "llvm-config")
    end

    if which
      config = File.join(which, "llvm-config")
      version = `#{config} --version`.strip
      parts = version.sub(/svn$/, "").split(".").map { |i| i.to_i }

      # 2.6svn is an unknown beast, don't trust it.
      if version == "2.6svn" or parts[0] < 2 or parts[1] < 6
        puts "too old of a version"
      elsif parts[0] != 2 or parts[1] != 6
        puts "only 2.6 is supported"
      else
        puts "found! (version #{version})"
        @llvm_configure = config
        @llvm = :config
        return true
      end
    else
      puts "not found"
    end

    false
  end

  def c_includes
    str = []
    if File.exists? "/usr/local/include"
      str << "-I/usr/local/include"
    end

    if File.exists? "/opt/local/include"
      str << "-I/opt/local/include"
    end

    return str.join(" ")
  end

  def env(which)
    ENV[which] || ""
  end

  def has_function(name, includes=[])
    print "Checking for function '#{name}': "
    tf = Tempfile.new("rbx-test")
    includes.each do |i|
      tf.puts "#include <#{i}>"
    end

    tf.puts "int main() { void* ptr = &#{name}; }"

    tf.close

    `#{compiler} -S -o - -x c #{c_includes} #{env('CFLAGS')} #{tf.path} 2>&1`
    status = ($?.exitstatus == 0)

    tf.unlink

    if status
      puts "found!"
    else
      puts "not found."
    end

    return status
  end

  def detect_features
    if @features["execinfo"] and has_function("backtrace", ["execinfo.h"])
      @defines << "HAS_EXECINFO"
    end
  end

  def process
    if @use_llvm
      puts "Configuring LLVM..."
      setup_auto
    else
      print "WARNING: LLVM disabled."
    end

    puts

    detect_features
  end

  def compiler
    ENV['CC'] || "gcc"
  end

  def write_config
    unless @use_llvm
      @llvm = :no
      @llvm_configure = ""
    end

    File.open @config, "w" do |f|
      f.puts <<-EOC
module Rubinius
  BUILD_CONFIG = {
    :llvm           => :#{@llvm},
    :llvm_configure => "#{@llvm_configure}",
    :compiler       => "#{compiler()}",
    :defines        => #{@defines.inspect},
    :host           => "#{@host}",
    :cpu            => "#{@cpu}",
    :vendor         => "#{@vendor}",
    :os             => "#{@os}",
    :bindir         => "#{@bindir}",
    :libdir         => "#{@libdir}",
    :runtime        => "#{@runtime}",
    :lib_path       => "#{@lib_path}",
    :ext_path       => "#{@ext_path}",
    :includedir     => "#{@includedir}",
    :mandir         => "#{@mandir}",
    :gemsdir        => "#{@gemsdir}",
    :sitedir        => "#{@sitedir}",
    :vendordir      => "#{@vendordir}",
    :program_name   => "#{@program_name}",
    :version        => "#{@version}",
    :libversion     => "#{@libversion}",
    :release_date   => "#{@release_date}",
    :config_version => #{@config_version}
  }
end
      EOC
    end

    Dir.mkdir "lib/rubinius" unless File.directory? "lib/rubinius"
    FileUtils.cp @config, "lib/rubinius/build_config.rb"

    Dir.mkdir "vm/gen" unless File.directory? "vm/gen"

    File.open "vm/gen/config.h", "w" do |f|
      f.puts <<-EOC
#define RBX_HOST          "#{@host}"
#define RBX_CPU           "#{@cpu}"
#define RBX_VENDOR        "#{@vendor}"
#define RBX_OS            "#{@os}"
#define RBX_BIN_PATH      "#{@bindir}"
#define RBX_GEMS_PATH     "#{@gemsdir}"
#define RBX_RUNTIME       "#{@runtime}"
#define RBX_LIB_PATH      "#{@lib_path}"
#define RBX_EXT_PATH      "#{@ext_path}"
#define RBX_HDR_PATH      "#{@includedir}"
#define RBX_SITE_PATH     "#{@sitedir}"
#define RBX_VENDOR_PATH   "#{@vendordir}"
#define RBX_VERSION       "#{@version}"
#define RBX_LIB_VERSION   "#{@libversion}"
#define RBX_BUILD_REV     "#{@buildrev}"
#define RBX_LDSHARED      "#{@ldshared}"
#define RBX_RELEASE_DATE  "#{@release_date}"
      EOC
    end
  end

  def print_debug
    puts "\nUsing the following configuration to build"
    puts "------------------------------------------"
    system "cat config.rb"
    puts "\nSetting the following defines for the VM"
    puts "----------------------------------------"
    system "cat vm/gen/config.h"
  end

  def run
    options
    parse ARGV
    process
    write_config
    print_debug if @verbose
    puts "\nConfigured. Run 'rake' to build and run VM tests and rubyspecs"
  end
end

STDOUT.sync = true
Configure.new(root).run
