#!/usr/bin/ruby
# encoding: utf-8
#
# Copyright © 2013, Lucas Nussbaum <lucas@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 3 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
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Always exit with a return code 0 (we don't want break apt/dpkg even if an error occured).
Signal.trap('EXIT') { exit 0 }

require 'pp'
require 'debian'
require 'net/http'
require 'zlib'
require 'stringio'
require 'json'
require 'optparse'
require 'fileutils'
require 'time'

unless ENV['HOME']
  puts "how-can-i-help: Unable to resolve your $HOME directory - cannot continue."
  exit(0)
end

HOME=Dir.home

unless Dir.exists?(HOME)
  puts "how-can-i-help: Your $HOME seems to point to a nonexistent location - cannot continue."
  exit(0)
end

HELPITEMS_URL = 'http://udd.debian.org/how-can-i-help.json.gz'
CACHEDIR = "#{HOME}/.cache/how-can-i-help"
CONFIGDIR = "#{HOME}/.config/how-can-i-help"
SEEN_LOCAL = "#{CACHEDIR}/seen.json"
CACHE = "#{CACHEDIR}/how-can-i-help.json.gz"
PACKAGES = "#{CONFIGDIR}/packages"
IGNORED_TYPES = "#{CONFIGDIR}/ignored"
ENV['LC_ALL'] = 'C.UTF-8'

include Debian

$quiet = false
$all = false
$old = false
$proxy_url = ENV["HTTP_PROXY"] || ENV["http_proxy"]

optparse = OptionParser.new do |opts|
  opts.on('-h', '--help', 'show help') do
    puts opts
    exit
  end

  opts.on('-v', '--version', 'show version') do
    puts "Please use, e.g., 'dpkg -l how-can-i-help'"
    exit(0)
  end

  opts.on('-a', '--all',
          'show opportunities for contribution to all packages') do
    $all = true
  end

  opts.on('-o', '--old', 'show old opportunities for contribution') do
    $old = true
  end

  opts.on('-q', '--quiet', 'do not display header and footer') do
    $quiet = true
  end

end
optparse.parse!

def system_r(s)
  system(s) or raise
end

FileUtils.mkdir_p CACHEDIR unless File.exists?(CACHEDIR)

if File::exists?(SEEN_LOCAL)
  seen = JSON::parse(IO::read(SEEN_LOCAL))
else
  seen = []
end

# Cache request data
uri = URI(HELPITEMS_URL)
request = Net::HTTP::Get.new(uri.request_uri)
if File::exists?(CACHE)
  stat = File.stat CACHE
  request['If-Modified-Since'] = stat.mtime.httpdate
end

# Dealing with proxy, authenticated or not
proxy_uri = $proxy_url.nil? ? OpenStruct.new : URI.parse($proxy_url)
proxy_user, proxy_pass = proxy_uri.userinfo.split(/:/) if proxy_uri.userinfo
http_object = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port,
                     proxy_user, proxy_pass)
# proceeding get_response

begin
  response = http_object.request(request)
rescue
  puts "how-can-i-help: Error downloading data file: #{$!}"
  exit(0)
end
open CACHE, 'w' do |cc|
  cc.write response.body.to_s
end if response.is_a?(Net::HTTPSuccess)

gz = Zlib::GzipReader.open(CACHE)
helpitems = JSON::parse(gz.read)

# get installed packages
packages = []
str = `dpkg -l`
if str.respond_to?(:force_encoding)
  str.force_encoding('utf-8')
end
str.split(/\n/).each do |l|
  if l =~ /^(i|h).\s+([^ ]+)\s/
    packages << $2
  end
end

# add user defined packages
if File.file?(PACKAGES)
  additionals = File.read(PACKAGES).gsub(/\s+/m, ' ').strip.split(" ")
  packages += additionals
end

# add user defined ignored types
ignored_types = []
if File.file?(IGNORED_TYPES)
  selected_type = File.read(IGNORED_TYPES).gsub(/\s+/m, ' ').strip.split(" ")
  ignored_types += selected_type
end

helpitems_filtered = []
helpitems.each do |hi|
  next if (not $old) and seen.include?(hi['hash'])
  next if ignored_types.include?(hi['type']) || (hi['type'] == 'wnpp' && ignored_types.include?(hi['wnpptype']))
  if $all
    helpitems_filtered << hi
  elsif hi['type'] == 'wnpp'
    if packages & hi['packages'] != []
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'gift'
    if packages.include?(hi['package'])
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'no-testing'
    if packages.include?(hi['package'])
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'testing-autorm'
    if packages & hi['packages'] != []
      helpitems_filtered << hi
    end
  elsif hi['type'] == 'rfs'
    if packages & hi['packages'] != []
      helpitems_filtered << hi
    end
  end
end

unless $quiet
  puts "======  How can you help?  (doc: http://wiki.debian.org/how-can-i-help ) ======"
  puts
end

wnpp = helpitems_filtered.select { |e| e['type'] == 'wnpp' }
gift = helpitems_filtered.select { |e| e['type'] == 'gift' }
notesting = helpitems_filtered.select { |e| e['type'] == 'no-testing' }
autoremoval = helpitems_filtered.select { |e| e['type'] == 'testing-autorm' }
rfs = helpitems_filtered.select { |e| e['type'] == 'rfs' }

def wnpptype(t)
  return 'O (Orphaned)' if t == 'O'
  return 'RFA (Maintainer looking for adopter)' if t == 'RFA'
  return 'RFH (Maintainer looking for help)' if t == 'RFH'
  return 'ITA (Someone working on adoption)' if t == 'ITA'
end

def wnppsortorder(t)
  return 0 if t == 'O'
  return 1 if t == 'RFA'
  return 2 if t == 'ITA'
  return 3 if t == 'RFH'
  return 4
end

def wnppcompare(a, b)
  c = wnppsortorder(a['wnpptype']) <=> wnppsortorder(b['wnpptype'])
  if c == 0
    return a['source'] <=> b['source']
  else
    return c
  end
end

if wnpp.length > 0
  puts $old ? 'Packages where help is needed, including orphaned ones (from WNPP):' : 'New packages where help is needed, including orphaned ones (from WNPP):'
  wnpp.sort { |a, b| wnppcompare(a,b) }.each do |r|
    puts " - #{r['source']} - https://bugs.debian.org/#{r['wnppbug']} - #{wnpptype(r['wnpptype'])}"
  end
  puts
end

if gift.length > 0
  puts $old ? 'Bugs suitable for new contributors (tagged \'gift\'):' : 'New bugs suitable for new contributors (tagged \'gift\'):'
  gift.sort { |a, b| [a['package'], a['bug'] ] <=> [ b['package'], b['bug'] ] }.each do |r|
    puts " - #{r['package']} - https://bugs.debian.org/#{r['bug']} - #{r['title']}"
  end
  puts
end

if notesting.length > 0
  puts $old ? 'Packages removed from Debian \'testing\' (the maintainer might need help):' : 'New packages removed from Debian \'testing\' (the maintainer might need help):'
  notesting.sort { |a, b| a['source'] <=> b['source'] }.each do |r|
    puts " - #{r['package']} - https://tracker.debian.org/pkg/#{r['source']}"
  end
  puts
end

if autoremoval.length > 0
  puts $old ? 'Packages going to be removed from Debian \'testing\' (the maintainer might need help):' : 'New packages going to be removed from Debian \'testing\' (the maintainer might need help):'
  autoremoval.sort { |a, b| a['source'] <=> b['source'] }.each do |r|
    bugs = r['bugs'].map { |b| "##{b}" }
    if bugs.count == 0
      bugs = ""
    elsif bugs.count > 1
      bugs = " (bugs: #{bugs.join(', ')})"
    else
      bugs = " (bug: #{bugs[0]})"
    end
    puts " - #{r['source']} - https://tracker.debian.org/pkg/#{r['source']} - removal on #{Time.at(r['removal_time']).to_date.to_s}#{bugs}"
  end
  puts
end

if rfs.length > 0
  puts $old ? 'Packages waiting for sponsorship (reviews/tests are also useful):' : 'New packages waiting for sponsorship (reviews/tests are also useful):'
  rfs.sort { |a, b| a['source'] <=> b['source'] }.each do |r|
    puts " - #{r['source']} - https://bugs.debian.org/#{r['id']} - #{r['title']}"
  end
  puts
end

if not $old and not $quiet
  puts "-----  Show old opportunities as well as new ones: how-can-i-help --old  -----"
end

if not $old
  seen = helpitems.map { |hi| hi['hash'] } & seen
  seen = seen + helpitems_filtered.map { |hi| hi['hash'] }
  File::open(SEEN_LOCAL, 'w') do |fd|
    JSON::dump(seen, fd)
  end
end
