#!/usr/bin/env newlisp

;; @module IRC
;; @description a basic irc library
;; @version early alpha! 0.1 2011-10-31 14:21:26
;; @author cormullion
;; Usage:
;; (IRC:init "newlithper") ; a username/nick (not that one obviously :-)
;; (IRC:connect "irc.freenode.net" 6667) ; irc/server
;; (IRC:join-channel {#newlisp}) ; join a room
;; either (IRC:read-irc-loop) ; loop - monitor only, no input
;; or     (IRC:session)       ; a command-line session, end with /QUIT

(context 'IRC)
    (define Inickname)
    (define Ichannels)
    (define Iserver)
    (define Iconnected)
    (define Icallbacks '())
    (define Idle-time 400) ; seconds
    (define Itime-stamp)   ; time since last message was processed

(define (register-callback callback-name callback-function)
    (println {registering callback for } callback-name { : } (sym (term callback-function) (prefix callback-function)))
    (push (list callback-name (sym (term callback-function) (prefix callback-function))) Icallbacks)) 

(define (do-callback callback-name data)
   (when (set 'func (lookup callback-name Icallbacks)) ; find first callback
         (if-not (catch (apply func (list data)) 'error)
                 (println {error in callback } callback-name {: } error))))

(define (do-callbacks callback-name data)
   (dolist (rf (ref-all callback-name Icallbacks))
        (set 'callback-entry (Icallbacks (first rf)))
        (when   (set 'func (last callback-entry))
                (if-not (catch (apply func (list data)) 'error)
                (println {error in callback } callback-name {: } error)))))

(define (init str)
   (set 'Inickname str)
   (set 'Iconnected nil)
   (set 'Ichannels '())
   (set 'Itime-stamp (time-of-day)))

(define (connect server port)
    (set 'Iserver     (net-connect server port))
    (net-send Iserver (format "USER %s %s %s :%s\r\n" Inickname Inickname Inickname Inickname))
    (net-send Iserver (format "NICK %s \r\n" Inickname))
    (set 'Iconnected true)
    (do-callbacks "connect" (list (list "server" server) (list "port" port))))

(define (identify password)
    (net-send Iserver (format "PRIVMSG nickserv :identify %s\r\n" password)))

(define (join-channel channel)
    (when (net-send Iserver (format "JOIN %s \r\n" channel))
          (push channel Ichannels)
          (do-callbacks "join-channel" (list (list "channel" channel) (list "nickname" Inickname)))))

(define (part chan)
    (if-not (empty? chan)
        ; leave specified
        (begin
            (net-send Iserver (format "PART %s\r\n" chan))
            (replace channel Ichannels)
            (do-callbacks "part" (list (list "channel" channel))))
        ; leave all
        (begin
            (dolist (channel Ichannels)
                (net-send Iserver (format "PART %s\r\n" channel))
                (replace channel Ichannels)
                (do-callbacks "part" (list (list "channel" channel)))))))

(define (do-quit message)
    (do-callbacks "quit" '()) ; chance to do stuff before quit...
    (net-send Iserver (format "QUIT :%s\r\n" message))
    (sleep 1000)
    (set 'Ichannels '())
    (close Iserver)
    (set 'Iconnected nil))

(define (privmsg user message)
    (net-send Iserver (format "PRIVMSG %s :%s\r\n" user message)))

(define (notice user message)
    (net-send Iserver (format "NOTICE %s :%s\r\n" user message)))

(define (send-to-server message (channel nil))
    (cond
        ((starts-with message {/}) ; default command character
            (set 'the-message (replace "^/" (copy message) {} 0)) ; keep original
            (net-send Iserver (format "%s \r\n" the-message)) ; send it
            ; do a quit
            (if (starts-with (lower-case the-message) "quit")
                (do-quit { enough})))
        (true 
            (if (nil? channel)
                ; say to all channels
                (dolist (c Ichannels)
                        (net-send Iserver (format "PRIVMSG %s :%s\r\n" c message)))
                ; say to specified channel
                (if (find channel Ichannels)
                    (net-send Iserver (format "PRIVMSG %s :%s\r\n" channel message))))))
    (do-callbacks "send-to-server" (list (list "channel" channel) (list "message" message))))

(define (process-command sender command text)
    (cond
        ((= sender "PING")
            (net-send Iserver (format "PONG %s\r\n" command)))
        ((or (= command "NOTICE") (= command "PRIVMSG"))
            (process-message sender command text))
        ((= command "JOIN")
            (set 'username (first (clean empty? (parse sender {!|:} 0))))
            (set 'channel  (last  (clean empty? (parse sender {!|:} 0))))
            (println {username } username { joined } channel)
            (do-callbacks "join" (list (list "channel" channel) (list "username" username))))
        (true
            nil)))

(define (process-message sender command text)
    (let ((username {} target {} message {}))
        (set 'username (first (clean empty? (parse sender {!|:} 0))))
        (set 'target   (trim  (first (clean empty? (parse text {!|:} 0)))))
        (set 'message  (slice text (+ (find {:} text) 1)))
        (cond 
            ((starts-with message "\001")
                (process-ctcp username target message))
            ((find target Ichannels)
                (cond 
                    ((= command {PRIVMSG})
                        (do-callbacks "channel-message" (list (list "channel" target) (list "username" username) (list "message" message))))
                    ((= command {NOTICE})
                        (do-callbacks "channel-notice"  (list (list "channel" target) (list "username" username) (list "message" message))))))
            ((= target Inickname)
                (cond 
                    ((= command {PRIVMSG})
                        (do-callbacks "private-message" (list (list "username" username) (list "message" message))))
                    ((= command {NOTICE})
                        (do-callbacks "private-notice"  (list (list "username" username) (list "message" message))))))
            (true                
                nil))))
  
(define (process-ctcp username target message)
    (cond
        ((starts-with message "\001VERSION\001")
            (net-send Iserver (format "NOTICE %s :\001VERSION %s\001\r\n" username version)))
        ((starts-with message "\001PING")
            (set 'data (first (rest (clean empty? (parse message { } 0)))))
            (set 'data (trim data "\001" "\001"))
            (net-send Iserver  (format "NOTICE %s :\001PING %s\001\r\n" username data)))
        ((starts-with message "\001ACTION")
            (set 'data (first (rest (clean empty? (parse message { } 0)))))
            (set 'data (join data { }))
            (set 'data (trim data "\001" "\001"))
            (if (find target Ichannels)
                (do-callbacks "channel-action" (list (list "username" username) (list "message" message))))
            (if (= target Inickname)
                (do-callbacks "private-action" (list (list "username" username) (list "message" message)))))
        ((starts-with message "\001TIME\001")
            (net-send Iserver (format "NOTICE %s:\001TIME :%s\001\r\n" username (date))))))

(define (parse-buffer raw-buffer)
    (let ((messages (clean empty? (parse raw-buffer "\r\n" 0)))
          (sender {} command {} text {}))
        ; check for elapsed time since last activity    
        (when (> (sub (time-of-day) Itime-stamp) (mul Idle-time 1000))
              (do-callbacks "idle-event")
              (set 'Itime-stamp (time-of-day)))
        (dolist (message messages)
            (set 'message-parts (parse message { }))           
            (unless (empty? message-parts)
                (set 'sender (first message-parts))
                (catch (set 'command (first (rest message-parts))) 'error)
                (catch (set 'text (join (rest (rest message-parts)) { })) 'error))
            (process-command sender command text))))

(define (read-irc)
    (let ((buffer {}))
        (when (!= (net-peek Iserver) 0) 
              (net-receive Iserver buffer 8192 "\n")
              (unless (empty? buffer)
                (parse-buffer buffer)))))

(define (read-irc-loop) ; monitoring
    (let ((buffer {}))       
        (while Iconnected    
            (read-irc)
            (sleep 1000))))

(define (print-raw-message data) ; example of using a callback
    (set 'raw-data (lookup "message" data))
    (set 'channel  (lookup "channel" data))
    (set 'message-text raw-data)
    (println (date (date-value) 0 {%H:%M:%S }) username {> } message-text))

(define (print-outgoing-message data)
    (set 'raw-data (lookup "message" data))
    (set 'channel  (lookup "channel" data))
    (set 'message-text raw-data)
    (println (date (date-value) 0 {%H:%M:%S }) Inickname {> } message-text))

(define (session); interactive terminal
    ; must add callbacks to display messages
    (register-callback "channel-message" 'print-raw-message)
    (register-callback "send-to-server"  'print-outgoing-message)
    (while Iconnected
        (while (zero? (peek 0))
            (read-irc))
        (send-to-server (string (read-line 0))))
    (println {finished session } (date))
    (exit))

; end of IRC code

