;;;; See LICENSE for licensing information.

(in-package :usocket)

(defun get-host-name ()
  (ccl::%stack-block ((resultbuf 256))
    (when (zerop (#_gethostname resultbuf 256))
      (ccl::%get-cstring resultbuf))))

(defparameter +openmcl-error-map+
  '((:address-in-use . address-in-use-error)
    (:connection-aborted . connection-aborted-error)
    (:no-buffer-space . no-buffers-error)
    (:connection-timed-out . timeout-error)
    (:connection-refused . connection-refused-error)
    (:host-unreachable . host-unreachable-error)
    (:host-down . host-down-error)
    (:network-down . network-down-error)
    (:address-not-available . address-not-available-error)
    (:network-reset . network-reset-error)
    (:connection-reset . connection-reset-error)
    (:shutdown . shutdown-error)
    (:access-denied . operation-not-permitted-error)))

(defparameter +openmcl-nameserver-error-map+
  '((:no-recovery . ns-no-recovery-error)
    (:try-again . ns-try-again-condition)
    (:host-not-found . ns-host-not-found-error)))

;; we need something which the openmcl implementors 'forgot' to do:
;; wait for more than one socket-or-fd

(defun input-available-p (sockets &optional ticks-to-wait)
  (ccl::rletz ((tv :timeval))
    (ccl::ticks-to-timeval ticks-to-wait tv)
    ;;### The trickery below can be moved to the wait-list now...
    (ccl::%stack-block ((infds ccl::*fd-set-size*))
      (ccl::fd-zero infds)
      (let ((max-fd -1))
        (dolist (sock sockets)
          (let ((fd (openmcl-socket:socket-os-fd (socket sock))))
            (when fd ;; may be NIL if closed
              (setf max-fd (max max-fd fd))
              (ccl::fd-set fd infds))))
        (let ((res (#_select (1+ max-fd)
                             infds (ccl::%null-ptr) (ccl::%null-ptr)
                             (if ticks-to-wait tv (ccl::%null-ptr)))))
          (when (> res 0)
            (dolist (sock sockets)
              (let ((fd (openmcl-socket:socket-os-fd (socket sock))))
                (when (and fd (ccl::fd-is-set fd infds))
                  (setf (state sock) :READ)))))
          sockets)))))

(defun raise-error-from-id (condition-id socket real-condition)
  (let ((usock-err (cdr (assoc condition-id +openmcl-error-map+))))
    (if usock-err
        (error usock-err :socket socket)
      (error 'unknown-error :socket socket :real-error real-condition))))

(defun handle-condition (condition &optional socket (host-or-ip nil))
  (typecase condition
    (openmcl-socket:socket-error
       (raise-error-from-id (openmcl-socket:socket-error-identifier condition)
                            socket condition))
    (ccl:input-timeout
       (error 'timeout-error :socket socket))
    (ccl:communication-deadline-expired
       (error 'deadline-timeout-error :socket socket))
    (ccl::socket-creation-error #| ugh! |#
       (let* ((condition-id (ccl::socket-creation-error-identifier condition))
	      (nameserver-error (cdr (assoc condition-id
					    +openmcl-nameserver-error-map+))))
	 (if nameserver-error
             (if (typep nameserver-error 'serious-condition)
                 (error nameserver-error :host-or-ip host-or-ip)
                 (signal nameserver-error :host-or-ip host-or-ip))
	   (raise-error-from-id condition-id socket condition))))))

(defun to-format (element-type protocol)
  (cond ((null element-type)
	 (ecase protocol ; default value of different protocol
	   (:stream :text)
	   (:datagram :binary)))
	((subtypep element-type 'character)
	 :text)
	(t :binary)))

#-ipv6
(defun socket-connect (host port &key (protocol :stream) element-type
		       timeout deadline nodelay
		       local-host local-port)
  (when (eq nodelay :if-supported)
    (setf nodelay t))
  (with-mapped-conditions (nil host)
    (ecase protocol
      (:stream
       (let ((mcl-sock
	      (openmcl-socket:make-socket :remote-host (host-to-hostname host)
					  :remote-port port
					  :local-host local-host
					  :local-port local-port
					  :format (to-format element-type protocol)
					  :external-format ccl:*default-external-format*
					  :deadline deadline
					  :nodelay nodelay
					  :connect-timeout timeout)))
	 (make-stream-socket :stream mcl-sock :socket mcl-sock)))
      (:datagram
       (let* ((mcl-sock
	       (openmcl-socket:make-socket :address-family :internet
					   :type :datagram
					   :local-host local-host
					   :local-port local-port
					   :input-timeout timeout
					   :format (to-format element-type protocol)
					   :external-format ccl:*default-external-format*))
	      (usocket (make-datagram-socket mcl-sock)))
	 (when (and host port)
	   (ccl::inet-connect (ccl::socket-device mcl-sock)
			      (ccl::host-as-inet-host host)
			      (ccl::port-as-inet-port port "udp")))
	 (setf (connected-p usocket) t)
	 usocket)))))

#-ipv6
(defun socket-listen (host port
                      &key reuseaddress
                           (reuse-address nil reuse-address-supplied-p)
                           (backlog 5)
                           (element-type 'character))
  (let* ((reuseaddress (if reuse-address-supplied-p reuse-address reuseaddress))
	 (real-host (host-to-hostname host))
         (sock (with-mapped-conditions (nil host)
                  (apply #'openmcl-socket:make-socket
                         (append (list :connect :passive
                                       :reuse-address reuseaddress
                                       :local-port port
                                       :backlog backlog
                                       :format (to-format element-type :stream))
                                 (unless (eq host *wildcard-host*)
                                   (list :local-host real-host)))))))
    (make-stream-server-socket sock :element-type element-type)))

(defmethod socket-accept ((usocket stream-server-usocket) &key element-type)
  (declare (ignore element-type)) ;; openmcl streams are bi/multivalent
  (let ((sock (with-mapped-conditions (usocket)
                 (openmcl-socket:accept-connection (socket usocket)))))
    (make-stream-socket :socket sock :stream sock)))

;; One close method is sufficient because sockets
;; and their associated objects are represented
;; by the same object.
(defmethod socket-close ((usocket usocket))
  (with-mapped-conditions (usocket)
    (close (socket usocket))))

(defmethod socket-shutdown ((usocket usocket) direction)
  (with-mapped-conditions (usocket)
    (openmcl-socket:shutdown (socket usocket) :direction direction)))

#-ipv6
(defmethod socket-send ((usocket datagram-usocket) buffer size &key host port (offset 0))
  (with-mapped-conditions (usocket host)
    (if (and host port)
	(openmcl-socket:send-to (socket usocket) buffer size
				:remote-host (host-to-hbo host)
				:remote-port port
				:offset offset)
	;; Clozure CL's socket function SEND-TO doesn't support operations on connected UDP sockets,
	;; so we have to define our own.
	(let* ((socket (socket usocket))
	       (fd (ccl::socket-device socket)))
	  (multiple-value-setq (buffer offset)
	    (ccl::verify-socket-buffer buffer offset size))
	  (ccl::%stack-block ((bufptr size))
	    (ccl::%copy-ivector-to-ptr buffer offset bufptr 0 size)
	    (ccl::socket-call socket "send"
	      (ccl::with-eagain fd :output
		(ccl::ignoring-eintr
		  (ccl::check-socket-error (#_send fd bufptr size 0))))))))))

(defmethod socket-receive ((usocket datagram-usocket) buffer length &key)
  (with-mapped-conditions (usocket)
    (openmcl-socket:receive-from (socket usocket) length :buffer buffer)))

(defun usocket-host-address (address)
  (cond
    ((integerp address)
     (hbo-to-vector-quad address))
    ((and (arrayp address)
          (= (length address) 16)
          (every #'= address #(0 0 0 0 0 0 0 0 0 0 #xff #xff)))
     (make-array 4 :displaced-to address :displaced-index-offset 12))
    (t
     address)))

(defmethod get-local-address ((usocket usocket))
  (usocket-host-address (openmcl-socket:local-host (socket usocket))))

(defmethod get-peer-address ((usocket stream-usocket))
  (usocket-host-address (openmcl-socket:remote-host (socket usocket))))

(defmethod get-local-port ((usocket usocket))
  (openmcl-socket:local-port (socket usocket)))

(defmethod get-peer-port ((usocket stream-usocket))
  (openmcl-socket:remote-port (socket usocket)))

(defmethod get-local-name ((usocket usocket))
  (values (get-local-address usocket)
          (get-local-port usocket)))

(defmethod get-peer-name ((usocket stream-usocket))
  (values (get-peer-address usocket)
          (get-peer-port usocket)))

(defun get-host-by-address (address)
  (with-mapped-conditions (nil address)
     (openmcl-socket:ipaddr-to-hostname (host-to-hbo address))))

(defun get-hosts-by-name (name)
  (with-mapped-conditions (nil name)
     (list (hbo-to-vector-quad (openmcl-socket:lookup-hostname
                                (host-to-hostname name))))))

(defun %setup-wait-list (wait-list)
  (declare (ignore wait-list)))

(defun %add-waiter (wait-list waiter)
  (declare (ignore wait-list waiter)))

(defun %remove-waiter (wait-list waiter)
  (declare (ignore wait-list waiter)))

(defun wait-for-input-internal (wait-list &key timeout)
  (with-mapped-conditions ()
    (let* ((ticks-timeout (truncate (* (or timeout 1)
                                       ccl::*ticks-per-second*))))
      (input-available-p (wait-list-waiters wait-list)
			 (when timeout ticks-timeout))
      wait-list)))

;;; Helper functions for option.lisp

(defun get-socket-option-reuseaddr (socket)
  (ccl::int-getsockopt (ccl::socket-device socket)
                                  #$SOL_SOCKET #$SO_REUSEADDR))

(defun set-socket-option-reuseaddr (socket value)
  (ccl::int-setsockopt (ccl::socket-device socket)
                                  #$SOL_SOCKET #$SO_REUSEADDR value))

(defun get-socket-option-broadcast (socket)
  (ccl::int-getsockopt (ccl::socket-device socket)
                                  #$SOL_SOCKET #$SO_BROADCAST))

(defun set-socket-option-broadcast (socket value)
  (ccl::int-setsockopt (ccl::socket-device socket)
                                  #$SOL_SOCKET #$SO_BROADCAST value))

(defun get-socket-option-tcp-nodelay (socket)
  (ccl::int-getsockopt (ccl::socket-device socket)
                                  #$IPPROTO_TCP #$TCP_NODELAY))

(defun set-socket-option-tcp-nodelay (socket value)
  (ccl::int-setsockopt (ccl::socket-device socket)
                                  #$IPPROTO_TCP #$TCP_NODELAY value))
