-- This is the main class file
-- Written by Moritz Hoffmann 2006, part of the orx-irc project
-- Released under the BSD license

-- The following lines will be kept and shop up as the beginning lines of the
-- final irc.cls file.
/****************************************************************************
** This is the object rexx IRC interface class
** If you find just one file called irc.cls you have a compiled version
** of the source. Go to sf.net/projects/orx-irc to get the full source
** and the documentation.
** There is a subversion tree a well.
****************************************************************************/
-- *************************************************************************
-- ** The IRC Class
-- ** This is the main class of the irc client. All communication with the server
-- ** passes the class
-- *************************************************************************

::CLASS irc PUBLIC
-- initialization, accepts a host object
::METHOD init
    expose users channels NumHandler ClientEvent TalkBack
    -- an array that contains the current user objects, ID is the position in the array
    users = .array~new
    -- an array that contains the current active channel objects and their ID  
    channels = .array~new
    -- a handler to handle incoming numeric messages
    NumHandler = .NumHandler~new(self)
    -- initialize the ClientEvent object
    ClientEvent = .ClientEvent~new
    -- create the talkback object
    TalkBack = .ClientTB~New(self,NumHandler)

    -- a directory to hold specific information about the IRC server
    self~settings = .directory~new
    self~settings~IsSupported = .Issupported~new
    -- debug that the class has been created succesfully
    call debug "[IRC instance created]",1

    -- return the current instance
    return self

-- make the users array public
::METHOD users    ATTRIBUTE
-- make the channels object public
::METHOD channels ATTRIBUTE
-- give access to the Talkback object
::METHOD TalkBack UNGUARDED
    expose TalkBack; return TalkBack

-- make the settings accessible
::METHOD Settings ATTRIBUTE
    
-- method connect connects to an IRC server
-- call: ~connect(host object,password,default nick name, user name, comment)
::METHOD Connect
    expose host serverpass socket
    -- tell the client the object has been initialized
    self~ClientEvent("irc.event.init",self)

    -- get the arguments
    use arg host, serverpass, nick, username, description
    call debug "[IRC: Connect method called]",1
    -- create a new line based socket
    socket = .LNSOCKET~new(host,self)
    -- now connect
    rc = socket~connect
    if rc \= 0 then do
	call debug "[IRC: Connect: socket return value:" rc", error connecting!", 16
        return
    end
    call debug "[IRC: Connect: socket connected]",8
    call SysSleep 0.5
    if serverpass \= .nil then do
	call debug "[IRC: Connect: PASS command will be written]",8
	self~writeln('PASS '||serverpass)
    end
    self~writeln('NICK' nick)
    self~writeln('USER' username '7 * :'description)
    call debug "[IRC: Connect: NICK and USER command written]",8
--  .user~new(self,nick) -- creates user with ID=1, the own user
-- the connect event is created by num rpl 001 to make sure the connection is up
-- when the command is issued
--    self~ClientEvent("irc.event.connect")

::METHOD DisConnect
    expose socket
    if socket~connected then do
	self~Writeln("QUIT :"arg(1))
	call debug "[IRC: DisConnect: Disconnecting]",1
        socket~disconnect
        self~ClientEvent("IRC.EVENT.DISCONNECT")
    end
    else call debug "[IRC DisConnect: Not connected, can not disconnect",16

::METHOD Connected
    expose socket
    return socket~connected

::METHOD Writeln
    expose socket
    call debug "[IRC: WriteLN:" arg(1),3
    socket~write(arg(1)||"0d0a"x)
    self~ClientEvent("IRC.EVENT.WRITE",arg(1))
    
::METHOD GetEventHandler
    expose ClientEvent
    return ClientEvent

-- This method forwards events to the client
::METHOD ClientEvent
    expose ClientEvent
    signal on syntax name err
    -- the argument count is 2, meaning the second parameter was supplied,
    -- which is the event's parameter
    if arg() = 2 then do
	-- get the name and message argument
	use arg name,message
        eventmessage =.message~new(ClientEvent,name,'I', message)~~send
    end
    -- only the name was supplied
    else do
	use arg name
        eventmessage =.message~new(ClientEvent,name)~~send
    end
    return
    
    -- forward all errors to the debug object to protect the main thread from
    -- terminating
    err:
    .debug~error(condition('O'))




-- This message gets called whenever there is new input from the socket
::METHOD Available
    -- add a blank line to the debug output to improve readability
    call debug ""time(),1
    call debug "[IRC: Available: |"arg(1), 3
    -- forward the incoming string to the private parseline method
    message = self~parseline(arg(1))
    -- if a message was returned it needs to be handled by the local event handler
    if message \= .nil then self~Event(message)

-- this method receives all events that come from the server
::METHOD Event
    expose NumHandler
    use arg message
-- ******************************
--     This method receives the messages created by the message parser. All
--     transformed messages that arrive pass through this method.
--     To forward the messages to their appropriate receiver objects, the
--     superclasses of the messages are analyzed.
--     For now, there's a receiver object for numerc replies.
-- ********************************
    call debug TSToStr(message~Timestamp) "[IRC: Event:" message~class,3
    if message= .nil then raise syntax 93.911
    superclasses = message~class~superclasses
    -- filter numeric replies
    if message~class = .nummsg then do
	NumHandler~event(message)
	self~ClientEvent("IRC.EVENT.NUMRPL."message~number,message)
	return
    end
    -- here all other messages get forwarded to their reciever
    do superclass over superclasses
	-- filter for server messages
	if superclass = .smsg then do -- filter for .smsg superclass
	    SELECT
		when message~class = .msgerror then do
    	        call debug "[IRC: Event: Error received, disconnecting!",9
        	self~disconnect
        	return
	    end
    	    when message~class = .msgping then self~writeln("PONG :"message~string)
	    when message~class = .snotice then self~ClientEvent("IRC.EVENT.SNOTICE",message)
    	    otherwise nop
    	    END
    	    return
	end
	if superclass = .umsg then do
    	    message~sender~event(message)
    	    return
	end
	if superclass = .umsgr then do
    	    message~sender~event(message)
    	    return
	end
    end

-- This is the parsing method
-- It converts the incoming string to it's appropriate message object
-- and returns it. If it failed .nil is returned instead.
::METHOD parseline PRIVATE
    expose events channels users
    call debug "[IRC: ParseLine",1
    parse arg line
    parse var line prefix command params
    params=params~strip
    command = command~strip

    -- there was no prefix, the prefix *always* starts with a colon
    if prefix~left(1) \= ':' then do
	params = command
	command = prefix
	prefix = ''
	origin='SERVER'
    end
    else do
	prefix = prefix~right(prefix~length-1)~strip
	parse var prefix nick'!'username'@'host
	if nick~pos('.') = 0 then do
	-- Message is from a server, nicks never contain dots
	-- This workaround fails if the server name doesn't contain dots...
	origin = 'NICK'
    end
	else origin = 'SERVER'
    end
    message=.nil
    call debug "[IRC: ParseLine: origin='"origin"'",8
    if origin = 'SERVER' then
	-- it's a numeric reply
	if command~datatype = 'NUM' then message=.nummsg~new(command,params)
	else do
	Select
	when command = 'PING' then do
	    parse var params . ':'ping_str
    	    message=.msgping~new(ping_str)
	end
	WHEN command = 'ERROR' then message=.msgerror~new
	WHEN command = 'NOTICE' then do
	    if prefix = '' then parse var line ':' text
    	    else parse var params ':' text
    	    message=.snotice~new(text)
	end
        WHEN command = 'WALLOPS' then do
    	    if prefix = '' then parse var line ':' text
    	    else parse var params ':' text
    	    message=.msgwallops~new(text)
	end
	WHEN command = "MODE" then do
    	    if prefix = '' then parse var line . target modifier
    	    else parse var params target modifier
    	    message = .smode~new(prefix,self~GetreceiverObject(target),modifier)
	end
	otherwise nop
	END
    end
    else do
	nickobject = self~GetReceiverObject(nick)
	if nickobject = .nil then do
	    nickobject = .user~new(self,prefix,users,channels)
	end
	if (\ nickobject~hashostmark) & (username <> '') & (host <> '') then do
	    nickobject~SetHostMark(nickobject~makestring'!'username'@'host)
	end
	parse var params commands ':'last
	commands = commands~strip; last=last~strip
	SELECT
	WHEN command = 'NICK'    then message= .msgnick~new(nickobject,last)
	WHEN command = 'QUIT'    then message= .msgquit~new(nickobject,last)
	WHEN command = 'KILL'    then do
	    message= .msgkill~new(nickobject, self~GetReceiverObject(commands), last)
	end
	WHEN command = 'JOIN'    then message= .msgjoin~new(nickobject,last)
	WHEN command = 'PART'    then message= .msgpart~new(nickobject,self~GetReceiverObject(commands),last)
	WHEN command = 'KICK'    then do
    	    parse value commands~space with channel targetnick
    	    message=.msgkick~new(nickobject,self~GetReceiverObject(targetnick),self~GetReceiverObject(channel),last)
	end
        WHEN command = 'TOPIC'   then do
    	    message= .msgtopic~new(nickobject,self~GetReceiverObject(commands),last)
        end
        WHEN command = 'MODE'    then do
	    parse var params receiver string
            message= .msgmode~new(nickobject,self~GetReceiverObject(receiver),string)
        end
        WHEN command = 'PRIVMSG' then do
    	    message= .privmsg~new(nickobject,self~GetReceiverObject(commands),last)
        end
        WHEN command = 'NOTICE'  then do
    	    message= .unotice~new(nickobject,self~GetReceiverObject(commands),last)
        end
        WHEN command = 'INVITE'  then do
            message= .invite~new(nickobject,self~GetReceiverObject(commands),last)
        end
        OTHERWISE call debug "[IRC: ParseLine:" nickobject~nick 'sent unknown command' command '(params='params')',24
	END
    end
    return message

-- this message returns a channel or nick object if it's known or the nil object
::METHOD GetReceiverObject
  expose users channels
  parse arg string
  call debug "[IRC: GetReceiverObject:" string,3
  string = IRCUpper(string)
  if users~items>0 then
    do i = 1 to users~last
      if users~hasindex(i) then
        if IRCUpper(users[i]) = string then do
          call debug "[IRC: GetReceiverObject: found" string,4
          return users[i]
        end
    end
  if channels~items>0 then
    do i = 1 to channels~last
      if channels~hasindex(i) then
        if IRCUpper(channels[i]) = string then do
          call debug "[IRC: GetReceiverObject: found" string "id:" i,4
          return channels[i]
        end
    end
  call debug "[IRC: GetReceiverObject:" string "not found, returning NIL.",4
  return .nil


