(in-package #:ircbot)

(defvar *max-lag* 60)
(defvar *ping-freq* 30)


(defclass ircbot ()
  ((connection :accessor ircbot-connection :initform nil)
   (channel :reader ircbot-channel :initarg :channel)
   (server :reader ircbot-server :initarg :server)
   (port :reader ircbot-port :initarg :port)
   (nick :reader ircbot-nick :initarg :nick)
   (password :reader ircbot-password :initarg :password)
   (connection-security :reader ircbot-connection-security
                        :initarg :connection-security
                        :initform :none)
   (run-thread :accessor ircbot-run-thread :initform nil)
   (ping-thread :accessor ircbot-ping-thread :initform nil)
   (lag :accessor ircbot-lag :initform nil)
   (lag-track :accessor ircbot-lag-track :initform nil)))

(defmethod ircbot-check-nick ((bot ircbot) message)
  (destructuring-bind (target msgtext) (arguments message)
    (declare (ignore msgtext))
    (if (string= target (ircbot-nick bot))
        (ircbot-nickserv-auth bot)
        (ircbot-nickserv-ghost bot))))

(defmethod ircbot-connect :around ((bot ircbot))
  (let ((conn (connect :nickname (ircbot-nick bot)
                       :server (ircbot-server bot)
                       :port (ircbot-port bot)
                       :connection-security (ircbot-connection-security bot))))
    (setf (ircbot-connection bot) conn)
    (call-next-method)
    (read-message-loop conn)))

(defmethod ircbot-connect ((bot ircbot))
  (let ((conn (ircbot-connection bot)))
    (add-hook conn 'irc-err_nicknameinuse-message (lambda (message)
                                                    (declare (ignore message))
                                                    (ircbot-randomize-nick bot)))
    (add-hook conn 'irc-kick-message (lambda (message)
                                       (declare (ignore message))
                                       (join (ircbot-connection bot)
                                             (ircbot-channel bot))))
    (add-hook conn 'irc-notice-message (lambda (message)
                                         (ircbot-handle-nickserv bot message)))
    (add-hook conn 'irc-pong-message (lambda (message)
                                       (ircbot-handle-pong bot message)))
    (add-hook conn 'irc-rpl_welcome-message (lambda (message)
                                              (ircbot-start-ping-thread bot)
                                              (ircbot-check-nick bot message)))))

(defmethod ircbot-connect-thread ((bot ircbot))
  (setf (ircbot-run-thread bot)
        (sb-thread:make-thread (lambda () (ircbot-connect bot))
                               :name "ircbot-run")))

(defmethod ircbot-disconnect ((bot ircbot) &optional (quit-msg "..."))
  (sb-sys:without-interrupts
    (quit (ircbot-connection bot) quit-msg)
    (setf (ircbot-lag-track bot) nil)
    (setf (ircbot-connection bot) nil)
    (if (not (null (ircbot-run-thread bot)))
        (sb-thread:terminate-thread (ircbot-run-thread bot)))
    (sb-thread:terminate-thread (ircbot-ping-thread bot))))

(defmethod ircbot-reconnect ((bot ircbot) &optional (quit-msg "..."))
  (let ((threaded-p (not (null (ircbot-run-thread bot)))))
    (ircbot-disconnect bot quit-msg)
    (if threaded-p
        (ircbot-connect-thread bot)
        (ircbot-connect bot))))

(defmethod ircbot-handle-nickserv ((bot ircbot) message)
  (let ((conn (ircbot-connection bot)))
    (if (string= (host message) "services.")
        (destructuring-bind (target msgtext) (arguments message)
          (declare (ignore target))
          (cond ((string= msgtext "This nickname is registered. Please choose a different nickname, or identify via /msg NickServ identify <password>.")
                 (ircbot-nickserv-auth bot))
                ((string= msgtext (format nil "~A has been ghosted." (ircbot-nick bot)))
                 (nick conn (ircbot-nick bot)))
                ((string= msgtext (format nil "~A is not online." (ircbot-nick bot)))
                 (ircbot-nickserv-auth bot))
                ((string= msgtext (format nil "You are now identified for ~A." (ircbot-nick bot)))
                 (join conn (ircbot-channel bot))))))))

(defmethod ircbot-handle-pong ((bot ircbot) message)
  (destructuring-bind (server ping) (arguments message)
    (declare (ignore server))
    (let ((response (ignore-errors (parse-integer ping))))
      (when response
        (setf (ircbot-lag-track bot) (delete response (ircbot-lag-track bot) :test #'=))
        (setf (ircbot-lag bot) (- (received-time message) response))))))

(defmethod ircbot-nickserv-auth ((bot ircbot))
  (privmsg (ircbot-connection bot) "NickServ"
           (format nil "identify ~A" (ircbot-password bot))))

(defmethod ircbot-nickserv-ghost ((bot ircbot))
  (privmsg (ircbot-connection bot) "NickServ"
           (format nil "ghost ~A ~A" (ircbot-nick bot) (ircbot-password bot))))

(defmethod ircbot-randomize-nick ((bot ircbot))
  (nick (ircbot-connection bot)
        (format nil "~A-~A" (ircbot-nick bot) (+ (random 90000) 10000))))

(defmethod ircbot-send-message ((bot ircbot) target message-text)
  (privmsg (ircbot-connection bot) target message-text))

(defmethod ircbot-start-ping-thread ((bot ircbot))
  (let ((conn (ircbot-connection bot)))
    (setf (ircbot-ping-thread bot)
          (sb-thread:make-thread
           (lambda ()
             (loop
                do (progn (sleep *ping-freq*)
                          (let ((ct (get-universal-time)))
                            (push ct (ircbot-lag-track bot))
                            (ping conn (princ-to-string ct))))
                until (ircbot-timed-out-p bot))
             (ircbot-reconnect bot))
           :name "ircbot-ping"))))

(defmethod ircbot-timed-out-p ((bot ircbot))
  (loop
     with ct = (get-universal-time)
     for v in (ircbot-lag-track bot)
     when (> (- ct v) *max-lag*)
     do (return t)))


(defun make-ircbot (server port nick password channel)
  (make-instance 'ircbot
                 :server server
                 :port port
                 :nick nick
                 :password password
                 :channel channel))
