#!/usr/bin/ruby -d
#
# erica
#   -- End user interface for RICA
#   NISHI Takao <zophos@koka-in.org>
#
# $Id: erica.rb,v 1.6 2001/11/11 07:48:57 zophos Exp $
#

require 'gtk'
require 'rica'
require 'connectiondiag'
require 'ctcpresponder'
require 'logholder'

include Rica

ERICA_DEFAULT_TITLE='erica'
ERICA_DEFAULT_TREE_WIDTH=100
ERICA_DEFAULT_TREE_HEIGHT=400
ERICA_DEFAULT_CLOG_WIDTH=400
ERICA_DEFAULT_CLOG_HEIGHT=250
ERICA_DEFAULT_WIDTH=500
ERICA_DEFAULT_HEIGHT=400

################################################
#
# Resouces
#
class Resouces
    include Singleton

    RESOUCE_SEARCH_PATH=[".",ENV["HOME"]]
    GTK_RESOUCE_FILE=".gtkrc"

    def initialize
	load

	@fontset=nil
	@tscolor=Gdk::Color.new(0,0,65535)
	@nickcolor=Gdk::Color.new(0,32767,0)
	@systemcolor=Gdk::Color.new(0,32767,0)
	@msgcolor=nil
	@bgcolor=nil
    end

    attr_accessor :fontset
    attr_accessor :tscolor
    attr_accessor :nickcolor
    attr_accessor :systemcolor
    attr_accessor :msgcolor
    attr_accessor :bgcolor

    def load
	RESOUCE_SEARCH_PATH.each{|path|
	    f=path.to_s+"/"+GTK_RESOUCE_FILE
	    if(FileTest::exist?(f))
		Gtk::RC.parse(f)
		break
	    end
	}
    end
end


################################################
#
# Servers, channels and users tree
#  (Gtk::Tree wrapper)
#
class BuffersTree<Gtk::ScrolledWindow
    include Event
    
    def initialize(buffer)
	@buffer=buffer

	@node=Hash.new
	@node["/"]=Gtk::Tree.new
	@node["/"].set_selection_mode(Gtk::SELECTION_SINGLE)
	@node["/"].show

	super()
	self.add_with_viewport(@node["/"])
	self.show

	@contents=Hash.new
	@selection=[]
    end

    def selectElement(*elements)
	el=getElement(*elements)
	unless(el.nil?)
	    @selection.push(el)
	    @selection.uniq!
	    el.select
	end
    end

    def deselectElement(*elements)
	el=getElement(*elements)
	unless(el.nil?)
	    el.deselect
	    @selection.delete(el)
	end
    end

    def selectSingleElement(*elements)
	@selection.each{|el|
	    begin
		el.deselect
	    rescue ArgumentError
	    end
	}
	@selection.clear
	selectElement(*elements)
    end

    alias push dispatch

    #
    # override methods of Rica::Event
    #
    def on_link_established(msg)
	addElement(msg.server)
    end

    def on_link_closed(msg)
	removeElement(msg.server)
    end

    def on_recv_cmnd_nick(msg)
	msg.add_info.each{|ch|
	    refreshNames(msg.server,ch)
	}
    end

    def on_recv_cmnd_quit(msg)
	if(msg.isSelfMessage?)
	    removeElement(msg.server)
	else
	    msg.add_info.each{|ch|
		removeElement(msg.server,ch,msg.fromNick)
	    }
	end
    end

    def on_recv_cmnd_join(msg)
	if(msg.isSelfMessage?)
	    addElement(msg.server,msg.to)
	    showElement(msg.server,msg.to)
	else
	    refreshNames(msg.server,msg.add_info[0].to_s)
	end
    end

    def on_recv_cmnd_part(msg)
	if(msg.isSelfMessage?)
	    removeElement(msg.server,msg.to)
	else
	    removeElement(msg.server,msg.to,msg.fromNick)
	end
    end
    
    def on_recv_cmnd_mode(msg)
	refreshNames(msg.server,msg.to)
    end

    def on_recv_cmnd_kick(msg)
	if(msg.to==msg.selfNick)
	    removeElement(msg.server,msg.to)
	else
	    removeElement(msg.server,msg.to,msg.args[0])
	end
    end
    
    def on_recv_cmnd_privmsg(msg)
	s=msg.server.to_s
	c=msg.to.to_s
	if((msg.isPriv?)&&(!msg.isSelfMessage?))
	    c=msg.fromNick
	end

	unless(hasElement?(s,c))
	    unless(c.empty?)
		addElement(s,c)
		showElement(s,c)
	    end
	end
    end
    alias on_recv_cmnd_notice on_recv_cmnd_privmsg

    def on_recv_rpl_endofname(msg)
	refreshNames(msg.server,msg.add_info[0].to_s)
    end

    private
    def hasElement?(*elements)
	if(getElement(*elements))
	    return true
	else
	    return false
	end
    end

    def getElement(*elements)
	el="/"+elements.join("/").downcase
	return @contents[el]
    end

    def getParent(*elements)
	elements.pop
	el="/"+elements.join("/").downcase
	return @node[el]
    end

    def addElement(*elements)
	buf=elements.dup
	str=buf.pop

	node="/"+buf.join("/").downcase
	el="/"+elements.join("/").downcase
	if(str.instance_of?(LogHolder::ChannelUserInfo))
	    el=node+"/"+str.nick.downcase
	end
	str=String(str)

	if(@contents.has_key?(el))
	    return
	end
	if(buf.size>0)
	    addElement(*buf)
	end

	@contents[el]=Gtk::TreeItem.new(str)
	@contents[el].signal_connect('select'){|widget|
	    begin
		@selection.each{|el|
		    unless(el==widget)
			el.deselect
		    end
		}
		@selection=[widget]
		@buffer.body.notify_signal(self,'select',*elements)
	    rescue NameError
	    end
	}
	@contents[el].signal_connect('deselect'){|widget|
	    @selection.delete(widget)
	}
	@contents[el].show

	unless(@node.has_key?(node))
	    @node[node]=Gtk::Tree.new
	    @node[node].set_selection_mode(Gtk::SELECTION_SINGLE)
	    @contents[node].set_subtree(@node[node])
	end
	@node[node].append(@contents[el])
    end

    def removeChild(*elements)
	nd="/"+elements.join("/").downcase
	n=@node[nd]
	if(n.nil?)
	    return
	end

	tag="^"+nd+"/"
	keys=@contents.keys
	keys.each{|i|
	    if(i=~tag)
		if(@node.has_key?(i))
		    x=i.split("/")
		    removeChild(*x)
		end
		begin
		    n.remove_item(@contents[i])
		rescue
		end
		@contents.delete(i)
		@contents.rehash
	    end
	}

	@node.delete(nd)
	@node.rehash
    end

    def removeElement(*elements)
	removeChild(*elements)

	el="/"+elements.join("/").downcase
	elements.pop
	nd="/"+elements.join("/").downcase
	
	n=@node[nd]
	i=@contents[el]
	if((n.nil?)||(i.nil?))
	    return
	end
	
	n.remove_item(i)
	@contents.delete(el)

	@contents.rehash
    end

    def showElement(*elements)
	while(elements.size>0)
	    el=getElement(*elements)
	    unless(el.nil?)
		el.expand
	    end
	    elements.pop
	end
    end

    def refreshNames(server,channel)
	begin
	    buf=@buffer[server][channel].namesArray
	    el=getElement(server,channel)
	    exp=nil
	    if(el.instance_of?(Gtk::TreeItem))
		exp=el.expanded?
	    end
	    removeChild(server,channel)
	    buf.sort.each{|nick|
		addElement(server,channel,nick)
	    }
	    if(exp)
		el.expand
	    end
	rescue NameError
	end
    end
end


################################################
#
# Common Log area
#  (Gtk::Text wrapper)
#
class LogArea<Gtk::HBox
    MAX_LOG_SIZE=32768		#32KB
    EXPIRE_LOG_SIZE=16384	#16KB

    def initialize(autofreeze=false)
	@active=false
	@autofreeze=autofreeze

	@vadj=Gtk::Adjustment.new(0,0,0,0,0,0)
	@vs=Gtk::VScrollbar.new(@vadj)
	@vs.show
	@textarea=Gtk::Text.new(nil, @vadj)
	@textarea.set_editable(false)
	@textarea.show

	super(false,0)
	self.pack_start(@textarea,true,true,0)
	self.pack_start(@vs,false,false,0)
	self.show

	@lastmsg=nil

	@fontset=nil
	@tscolor=Gdk::Color.new(0,0,65535)
	@nickcolor=Gdk::Color.new(0,32767,0)
	@systemcolor=Gdk::Color.new(0,32767,0)
	@msgcolor=nil
	@bgcolor=nil
    end

    attr_accessor :active

    def scrolled?
	if(@vadj.value==@vadj.upper-@vadj.page_size)
	    return false
	else
	    return true
	end
    end

    def clear
	@textarea.delete_text(0,@textarea.get_length)
    end

    def appendLine(ircMessage,showts=true,showchannel=true,uniq=true)
	if(uniq)
	    begin
		if(@lastmsg.string("%H:%M:%S %t:%f %o")==
		   ircMessage.string("%H:%M:%S %t:%f %o"))
		    return
		end
	    rescue NameError
	    end
	    @lastmsg=ircMessage
	end

	freezing=false
	if(@autofreeze&&@active&&self.scrolled?)
	    freezing=true
	    @textarea.freeze
	end

	if(showts)
	    @textarea.insert(@fontset,@tscolor,@bgcolor,
			     ircMessage.timestamp.strftime("%H:%M "))
	end
	case ircMessage.command
	when Event::RECV_CMND_PRIVMSG,Event::RECV_CMND_NOTICE
	    prifix="<"
	    suffix=">"
	    if(ircMessage.isSelfMessage?)
		prifix=">"
		suffix="<"
	    elsif(ircMessage.isPriv?)
		prifix="="
		suffix="="
	    end
	    @textarea.insert(@fontset,@nickcolor,@bgcolor,prifix)
	    if(showchannel)
		@textarea.insert(@fontset,@nickcolor,@bgcolor,
				 ircMessage.to.to_s+"@"+
				 ircMessage.server.to_s+" ")
	    end
	    @textarea.insert(@fontset,@nickcolor,@bgcolor,
			     ircMessage.fromNick.to_s+suffix+" ")
	    
	    @textarea.insert(@fontset,@msgcolor,@bgcolor,
			     ircMessage.string("%a"))
	else
	    if(showchannel)
		@textarea.insert(@fontset,@systemcolor,@bgcolor,
			ircMessage.string("*** %s %t:%f %C %a"))
	    else
		@textarea.insert(@fontset,@systemcolor,@bgcolor,
			ircMessage.string("*** %f %C %a"))
	    end
	end
	@textarea.insert(@fontset,@msgcolor,@bgcolor,"\n")

	if(@textarea.get_length>MAX_LOG_SIZE)
	    if(@active&&(!freezing))
		freezing=true
		@textarea.freeze
	    end
	    @textarea.delete_text(0,EXPIRE_LOG_SIZE)
	    @textarea.set_point(@textarea.get_length)
	end

	if(@active&&freezing)
	    @textarea.thaw
	end
    end
end


################################################
#
# Log buffer
#  (CommonLogArea and Gtk::Label wrapper)
#
# This class is used for log buffering
#
class LogBuffer<Gtk::VBox
    include Rica::Event

    def initialize
	@topicarea=Gtk::Label.new(ERICA_DEFAULT_TITLE)
	@topicarea.jtype=Gtk::JUSTIFY_LEFT
	@topicarea.show

	@logarea=LogArea.new(true)

	super(false,0)
	self.pack_start(@topicarea,false,true,0)
	self.pack_start(@logarea,true,true,0)
	self.show

	@buffer=nil
	@server=""
	@channel=""
	@nick=""

	@show_timestamp=true
	@show_channel=false
    end

    attr_accessor :show_timestamp
    attr_accessor :show_channel

    def active=(x)
	@logarea.active=x
    end

    def scrolled?
	return @logarea.scrolled?
    end

    def bind(buffer)
	@buffer=buffer
	@server=buffer.server
	@channel=buffer.channel
    end

    def push(msg)
	@nick=msg.selfNick
	dispatch(msg)
	@logarea.appendLine(msg,@show_timestamp,@show_channel)
    end

    def refresh
	setTopic
    end

    def on_recv_cmnd_join(msg)
	if(msg.isSelfMessage?)
	    @server=msg.server
	    @channel=msg.to
	    setTopic
	end
    end

    def on_recv_cmnd_topic(msg)
	setTopic
    end
    def on_recv_cmnd_mode(msg)
	setTopic
    end
    def on_recv_rpl_umodeis(msg)
	setTopic
    end
    def on_recv_rpl_channelmodeis(msg)
	setTopic
    end
    def on_recv_rpl_notopic(msg)
	setTopic
    end
    def on_recv_rpl_topic(msg)
	setTopic
    end

    private
    def setTopic
	begin
	    if(@buffer.console)
		c=@nick
	    else
		c=@channel
	    end
	    @topicarea.set(sprintf("%s@%s[%s] %s",
				   c,
				   @server,
				   @buffer.mode,
				   @buffer.topic)
			   )
	rescue NameError,TypeError
	    if(@channel.empty?&&@server.empty?)
		@topicarea.set(ERICA_DEFAULT_TITLE)
	    else
		@topicarea.set(sprintf("%s@%s[]",
				       @channel,
				       @server)
			       )
	    end
	end
    end

end


################################################
#
# Current talking channel log area
#  (Gtk::Notebook wrapper)
#
class CurrentChannelArea<Gtk::Notebook
    def initialize(buffer)
	@buffer=buffer

	@console=LogBuffer.new	

	super()
	self.set_show_tabs(false)
	self.set_show_border(false)
	self.insert_page(@console,nil,0)
	self.show

	@server=nil
	@channel=nil
    end	   

    attr_reader :console
    attr_reader :server
    attr_reader :channel

    def setActiveBuffer(server,channel)
	unless(@server.to_s.downcase==server.to_s.downcase&&
	       @channel.to_s.downcase==channel.to_s.downcase)

	    self.get_nth_page(self.get_current_page).active=false

	    @server=server
	    @channel=channel
	    l=nil
	    if(server.to_s.empty?&&channel.to_s.empty?)
		l=@console
	    else
		l=@buffer[@server][@channel].log
	    end

	    p=self.page_num(l)
	    if(p>=0)
		self.set_page(p)
		l.active=true
		l.refresh
	    else
		l.bind(@buffer[@server][@channel])
		l.active=true
		self.insert_page(l,nil,0)
		self.set_page(0)
	    end
	end
    end

    def write(msg)
	if(msg.server.downcase==@server.to_s.downcase)
	    begin
		if(msg.add_info.index(@channel.to_s.downcase))
		    if(msg.add_info.size>1)
			return nil
		    else
			if(self.get_nth_page(self.get_current_page).scrolled?)
			    return false
			else
			    return true
			end
		    end
		end
	    rescue NameError
		return false
	    end
	end
	return false
    end

end


################################################
#
# Entry Area
# (Gtk::Entry wrapper and command parser)
#
class EntryArea<Gtk::Entry
    def initialize(buffer)
	@buffer=buffer

	super()
	self.signal_connect('activate'){|widget|
	    parse(self.get_text)
	    self.set_text("")
	}
	self.show
    end

    private
    def parse(str)
	server=@buffer.getCurrentServer
	channel=@buffer.getCurrentChannel

	str.split("\n").each{|s|
	    unless(s.empty?)
		#
		# /hoge (except //hoge) is assumed as command 'hoge'.
		# //hoge is treated as message '/hoge'.
		#
		if(s=~/^\/([^\/].+)/)
		    exec_cmnd(@buffer,server,channel,$1.to_s)
		else
		    if(s=~/^\/(.+)/)
			s=$1.to_s
		    end
		    unless(server.to_s.empty?)
			@buffer.cmnd_privmsg(server,channel,s)
		    end
		end
	    end
	}
    end
	
    def exec_cmnd(buffer,server,channel,str)
	cmndAr=str.strip.split(" ")

	case cmndAr[0].to_s.upcase

	when "OPEN"
	    #
	    # /OPEN SERVER:port:passwd:alias NICK:login:real name
	    #
	    server=""
	    port=6667
	    passwd=""
	    serveralias=nil
	    nick=""
	    user=""
	    realname=""

	    begin
		tmp=cmndAr[1].split(":",4)
		server=tmp[0]
		unless(tmp[1].nil?)
		    port=tmp[1].to_i
		end
		passwd=tmp[2].to_s
		serveralias=tmp[3]
	    
		cmndAr[2]=cmndAr[2..-1].join(" ")
		tmp=cmndAr[2].split(":",3)
		nick=tmp[0].to_s
		user=tmp[1].to_s
		realname=tmp[2].to_s
		
		if(user.empty?)
		    user=nick
		end
		if(realname.empty?)
		    realname=user
		end
	    rescue NameError
		return false
	    end
	    
	    return buffer.open([server,port,passwd,serveralias],
			       [user,realname],
			       nick)
	when "CLOSE"
	    #
	    # /CLOSE server
	    #
	    unless(cmndAr[1].nil?)
		server=cmndAr[1].to_s
	    end
	    return buffer.close(server)

	when "EXIT"
	    return buffer.exit
	else
	    return buffer.directcommand(server,str)
	end
    end
end


################################################
#
# Buffers Controle Base
#
class BuffersControler<Gtk::HBox
    include Rica::Event

    def initialize(buffer)
	@buffer=buffer

	@tree=BuffersTree.new(@buffer)
	@currentLog=CurrentChannelArea.new(@buffer)
	@entry=EntryArea.new(@buffer)
	@otherLog=LogArea.new(false)
	@otherLog.active=true

	super(false,0)
	self.pack_start(self.layout,true,true,0)
	self.show
	
	@currentServer=nil
	@currentChnl=nil
    end

    attr_reader :tree
    attr_reader :currentLog
    attr_reader :entry
    attr_reader :otherLog

    attr_reader :currentServer
    attr_reader :currentChnl

    def layout
	buf=Array.new

	buf.push(Gtk::HPaned.new)
	buf[-1].show

	buf[-1].add(@tree)

	buf.push(Gtk::VPaned.new)
	buf[-1].show
	buf.push(Gtk::VBox.new(false,0))
	buf[-1].show
	buf[-1].pack_start(@currentLog,true,true,0)
	buf[-1].pack_start(@entry,false,false,0)
	tmp=buf.pop
	buf[-1].add(tmp)
	buf[-1].add(@otherLog)
	tmp=buf.pop
	buf[-1].add(tmp)

	return buf[0]
    end

    def isCurrentChnl?(server,channel)
	ret=false
	
	begin
	    if(@currentServer.downcase==server.downcase&&
	       @currentChnl.downcase==channel.downcase)
		ret=true
	    end
	rescue NameError
	    ret=false
	end

	return ret
    end

    def setActiveBuffer(server,channel,noselect=false)
	@currentServer=server.to_s
	@currentChnl=channel.to_s

	@currentLog.setActiveBuffer(@currentServer,@currentChnl)
	
	unless(noselect)
	    if(@currentChnl.empty?)
		@tree.selectSingleElement(@currentServer)
	    else
		@tree.selectSingleElement(@currentServer,@currentChnl)
	    end
	end
	
	@buffer.setWindowTitle(channel.to_s+"@"+server.to_s)
    end

    def notify_signal(obj,signal,*args)
	if(obj==@tree)
	    case signal
	    when 'select'
		self.setActiveBuffer(args[0].to_s,args[1].to_s,true)
	    end
	end
    end

    def write(msg)
	unless(@currentLog.write(msg))
	    @otherLog.appendLine(msg)
	end
    end

    def default_action(msg)
	@tree.push(msg)
	self.write(msg)
    end

    def on_link_established(msg)
	@tree.push(msg)

	@currentServer=msg.server
	@currentChnl=""
	self.setActiveBuffer(msg.server,"")
    end

    def on_link_closed(msg)
	@tree.push(msg)

	@currentServer=nil
	@currentChnl=nil
	self.setActiveBuffer("","")
    end

    def on_recv_cmnd_quit(msg)
	if(msg.isSelfMessage?)
	    @tree.push(msg)

	    @currentServer=nil
	    @currentChnl=nil
	    self.setActiveBuffer("","")
	else
	    self.default_action(msg)
	end
    end

    def on_recv_cmnd_join(msg)
	self.default_action(msg)
	if(msg.isSelfMessage?)
	    self.setActiveBuffer(msg.server,msg.args[0])
	end
    end

    def on_recv_cmnd_part(msg)
	self.default_action(msg)
	if(msg.isSelfMessage?)
	    @currentChnl=""
	    self.setActiveBuffer(msg.server,"")
	end
    end

    def on_recv_cmnd_kick(msg)
	self.default_action(msg)
	if(msg.to==msg.selfNick)
	    @currentChnl=""
	    self.setActiveBuffer(msg.server,"")
	end
    end

    def on_recv_cmnd_ping(msg)
	# NOP
    end
    alias on_recv_cmnd_pong on_recv_cmnd_ping
end


class MainWindow<LogHolder::Logger
    def initialize
	super(LogBuffer)

	@currentServer=nil
	@currentChnl=nil

	setWindow
    end

    attr_reader :window
    attr_reader :menu_bar
    attr_reader :body
    attr_reader :statusbar
    attr_reader :rootbox

    def getCurrentServer
	return @body.currentServer
    end

    def getCurrentChannel
	return @body.currentChnl
    end

    def setWindowTitle(str)
	@window.set_title(ERICA_DEFAULT_TITLE+": "+str.to_s)
    end

    def exit
	begin
	    self.closeAll
	rescue
	end
	Gtk.main_quit
    end

    def dispatch(msg)
	msg=super
	if(msg.instance_of?(Rica::Message))
	    @body.dispatch(msg)
	end
    end

    private
    def setWindow
	@window=Gtk::Window.new(Gtk::WINDOW_TOPLEVEL)
	@window.set_title(ERICA_DEFAULT_TITLE)
	@window.set_default_size(ERICA_DEFAULT_WIDTH,
				 ERICA_DEFAULT_HEIGHT)

	setSignals
	layout
	@window.show
    end

    def setSignals
	@window.signal_connect('destroy'){
	    Gtk.main_quit
	}
    end

    def layout
	@rootbox=Gtk::VBox.new(false,0)
	
	@menubar=setmenu
	@menubar.show
	
	@body=BuffersControler.new(self)
	@body.tree.set_usize(ERICA_DEFAULT_TREE_WIDTH,
				  ERICA_DEFAULT_TREE_HEIGHT)
	@body.currentLog.set_usize(ERICA_DEFAULT_CLOG_WIDTH,
					ERICA_DEFAULT_CLOG_HEIGHT)

	@statusbar=Gtk::Statusbar.new
	@statusbar.show
	
	@rootbox.pack_start(@menubar,false,false,0)
	@rootbox.pack_start(@body,true,true,0)
	@rootbox.pack_start(@statusbar,false,false,0)
	
	@rootbox.show
	@window.add(rootbox)
    end

    def setmenu
	menubar=Gtk::MenuBar.new
	
	menu_file=Gtk::MenuItem.new('File')
	menu_file.show
	menubar.append(menu_file)

	return menubar
    end
end

connectiondiag=Rica::ConnectionDiag.new
ctcp=Rica::CtcpResponder.new
ctcp.version="Erica 0.2 with "+ctcp.version
window=MainWindow.new
Gtk.main


syntax highlighted by Code2HTML, v. 0.9.1