;;;; See LICENSE for licensing information.

(in-package :usocket)

(defparameter +scl-error-map+
  (append +unix-errno-condition-map+
          +unix-errno-error-map+))

(defun scl-map-socket-error (err &key condition socket)
  (let ((usock-err (cdr (assoc err +scl-error-map+ :test #'member))))
    (cond (usock-err
       (if (subtypep usock-err 'error)
           (error usock-err :socket socket)
           (signal usock-err :socket socket)))
      (t
       (error 'unknown-error
          :socket socket
          :real-error condition)))))

(defun handle-condition (condition &optional (socket nil) (host-or-ip nil))
  "Dispatch correct usocket condition."
  (typecase condition
    (ext::socket-error
     (scl-map-socket-error (ext::socket-errno condition)
			   :socket socket
			   :condition condition))))

(defun socket-connect (host port &key (protocol :stream) (element-type 'character)
                       timeout deadline (nodelay t nodelay-specified)
		       (local-host nil local-host-p)
		       (local-port nil local-port-p)
		       &aux
		       (patch-udp-p (fboundp 'ext::inet-socket-send-to)))
  (when (and nodelay-specified 
             (not (eq nodelay :if-supported)))
    (unsupported 'nodelay 'socket-connect))
  (when deadline (unsupported 'deadline 'socket-connect))
  (when timeout (unsupported 'timeout 'socket-connect))
  (when (and local-host-p (not patch-udp-p))
     (unsupported 'local-host 'socket-connect :minimum "1.3.9"))
  (when (and local-port-p (not patch-udp-p))
     (unsupported 'local-port 'socket-connect :minimum "1.3.9"))

  (let ((socket))
    (ecase protocol
      (:stream
       (setf socket (let ((args (list (host-to-hbo host) port :kind protocol)))
		      (when (and patch-udp-p (or local-host-p local-port-p))
			(nconc args (list :local-host (when local-host
							(host-to-hbo local-host))
					  :local-port local-port)))
		      (with-mapped-conditions (socket)
			(apply #'ext:connect-to-inet-socket args))))
       (let ((stream (sys:make-fd-stream socket :input t :output t
					 :element-type element-type
					 :buffering :full)))
	 (make-stream-socket :socket socket :stream stream)))
      (:datagram
       (when (not patch-udp-p)
	 (error 'unsupported
		:feature '(protocol :datagram)
		:context 'socket-connect
		:minumum "1.3.9"))
       (setf socket
	     (if (and host port)
		 (let ((args (list (host-to-hbo host) port :kind protocol)))
		   (when (and patch-udp-p (or local-host-p local-port-p))
		     (nconc args (list :local-host (when local-host
						     (host-to-hbo local-host))
				       :local-port local-port)))
		   (with-mapped-conditions (socket)
		     (apply #'ext:connect-to-inet-socket args)))
		 (if (or local-host-p local-port-p)
		     (with-mapped-conditions ()
		       (ext:create-inet-listener (or local-port 0)
						 protocol
						 :host (when local-host
							 (if (ip= local-host *wildcard-host*)
							     0
							     (host-to-hbo local-host)))))
		     (with-mapped-conditions ()
		       (ext:create-inet-socket protocol)))))
       (let ((usocket (make-datagram-socket socket :connected-p (and host port t))))
	 (ext:finalize usocket #'(lambda ()
				   (when (%open-p usocket)
				     (ext:close-socket socket))))
	 usocket)))))

(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))
         (host (if (ip= host *wildcard-host*)
                   0
                 (host-to-hbo host)))
         (server-sock
          (with-mapped-conditions ()
            (ext:create-inet-listener port :stream
                                      :host host
                                      :reuse-address reuseaddress
                                      :backlog backlog))))
   (make-stream-server-socket server-sock :element-type element-type)))

(defmethod socket-accept ((usocket stream-server-usocket) &key element-type)
  (with-mapped-conditions (usocket)
    (let* ((sock (ext:accept-tcp-connection (socket usocket)))
           (stream (sys:make-fd-stream sock :input t :output t
                                      :element-type (or element-type
                                                        (element-type usocket))
                                      :buffering :full)))
      (make-stream-socket :socket sock :stream stream))))

;; Sockets and their associated streams are modelled as
;; different objects. Be sure to close the socket stream
;; when closing stream-sockets; it makes sure buffers
;; are flushed and the socket is closed correctly afterwards.
(defmethod socket-close ((usocket usocket))
  "Close socket."
  (with-mapped-conditions (usocket)
    (ext:close-socket (socket usocket))))

(defmethod socket-close ((usocket stream-usocket))
  "Close socket."
  (with-mapped-conditions (usocket)
    (close (socket-stream usocket) :abort t)))

(defmethod socket-close :after ((socket datagram-usocket))
  (setf (%open-p socket) nil))

(defmethod socket-shutdown ((usocket usocket) direction)
  (declare (ignore usocket direction))
  (unsupported "shutdown" 'socket-shutdown))

(defmethod socket-send ((usocket datagram-usocket) buffer size &key host port)
  (let ((s (socket usocket))
	(host (if host (host-to-hbo host)))
	(real-buffer (if (zerop offset)
			 buffer
			 (subseq buffer offset (+ offset size)))))
    (multiple-value-bind (result errno)
	(ext:inet-socket-send-to s real-buffer size
				 :remote-host host :remote-port port)
      (or result
	  (scl-map-socket-error errno :socket usocket)))))

(defmethod socket-receive ((socket datagram-usocket) buffer length &key)
  (declare (values (simple-array (unsigned-byte 8) (*)) ; buffer
		   (integer 0)                          ; size
		   (unsigned-byte 32)                   ; host
		   (unsigned-byte 16)))                 ; port
  (let ((s (socket socket)))
    (let ((real-buffer (or buffer
			   (make-array length :element-type '(unsigned-byte 8))))
	  (real-length (or length
			   (length buffer))))
      (multiple-value-bind (result errno remote-host remote-port)
	  (ext:inet-socket-receive-from s real-buffer real-length)
	(if result
	    (values real-buffer result remote-host remote-port)
	    (scl-map-socket-error errno :socket socket))))))

(defmethod get-local-name ((usocket usocket))
  (multiple-value-bind (address port)
      (with-mapped-conditions (usocket)
        (ext:get-socket-host-and-port (socket usocket)))
    (values (hbo-to-vector-quad address) port)))

(defmethod get-peer-name ((usocket stream-usocket))
  (multiple-value-bind (address port)
      (with-mapped-conditions (usocket)
        (ext:get-peer-host-and-port (socket usocket)))
    (values (hbo-to-vector-quad address) port)))

(defmethod get-local-address ((usocket usocket))
  (nth-value 0 (get-local-name usocket)))

(defmethod get-peer-address ((usocket stream-usocket))
  (nth-value 0 (get-peer-name usocket)))

(defmethod get-local-port ((usocket usocket))
  (nth-value 1 (get-local-name usocket)))

(defmethod get-peer-port ((usocket stream-usocket))
  (nth-value 1 (get-peer-name usocket)))


(defun get-host-by-address (address)
  (multiple-value-bind (host errno)
      (ext:lookup-host-entry (host-byte-order address))
    (cond (host
           (ext:host-entry-name host))
          (t
           (let ((condition (cdr (assoc errno +unix-ns-error-map+))))
             (cond (condition
                    (error condition :host-or-ip address))
                   (t
                    (error 'ns-unknown-error :host-or-ip address
                           :real-error errno))))))))

(defun get-hosts-by-name (name)
  (multiple-value-bind (host errno)
      (ext:lookup-host-entry name)
    (cond (host
           (mapcar #'hbo-to-vector-quad
                   (ext:host-entry-addr-list host)))
          (t
           (let ((condition (cdr (assoc errno +unix-ns-error-map+))))
             (cond (condition
                    (error condition :host-or-ip name))
                   (t
                    (error 'ns-unknown-error :host-or-ip name
                           :real-error errno))))))))

(defun get-host-name ()
  (unix:unix-gethostname))


;;
;;
;;  WAIT-LIST part
;;


(defun %add-waiter (wl waiter)
  (declare (ignore wl waiter)))

(defun %remove-waiter (wl waiter)
  (declare (ignore wl waiter)))

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

(defun wait-for-input-internal (wait-list &key timeout)
  (let* ((sockets (wait-list-waiters wait-list))
         (pollfd-size (alien:alien-size (alien:struct unix::pollfd) :bytes))
         (nfds (length sockets))
         (bytes (* nfds pollfd-size)))
    (alien:with-bytes (fds-sap bytes)
      (do ((sockets sockets (rest sockets))
          (base 0 (+ base 8)))
         ((endp sockets))
       (let ((fd (socket (first sockets))))
         (setf (sys:sap-ref-32 fds-sap base) fd)
         (setf (sys:sap-ref-16 fds-sap (+ base 4)) unix::pollin)))
      (multiple-value-bind (result errno)
         (let ((thread:*thread-whostate* "Poll wait")
               (timeout (if timeout
                            (truncate (* timeout 1000))
                            -1)))
           (declare (inline unix:unix-poll))
           (unix:unix-poll (alien:sap-alien fds-sap
                                            (* (alien:struct unix::pollfd)))
                           nfds timeout))
       (cond ((not result)
              (error "~@<Polling error: ~A~:@>"
                     (unix:get-unix-error-msg errno)))
             (t
              (do ((sockets sockets (rest sockets))
                   (base 0 (+ base 8)))
                  ((endp sockets))
                (let ((flags (sys:sap-ref-16 fds-sap (+ base 6))))
                  (unless (zerop (logand flags unix::pollin))
                    (setf (state (first sockets)) :READ))))))))))

