;; SPKI SEXPs for Common Lisp

(in-package :spki-sexp)

(define-condition syntax-error (error) ())
(define-condition bad-length-prefix (syntax-error) ())
(define-condition bad-display-hint (syntax-error) ())
(define-condition bad-input-character (syntax-error) ())
(define-condition unexpected-close-paren (syntax-error) ())

(define-condition match-failure (error) ())

(defstruct display-hint hint body)

(defun write-integer (n output-stream)
  (labels ((w (n)
	     (when (plusp n)
	       (multiple-value-bind (top-half lower-digit)
		   (floor n 10)
		 (w top-half)
		 (write-byte (+ lower-digit 48) output-stream)))))
    (if (zerop n)
	(write-byte 48 output-stream)
	(w n))))

(defun write-sexp (sexp &optional (output-stream *standard-output*))
  (etypecase sexp
    ((array (unsigned-byte 8))
     (write-integer (length sexp) output-stream)
     (write-byte 58 output-stream) ;; #\:
     (write-sequence sexp output-stream))
    (cons
     (write-byte 40 output-stream) ;; #\(
     (loop for v in sexp do (write-sexp v output-stream))
     (write-byte 41 output-stream)) ;; #\)
    (display-hint
     (write-byte 91 output-stream)
     (write-sexp (display-hint-hint sexp) output-stream)
     (write-byte 93 output-stream)
     (write-sexp (display-hint-body sexp) output-stream))
    (string
     (write-sexp (flexi-streams:string-to-octets sexp :external-format :utf-8) output-stream))))

(defun read-simple-string (input-stream &optional (len 0))
  (loop (let ((c (read-byte input-stream)))
	  (if (eql c 58) ;; #\:
	      (let ((buf (make-array len :element-type '(unsigned-byte 8))))
		(read-sequence buf input-stream)
		(return buf))
	      (let ((v (digit-char-p c)))
		(if v
		    (setq len (+ (* len 10) v))
		    (error 'bad-length-prefix)))))))

(defun read-sexp-list (input-stream)
  (loop for v = (read-sexp-inner input-stream)
     until (eq v 'end-of-list-marker)
     collect v))

(defun read-sexp-inner (input-stream)
  (let (result)
    (tagbody :retry
       (setq result
	     (let ((c (read-byte input-stream)))
	       (cond
		 ((eql c 40) (read-sexp-list input-stream)) ;; #\(
		 ((eql c 41) 'end-of-list-marker) ;; #\)
		 ((eql c 91) ;; #\[
		  (let ((hint (read-simple-string input-stream)))
		    (when (not (eql (read-byte input-stream) 93)) ;; #\]
		      (error 'bad-display-hint))
		    (make-display-hint :hint hint :body (read-simple-string input-stream))))
		 ((<= 48 c 57) (read-simple-string input-stream (- c 48))) ;; digits
		 ((<= c 32) ;; whitespace - convenience for testing
		  (go :retry))
		 (t (error 'bad-input-character))))))
    result))

(defun read-sexp (&optional (input-stream *standard-input*))
  (let ((v (read-sexp-inner input-stream)))
    (if (eq v 'end-of-list-marker)
	(error 'unexpected-close-paren)
	v)))

(defun convert-sexp (val)
  (etypecase val
    ((array (unsigned-byte 8)) val)
    (cons (cons (convert-sexp (car val))
		(convert-sexp (cdr val))))
    (null nil)
    (display-hint (make-display-hint
		   :hint (convert-sexp (display-hint-hint val))
		   :body (convert-sexp (display-hint-body val))))
    (string (flexi-streams:string-to-octets val :external-format :utf-8))))

(defmacro sexp-quote (val)
  `(quote ,(convert-sexp val)))

(defun build-sexp (stx)
  (etypecase stx
    ((array (unsigned-byte 8)) stx)
    (cons (if (eq (car stx) '=)
	      (cadr stx)
	      `(cons ,(build-sexp (car stx))
		     ,(build-sexp (cdr stx)))))
    (null 'nil)
    (display-hint `(make-display-hint
		    :hint ,(build-sexp (display-hint-hint stx))
		    :body ,(build-sexp (display-hint-body stx))))
    (string (flexi-streams:string-to-octets stx :external-format :utf-8))))

(defmacro sexp-build (template)
  (build-sexp template))

(defun convert-match-pattern (pattern)
  (etypecase pattern
    ((array (unsigned-byte 8)) `(array (1 (unsigned-byte 8)) ,(coerce pattern 'list)))
    (cons `(cons ,(convert-match-pattern (car pattern))
		 ,(convert-match-pattern (cdr pattern))))
    (null 'nil)
    (display-hint `(struct display-hint
			   (:hint ,(convert-match-pattern (display-hint-hint pattern)))
			   (:body ,(convert-match-pattern (display-hint-body pattern)))))
    (string (convert-match-pattern
	     (flexi-streams:string-to-octets pattern :external-format :utf-8)))
    (symbol pattern)))

(defmacro match-sexp (val &rest clauses)
  `(cl-match:match ,val
     ,@(mapcar (lambda (clause)
		 `(,(convert-match-pattern (car clause)) ,@(cdr clause)))
	       clauses)))

(defmacro ematch-sexp (val &rest clauses)
  `(match-sexp ,val ,@clauses (_ (error 'match-failure))))

;; Useful for testing
(defun read-from-string (str &optional (external-format :utf-8))
  (read-sexp (flexi-streams:make-flexi-stream
	      (flexi-streams:make-in-memory-input-stream
	       (flexi-streams:string-to-octets str :external-format external-format))
	      :external-format external-format)))
