#!/usr/bin/ruby -I/usr/share/apt-listbugs
#
# apt-listbugs: retrieves bug reports and lists them
#
# Copyright (C) 2002  Masato Taruishi <taru@debian.org>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License with
#  the Debian GNU/Linux distribution in file /usr/share/common-licenses/GPL;
#  if not, write to the Free Software Foundation, Inc., 59 Temple Place,
#  Suite 330, Boston, MA  02111-1307  USA
#
#  $Id: apt-listbugs,v 1.21 2004/06/24 05:04:22 taru Exp $
#
=begin

== NAME

apt-listbugs - Lists critical bugs before each apt upgrade/installation

== SYNOPSIS

apt-listbugs [options] <command> [arguments]

== DESCRIPTION

apt-listbugs is a tool which retrieves bug reports from the Debian
Bug Tracking System and lists them. Especially, it is intended to
be invoked before each upgrade by apt in order to check whether
the upgrade/installation is safe. 

== USAGE

apt-listbugs [-h] [-s <severities>] [-S <stats>] [-l] [-g] [-D] [-H <hostname>] [-p <port>] [-R] [-f] [-c <dir>] [-t <minutes>] <command> [arguments]

== OPTIONS

* -h | --help

  Print usage help and exit

* -s <severities> | --severity <severities>

  Severities you want to see separeted by comma [critical,grave]

* -S <stats> | --stats <stats>

  Status you want to see separated by commna [outstanding,pending upload,resolved,done,open,]

* -l | --showless

  By default, apt-listbugs ignores bugs already existed in your system
  as many as possible. This option shows them, too.

* -g | --showgreater

  By default, apt-listbugs ignores newer bugs than upgrade packages
  as many as possible. This option shows them, too. If both -l and -g 
  specified, apt-listbugs doesn't access to cgi. These two options
  are currently enabled by default because bugs.debian.org rejects
  apt-listbugs.

* -D | --show-downgrade

  Shows bugs of downgraded packages. 

* -H <hostname> | --hostname <hostname>

  Specifies the hostname of Debian Bug Tracking System [osdn.debian.or.jp].

* -p <port> | --port <port>

  Specifies the port number of the web interface of Debian Bug Tracking System [80].

* -R | --release-critical

  Uses the daily release-critical bug report of Debian Bug Tracking System.

* -I | --index

  Uses the raw index.db of debbugs.

* -X | --indexdir

  Specifies indexdir [/~taru/apt-listbugs/].

* -P | --pin-priority

  Specifies Pin-Priority value [1000].

* -T | --title

  Specifies the title of rss output.

* -f | --force-download

  Ignores the stored local cache and retrieve fresh bugs.

* -q | --quiet

  Don't display progress bar.

* -c <cache_dir> | --cache-dir <cache_dir>

  Specifies the directory of the local cache [/var/cache/apt-listbugs/].

* -t <minutes> | --timer <minutes>

  Specifies the expire timer of the cache [60].

* -C <apt.conf> | --aptconf <apt.conf>

  Specifies the apt configuration file to use.

== COMMANDS

* apt

  Reads filenames from standard input (typically provided by apt).

* list [<package1> <package2>...]

  Reads package names from the arguments and simply lists bugs of these
  packages.

* rss [<package1> <package2>...]

  Reads package names from the arguments and lists bugs of these packages
  in rss format.

== BUGS

Note that apt-listbugs can't probe all the critical bug reports
that really applies to the package of the version. This means
that some bugs are listed
because of a conservative reason even if the bugs don't actually apply
to the version. You need to review the bug.

Currently, cgi access doesn't work completely because bugs.debian.org
rejects apt-listbugs. I don't enable this until debbugs supports
complete ldap support.

== AUTHOR

apt-listbugs was written by Masato Taruishi <taru@debian.org>.

== SEE ALSO

apt.conf(5), sensible-browser(1), www-browser(1), querybts(1)

=end

$VERSION = "$Id: apt-listbugs,v 1.21 2004/06/24 05:04:22 taru Exp $"

require 'getoptlong'
require 'debian'
require 'debian/bug'
require 'debian/bts'
require 'thread'
require 'tempfile'
require 'intl'

$intl = Intl.new("apt-listbugs")

module Debug

  def debug (msg)
    $stderr.puts msg if $DEBUG
  end

end


# ad-hoc
require 'debian/mytempfile'
class HtmlTempfile < MyTempfile
  def _tmpname(basename,tmpdir,n)
    sprintf('%s/%s%d.%d.html', tmpdir, basename, $$, n)
  end
end

class Config

  QUERYBTS = "/usr/bin/querybts"
  WWW_BROWSER = "/usr/bin/www-browser"
  SENSIBLE_BROWSER = "/usr/bin/sensible-browser"

  def usage
    $stderr.print $intl._("Usage: "), File.basename($0),
      $intl._(" [options] <command> [arguments]"),
      "\n",
      $intl._("Options:\n"),
      $intl._(" -h               : Display this help and exit.\n"),
      sprintf($intl._(" -s <severities>  : Severities you want to see [%s].\n"), @severity.join(',')),
      sprintf($intl._(" -S <stats>       : Stats you want to see [%s].\n"), @stats.join(',')),
      $intl._(" -l               : Show bugs already existed in your system.\n"),
      $intl._(" -g               : Show newer bugs than upgrade packages.\n"),
      $intl._(" -D               : Show downgraded packages, too.\n"),
      sprintf($intl._(" -H <hostname>    : Hostname of Debian Bug Tracking System [%s].\n"), @hostname),
      sprintf($intl._(" -p <port>        : Port number of the server [%s]\n"), @port),
      $intl._(" -R               : Use Release-Critical bug reports\n"),
      $intl._(" -I               : Use debbugs index.db\n"),
      sprintf($intl._(" --indexdir       : Directory where index.db located [%s]\n"), @indexdir),
      sprintf($intl._(" --pin-priority   : Specifies Pin-Priority value [%s]\n"), @pin_priority),
      $intl._(" -T               : Specifies the title of rss output.\n"),
      $intl._(" -f               : Retrieve bug reports from BTS forcibly.\n"),
      $intl._(" -q               : Don't display progress bar.\n"),
      sprintf($intl._(" -c <dir>         : Specify cache_dir [%s].\n"), @cache_dir),
      $intl._(" -t <minutes>     : Specify cache expire timer in minutes") + "[#{60}].\n",
      $intl._(" -C <apt.conf>    : Specify apt.conf\n"),
#      $intl._(" -L               : Use LDAP\n"),
#      $intl._(" -d               : Debug.\n"),
      $intl._("Commands:\n"),
      $intl._(" apt              : apt mode\n"),
      $intl._(" list <pkg...>    : list bug reports of the specified packages\n"), 
      $intl._(" rss <pkg...>     : list bug reports of the specified packages in rss\n"), 
      $intl._("See the manual page for the long options.\n")
  end

  def initialize
    @severity = ["critical", "grave"]
    @stats = ["outstanding", "pending upload", "resolved", "done", "open", ""]
    # FIXME: disabling cgi access 
    @showless = true
    @showgreater = true
    @show_downgrade = false
    # FIXME: temporarl default server
    @hostname = "osdn.debian.or.jp"
    @port = 80
    @use_local_cache = true
    @quiet = false
    @command = nil

    @index = true
    @release_critical = false
    @security = false

    @parser = nil
    @extra_parser = nil
    @querybts = nil

    @ignore_bugs = read_ignore_bugs("/etc/apt/listbugs/ignore_bugs")
    @frontend = ConsoleFrontend.new
    @cache_dir = "/var/cache/apt-listbugs/"
    if ! FileTest.writable?( @cache_dir ) then
      @cache_dir = "#{ENV["HOME"]}/.apt-listbugs/cache"
    end
    @cache_expire_timer = 60 * 60
    @indexdir = "/~taru/apt-listbugs/"
    @pin_priority = "1000"
    @apt_conf = nil

  end

  attr_accessor :severity, :stats, :showless, :showgreater, :quiet, :title
  attr_accessor :show_downgrade, :hostname, :release_critical
  attr_accessor :use_local_cache, :frontend, :pin_priority
  attr_reader :command, :parser, :extra_parser, :querybts, :ignore_bugs, :browser

  def parse_options
    opt_parser = GetoptLong.new
    opt_parser.set_options(['--help', '-h', GetoptLong::NO_ARGUMENT],
			   ['--severity', '-s', GetoptLong::REQUIRED_ARGUMENT],
			   ['--stats', '-S', GetoptLong::REQUIRED_ARGUMENT],
			   ['--showless', '-l', GetoptLong::NO_ARGUMENT],
			   ['--showgreater', '-g', GetoptLong::NO_ARGUMENT],
			   ['--show-downgrade', '-D', GetoptLong::NO_ARGUMENT],
			   ['--hostname', '-H', GetoptLong::REQUIRED_ARGUMENT],
			   ['--port', '-p', GetoptLong::REQUIRED_ARGUMENT],
			   ['--release-critical', '-R', GetoptLong::NO_ARGUMENT],
			   ['--index', '-I', GetoptLong::NO_ARGUMENT],
			   ['--indexdir', '-X', GetoptLong::REQUIRED_ARGUMENT],
			   ['--pin-priority', '-P', GetoptLong::REQUIRED_ARGUMENT],
                           ['--security', '-A', GetoptLong::NO_ARGUMENT],
			   ['--title','-T', GetoptLong::REQUIRED_ARGUMENT],
			   ['--force-download', '-f', GetoptLong::NO_ARGUMENT],
			   ['--quiet', '-q', GetoptLong::NO_ARGUMENT],
			   ['--cache-dir', '-c', GetoptLong::REQUIRED_ARGUMENT],
			   ['--aptconf', '-C', GetoptLong::REQUIRED_ARGUMENT],
			   ['--ldap', '-L', GetoptLong::NO_ARGUMENT],
			   ['--timer', '-t', GetoptLong::REQUIRED_ARGUMENT],
			   ['--debug', '-d', GetoptLong::NO_ARGUMENT]
			   );
    
    ldap = false
    begin
      opt_parser.each_option do |optname, optargs|
	case optname
	when '--help'
	  usage
	  exit 0
	when '--severity'
	  if @release_critical == true
	    STDERR.puts $intl._("can't be used -s and -R together")
	    exit 1
	  end
	  @severity = optargs.split(',')
	when '--stats'
	  @stats = optargs.split(',')
	when '--showless'
	  @showless = true
	when '--showgreater'
	  @showgreater = true
	when '--show-downgrade'
	  @show_downgrade = true
	when '--hostname'
	  @hostname = optargs
	when '--port'
	  @port = optargs.to_i
	when '--release-critical'
	  @release_critical = true
	  @severity = ["release-critical"]
	when '--index'
	  @index = true
        when '--security'
          @security = true
          @severity = ["debian-security-announce"]
	when '--indexdir'
	  @indexdir = optargs
	when '--pin-priority'
	  @pin_priority = optargs
	when '--title'
	  @title = optargs
	when '--force-download'
	  @use_local_cache = false
	when '--quiet'
	  @quiet = true
	when '--cache-dir'
	  @cache_dir = optargs
	when '--timer'
	  @cache_expire_timer = optargs.to_i * 60
	when '--aptconf'
	  @apt_conf = " -c " + optargs
	when '--ldap'
	  ldap = true
	when '--debug'
	  $DEBUG = 1
	end
      end
    rescue GetoptLong::AmbigousOption, GetoptLong::NeedlessArgument,
	GetoptLong::MissingArgument, GetoptLong::InvalidOption
      usage
      exit 1
    end
 
    @title = "Debian Bugs of #{Socket.gethostname} (#{@severity.join(', ')})" if ! @title

    # http_proxy check
    if ENV["http_proxy"] == nil &&
      /http_proxy='(.*)'/ =~ `apt-config #{@apt_conf} shell http_proxy acquire::http::proxy`; then
      puts $1 if $DEBUG
      ENV["http_proxy"] = $1
    end

    # command 
    command = ARGV.shift
    case command
    when nil
      STDERR.puts $intl._("E: You need to specify a command.")
      usage
      exit 1
    when "list"
      @command = "list"
    when "apt"
      @command = "apt"
    when "rss"
      @command = "rss"
    else
      STDERR.puts $intl._("E: Unknown command ") +  "'#{command}'."
      usage
      exit 1
    end

    if version_check == true || @command == "rss"
      http_acquire =
	Debian::BTS::Acquire::HTTP.new(@hostname, @port, @cache_dir, @cache_expire_timer)
      @extra_parser = Debian::BTS::Parser::CGI.new(http_acquire)

      if @index == true
	@parser =
	  Debian::BTS::Parser::Index.new(http_acquire, @indexdir)
      else
	@parser = @extra_parser
      end
      if @use_local_cache == false
	@extra_parser.acquire.use_cache = false
      end
    end

    if ldap == true
      @parser = Debian::BTS::LDAP.new(@hostname, 
					 @use_local_cache,
					 @cache_dir,
					 @cache_expire_timer)
    else
      if @release_critical == true
	http_acquire =
	  Debian::BTS::Acquire::HTTP.new(@hostname, @port, @cache_dir, @cache_expire_timer) if @extra_parser == nil
	@parser =
          Debian::BTS::Parser::ReleaseCritical.new(http_acquire)
      elsif @security == true
        http_acquire = 
            Debian::BTS::Acquire::HTTP.new(@hostname, @port, @cache_dir, @cache_expire_timer) if @extra_parser == nil
        @parser = 
          Debian::BTS::Parser::DSA.new(http_acquire)
      else
	if @extra_parser == nil
	  http_acquire =
	    Debian::BTS::Acquire::HTTP.new(@hostname, @port, @cache_dir, @cache_expire_timer)
	  if @index == true
	    @parser =
	      Debian::BTS::Parser::Index.new(http_acquire, @indexdir)	    
	  else
	    @parser =
	      Debian::BTS::Parser::CGI.new(http_acquire)
	  end
	end
      end

      if @use_local_cache == false
	@parser.acquire.use_cache = false
#	@extra_parser.acquire.use_cache = false if @extra_parser != nil
      end
    end

    http_acquire.user_agent = "apt-listbugs/#{$VERSION}"

    if FileTest.executable?("#{QUERYBTS}")
      @querybts = QUERYBTS
    end

    if FileTest.executable?("#{SENSIBLE_BROWSER}")
      @browser = SENSIBLE_BROWSER
    else
      @browser = WWW_BROWSER
    end

  end

  def read_ignore_bugs(path)
    ignore_bugs = IgnoreBugs.new(path)
  end

  def version_check
      if @showless == true && @showgreater == true
	false
      else
	true
      end
  end
end

class IgnoreBugs < Array

  @@path_mutex = {}

  def initialize(path)
    super()
    @path = path
    @@path_mutex[path] = Mutex.new if @@path_mutex[path] == nil

    if FileTest.exist?(path)
      open(path).each do |bug|
        if /\s*#/ =~ bug
	  next
        end
        if /\s*(\S+)/ =~ bug
	  self << $1
        end
      end
    end

  end

  def add(entry)
    @@path_mutex[@path].synchronize {
      open(@path, "a") { |file|
	file.puts entry
      }
    }
    self << entry
  end

end

class Viewer

  def initialize(config)
    @config = config
  end

  class SimpleViewer < Viewer

    DeprecatedWarning = $intl._("********** on_hold IS DEPRECATED. USE p INSTEAD to use pin **********")
    DeprecatedWarningHeader = "*" * DeprecatedWarning.length

    def view(new_pkgs, cur_pkgs, bugs)

      if display_bugs(bugs, new_pkgs.keys, cur_pkgs, new_pkgs) == false
        return true
      end

      if @config.command == "list"
	return true
      end

      answer = "n"
      hold_pkgs = []
      while true
	ask_str = $intl._("Are you sure to install/upgrade these packages?")
	if @config.querybts != nil || @config.browser != nil
	  if hold_pkgs.empty?
	    ask_str << " [Y/n/?/...] "
	  else
	    ask_str << "[N/?/...] "
	  end
	else
	  ask_str << " [Y/n] "
	end
	a = @config.frontend.ask ask_str
	if a == ""
	  if hold_pkgs.empty?
	    answer = "y"
	  else
	    answer = "n"
	  end
	else
	  answer = a.downcase
	end
	case answer
	when "y"
	  if hold_pkgs.empty?
	    bugs.each do |bug|
	      if ! @config.ignore_bugs.include?(bug.bug_number)
	        @config.ignore_bugs.add(bug)
	        @config.ignore_bugs.add(bug.bug_number)
	      end
	    end
	    return true
	  end
	when  "n"
	  return false
	when /^(\d+)$/
	  if @config.querybts != nil
	    system("#{@config.querybts} #{$1} < /dev/tty")
	  end
	when /^i\s+(\d+)$/
	  if ! @config.ignore_bugs.include?($1)
	    @config.ignore_bugs.add($1)
	    Factory::BugsFactory.delete_ignore_bugs(bugs)
	    @config.frontend.puts sprintf($intl._("%s ignored"), $1)
	  else
	    @config.frontend.puts sprintf($intl._("%s already ignored"), $1)
	  end
	when "r"
	  display_bugs(bugs, new_pkgs.keys - hold_pkgs, cur_pkgs, new_pkgs)

	when /^(h|p)\s+(.+)$/
	  key = $1
	  if key == "h"
	    @config.frontend.puts DeprecatedWarningHeader
	    @config.frontend.puts DeprecatedWarning
	    @config.frontend.puts DeprecatedWarningHeader
	  end
	  pkgs = $2.split(/\s+/)
	  if key == "h"
	    h = on_hold(pkgs)
	  else
	    h = pinned(pkgs, cur_pkgs, bugs)
	  end
	  hold_pkgs.concat(h) if h != nil

	when "w"
	  puts bugs if $DEBUG
	  display_bugs_as_html(bugs, cur_pkgs.keys - hold_pkgs, cur_pkgs, new_pkgs) if @config.browser != nil

        when /(h|p)/
	  key = $1
	  if key == "h"
	    @config.frontend.puts DeprecatedWarningHeader
	    @config.frontend.puts DeprecatedWarning
	    @config.frontend.puts DeprecatedWarningHeader
	  end
	  pkgs = {}
	  if key == "p"
	    bugs.each do |bug|
	      ## FIXME: need to parse preferences correctly?
	      if ! system("grep -q \"Package: #{bug.pkg_name}\" /etc/apt/preferences 2> /dev/null")
	        pkgs[bug.pkg_name] = 1
	      end
	    end
	  else
            bugs.each do |bug|
              pkgs[bug.pkg_name] = 1
            end
	  end
	  if pkgs.size != 0
	    if @config.frontend.yes_or_no? sprintf($intl._("The following %s packages will be pinned or on hold:\n %s\nAre you sure "), pkgs.size, pkgs.keys.join(', '))
	      if key == "h"
                h = on_hold(pkgs.keys)
              else
                h = pinned(pkgs.keys, cur_pkgs, bugs)
              end
            end
	    hold_pkgs.concat(h) if h != nil
	  else
	    @config.frontend.puts sprintf($intl._("Every packages already pinned or on hold. Ignoring %s command."), key)
	  end
	else
	  if hold_pkgs.empty?
	    @config.frontend.puts $intl._("     y     - continue the apt installation.\n")
	  end
	  @config.frontend.puts "" +
	    $intl._("     n     - stop the apt installation.\n") +
	    $intl._("   <num>   - query the specified bug number (uses querybts).\n") +
	    $intl._("     r     - redisplay bug lists.\n") +
	    $intl._(" p <pkg..> - make pkgs pinned: need to restart apt to enable.\n") +
	    $intl._(" p         - make all the above pkgs pinned. need to restart.\n") +
	    $intl._(" i <num>   - make bug_number <num> ignored.\n") + 
	    $intl._("     ?     - print this help.\n")
	  if @config.browser != nil
	    @config.frontend.puts sprintf($intl._("     w     - display bug lists in html (uses %s).\n"), File.basename(@config.browser))
	  end
	end
      end
    end

    def bugs_of_pkg( bugs, pkg )
      b = []
      bugs.each do |bug|
        b << bug if bug.pkg_name == pkg
      end
      b
    end

    def pinned(pkgs, cur_pkgs, bugs)
      holdstr = ""
      pkgs.each do |pkg|
        if cur_pkgs[pkg] == nil
	  @config.frontend.puts sprintf($intl._("Newly instllation package '%s' ignored"), pkg)
	  next
	end
	holdstr << "
Explanation: Pinned by apt-listbugs at #{Time.now}"
        bugs_of_pkg( bugs, pkg ).each do |bug|
	  holdstr << "
Explanation:   ##{bug.bug_number}: #{bug.desc.gsub("'","\\'")}"
        end
	holdstr << "
Package: #{pkg}
Pin: version #{cur_pkgs[pkg]['version']}
Pin-Priority: #{@config.pin_priority}
"
      end
      $stderr.puts holdstr if $DEBUG
      File.open("/etc/apt/preferences", "a") { |io|
        io.puts holdstr
	@config.frontend.puts sprintf($intl._("%s pinned by adding Pin preferences in /etc/apt/preferences. You need to restart apt to enable"), pkgs.join(' '))
	return pkgs
      }
      return nil
    end

    def on_hold (pkgs)
      holdstr = ""
      pkgs.each do |pkg|
        holdstr << "#{pkg} hold\n"
      end
      if system("echo '#{holdstr}' | dpkg --set-selections")
        @config.frontend.puts sprintf($intl._("%s held: you need to restart apt to enable"), pkgs.join(' '))
        return pkgs
      end
      return nil
    end

    def display_bugs(bugs, pkgs, cur_pkgs, new_pkgs)
      p_bug_numbers = []
      bugs_statistics = {}
      @config.stats.each do |stat|
	@config.severity.each do |severity|
	  pkgs.each do |pkg|
	    bug_exist = 0
	    bugs_statistics[pkg] = 0 unless bugs_statistics[pkg]
	    bugs.each_by_category(pkg, severity, stat) do |bug|
	      next if p_bug_numbers.include?(bug.bug_number)
	      bugs_statistics[pkg] += 1
	      p_bug_numbers << bug.bug_number
	      if bug_exist == 0
		buf = sprintf($intl._("%s bugs of %s ("), severity, pkg)
		buf += "#{cur_pkgs[pkg]['version']} " if cur_pkgs[pkg] != nil
		buf += "-> #{new_pkgs[pkg]['version']}) <#{bug.stat}>"
		@config.frontend.puts buf
		bug_exist = 1
	      end
	      bug_str = " ##{bug.bug_number} - #{bug.desc}"
	      @config.frontend.puts bug_str
	      if bug.mergeids.size > 0
		bug_str =  $intl._("   Merged with:")
		bug.mergeids.each do |m|
		  bug_str << " #{m}"
		  p_bug_numbers << m
		end
		@config.frontend.puts bug_str
	      end
	    end
	  end
	end
      end
      stat_str_ary = []
      bugs_statistics.each do |pkg, num|
	if num > 0
	  if num > 1
	    buf = sprintf($intl._("%s(%s bugs)"), pkg, num)
	  else
	    buf = sprintf($intl._("%s(%s bug)"), pkg, num)
	  end
	  stat_str_ary << buf
	end
      end
      if stat_str_ary.size > 0
	@config.frontend.puts $intl._("Summary:\n ") + stat_str_ary.join(', ')
	return true
      else
        return false
      end
    end

    def each_state_table(o, bugs, stats)
      stats.each do |stat|
	sub = bugs.sub("stat", stat)
	if sub.size > 0
	  o.puts "<table border=2 width=100%>"
	  o.puts sprintf($intl._(" <caption>Bug reports which are %s "), stat) + 
	    $intl._("in the latest versions</caption>")
	  o.puts $intl._(" <tr><th>package</th><th>severity</th><th>bug number</th><th>description</th></tr>")	  
	  yield sub
	  o.puts "</table><br>"
	end
      end
    end

    def display_bugs_as_html(bugs, pkgs, cur_pkgs, new_pkgs)
      bug_exist_for_stat = 0
      bug_exist_for_pkg = 0
      bug_exist = 0
      displayed_pkgs = []

      tmp = HtmlTempfile.new("apt-listbugs")

      tmp.puts $intl._("<html><head><title>critical bugs for your upgrade</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=ISO-8859-1\"></head><body>")
      tmp.puts $intl._("<h1 align=\"center\">Critical bugs for your upgrade</h1>")
      tmp.puts $intl._("<p align=\"right\">by apt-listbugs</p><hr>")
      tmp.puts $intl._("<h2>Bug reports</h2>")

      each_state_table(tmp, bugs, @config.stats) do |bugs|
	bugs.each do |bug|
	  tmp.puts "<tr><td>#{bug.pkg_name}</td><td>#{bug.severity}</td><td><a href=\"http://bugs.debian.org/cgi-bin/bugreport.cgi?archive=no&bug=#{bug.bug_number}\">##{bug.bug_number}</a></td><td>#{bug.desc}</td></tr>"
	  displayed_pkgs << bug.pkg_name if !displayed_pkgs.include?(bug.pkg_name)
	end
      end

      tmp.puts $intl._("<h2>Package upgrade information in question</h2>")
      tmp.puts "<ul>"
      displayed_pkgs.each do |pkg|
	tmp.puts "<li>#{pkg}("
	tmp.puts "#{cur_pkgs[pkg]['version']} " if cur_pkgs[pkg] != nil
	tmp.puts "-&gt #{new_pkgs[pkg]['version']})"
      end
      tmp.puts "</ul>"

      tmp.puts "</body></html>"
      tmp.close

      puts "Invoking www-browswer for #{tmp.path}" if $DEBUG
      system("#{@config.browser} #{tmp.path} < /dev/tty")

    end
  end


  class RSSViewer < Viewer

    def initialize(config)
      super(config)
      @rss_start = <<EOF
<?xml version="1.0"?>

<rdf:RDF 
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns="http://purl.org/rss/1.0/"
>
  <channel>
    <title>#{config.title}</title>
    <link>http://bugs.debian.org/</link>
    <description>#{config.title}</description>
  </channel>
EOF

      @rss_end = <<EOF

</rdf:RDF>
EOF
    end

    def encode(str)
      buf = str.gsub("<", "&lt;")
      buf
    end

    def view(new_pkgs, cur_pkgs, bugs)
      @config.frontend.puts @rss_start
      bugs.each do |bug|
        if @config.stats.include?( bug.stat )
          url = "http://bugs.debian.org/cgi-bin/bugreport.cgi?archive=no&amp;bug=#{bug.bug_number}"
	  buf = "  <item rdf:about=\"#{url}\">\n"
	  buf << "    <title>" << encode(bug.desc) << "</title>\n"
	  buf << "    <link>#{url}</link>\n"
#	  buf << "    <description>&lt;pre&gt;#{@config.extra_parser.first_contact(bug)}&lt;/pre&gt;</description>\n"
	  buf << "    <description>\n"
	  buf << "      &lt;ul&gt;\n"
	  buf << "        &lt;li&gt;Bug##{bug.bug_number}&lt;/li&gt;\n"
	  buf << "        &lt;li&gt;Package: #{bug.pkg_name}&lt;/li&gt;\n"
	  buf << "        &lt;li&gt;Severity: #{bug.severity}&lt;/li&gt;\n"
	  buf << "        &lt;li&gt;Status: #{bug.stat}&lt;/li&gt;\n"
	  buf << "        &lt;li&gt;Tags: #{bug.tags.join(',')}&lt;/li&gt;\n" if bug.tags != nil
	  if bug.mergeids.size > 0
	    buf << "        &lt;li&gt;Merged with:\n"
	    bug.mergeids.each do |id|
	      url  = "http://bugs.debian.org/cgi-bin/bugreport.cgi?archive=no&amp;bug=#{id}"
	      buf << "          &lt;a href=\"#{url}\"&gt;#{id}&lt;/a&gt;"
	    end
	    buf << "\n        &lt;/li&gt;\n"
	  end
	  buf << "      &lt;/ul&gt;\n"
	  buf << "    </description>\n"
          buf << "    <dc:subject>#{bug.pkg_name}</dc:subject>\n"
	  buf << "  </item>"
	  @config.frontend.puts buf
	end
      end
      @config.frontend.puts @rss_end
    end
    
  end

end


module Factory

  Done = $intl._("Done") 
  CONCURRENCY_LEVEL = 3

  def done?(done)
    Done == done
  end

  def config
      @@config
  end

  def config=(c)
      @@config = c
  end

  def create(arg, *args)
    raise $intl._("Not Implemented")
  end

  module_function :config, :config=, :create, :done?
  public :create

  module PackageFactory
    extend Factory

    module ListFactory
      include Factory
      def field(pkgname)
	f = {}
	f["package"] = pkgname
	f
      end
      module_function :field
    end

    module AptFactory
      include Factory
      DPKG = "/usr/bin/dpkg"
      def field(pkgpath)
	# TODO: this is now very slow
	# field = Debian::Dpkg.field(pkg)
	field = {}
	f =  `#{DPKG} -I #{pkgpath}`
	f.each do |line|
	  case line
	  when /Package: (.*)/
	    field["package"] = $1
	  when /Version: (.*)/
	    field["version"] = $1
	  end
	end
	closes = []
	chfile = nil
	if /-/ =~ field["version"]
	  chfile = "changelog.Debian.gz"
	else
	  chfile = "changelog.gz"
	end
	changelog = `/usr/bin/dpkg-deb --fsys-tarfile #{pkgpath} | tar -x -O ./usr/share/doc/#{field["package"]}/#{chfile} -f - 2> /dev/null | gzip -d 2> /dev/null`
#	puts changelog if $DEBUG	
	changelog.each_line do |line|
#	  if /(C|c)loses:\s+((Bug)?((#\d+)(\s*,\s*(#\d+))*))/ =~ line
	  if /closes:\s*((?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*)/i =~ line
	    c = $1.split(/,\s*/)
	    c.each do |bug|
	      bug.gsub!(/\D/, '')
	    end
	    closes.concat c
	  end
	end
	field["closes"] = closes
	field
      end

      module_function :field

    end

    def create(pkgnames, *args)
      # now reading package fields
      step = 100.0 / pkgnames.size.to_f
      pkgs = {}
      reading = $intl._("Reading package fields...")
      pkgnames.each_index do |index|
	if block_given?
	  yield reading, (index * step).to_i.to_s + "%"
	end
	pkgnames[index].chomp!
	f = nil
	case config.command
	when "apt"
	  f = AptFactory.field(pkgnames[index])
	when "list"
	  f = ListFactory.field(pkgnames[index])
	when "rss"
	  f = ListFactory.field(pkgnames[index])
	else
	  raise $intl._("Not Implemented")
	end
	pkgs[f["package"]] = f
      end
      if block_given?
	yield reading, Done
      end
      pkgs
    end

    def delete_ignore_pkgs(new_pkgs)
      new_pkgs.delete_if { |name, pkg|
	config.ignore_bugs.include?(name)
      }
    end

    module_function :create, :delete_ignore_pkgs

  end

  module StatusFactory
    extend Factory

    ReadStatusMsg = $intl._("Reading package status...")

    def create(pkgs, *args)
      # creating status database
      if block_given?
	yield ReadStatusMsg, "0%"
      end
      pkgres = []
      pkgs.each_key do |pkg|
	pkgres << Regexp.quote(pkg)
      end
      i=0
      max=pkgres.size
      step=(pkgres.size/100)*10+1
      status = Debian::Packages.new(Debian::Dpkg::STATUS_FILE, pkgres) do
	yield ReadStatusMsg, 
          "#{(i.to_f/max.to_f*100).to_i}%" if (i % step) == 0
	i += 1
      end
      if block_given?
	yield ReadStatusMsg, Done
      end
      status
    end

    def delete_downgraded(cur_pkgs, new_pkgs)
      new_pkgs.delete_if { |name, pkg|
	val = false
	ver = pkg["version"]
	cur_ver = cur_pkgs[name]["version"] if cur_pkgs[name] != nil
	if ver != nil && cur_ver != nil
	  val = true if Debian::Dpkg.compare_versions(ver, "lt", cur_ver)
	end
	val
      }
    end

    module_function :create, :delete_downgraded

  end

  module BugsFactory
    extend Factory

    RetrvReleaseMsg = $intl._("Retrieving release-critical bug reports...")
    RetrvExtMsg = $intl._("Retrieving extra bug reports...")
    RetrvBTSMsg = $intl._("Retrieving bug reports...")
    ErrorLimit = 5

    def create(new_pkgs, *args, &progress)
      cur_pkgs = args[0]
      bugs = Debian::Bugs.new
      pkg_step = 100 / new_pkgs.size.to_f
      if config.release_critical == true
	if block_given?
	  yield RetrvReleaseMsg, ""
	end
	bugs = config.parser.parse(nil, config.severity)
	bugs.delete_if { |bug|
	  val = true
	  new_pkgs.each_key do |pkg_name|
	    if pkg_name == bug.pkg_name
	      val = false
	      break
	    end
	  end
	  val
	}
	if block_given?
	  yield RetrvReleaseMsg, Done
	end
      else
        error = []
	i = 0.0
	size = new_pkgs.size
	mutex = Mutex.new
	threads = []
	yield RetrvBTSMsg, "0% [0/#{size}]"
        new_pkgs.each_key do |pkg|
	  threads << Thread.new {
	    begin
	      bug = config.parser.parse(pkg, config.severity)
	      mutex.synchronize {
	        bugs.concat bug
	        if block_given?
	  	  percentage = (pkg_step * i).to_i
		  yield RetrvBTSMsg, percentage.to_s +
		    "% [#{i.to_i}/#{size}]"
	        end
	        i += 1
	      }
	    rescue
	      config.frontend.puts " W: #{$!}: #{pkg}"
	      error << pkg
	      raise $intl._("Too many errors while retrieving bug reports") if error.size > ErrorLimit
	      bug = []
	    end
	  }
          if threads.size > CONCURRENCY_LEVEL
            threads.each do |thread|
              if thread.status == false
                thread.join
                threads.delete(thread)
              end
            end
          end
	  threads.shift.join if threads.size > CONCURRENCY_LEVEL
        end
	threads.each do |thread|
	  thread.join
	end
        if block_given?
          yield RetrvBTSMsg, Done
	  if error.size > 0
	    config.frontend.puts $intl._("Bugs of the following packages couldn't be fetched:")
	    config.frontend.puts " " + error.join(' ')
	    config.frontend.puts $intl._("Assuming that there is no bugs of these packages.")
	    raise $intl._("Exiting with error") if ! config.frontend.yes_or_no?($intl._("Are you sure"))
	  end
        end
      end
      bugs
    end

    def delete_ignore_bugs(bugs)
      # ignoring ignore_bugs
      bugs.delete_if { |bug| config.ignore_bugs.include?(bug.bug_number) }
    end

    def read_extra_bug_reports(bugs)
      # reading extra_bug_reports
      size = bugs.size
      i = 0.0
      if size != 0
	threads = []
	mutex = Mutex.new
	bug_step = 100.0 / size.to_f
	yield RetrvExtMsg, "0% [0/#{size}]"
	bugs.each_index do |bug_index|

	  threads << Thread.new {
	    if config.extra_parser
	      config.extra_parser.submit_version(bugs[bug_index])
	    end

	    mutex.synchronize {
	      if block_given?
		percentage = (bug_step * i).to_i
		yield RetrvExtMsg, percentage.to_s +
		  "% [#{i.to_i}/#{size}]"
	      end
	      i += 1
	    }
	  }
          if threads.size > CONCURRENCY_LEVEL
            threads.each do |thread|
              if thread.status == false
                thread.join
                threads.delete(thread)
              end
            end
          end
	  threads.shift.join if threads.size > CONCURRENCY_LEVEL
	end
	threads.each do |thread|
	  thread.join
	end
	if block_given?
	  yield RetrvExtMsg, Done
	end
      end
    end

    def delete_threshold_bugs (bugs, cur_pkgs, new_pkgs)
      # ignoring threshold bugs
      bugs.delete_if { |bug|
	val = false
	name = bug.pkg_name
	new_ver = nil
	cur_ver = nil
	new_ver = new_pkgs[name]["version"] if new_pkgs[name] != nil
	cur_ver = cur_pkgs[name]["version"] if cur_pkgs[name] != nil
	submit_ver = config.extra_parser.submit_version(bug)
	if submit_ver != nil
	  if new_ver != nil && config.showgreater == false
	    puts "#{bug} since #{submit_ver}. New Ver: #{new_ver}" if $DEBUG
	    if Debian::Dpkg.compare_versions(submit_ver, "gt", new_ver)
	      val = true
	    end
	  end
	  if val == false && cur_ver != nil && config.showless == false
	    puts "#{bug} since #{submit_ver}. Cur Ver: #{cur_ver}" if $DEBUG
	    if Debian::Dpkg.compare_versions(submit_ver, "le", cur_ver)
	      val = true
	    end
	  end
	end
	val
      }

      bugs
    end

    def delete_resolved_bugs (bugs, new_pkgs)
      new_pkgs.each do |pkg_name, new_pkg|
	bugs.delete_if { |bug|
	  val = false
	  if new_pkg["closes"] != nil
	    if new_pkg["closes"].include?(bug.bug_number)
	      puts "#{bug.bug_number} is closed by #{pkg_name}" if $DEBUG
	      val = true
	    else
	      # TODO: handle repoen
	      bug.mergeids.each do |mergedbugid|
		if new_pkg["closes"].include?(mergedbugid)
		  puts "#{bug.bug_number} is merged with #{mergedbugid} which is closed by #{pkg_name}" if $DEBUG
		  val = true
		end
	      end
	    end
	  end
	  val
	}
      end
    end
    module_function :create, :delete_ignore_bugs,
      :read_extra_bug_reports, :delete_threshold_bugs,
      :delete_resolved_bugs

  end

end

class ConsoleFrontend

  def initialize
    @tty = nil
    @old = ""
  end

  def progress(msg, val)
    $stderr.print "\r"
    $stderr.print " " * @old.length
    $stderr.print "\r"
    @old = "#{msg} #{val}"
    $stderr.print @old
    $stderr.flush
    $stderr.puts "" if Factory.done?(val)
  end

  def puts(msg)
    $stdout.puts msg
  end
  
  def ask(msg)
    $stdout.print "#{msg} "
    @tty = open("/dev/tty") if @tty == nil
    line = @tty.gets
    if line != nil
      line.chomp!
    end
    return line
  end

  def yes_or_no?(msg, default = true)
    while true
      if default == true
	msg << "[Y/n]?"
      else
	msg << "[y/N]?"
      end
      a = ask msg
      if a == ""
	return default
      elsif a == "Y" || a == "y"
	return true
      elsif a == "N" || a == "n"
	return false
      end
    end
  end

  def close
    @tty.close if @tty
  end

end

## main from here

trap("INT") { $stderr.puts "Interrupted"; exit(0) }

# handle options
config = Config.new
config.parse_options
Factory.config = config

# handle arguments
pkgnames = []
holdpkgs = {}
case config.command
when "apt"
  STDIN.each do |pkg|
    pkgnames << pkg
  end
when "list"
  ARGV.each do |pkg|
    pkgnames << pkg
  end
when "rss"
  ARGV.each do |pkg|
    pkgnames << pkg
  end
end

exit 0 if pkgnames.size == 0

# creating new packages database
new_pkgs = Factory::PackageFactory.create(pkgnames) do |msg, val|
  config.frontend.progress(msg, val) if config.quiet == false
end
Factory::PackageFactory.delete_ignore_pkgs(new_pkgs)

# exitting if no new packages is found
exit 0 if new_pkgs.size == 0

# creating current packages database
cur_pkgs = Factory::StatusFactory.create(new_pkgs) do |msg, val|
  config.frontend.progress(msg, val) if config.quiet == false
end
if config.show_downgrade == false
  Factory::StatusFactory.delete_downgraded(cur_pkgs, new_pkgs)
end

# reading bug reports
begin
  bugs = Factory::BugsFactory.create(new_pkgs, cur_pkgs) do |msg, val|
    config.frontend.progress(msg, val) if config.quiet == false
  end
rescue
  config.frontend.puts " ... E: #{$!}"
  exit 10
end

Factory::BugsFactory.delete_ignore_bugs(bugs)
Factory::BugsFactory.delete_resolved_bugs(bugs, new_pkgs)
begin
if config.version_check == true
  Factory::BugsFactory.read_extra_bug_reports(bugs) do |msg, val|
    config.frontend.progress(msg, val)
  end
  Factory::BugsFactory.delete_threshold_bugs(bugs, cur_pkgs, new_pkgs)
end
rescue
  config.frontend.puts " ... E: #{$!}"
  exit 1
end

exit 0 if config.command != "rss" && bugs.size == 0

# read done. now starting viewer
viewer = nil
case config.command
when "apt"
  viewer = Viewer::SimpleViewer.new(config)
when "list"
  viewer = Viewer::SimpleViewer.new(config)
when "rss"
  viewer = Viewer::RSSViewer.new(config)
end
if viewer.view(new_pkgs, cur_pkgs, bugs) == false

  ErrorWarning =  $intl._("****** Exit with an error by force in order to stop the installation. ******")
  ErrorWarningHeader = "*" * ErrorWarning.length
  config.frontend.puts ErrorWarningHeader
  config.frontend.puts ErrorWarning
  config.frontend.puts ErrorWarningHeader
  config.frontend.close
  exit 10
end
config.frontend.close
