#require 'rubygems'
#require 'IRC'
# thread safe state reservation
require 'state'
require 'shellwords'

#bot = IRC.new("SelfishBot", "localhost", "6667", "Markus")

$LOAD_PATH.unshift( File.join( File.dirname(__FILE__), 'mods' ))

class Bot
    @irc = nil
    @s = nil

    def initialize(nick, server, port, realname, channels, options = {})
	@s			    = State.new
	@s[:nicks]		    = nick # an array of nicknames
	@s[:current_nick]	    = 0
	@s[:timeout]		    = options[:timeout]		|| 0
	@s[:channels]		    = channels			|| Array.new

	@s[:keyword_hooks]	    = options[:keyword_hooks]	|| Hash.new
	@s[:admin_hooks]	    = options[:admin_hooks]	|| Hash.new
	@s[:mutex_calls]	    = options[:mutex_calls]	|| Array.new
	@s[:admin_users]	    = options[:admin_users]	|| Hash.new
	@s[:callbacks]		    = options[:callbacks]   	|| Hash.new

	@s[:modules]		    = Hash.new

	@s[:keyword_hooks].merge!({ 'say'			=> self.method(:_a_say),
				    'login'			=> self.method(:login)})
	@s[:admin_hooks].merge!({   'quit'			=> self.method(:_a_quit), 
				    'addnick'			=> self.method(:_a_addnick), 
				    'remnick'			=> self.method(:_a_remnick), 
				    'listnicks'			=> self.method(:_a_listnicks), 
				    'joinchan'			=> self.method(:_a_joinchan), 
				    'partchan'			=> self.method(:_a_partchan), 
				    'listchans'			=> self.method(:_a_listchans), 
				    'listadmins'		=> self.method(:_a_listadmins), 
				    'listlogins'		=> self.method(:_a_listlogins), 
				    'addadmin'			=> self.method(:_a_addadmin), 
				    'remadmin'			=> self.method(:_a_remadmin), 
				    'listadminhooks'		=> self.method(:_a_listadminhooks), 
				    'listhooks'			=> self.method(:_a_listhooks), 
				    'savestate'			=> self.method(:_a_savestate), 
				    'loadstate'			=> self.method(:_a_loadstate), 
				    'changenick'		=> self.method(:_a_changenick), 
				    'sleep'			=> self.method(:_a_sleep),  
				    'logout'			=> self.method(:_a_logout),
				    'regmod'	    		=> self.method(:_a_regmod),
				    'unregmod'			=> self.method(:_a_unregmod),
				    'lsmod'			=> self.method(:_a_lsmod)
				    })

	@s[:mutex_calls]	    |= @s[:admin_hooks].keys	| @s[:keyword_hooks].keys

	@irc = IRC.new(get_current_nick, server, port.to_s, realname, options)

	own_callbacks = {	    'nicknameinuse'		=> self.method(:nicknameinuse),
				    'privmsg'			=> self.method(:privmsg), 
				    'endofmotd'			=> self.method(:endofmotd), 
				    'kick'			=> self.method(:kick) }

	
	@s[:callbacks].merge(own_callbacks).each{|ename, method|	
	    if own_callbacks[ename]	# own callbacks take precedence!
		IRCEvent.add_callback( ename, own_callbacks[ename] )
	    else			# else the framework may call directly
		IRCEvent.add_callback( ename, method )
	    end
	}
    end


    # Handles the nickname in use event
    def nicknameinuse(event)
	sleep(@s[:timeout])
	try_next_nick
	@irc.ch_nick(get_current_nick)
	
	handle_callback('nicknameinuse', event)
    end

    # handles privmsg
    def privmsg(event)
	if event.message.match(/^\![\-\_a-zA-Z0-9]+/) 
	    argv = Shellwords.shellwords(event.message)
	    cmd = argv[0][1..-1]
	 
	    # is this a private message sent through /msg?
	    if event.channel == get_current_nick
	       event.channel = event.from
	    end	       
	    process_command(argv.length - 1, argv[1..-1], cmd, event)
	end

	handle_callback('privmsg', event)
    end


    
    def endofmotd(event)
	#log("Joining channel")
	@s[:channels].each{|chan|
	    @irc.add_channel(chan)
	}
	
	
	handle_callback('endofmotd', event)
    end

    
    def kick(event)
	if event.target == get_current_nick
	    @irc.part(event.channel)
	    @irc.add_channel(event.channel)
	end
	
	
	handle_callback('kick', event)
    end


    def connect 
	@irc.connect 
    end

    
    # register a module for calls
    def register_module(req, classname, name)
	require(req)
	state = State.new
	obj = Object::const_get(classname).new(self, @irc, state)
	@s[:modules][name] = obj
    end

    # unregister a module, not from IRC
    def unregister_module(name)
	if @s[:modules][name] != nil
	    @s[:modules][name].unregister()
	    return true
	end
	return false
    end

    private

    


    def login(event, cmd, pw=nil)
	if @s[:admin_users].include?(event.from)
	    if pw == @s[:admin_users][event.from][:pw]

		@s[:admin_users][event.from][:host] = event.stats[2]
		@irc.send_notice(event.from, "Logged in successfully from #{event.stats[2]}")
		return
	    
	    else
		@irc.send_notice(event.from, "Failed to log in: wrong password.")
		return
	    end
	end
	@irc.send_notice(event.from, "Failed to log in: you're not listed as admin.")
    end
   
    def _a_lsmod(event, cmd)
	str = @s[:modules].keys.join(", ")
	@irc.send_notice(event.from, "#{str}"
    end

    def _a_regmod(event, cmd, req, classname, name)
	register_module(req, classname, name)
	@irc.send_notice(event.from, "#{name} registered.")
    end

    def _a_unregmod(event, cmd, name)
	if unregister_module(name)
	    @irc.send_notice(event.from, "#{name} successfully unregistered.")
	    return
	end
	@irc.send_notice(event.from, "#{name} could not be unregistered.")
    end

    def _a_logout(event, cmd, uname=nil)
	un = uname || event.from

	if @s[:admin_users].include?(un) and event.stats[2] != nil
	    @s[:admin_users][un][:host] = nil
	    @irc.send_notice(event.from, "Logged #{un} out.")
	    return 
	end
	@irc.send_notice(event.from, "#{un} is not logged in!")
    end



    def _a_quit(event, cmd, reason=nil)
	@irc.send_quit(reason)
    end

    def _a_say(event, cmd, string, channel=nil)
	if not channel
	    channel = event.channel
	end

	@irc.send_message(channel, string)
    end

    def _a_addnick(event, cmd, nick)
	
	@s[:nicks] |= [nick]
	#assign new nick
	@s[:current_nick] = @s[:nicks].length - 1
	#apply!
	@irc.ch_nick(get_current_nick)
    end

    def _a_changenick(event, cmd, nick)
	if @s[:nicks].include?(nick)
	    @s[:current_nick] = @s[:nicks].index(nick)
	    @irc.ch_nick(get_current_nick)
	end
    end

    def _a_remnick(event, cmd, nick)
	if nick == get_current_nick 
	    return
	end
	cnick = get_current_nick
	@s[:nicks] = @s[:nicks].delete_if{|n| n == nick}
	@s[:current_nick] = @s[:nicks].index(cnick)
	#apply!
	#@irc.ch_nick(get_current_nick)
    end

    def _a_listnicks(event, cmd)
	str = @s[:nicks].join(', ').gsub(get_current_nick, "*#{get_current_nick}")
	@irc.send_notice(event.from, "Nicknames: #{str}.")
    end

    def _a_listchans(event, cmd)
	str = @s[:channels].join(', ').gsub(event.channel, "*#{event.channel}")
	@irc.send_notice(event.from, "Channels: #{str}.")
    end

    def _a_joinchan(event, cmd, chan)
	if @s[:channels].include? chan
	    return
	end

	@s[:channels] |= [chan]
	@irc.add_channel(chan)
    end

    def _a_partchan(event, cmd, chan)
	if @s[:channels].include?(chan)
	    @irc.part(chan)
	    @s[:channels] = @s[:channels].delete_if{ |c| c == chan}
	end
    end

    def _a_listlogins(event, cmd, hostnames=nil)
	str = ""
	@s[:admin_users].keys.each{ |n|
	    if @s[:admin_users][n][:host] != nil
		str += "(#{n} => #{@s[:admin_users][n][:host]}) "
	    end
	}
	@irc.send_notice(event.from, str)
    end

    def _a_listadmins(event, cmd)
	str = @s[:admin_users].keys.join(', ').gsub(event.from, "*#{event.from}")
	@irc.send_notice(event.from, "Admins: #{str}.")
    end

    def _a_addadmin(event, cmd, uname, pw)
	@s[:admin_users][uname] = {:pw => pw}
    end

    def _a_remadmin(event, cmd, uname)
	@s[:admin_users] = @s[:admin_users].delete_if{ |k, v| k == uname}
    end

    def _a_listadminhooks(event, cmd)
	str = @s[:admin_hooks].keys.join(", ")
	@irc.send_notice(event.from, "Admin hooks: #{str}.")
    end
    
    def _a_listhooks(event, cmd)
	str = @s[:keyword_hooks].keys.join(", ")
	@irc.send_notice(event.from, "Hooks: #{str}.")
    end

    def _a_sleep(event, cmd, time=1)
	if time.to_i.abs > 1 and time.to_i.abs < 84600
	    time = time.to_i.abs
	end
	sleep(time)
    end

    def _a_savestate(event, cmd, filename, overwrite=false)
	if overwrite.to_s.downcase == "overwrite"
	    overwrite = true
	end

	if @s._dump(filename, overwrite)
	    @irc.send_notice(event.from, "Dump saved to #{filename}.")
	else
	    @irc.send_notice(event.from, "Failed to save to #{filename}.")
	end
    end

    def _a_loadstate(event, cmd, filename)
	if @s._load(filename)
	    @irc.send_notice(event.from, "Dump loaded from #{filename}.")
	else
	    @irc.send_notice(event.from, "Failed to load from #{filename}.")
	end
    end
    
    def send_command(argc, argv, cmd, event, method)
	mex = @s[:mutex_calls].include?(cmd)

	if mex
	    @s.lock()
	end


	begin
	   log(argv.join(" - "))
	    method.call(event, cmd, *argv)
	    #self.send(prefix + cmd, event, *argv)
	rescue ArgumentError => ae
	    args = ae.to_s.split(/[^0-9]+/).delete_if{ |x| x.length == 0 }
	    @irc.send_notice(event.from, "Error: wrong number of arguments (#{args[0].to_i - 2} for #{args[1].to_i - 2}).")
	    log(ae.to_s)
	rescue Exception => e
	    @irc.send_notice(event.from, "Error: #{e.to_s}")
	    #e.backtrace.each{|line| @irc.send_notice(event.from, "     #{line}") }

	    log(e.to_s + "\n" + e.backtrace.join("\n"))
	ensure
	    if mex
		@s.unlock()
	    end
	end	    

    end

    def process_command(argc, argv, cmd, event)
	#log("argc: #{argc}, argv: [#{argv.join(', ')}], cmd: #{cmd}")
	if cmd == get_current_nick
	    if argc > 1
		return process_command(argc - 1, argv[1..-1], argv[0], event)
	    else
		return
	    end
	end


	if @s[:admin_users].include?(event.from) and event.stats[2] == @s[:admin_users][event.from][:host]
#log("user authed")
	    if @s[:admin_hooks][cmd]
		send_command(argc, argv, cmd, event, @s[:admin_hooks][cmd])
	    end
	end

	if @s[:keyword_hooks][cmd]
	    send_command(argc, argv, cmd, event, @s[:keyword_hooks][cmd])
	end
    end

    def handle_callback(cbname, event)
	#fire callbacks as if i am the low level framework
	if @s[:callbacks][cbname]
	    @s[:callbacks][cbname].call(event)
	end
    end

    def try_next_nick
	@s[:current_nick] = (@s[:current_nick] + 1) % @s[:nicks].length
    end

    def get_current_nick
	return @s[:nicks][@s[:current_nick]]
    end

    def log(str)
	print(str + "\n")
    end
end

