#
#	port.rb
#
#	Plugin to rbot (http://ruby-rbot.org/), an irc bot, to provide
#	services related to MacPorts for the #macports channel on freenode.net.
#
#	By James D. Berry
#
#	$Id$
#


require 'fileutils.rb'
require 'stringio'


module Shuffle

	def shuffle!
		size.downto(2) do |i|
		r = rand(i)
		tmp = self[i-1]
		self[i-1] = self[r]
		self[r] = tmp
	  end
	  self
	end
	
	def shuffle
		Array.new(self).extend(Shuffle).shuffle!
	end
	
end



class PortPlugin < Plugin

	def help(plugin, topic="")
		case topic
		  when "info"
			return "info <portname> => show info for port <portname>"
		  when "maintainer"
			return "maintainer <portname> => show maintainer of port <portname>"
		  when "version"
			return "version <portname> => show version of port <portname>"
		  when "search"
			return "search <query> => show ports matching <query>"
		  when "herald"
			return "herald enable|disable => enable or disable heralding by port"
		  when "remember"
			return "remember <nick> email <address> => remember what port maintainer email <nick> belongs to; " +
					"remember <nick> timezone <timezone> => remember the local timezone for <nick>; " +
					"remember <nick> location <location> => remember the physical location for <nick>; " +
					"remember <nick> name <name> => remember the real name for <nick>"
		  when "forget"
			return "forget <nick> => forget all information for <nick>; " +
				"forget email <nick> => forget maintainer email for <nick>; " +
				"forget timezone <nick> => forget local timezone for <nick>; " +
				"forget location <nick> => forget physical location for <nick>; " +
				"forget name <nick> => forget real name for <nick>"
		  when "whois"
		  	return "whois <nick> => give a summary of information for <nick>"
		  when "whereis"
		  	return "whereis <nick> => tell what is known about timezone and location of <nick>"
		  else
			return "port (MacPorts) commands: info, maintainer, version, herald, remember, forget, whois, whereis"
		end
	end
	
	def runCmd(cmd, args, input=nil, output=nil, env={})
		# Open pipes for stdin, and stdout
		stdin,   inwrite = IO.pipe
		outread, stdout  = IO.pipe
	
		# Fork the child
		pid = fork {
			# In child
			
			# Change the environment
			ENV.update(env)
	
			# Redirect IO to the pipes
			inwrite.close
			$stdin.reopen(stdin)
			stdin.close
			
			outread.close
			$stdout.reopen(stdout)
			$stderr.reopen(stdout)
			stdout.close
						
			# Execute the command
			exec(cmd, *args)
			# shouldn't return
		}
		
		# Close unneeded pipe-ends in this process
		[stdin, stdout].each { |io| io.close }
		
		# In order to avoid deadlock, invoke a secondary process to write the input
		open("|-", "w+") { |p|
			if (p == nil)
				# In child
				FileUtils.copy_stream(input, inwrite) if !input.nil?
				inwrite.close
			end
		}
		
		# Close remaining write pipe into command
		inwrite.close
	
		# Read the stdout from the command, writing it to output
		FileUtils.copy_stream(outread, output) if !output.nil?
		
		# Close remaining pipe ends
		[outread].each { |io| io.close }
		
		# Collect the return code
		pid, rc = Process.waitpid2(pid)
		rc >>= 8
		
		return rc
	end

	def callPort(*args)
		Utils.safe_exec("/opt/local/bin/port", *args)
	end
	
	def doPort(m, *args)
		Thread.new do
			text = callPort(*args)
			m.reply text
		end
	end
	
	def info(m, params)
		if !isPseudo(m, params[:portname])
			doPort(m, "info", params[:portname])
		end
	end
	
	def portmaintainer(m, params)
		if !isPseudo(m, params[:portname])
			doPort(m, "info", "--maintainer", params[:portname])
		end
	end
	
	def portversion(m, params)
		if !isPseudo(m, params[:portname])
			doPort(m, "info", "--version", "--revision", "--line", "--pretty", params[:portname])
		end
	end
	
	def isPseudo(m, portname)
		case portname
			when "all", "current", "active", "inactive", "installed", "uninstalled", "outdated", "obsolete", "requested", "unrequested", "leaves"
		        ret = true
		    when /:/, /\*/, /\?/, /\{/, /\}/, /\[/, /\]/
		        ret = true
		    else
		        ret = false
		end
		if ret
		    m.reply "no globs or pseudo ports, please"
		end
		return ret
	end
	
	def portsearch(m, params)
		doPort(m, "search", params[:query])
	end
	
	def herald_enable(m, params)
		@registry['herald_enable'] = true
		m.okay
	end
	
	def herald_disable(m, params)
		@registry['herald_enable'] = false
		m.okay
	end
	
	def rememberEmail(m, params)
		nick = params[:nick]
		email = params[:email]
		@registry["email_#{nick}"] = email
		m.reply "okay, #{nick} is #{email}"
	end
	
	def rememberTimeZone(m, params)
		nick = params[:nick]
		timezone = params[:timezone]
		@registry["timezone_#{nick}"] = timezone
		m.reply "okay, #{nick} is in timezone #{timezone}"
	end
	
	def rememberLocation(m, params)
		nick = params[:nick]
		location = params[:location].join(' ')
		@registry["location_#{nick}"] = location
		m.reply "okay, #{nick} is in #{location}"
	end

	def rememberName(m, params)
		nick = params[:nick]
		name = params[:name].join(' ')
		@registry["name_#{nick}"] = name
		m.reply "okay, #{nick} is #{name}"
	end
	
	def forget(m, params)
		nick = params[:nick]
		@registry.delete("email_#{nick}")
		@registry.delete("timezone_#{nick}")
		@registry.delete("location_#{nick}")
		@registry.delete("name_#{nick}")
		m.okay
	end

	def forgetEmail(m, params)
		nick = params[:nick]
		@registry.delete("email_#{nick}")
		m.okay
	end

	def forgetTimeZone(m, params)
		nick = params[:nick]
		@registry.delete("timezone_#{nick}")
		m.okay
	end

	def forgetLocation(m, params)
		nick = params[:nick]
		@registry.delete("location_#{nick}")
		m.okay
	end

	def forgetName(m, params)
		nick = params[:nick]
		@registry.delete("name_#{nick}")
		m.okay
	end

	def whois(m, params)
		nick = params[:nick]
		email = @registry["email_#{nick}"]
		if email
			heraldUser m.replyto, nick
		else
			m.reply "I don't know #{nick}"
		end
	end
	
	def localTimeInTimeZone(timeZone)
		localTime = nil
		StringIO.open("", "r+") { |o|
			runCmd("/bin/date", "+%a %H:%M %Z", nil, o, { "TZ" => timeZone } )
			o.close_write
			localTime = o.string
		}
		return localTime
	end
	
	def whereisNick(nick)
		location = @registry["location_#{nick}"]
		timeZone = @registry["timezone_#{nick}"]
		
		localTime = nil
		if timeZone
			localTime = localTimeInTimeZone(timeZone)
		end
		
		if location && localTime
			whereis = "is in #{location}; local time is #{localTime}"
		elsif location
			whereis = "is in #{location}"
		elsif localTime
			whereis = "is at local time #{localTime}"
		else
			whereis = nil
		end
	end
	
	def whereis(m, params)
		nick = params[:nick]
		
		where = whereisNick(nick)
		if where
			m.reply "#{nick} #{where}"
		else
			m.reply "I don't know where #{nick} is"
		end
	end
	
	def textEnumeration(a)
		sz = a.size
		case sz
			when 0
				return ""
			when 1
				return a[0]
			when 2
				return a.join(' and ')
			else
				return a.slice(0, sz-1).join(', ') + ' and ' + a[sz-1]
		end
	end
	
	def heraldUser(where, nick)
		Thread.new do
			email = @registry["email_#{nick}"]
			if email
				name = @registry["name_#{nick}"]
				text = callPort("echo", "maintainer:(?:^|[\s{])#{email}(?:[\s}]|$)")
				ports = text.split(/\s+/)
				portCount = ports.size
				showMax = 4
				somePorts = ports.extend(Shuffle).shuffle!.slice(0, showMax)

				msg = "#{nick} is"
				if name
				    msg += " #{name}"
				end
				if nick != email
				    if name
				        msg += " (#{email})"
				    else
				        msg += " #{email}"
				    end
				end

				if portCount > 0
				    if name || nick != email
				        msg +=  " and"
				    end
				    msg += " maintainer of " + textEnumeration(somePorts)
				    if portCount > showMax
				        msg += " (of #{portCount} total)"
				    end
				end

				whereis = whereisNick(nick)
				if whereis
					msg = msg + " and #{whereis}"
				end

				@bot.say where, msg
			end
		end
	end
	
	def maybeHerald(where, nick)
		now = Time.new
		minSecondsBetween = 60*10
		
		doHerald = true
		email = @registry["email_#{nick}"]
		if (email)
			lastHerald = @registry["lastherald_#{email}"]
			if (lastHerald)
				secondsAgo = now - lastHerald
				doHerald = secondsAgo > minSecondsBetween
			end
			@registry["lastherald_#{email}"] = now
		end
		
		heraldUser where, nick if doHerald
	end
	
	def join(m)
		maybeHerald m.target, m.sourcenick
	end
	
	def part(m)
		nick = m.sourcenick
		email = @registry["email_#{nick}"]
		@registry["lastherald_#{email}"] = Time.new if email
	end
	
	def nick(m)
  		newnick = m.message
		@bot.channels.each_value { |c|
			if(c.users.has_key?(newnick))
				maybeHerald c.name, newnick
			end
		}
	end

end

plugin = PortPlugin.new
plugin.map 'port info :portname', :action => 'info'
plugin.map 'port maintainer :portname', :action => 'portmaintainer'
plugin.map 'port version :portname', :action => 'portversion'
#plugin.map 'port search :query', :action => 'portsearch'

plugin.map 'port herald enable', :action => 'herald_enable'
plugin.map 'port herald disable', :action => 'herald_disable'
plugin.map 'port remember :nick timezone :timezone', :action => 'rememberTimeZone'
plugin.map 'port remember :nick email :email', :action => 'rememberEmail'
plugin.map 'port remember :nick location *location', :action => 'rememberLocation'
plugin.map 'port remember :nick name *name', :action => 'rememberName'
plugin.map 'port forget :nick', :action => 'forget'
plugin.map 'port forget :nick email', :action => 'forgetEmail'
plugin.map 'port forget :nick timezone', :action => 'forgetTimeZone'
plugin.map 'port forget :nick location', :action => 'forgetLocation'
plugin.map 'port forget :nick name', :action => 'forgetName'
plugin.map 'port whois :nick', :action => 'whois'
plugin.map 'port whereis :nick', :action => 'whereis'
