require "enumerator"

require 'reap/task'
require "reap/vendor/http-access2"

#  ___     _                    _____        _
# | _ \___| |___ __ _ ___ ___  |_   _|_ _ __| |__
# |   / -_) / -_) _` (_-</ -_)   | |/ _` (_-< / /
# |_|_\___|_\___\__,_/__/\___|   |_|\__,_/__/_\_\
#

# = Release Task
#
# This task releases files to RubyForge and other GForge instaces
# or SourceForge clones. In its most simple usage it looks like:
#
#   project = MetaProject::Project::XForge::RubyForge.new('xforge')
#   # Create a new release of the xforge project on Rubyforge.
#   task :release => [:gem] do
#     Rake::XForge::Release.new(project) {}
#   end
#
# The previous example will use defaults where it can. It will prompt
# you for your xForge user name and password before it uploads all
# gems under the pkg folder and creates a RubyForge release.
#
# While defaults are nice, you may want a little more control. You can
# specify additional attributes:
#
# * #user_name
# * #password
# * #changes_file
# * #version
# * #files
# * #release_name
# * #release_notes
# * #release_changes
# * #package_name
#
# Example:
#   project = MetaProject::Project::XForge::RubyForge.new('xforge')
#   task :release => [:gem] do
#     release_files = FileList[ 'pkg/*.gem', 'CHANGES' ]
#
#     Rake::XForge::Release.new(project) do |xf|
#       # Never hardcode user name and password in the Rakefile!
#       xf.user_name = ENV['RUBYFORGE_USER']
#       xf.password = ENV['RUBYFORGE_PASSWORD']
#       xf.files = release_files.to_a
#       xf.release_name = "XForge 0.1"
#   end
#
# This can be invoked from the command line:
#
#   rake release RUBYFORGE_USER=myuser \
#                RUBYFORGE_PASSWORD=mypassword
#
# If you don't like blocks, you can do like this:
#
#   project = MetaProject::Project::XForge::RubyForge.new('xforge')
#   task :release => [:gem] do
#     xf = Rake::XForge::Release.new(project)
#     ... # Set additional attributes
#     xf.execute
#   end

class Reap::Release < Reap::Task

  section_required true

  task_desc "Release distribution files."

  task_help %{

    reap release

    Release distribution to Rubyforge, or other
    GForge based service.

      host           URL of host service
      username       Username of host service
      project        Project name at host
      package        Package name
      groupid        Group id number
      architecture   Architecture (Any, i386, PPC, etc.)
      release        Release name (default is "%s")
      private        Private release?
      changelog      Change log file
      notelog        Release notes file
      dir            Distribution directory
      exclude        Distribution types to exclude

    The release option can be a template by using %s in the
    string. The version number of your project will be sub'd
    in for the %s. This saves you from having to update
    the release name before every release.

  }

  alias_method :rel, :task

  def run

    # setup

    rel.host      ||=  'rubyforge.org'
    rel.host.chomp!('/')

    rel.username  ||= master.rubyforge.username
    #rel.password  ||= master.rubyforge.password

    rel.project   ||= master.rubyforge.project || master.name
    rel.package   ||= master.rubyforge.package
    #rel.packageid ||= master.rubyforge.packageid
    rel.groupid   ||= master.rubyforge.groupid
    rel.private   ||= false
    rel.processor ||= 'Any'

    rel.date      ||= Time::now.strftime('%Y-%m-%d %H:%M')

    rel.exclude   ||= []
    rel.exclude << 'tar.gz' if rel.exclude.include?( 'tgz' )
    rel.exclude << 'tgz' if rel.exclude.include?( 'tar.gz' )
    rel.exclude << 'tar.bz2' if rel.exclude.include?( 'tbz' )
    rel.exclude << 'tbz' if rel.exclude.include?( 'tar.bz2' )

    rel.cookie_jar ||= File::join(File::expand_path("~"), ".rubyforge.cookie_jar")

    # do not inherit these

    rel.dir = section.dir || master.project.dir
    rel.release = (section.release || "%s") % (master.version || rel.date)

    # verify

    abort "missing field -- package"   unless rel.package
    #abort "missing field -- packageid" unless rel.packageid
    abort "missing field -- groupid"   unless rel.groupid
    # add more...

#     case rel.host
#     when 'rubyforge', 'rubyforge.org'
#     else
#       puts %{Unrecognized release host '#{rel.host}'. Skipped.}
#       skip = true
#     end

    # release

    puts "Reap is preparing release ..."

    rtypes = [ 'tgz', 'tbz', 'tar.gz', 'tar.bz2', 'deb', 'gem', 'ebuild' ]
    rtypes -= rel.exclude
    rtypes = rtypes.collect { |rt| Regexp.escape( rt ) }
    re_rtypes = Regexp.new( '[.](' << rtypes.join('|') << ')$' )

    dir = File.join( rel.dir, "#{rel.name}-#{rel.version}" )
    files = Dir.entries(dir).select { |f|
      f =~ re_rtypes or f == 'PKGBUILD'
    }
    files = files.collect { |f| File.join( dir, f ) }

   if files.empty?
     puts "No files to release at #{dir}."
     exit -1
   end

    # ask for password
    unless $PASSWORD
      print "Password for #{rel.username}: "
      until passwd = $stdin.gets.strip ; sleep 1 ; end
      @password = passwd
    else
      @password = $PASSWORD
    end

    login {

      unless package?
        puts "Package (#{rel.package}) does not exist."
        puts "Use -f option to force package creation."
        exit 0 unless $FORCE
        create_package
        puts "Created package -- #{rel.package}"
      end

      if release?
        unless $FORCE
          puts "Release (#{rel.release}) already exists."
          puts "Use -f option to force re-release."
          exit 0
        end
        files.each do |f|
          remove_file( f ) if file?( f )
          puts "Removed old file -- #{ f }"
          add_file( f )
          puts "Added new file   -- #{ f }"
        end
      else
        add_release( files.first )
        unless release?
          puts "Release creation failed -- #{rel.release}"
          exit -1
        end
        puts "Added release -- #{ rel.release }"
        puts "Added file -- #{ files.first }"
        files[1..-1].each do |f|
          add_file( f )
          puts "Added file -- #{ f }"
        end
      end

    }

    puts "Release complete!"
  end

private

  FILETYPES = {
    ".deb"     => 1000,
    ".rpm"     => 2000,
    ".zip"     => 3000,
    ".bz2"     => 3100,
    ".gz"      => 3110,
    ".src.zip" => 5000,
    ".src.bz2" => 5010,
    ".src.gz"  => 5020,
    ".src.rpm" => 5100,
    ".src"     => 5900,
    ".jpg"     => 8000,
    ".txt"     => 8100,
    ".text"    => 8100,
    ".htm"     => 8200,
    ".html"    => 8200,
    ".pdf"     => 8300,
    ".oth"     => 9999,
    ".ebuild"  => 1300,
    ".exe"     => 1100,
    ".dmg"     => 1200,
    ".tar.gz"  => 5000,
    ".tgz"     => 5000,
    ".gem"     => 1400,
    ".pgp"     => 8150,
    ".sig"     => 8150,
  }

  PROCESSORS = {
    "i386"       => 1000,
    "IA64"       => 6000,
    "Alpha"      => 7000,
    "Any"        => 8000,
    "PPC"        => 2000,
    "MIPS"       => 3000,
    "Sparc"      => 4000,
    "UltraSparc" => 5000,
    "Other"      => 9999,
  }

  # login

  def login( &block )
    # login
    page = "/account/login.php"
    method = "post_content"
    form = {
      "return_to"      => "",
      "form_loginname" => rel.username,
      "form_pw"        => @password,
      "login"          => "Login"
    }
    http_transaction( page, method, form )

    # do whatever
    block.call

    # logout
    page = "/account/logout.php"
    method = "post_content"
    form = {}
    http_transaction( page, method, form )
  end

  # Package exists?

  def package?
    page = "/frs/"
    method = "post_content"
    form = {
      "group_id"   => rel.groupid
    }
    scrape = http_transaction( page, method, form )

    restr = ''
    restr << Regexp.escape( rel.package )
    restr << '\s*'
    restr << Regexp.escape( %{<a href="/frs/monitor.php?filemodule_id=} )
    restr << '(\d+)'
    restr << Regexp.escape( %{&group_id=#{rel.groupid}} )
    re = Regexp.new( restr )

    md = re.match( scrape )
    if md
      rel.packageid = md[1]
    end
  end

  # Create a new package.

  def create_package
    page = "/frs/admin/index.php"
    method = "post_content"
    form = {
      "group_id"     => rel.groupid,
      "package_name" => rel.package,
      "func"         => "add_package",
      "is_public"    => (rel.private ? 0 : 1),
      "submit"       => "Create This Package"
    }
    http_transaction( page, method, form )
  end

  # Release exits?

  def release?
    page = "/frs/admin/showreleases.php"
    method = "post_content"
    form = {
      "package_id" => rel.packageid,
      "group_id"   => rel.groupid
    }
    scrape = http_transaction( page, method, form )

    restr = ''
    restr << Regexp.escape( %{"editrelease.php?group_id=#{rel.groupid}} )
    restr << Regexp.escape( %{&amp;package_id=#{rel.packageid}} )
    restr << Regexp.escape( %{&amp;release_id=} )
    restr << '(\d+)'
    restr << Regexp.escape( %{">#{rel.release}} )
    re = Regexp.new( restr )

    md = re.match( scrape )
    if md
      rel.releaseid = md[1]
    end
  end

  # Add a new release.

  def add_release( userfile )
    page = "/frs/admin/qrs.php"
    method = "post_content"

    type_id = rel.type || userfile[%r|\.[^\./]+$|]
    type_id = FILETYPES[type_id]
    proc_id = PROCESSORS[rel.processor]

    # how to use these?
    notes   = rel.notes ? rel.notes : ( rel.notelog ? open(rel.notelog) : nil )
    changes = rel.changes ? rel.changes : ( rel.changelog ? open(rel.changelog) : nil )

    userfile = open(userfile)

    preformatted = '1'

    form = {
      "group_id"     => rel.groupid,
      "package_id"   => rel.packageid,
      "release_name" => rel.release,
      "release_date" => rel.date,
      "type_id"      => type_id,
      "processor_id" => proc_id,
      "preformatted" => preformatted,
      "userfile"     => userfile,
      "submit"       => "Release File"
    }

    boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__')
    extheader = { 'content-type'=>"multipart/form-data; boundary=___#{ boundary }___" }

    http_transaction( page, method, form, extheader )
  end

  # Does file exist?

  # Note this is a little bit fragile.
  # If two releases have the same exact file name in them
  # there could be a problem --that's probably not likely,
  # maybe even impossible, but as of yet, I can't yet rule it out.

  def file?( file )
    page = "/frs/"
    method = "post_content"
    form = {
      "group_id"   => rel.groupid
    }
    scrape = http_transaction( page, method, form )

    restr = ''
    restr << Regexp.escape( rel.package )
    restr << '\s*'
    restr << Regexp.escape( %{<a href="/frs/download.php/} )
    restr << '(\d+)'
    restr << Regexp.escape( %{/#{file}} )
    re = Regexp.new( restr )

    md = re.match( scrape )
    if md
      rel.fileid = md[1]
    end
  end

  # Remove file from release.

  def remove_file( file )
    page="/frs/admin/editrelease.php"
    method = "post_content"
    form = {
      "group_id"     => rel.groupid,
      "package_id"   => rel.packageid,
      "release_id"   => rel.releaseid,
      "file_id"      => rel.fileid,
      "step3"        => "Delete File",
      "im_sure"      => '1',
      "submit"       => "Delete File "
    }

    http_transaction( page, method, form )
  end

  # Add file to release.

  def add_file( userfile )
    page = '/frs/admin/editrelease.php'
    method = "post_content"

    type_id = rel.type || userfile[%r|\.[^\./]+$|]
    type_id = FILETYPES[type_id]
    proc_id = PROCESSORS[rel.processor]

    userfile = open( userfile )

    form = {
      "group_id"     => rel.groupid,
      "package_id"   => rel.packageid,
      "release_id"   => rel.releaseid,
      "step2"        => '1',
      "userfile"     => userfile,
      "type_id"      => type_id,
      "processor_id" => proc_id,
      "submit"       => "Add This File"
    }

    boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__')
    extheader = { 'content-type'=>"multipart/form-data; boundary=___#{ boundary }___" }

    http_transaction( page, method, form, extheader )
  end

  # http transaction

  def http_transaction( page, method, form, extheader={} )
    client = HTTPAccess2::Client::new ENV['HTTP_PROXY']
    client.debug_dev = STDERR if ENV['DEBUG']
    client.set_cookie_store( rel.cookie_jar )
    # fixes http-access2 bug
    client.redirect_uri_callback = lambda do |res|
      page = res.header['location'].first
      page = page =~ %r/http/ ? page : "http://#{ rel.host }/#{ page }"
      page
    end
    page.sub!(/^\//, '')
    uri = "http://#{ rel.host }/#{ page }"
    response = client.send method, uri, form, extheader
    client.save_cookie_store
    return response
  end

end

# fixes http-access2 bug
BEGIN {
   require "reap/vendor/http-access2"
   module WebAgent::CookieUtils
     def domain_match(host, domain)
       case domain
       when /\d+\.\d+\.\d+\.\d+/
         return (host == domain)
       when '.'
         return true
       when /^\./
         #return tail_match?(domain, host)
         return tail_match?(host, domain)
       else
         return (host == domain)
       end
     end
   end
}