#!/usr/bin/env ruby
# -*- 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/pkgdepfix,v 1.16 2001/08/18 08:21:31 knu Exp $
RCS_REVISION = RCS_ID.split[2]
MYNAME = File.basename($0)

require "optparse"
require "pkgtools"
require "readline"

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

$fix_origin = false

def main(argv)
  usage = <<-"EOF"
usage: #{MYNAME} [-hov] [-s /old_pkgname/new_pkgname/]
    EOF

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

#{usage}
    EOF

  OptionParser.new(banner, COLUMNSIZE) do |opts|
    opts.on("-h", "--help", "Show this message") {
      print opts
      exit 0
    }

    opts.on("-s", "--substitute=/OLD/NEW/", "Substitute all the dependencies recorded#{NEXTLINE}as OLD with NEW and exit") {
      |expr|
      if expr.empty?
	warning_message "Illegal expression: " + expr
	print opts
	exit 64
      end

      sep = expr.slice!(0,1)

      oldpkgname, newpkgname = expr.split(sep)

      if $verbose
	progress_message "Replacing dependencies: #{oldpkgname} -> #{newpkgname}"
      end

      update_pkgdep(oldpkgname, newpkgname)

      exit 0
    }

    opts.on("-o", "--fix-origin", "Turn on the origin fixer") {
      |$fix_origin|
    }

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

    opts.on_tail '
Environment Variables [default]:
    PORTSDIR                 ports directory [/usr/ports]
    PKG_DBDIR                packages DB directory [/var/db/pkg]
    PORTS_INDEX              ports index file [$PORTSDIR/INDEX]
    PORTS_DBDIR              ports db directory [$PORTSDIR]'

    begin
      argv = opts.parse(*argv)

      unless argv.empty?
	print opts
	exit 64
      end

      $pkgnames = $pkgdb.glob('*', false)

      req_hash = {}
      fix_hash = {}
      all_list = []

      $pkgnames.each do |pkgname|
	puts "Checking #{pkgname}"

	contents_file = $pkgdb.pkg_contents(pkgname)

	# fix missing or stale origins
	if $fix_origin
	  pkg = PkgInfo.new(pkgname)

	  origin = pkg.origin

	  if origin
	    portdir = File.join($ports_dir, origin)

	    unless File.directory? portdir
	      puts "Stale origin: '#{origin}'"
	      origin = nil
	    end
	  else
	    puts "Missing origin."
	  end

	  unless origin
	    print "Guessing... "
	    STDOUT.flush

	    begin
	      guess = $portsdb.glob(pkg.name).max { |a, b|
		matchlen(pkgname, a.pkgname.to_s) <=> matchlen(pkgname, b.pkgname.to_s)
	      }

	      if guess
		guess = guess.origin

		puts ''

		if confirm_port(guess)
		  origin = guess
		end
	      else
		puts "no idea."
	      end
	    rescue => e
	      puts e.message	    
	    end

	    unless origin
	      origin = input_port('New origin?')
	    end

	    case origin
	    when '.'
	      puts "Abort."
	      exit
	    when :skip
	      puts "Skipped."
	    else
	      begin
		File.open(contents_file, "r+") do |f|
		  lines = f.reject { |line| /^@comment +ORIGIN:/ =~ line }

		  f.rewind
		  f.print lines
		  f.puts "@comment ORIGIN:" + origin
		  len = f.pos
		  f.close
		  File.truncate contents_file, len
		end

		puts "Fixed. (-> #{origin})"
	      rescue => e
		puts "Failed to rewrite #{contents_file}: " + e.message
	      end
	    end
	  end
	end

	deps = $pkgdb.pkgdep(pkgname) or next

	deps.each do |dep|
	  catch(:next_dep) {
	    unless $pkgnames.include?(dep)
	      puts "Stale dependency: #{pkgname} -> #{dep}:"

	      fix = fix_hash[dep]

	      begin
		deppkg = PkgInfo.new(dep)
		deppkgname = deppkg.fullname
		deppkg_re = /^#{Regexp.quote(deppkg.name)}-[^-]+$/

		if fix.nil?
		  fix = $pkgnames.select { |name|
		    deppkg_re =~ name
		  }.max { |a, b|
		    matchlen(deppkgname, a) <=> matchlen(deppkgname, b)
		  }
		end
	      rescue => e
		puts e.message
	      end

	      catch(:fix_pkgdep) {
		if fix
		  skip = (fix == :skip)

		  if all_list.include?(dep)
		    if skip
		      puts "Skipped."
		      throw :next_dep
		    end

		    throw :fix_pkgdep
		  end

		  case fix
		  when :skip
		    prompt = "Skip this?"
		  when :delete
		    prompt = "Delete this?"
		  else
		    prompt = fix + " ?"
		  end

		  ans = prompt_yesnoall(prompt, true)

		  if ans == :all
		    all_list << dep
		  end

		  if ans
		    if skip
		      throw :next_dep
		    end

		    throw :fix_pkgdep
		  end
		end

		fix = input_pkg('New dependency?', true)

		case fix
		when '.'
		  puts "Abort."
		  exit
		when :skip
		  puts "Skipped."
		  fix_hash[dep] = :skip
		  throw :next_dep
		when :skip_all
		  fix_hash[dep] = :skip
		  all_list << dep
		  throw :next_dep
		when :delete_all
		  all_list << dep
		  fix = :delete
		end
	      }

	      begin
		File.open(contents_file, "r+") do |f|
		  lines = []

		  f.each { |line|
		    if /^(@pkgdep +)#{Regexp.quote(dep)}$/ =~ line
		      lines << $1 + fix + "\n" unless fix == :delete
		    else
		      lines << line
		    end
		  }

		  f.rewind
		  f.print lines
		  f.truncate f.pos
		  f.close

		  case fix
		  when :delete
		    puts "Deleted."
		  else
		    puts "Fixed. (-> #{fix})"
		  end

		  fix_hash[dep] = fix
		  dep = fix
		end
	      rescue => e
		puts "Failed to rewrite #{contents_file}: " + e.message
	      end
	    end

	    unless fix == :delete
	      if req_hash.include?(dep)
		req_hash[dep] << pkgname
	      else
		req_hash[dep] = [pkgname]
	      end
	    end
	  }
	end
      end

      # reconstruct all the +REQUIRED_BY files
      tsort = TSort.new
      indep_pkgnames = []

      $pkgnames.each do |pkgname|
	req_file = $pkgdb.pkg_required_by(pkgname)

	if req_hash.include?(pkgname)
	  req_by = req_hash[pkgname]

	  req_by.each { |req|
	    tsort.add(req, pkgname)
	  }

	  File.open(req_file, "w") do |f|
	    f.puts req_by.sort
	  end
	else
	  indep_pkgnames << pkgname

	  File.unlink(req_file) if File.exist?(req_file)
	end
      end

      # unlink cyclic dependencies
      skip_all = false

      tsort.tsort! do |cycle|
	puts "Cyclic dependencies: #{cycle.join(' -> ')} -> (#{cycle[0]})"

	i = nil

	loop do
	  ans = skip_all || input_pkg('Unlink which dependency?', false, cycle)

	  case ans
	  when '.'
	    puts "Abort."
	    exit
	  else
	    i = cycle.index(ans)

	    if cycle[i + 1].nil?
	      a, b = cycle.last, cycle.first
	    else
	      a, b = cycle.indices(i, i + 1)
	    end

	    if prompt_yesno("Unlink #{a} -> #{b} ?", true)
	      file = $pkgdb.pkg_contents(a)

	      File.open(file, "r+") do |f|
		lines = f.select { |line|
		  /^(@pkgdep +)#{Regexp.quote(b)}$/ !~ line
		}

		f.rewind
		f.print lines
		f.truncate f.pos
		f.close
	      end

	      file = $pkgdb.pkg_required_by(b)

	      File.open(file, "r+") do |f|
		lines = f.select { |line|
		  line.chomp != a
		}

		if lines.empty?
		  f.close
		  File.unlink(file)
		else
		  f.rewind
		  f.print lines
		  f.truncate f.pos
		  f.close
		end
	      end		

	      puts 'Done.'
	      break
	    end
	  end
	end

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

  0
end

def input_pkg(message = 'Which package?', fullspec = false, pkgnames = $pkgnames)
  Readline.completion_proc = proc { |head|
    len = head.size
    pkgnames.select { |pkgname| head == pkgname[0,len] }
  }

  loop do
    input = Readline.readline(message + ' (? to help): ', true)

    if input.nil?
      if fullspec
	ans = prompt_yesnoall("\nDelete this?", true)

	if ans == :all
	  return :delete_all
	elsif ans
	  return :delete
	end
      end

      next
    end

    input.strip!

    case input
    when '.'
      return '.'
    when '?'
      if fullspec
	print <<-EOF
 [Enter] to skip, [Ctrl]+[D] to delete, [.][Enter] to abort, [Tab] to complete
	EOF
      else
	print <<-EOF
 [.][Enter] to abort, [Tab] to complete
	EOF
      end
      next
    when ''
      if fullspec
	ans = prompt_yesnoall("Skip this?", true)

	if ans == :all
	  return :skip_all
	elsif ans
	  return :skip
	end
      end

      next
    else
      if pkgnames.include?(input)
	return input
      end

      if pkgnames == $pkgnames
	puts "No such package installed: #{input}"
      else
	puts "Please choose one of these: #{pkgnames.join(' ')}"
      end
    end
  end

  # not reached
end

def input_port(message = 'Which port?')
  Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC

  Dir.chdir($ports_dir)

  loop do
    input = Readline.readline(message + ' (? to help): ', true)

    input.nil? and next

    input.strip!
    input.chomp!('/')

    case input
    when '.'
      return '.'
    when '?'
      print <<-EOF
[Enter] to skip, [.][Enter] to abort, [Tab] to complete
      EOF
      next
    when ''
      if prompt_yesno("Skip this?", true)
	return :skip
      end

      next
    else
      confirm_port(input) and return input
    end
  end

  # not reached
end

def confirm_port(origin)
  Dir.chdir($ports_dir)

  if %r,^[^/]+/[^/]+$, =~ origin
    makefile = File.join(origin, 'Makefile')

    unless File.file? makefile
      return prompt_yesno("Makefile not found in #{origin}.  Force it?", false)
    end

    pkgname = `cd #{origin} && make -V PKGNAME`.chomp

    return prompt_yesno("#{origin} (#{pkgname}): Use this?", true)
  end

  puts "Not in due form <category/portname>: #{input}"

  false
end

def signal_handler(sig)
  puts "\nInterrupted."

  system 'stty', 'sane'
  exit
end

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

  exit main(ARGV)
end
