;;; This is experimental code that uses the s-expression
;;; representation of a C/C++ library interface to generate Foreign
;;; Function Interface definitions for use with Kevin Rosenberg's
;;; UFFI.
;;;
;;; Written by Matthias Koeppe <mkoeppe@mail.math.uni-magdeburg.de>

(eval-when (:compile-toplevel :load-toplevel :execute)
  (require 'port)				; from CLOCC
  (require 'uffi))

(in-package :cl-user)

;; Interaction with the SWIG binary

(defvar *swig-source-directory* #p"/home/mkoeppe/s/swig1.3/")

(defvar *swig-program* (merge-pathnames "preinst-swig" *swig-source-directory*))

(defun run-swig (swig-interface-file-name &key directory-search-list module
		 ignore-errors c++)
  (let ((temp-file-name "/tmp/swig.lsp"))
    (let ((process
	   (port:run-prog (namestring *swig-program*)
			  :output t
			  :args `(,@(and c++ '("-c++"))
				  "-sexp"
				  ,@(mapcar (lambda (dir)
				       (concatenate 'string
						    "-I" (namestring dir)))
					    directory-search-list)
				  ,@(and module
					 `("-module" ,module))
				  "-o" ,temp-file-name
				  ,(namestring swig-interface-file-name)))))
      #+cmu (unless (or (zerop (ext:process-exit-code process))
			ignore-errors)
	      (error "Process swig exited abnormally"))
      (with-open-file (s temp-file-name)
	(read s)))))

;; Type system

(defun parse-swigtype (type-string &key start end junk-ok)
  "Parse TYPE-STRING as SWIG's internal representation of C/C++
types. Return two values: The type description (an improper list) and
the terminating index into TYPE-STRING."
  ;; SWIG's internal representation is described in Source/Swig/stype.c
  (unless start
    (setq start 0))
  (unless end
    (setq end (length type-string)))
  (flet ((prefix-match (prefix)
	   (let ((position (mismatch prefix type-string :start2 start :end2 end)))
	     (or (not position)
		 (= position (length prefix)))))
	 (bad-type-error (reason)
	   (error "Bad SWIG type (~A): ~A" reason
		  (subseq type-string start end)))
	 (type-char (index)
	   (and (< index (length type-string))
		(char type-string index)))	       
	 (cons-and-recurse (prefix start end)
	   (multiple-value-bind (type-description index)
	       (parse-swigtype type-string :start start :end end
				:junk-ok junk-ok)
	     (values (cons prefix type-description)
		     index))))
    (cond
      ((prefix-match "p.")		; pointer
       (cons-and-recurse '* (+ start 2) end))
      ((prefix-match "r.")		; C++ reference
       (cons-and-recurse '& (+ start 2) end))
      ((prefix-match "a(")		; array
       (let ((closing-paren (position #\) type-string
				  :start (+ start 2)
				  :end end)))
	 (unless closing-paren
	   (bad-type-error "missing right paren"))
	 (unless (eql (type-char (+ closing-paren 1)) #\.)
	   (bad-type-error "missing dot"))
	 (cons-and-recurse (list 'ARRAY (subseq type-string (+ start 2) closing-paren))
			   (+ closing-paren 2) end)))
      ((prefix-match "q(")		; qualifier (const, volatile)
       (let ((closing-paren (position #\) type-string
				  :start (+ start 2)
				  :end end)))
	 (unless closing-paren
	   (bad-type-error "missing right paren"))
	 (unless (eql (type-char (+ closing-paren 1)) #\.)
	   (bad-type-error "missing dot"))
	 (cons-and-recurse (list 'QUALIFIER (subseq type-string (+ start 2) closing-paren))
			   (+ closing-paren 2) end)))
      ((prefix-match "m(")		; C++ member pointer
       (multiple-value-bind (class-type class-end-index)
	   (parse-swigtype type-string :junk-ok t
			    :start (+ start 2) :end end)
	 (unless (eql (type-char class-end-index) #\))
	   (bad-type-error "missing right paren"))
	 (unless (eql (type-char (+ class-end-index 1)) #\.)
	   (bad-type-error "missing dot"))
	 (cons-and-recurse (list 'MEMBER-POINTER class-type)
			   (+ class-end-index 2) end)))	 
      ((prefix-match "f(")		; function
       (loop with index = (+ start 2) 
	     until (eql (type-char index) #\))
	     collect (multiple-value-bind (arg-type arg-end-index)
			 (parse-swigtype type-string :junk-ok t
					  :start index :end end)
		       (case (type-char arg-end-index)
			 (#\, (setq index (+ arg-end-index 1)))
			 (#\) (setq index arg-end-index))
			 (otherwise (bad-type-error "comma or right paren expected"))) 
		       arg-type)
	     into arg-types
	     finally (unless (eql (type-char (+ index 1)) #\.)
		       (bad-type-error "missing dot"))
	     (return (cons-and-recurse (cons 'FUNCTION arg-types)
				       (+ index 2) end))))
      ((prefix-match "v(")		;varargs
       (let ((closing-paren (position #\) type-string
				  :start (+ start 2)
				  :end end)))
	 (unless closing-paren
	   (bad-type-error "missing right paren"))
	 (values (list 'VARARGS (subseq type-string (+ start 2) closing-paren))
		 (+ closing-paren 1))))
      (t (let ((junk-position (position-if (lambda (char)
					     (member char '(#\, #\( #\) #\.)))
					   type-string
					   :start start :end end)))
	   (cond (junk-position		; found junk
		  (unless junk-ok
		    (bad-type-error "trailing junk"))
		  (values (subseq type-string start junk-position)
			  junk-position))
		 (t
		  (values (subseq type-string start end)
			  end))))))))

(defun swigtype-function-p (swigtype)
  "Check whether SWIGTYPE designates a function.  If so, the second
value is the list of argument types, and the third value is the return
type."
  (if (and (consp swigtype)
	   (consp (first swigtype))
	   (eql (first (first swigtype)) 'FUNCTION))
      (values t (rest (first swigtype)) (rest swigtype))
      (values nil nil nil)))
	      

;; UFFI

(defvar *uffi-definitions* '())

(defconstant *uffi-default-primitive-type-alist*
  '(("char" . :char)
    ("unsigned char" . :unsigned-byte)
    ("signed char" . :byte)
    ("short" . :short)
    ("signed short" . :short)
    ("unsigned short" . :unsigned-short)
    ("int" . :int)
    ("signed int" . :int)
    ("unsigned int" . :unsigned-int)
    ("long" . :long)
    ("signed long" . :long)
    ("unsigned long" . :unsigned-long)
    ("float" . :float)
    ("double" . :double)
    ((* . "char") . :cstring)
    ((* . "void") . :pointer-void)
    ("void" . :void)))

(defvar *uffi-primitive-type-alist* *uffi-default-primitive-type-alist*)

(defun uffi-type-spec (type-list)
  "Return the UFFI type spec equivalent to TYPE-LIST, or NIL if there
is no representation."
  (let ((primitive-type-pair
	 (assoc type-list *uffi-primitive-type-alist* :test 'equal)))
    (cond
      (primitive-type-pair
       (cdr primitive-type-pair))
      ((and (consp type-list)
	    (eql (first type-list) '*))
       (let ((base-type-spec (uffi-type-spec (rest type-list))))
	 (cond
	   ((not base-type-spec)
	    :pointer-void)
	   (t
	    (list '* base-type-spec)))))
      (t nil))))

;; Parse tree

(defvar *uffi-output* nil)

(defun emit-uffi-definition (uffi-definition)
  (format *uffi-output* "~&~S~%" uffi-definition)
  (push uffi-definition *uffi-definitions*))

(defun make-cl-symbol (c-identifier &key uninterned)
  (let ((name (substitute #\- #\_ (string-upcase c-identifier))))
    (if uninterned
	(make-symbol name)
	(intern name))))

(defvar *class-scope* '() "A stack of names of nested C++ classes.")

(defvar *struct-fields* '())

(defvar *linkage* :C "NIL or :C")

(defgeneric handle-node (node-type &key &allow-other-keys)
  (:documentation "Handle a node of SWIG's parse tree of a C/C++ program"))

(defmethod handle-node ((node-type t) &key &allow-other-keys)
  ;; do nothing for unknown node types
  nil)

(defmethod handle-node ((node-type (eql 'cdecl)) &key name decl storage parms type &allow-other-keys)
  (let ((swigtype (parse-swigtype (concatenate 'string decl type))))
    (let ((*print-pretty* nil) ; or FUNCTION would be printed as #' by cmucl
	  (*print-circle* t))
      (format *uffi-output* "~&;; C Declaration: ~A ~A ~A ~A~%;;  with-parms ~W~%;;   of-type ~W~%"
	      storage type name decl parms swigtype))
    (multiple-value-bind (function-p arg-swigtype-list return-swigtype)
	(swigtype-function-p swigtype)
      (declare (ignore arg-swigtype-list))
      (cond
	((and (null *class-scope*) function-p
	      (or (eql *linkage* :c)
		  (string= storage "externc")))
	 ;; ordinary top-level function with C linkage
	 (let ((argnum 0)
	       (argname-list '()))
	   (flet ((unique-argname (name)
		    ;; Sometimes the functions in SWIG interfaces
		    ;; do not have unique names.  Make them unique
		    ;; by adding a suffix.  Also avoid symbols
		    ;; that are specially bound.
		    (unless name
		      (setq name (format nil "arg~D" argnum)))
		    (let ((argname (make-cl-symbol name)))
		      (when (boundp argname) ;specially bound
			(setq argname (make-cl-symbol name :uninterned t)))
		      (push argname argname-list)
		      argname)))
	     (let ((uffi-arg-list
		    (mapcan (lambda (param)
			      (incf argnum)
			      (destructuring-bind (&key name type &allow-other-keys) param
				(let ((uffi-type (uffi-type-spec (parse-swigtype type))))
				  (cond
				    ((not uffi-type)
				     (format *uffi-output* "~&;; Warning: Cannot handle type ~S of argument `~A'~%"
					     type name)
				     (return-from handle-node))
				    ((eq uffi-type :void)
				     '())
				    (t
				     (let ((symbol (unique-argname name)))
				       (list `(,symbol ,uffi-type))))))))
			    parms))
		   (uffi-return-type
		    (uffi-type-spec return-swigtype)))
	       (unless uffi-return-type
		 (format *uffi-output* "~&;; Warning: Cannot handle return type `~S'~%"
			 return-swigtype)
		 (return-from handle-node))
	       (emit-uffi-definition `(UFFI:DEF-FUNCTION ,name ,uffi-arg-list :RETURNING ,uffi-return-type))))))
	((and (not (null *class-scope*)) (null (rest *class-scope*))
	      (not function-p))	; class/struct member (no nested structs)
	 (let ((uffi-type (uffi-type-spec swigtype)))
	   (unless  uffi-type
	     (format *uffi-output* "~&;; Warning: Cannot handle type ~S of struct field `~A'~%"
		     type name)
	     (return-from handle-node))
	   (push `(,(make-cl-symbol name) ,uffi-type) *struct-fields*)))))))

(defmethod handle-node ((node-type (eql 'class)) &key name children kind &allow-other-keys)
  (format *uffi-output* "~&;; Class ~A~%" name)
  (let ((*class-scope* (cons name *class-scope*))
	(*struct-fields* '()))
    (dolist (child children)
      (apply 'handle-node child))
    (emit-uffi-definition `(,(if (string= kind "union")
				 'UFFI:DEF-UNION
				 'UFFI:DEF-STRUCT)
			    ,(make-cl-symbol name) ,@(nreverse *struct-fields*)))))

(defmethod handle-node ((node-type (eql 'top)) &key children &allow-other-keys)
  (dolist (child children)
    (apply 'handle-node child)))
  
(defmethod handle-node ((node-type (eql 'include)) &key name children &allow-other-keys)
  (format *uffi-output* ";; INCLUDE ~A~%" name)
  (dolist (child children)
    (apply 'handle-node child)))

(defmethod handle-node ((node-type (eql 'extern)) &key name children &allow-other-keys)
  (format *uffi-output* ";; EXTERN \"C\" ~A~%" name)
  (let ((*linkage* :c))
    (dolist (child children)
      (apply 'handle-node child))))

;;(defun compute-uffi-definitions (swig-interface)
;;  (let ((*uffi-definitions* '()))
;;    (handle-node swig-interface)
;;    *uffi-definitions*))

;; Test instances

;;; Link to SWIG itself

#||

(defparameter *c++-compiler* "g++")

(defun stdc++-library (&key env)
  (let ((error-output (make-string-output-stream)))
    (let ((name-output (make-string-output-stream)))
      (let ((proc (ext:run-program
		   *c++-compiler*
		   '("-print-file-name=libstdc++.so")
		   :env env
		   :input nil
		   :output name-output
		   :error error-output)))
	(unless proc
	  (error "Could not run ~A" *c++-compiler*))
	(unless (zerop (ext:process-exit-code proc))
	  (system:serve-all-events 0)
	  (error "~A failed:~%~A" *c++-compiler*
		 (get-output-stream-string error-output))))
      (string-right-trim '(#\Newline) (get-output-stream-string name-output)))))

(defvar *swig-interface* nil)

(defvar *swig-uffi-pathname* #p"/tmp/swig-uffi.lisp")

(defun link-swig ()
  (setq *swig-interface*
	(run-swig (merge-pathnames "Source/swig.i" *swig-source-directory*)
		  :directory-search-list
		  (list (merge-pathnames "Source/" *swig-source-directory*))
		  :module "swig"
		  :ignore-errors t
		  :c++ t))
  (with-open-file (f *swig-uffi-pathname* :direction :output)
    (let ((*linkage* :c++)
	  (*uffi-definitions* '())
	  (*uffi-output* f)
	  (*uffi-primitive-type-alist* *uffi-default-primitive-type-alist*))
      (apply 'handle-node *swig-interface*)))
  (compile-file *swig-uffi-pathname*)
  (alien:load-foreign (merge-pathnames "Source/libswig.a"
				       *swig-source-directory*)
		      :libraries (list (stdc++-library)))
  ;; FIXME: UFFI stuffes a "-l" in front of the passed library names
  ;;  (uffi:load-foreign-library (merge-pathnames "Source/libswig.a"
  ;;                                              *swig-source-directory*)
  ;;                             :supporting-libraries
  ;;                             (list (stdc++-library)))
  (load (compile-file-pathname *swig-uffi-pathname*)))

||#

;;;; TODO:

;; * How to do type lookups?  Is everything important that SWIG knows
;;   about the types written out?  What to make of typemaps?
;;
;; * Wrapped functions should probably automatically COERCE their
;;   arguments (as of type DOUBLE-FLOAT), to make the functions more
;;   flexible?
;;
;; * Why are the functions created by FFI interpreted?
;;
;; * We can't deal with more complicated structs and C++ classes
;; directly with the FFI; we have to emit SWIG wrappers that access
;; those classes.
;;
;; * A CLOS layer where structure fields are mapped as slots.  It
;; looks like we need MOP functions to implement this.
;;
;; * Maybe modify SWIG so that key-value hashes are distinguished from
;; value-value hashes.
