#!/usr/bin/env ruby
#
# Copyright (c) 2000-2001 Akinori MUSHA
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

RCS_ID = %q$Idaemons: /home/cvs/pkgtools/portversion,v 1.18 2001/06/22 13:58:57 knu Exp $
RCS_REVISION = RCS_ID.split[2]
MYNAME = File.basename($0)

require "optparse"
require "pkgtools"
require "thread"

$command_output = false
$command = 'portupgrade -cC'
$inv_limit_chars = nil
$limit_chars = nil
$origin = nil
$make_args = ""
$num_jobs = 4
$test_version = false

COLUMNSIZE = 24
NEXTLINE = "\n%*s" % [5 + COLUMNSIZE, '']

INEQ_SIGNS = [
  '<',
  '=',
  '>',
  '?',
  '!',
  '#',
]

SUGGESTIONS = [
  'needs updating (port has %s)',
  'up-to-date with port',
  'succeeds port (port has %s)',
  'origin unknown',
  'port directory not found',
  'port Makefile possibly broken',
]

def main(argv)
  usage = "usage: #{MYNAME} [-hv] [-j max_jobs] [-l limit_chars] [-L inv_limit_chars]\n" +
    "         [[-o origin] [-m make_args] pkgname_glob ...]\n" +
    "       #{MYNAME} [-v] -t ver0 ver1 [ver2 ...]"

  banner = <<-"EOF"
#{MYNAME} rev.#{RCS_REVISION}

#{usage}
    EOF

  OptionParser.new(banner, COLUMNSIZE) do |opts|
    opts.on("-c", "--command-output", "Enable command output") {
      |$command_output|
    }

    opts.on("-C", "--command-args=ARGS", "Specify the arguments for portupgrade in command#{NEXTLINE}output (default: -cC)") {
      |command_args|
      $command = 'portupgrade ' + command_args
    }

    opts.on("-h", "--help", "Show this message") {
      print opts
      return 0
    }

    opts.on("-j", "--jobs=N", "Specify the maximum number of jobs that are run#{NEXTLINE}simultaneously (default: 4)") {
      |num_jobs|
      begin 
	num_jobs = num_jobs.to_i
	if num_jobs <= 0
	  raise ArgumentError
	end
	$num_jobs = num_jobs
      rescue
	print opts
	return 0
      end
    }

    opts.on("-l", "--limit=CHARS", "Only include the packages with the specified#{NEXTLINE}status flags") {
      |$limit_chars|
    }

    opts.on("-L", "--inv-limit=CHARS", "Exclude the packages with the specified#{NEXTLINE}status flags") {
      |$inv_limit_chars|
    }

    opts.on("-m", "--make-args=ARGS", "Specify the arguments to pass to make(1)") {
      |$make_args|
    }

    opts.on("-o", "--origin=ORIGIN", "Specify the port to upgrade the following pkg with") {
      |$origin|
      $origin.sub!(%r`^#{Regexp.quote($ports_dir)}/`, '')
    }

    opts.on("-t", "--test=VERSION", "Compare the version with the following one(s),#{NEXTLINE}and print the result(s)") {
      |$test_version|
      $test_version = PkgVersion.new($test_version)
    }

    opts.on("-v", "--verbose", "Be verbose") {
      |$verbose|
    }

    opts.on_tail <<-"EOF".chomp

pkgname_glob is one of these: a full pkgname, a pkgname w/o version,
or a shell glob pattern in which you can use the wildcards *, ?, and [..].
If none is given, all the installed packages are checked.
      EOF

    dummy = '*'
    argv.push dummy

    begin
      opts.order(*argv) do |arg|
	next if arg.empty?

	unless arg.equal? dummy
	  dummy.replace ''
	end

	if $test_version
	  version = PkgVersion.new(arg)
	  x = $test_version <=> version
	  sign = INEQ_SIGNS[if x < 0 then 0 elsif x == 0 then 1 else 2 end]

	  if $verbose
	    puts [$test_version, sign, version].join(' ')
	  else
	    puts sign
	  end
      
	  next
	end

	pkgs = $pkgdb.glob(arg)

	if pkgs.empty?
	  warning_message "No such package '#{arg}' is found."
	  next
	end

	check_pkgs(pkgs)
      end
    rescue OptionParser::ParseError => e
      STDERR.puts "#{MYNAME}: #{e}", usage
      exit 64
    end

    if $command_output
      printf %Q`\nif [ X"$pkgs" != X"" ]; then\n  %s $pkgs\nfi\n`, $command
    end
  end

  0
end

def check_pkg(pkg, origin = nil)
  pkgname = pkg.fullname

  puts "Checking #{pkgname}" if $verbose

  origin ||= pkg.origin

  if origin.nil?
    return [3, nil]		# origin unknown
  end

  portdir = File.join($ports_dir, origin)

  unless File.directory? portdir
    return [4, nil]		# origin not found
  end

  output = `cd #{portdir} && make -V PKGNAME #{$make_args} 2>&1`.to_a

  if output.size != 1
    if $verbose
      warning_message "Makefile of #{origin} is possibly broken:"
      output.each { |line| STDERR.print "\t" + line }
    end
    return [5, nil]
  end

  newpkgname = output[0].chomp

  newpkg = PkgInfo.new(newpkgname)

  cmp = newpkg.version <=> pkg.version

  if cmp > 0 then
    return [0, newpkg]	# >
  elsif cmp < 0 then
    return [2, newpkg]	# <
  end

  return [1, newpkg]		# =
end

def check_pkgs(pkgs)
  results = []
  mutex = Mutex.new
  done = Queue.new

  $num_jobs.times do
    Thread.start {
      pkg = nil

      loop do
	mutex.synchronize {
	  if pkgs.empty?
	    done.push true
	    Thread.exit
	  end

	  pkg = pkgs.shift
	}

	ineq, newpkg = check_pkg(pkg, $origin)

	unless $command_output && ineq != 0
	  sign = INEQ_SIGNS[ineq]

	  unless ($limit_chars && !$limit_chars.include?(sign)) ||
	      ($inv_limit_chars && $inv_limit_chars.include?(sign))
	    results.push [ pkg, ineq, newpkg ]
	  end
	end

	$origin = nil
      end
    }
  end

  $num_jobs.times {
    done.pop
  }  

  results.sort { |a, b| a[0] <=> b[0] }.each do |tuple|
    pkg, ineq, newpkg = *tuple

    if $command_output
      printf %Q`#\n#  %s\n#  %s\n#\npkgs="$pkgs %s"\n\n`,
	pkg.name,
	SUGGESTIONS[ineq] % [ newpkg ? newpkg.version : nil ],
	pkg.fullname
    else
      if $verbose then
	printf "%-26s  %s  %s\n",
	  pkg.fullname,
	  INEQ_SIGNS[ineq],
	  SUGGESTIONS[ineq] % [ newpkg ? newpkg.version : nil ]
      else
	printf "%-26s  %s\n",
	  pkg.name,
	  INEQ_SIGNS[ineq]
      end
    end
  end
end

def signal_handler(sig)
  puts "\nInterrupted."
  exit
end

if $0 == __FILE__
  for sig in [2, 3, 15]
    trap(sig) do
      signal_handler(sig)
    end
  end

  exit main(ARGV)
end
