;;; Copyright 2012-2020 Google LLC
;;;
;;; Use of this source code is governed by an MIT-style
;;; license that can be found in the LICENSE file or at
;;; https://opensource.org/licenses/MIT.

(in-package #:cl-protobufs.implementation)

;;; This file implements the protobuf Text Format parser and printer.
;;; The exported symbols are parse-text-format and print-text-format.

(defun print-text-format (object &key
                                 (stream *standard-output*)
                                 (pretty-print-p t))
    "Prints a protocol buffer message to a stream.
Parameters:
  OBJECT: The protocol buffer message to print.
  STREAM: The stream to print to.
  PRETTY-PRINT-P: When true, generate line breaks and other human readable output
    in the text format. When false, replace line breaks with spaces."
  (print-text-format-impl object :stream stream
                                 :pretty-print-p pretty-print-p))

(defun print-text-format-impl (object &key
                                      (indent 0)
                                      (stream *standard-output*)
                                      (pretty-print-p t))
  "Prints a protocol buffer message to a stream.
Parameters:
  OBJECT: The protocol buffer message to print.
  INDENT: Indent the output by INDENT spaces. Only used for pretty-printing.
  STREAM: The stream to print to.
  PRETTY-PRINT-P: When true, generate line breaks and other human readable output
    in the text format. When false, replace line breaks with spaces."
  (let* ((type (type-of object))
         (message (find-message-descriptor type :error-p t)))
    (dolist (field (proto-fields message))
      (when (if (eq (slot-value field 'kind) :extends)
                (has-extension object (slot-value field 'external-field-name))
                (has-field object (slot-value field 'external-field-name)))
        (let* ((value
                (if (eq (slot-value field 'kind) :extends)
                    (get-extension object (slot-value field 'external-field-name))
                    (proto-slot-value object (slot-value field 'external-field-name)))))
          (if (eq (proto-label field) :repeated)
              (doseq (val value)
                     (print-field val
                                  (proto-class field)
                                  (proto-name field)
                                  :indent indent
                                  :stream stream
                                  :pretty-print-p pretty-print-p))
              (print-field value
                           (proto-class field)
                           (proto-name field)
                           :indent indent
                           :stream stream
                           :pretty-print-p pretty-print-p)))))
    (dolist (oneof (proto-oneofs message))
      (let* ((oneof-data (slot-value object (oneof-descriptor-internal-name oneof)))
             (set-field (oneof-set-field oneof-data)))
        (when set-field
          (let ((field-desc (aref (oneof-descriptor-fields oneof) set-field)))
            (print-field (oneof-value oneof-data)
                         (proto-class field-desc)
                         (proto-name field-desc)
                         :indent indent
                         :stream stream
                         :pretty-print-p pretty-print-p)))))
    nil))

(defun print-field (value type name
                    &key (indent 0) (stream *standard-output*) (pretty-print-p t))
  "Print the text format of a single field which is not repeated.
Parameters:
  VALUE: The value in the field to print.
  TYPE: The protobuf type to print. This is obtained from
    the PROTO-CLASS slot in the field-descriptor.
  NAME: The name of the field. This is printed before the value.
  INDENT: If supplied, indent the text by INDENT spaces.
  STREAM: The stream to output to.
  PRINT-NAME: Whether or not to print the name of the field.
  PRETTY-PRINT-P: When true, print newlines and indentation."
  ;; If VALUE is NIL and the type is not boolean, there is nothing to do.
  (unless (or value (eq type 'boolean) (eq type 'symbol))
    (return-from print-field nil))
  (let (desc)
    (cond
      ((scalarp type)
       (print-scalar value type name stream
                     (and pretty-print-p indent)))
      ((typep (setq desc (or (find-message-descriptor type)
                             (find-enum-descriptor type)
                             (find-map-descriptor type)))
              'message-descriptor)
       (print-message-brace t name pretty-print-p indent stream)
       (print-text-format-impl value :indent (+ indent 2)
                                     :stream stream
                                     :pretty-print-p pretty-print-p)
       (print-message-brace nil name pretty-print-p indent stream))
      ((typep desc 'enum-descriptor)
       (print-enum value desc name stream (and pretty-print-p indent)))
      ((typep desc 'map-descriptor)
       (loop for k being the hash-keys of value using (hash-value v)
             do (if pretty-print-p
                    (format stream "~&~V,0T~A { " indent name)
                    (format stream "~A { " name))
                (print-scalar k (proto-key-type desc) "key" stream nil)
                (print-field v (proto-value-type desc) "value"
                             :stream stream
                             :pretty-print-p nil)
                (format stream "}")
                (when pretty-print-p
                  (format stream "~%"))))
      ;; This case only happens when the user specifies a custom type and
      ;; doesn't support it above.
      (t
       (error 'unknown-type
              :format-control "unknown type ~S, while printing non-repeated field ~S"
              :format-arguments (list type name))))))

(defun print-scalar (val type name stream indent)
  "Print scalar value to stream
 Parameters:
  VAL: The data for the value to print.
  TYPE: The type of val.
  NAME: The name to print before the value. If nil, then no
        name will be printed.
  STREAM: The stream to print to.
  INDENT: Either a number or nil.
          - If indent is a number, indent this print
            by (+ indent 2) and write a newline at
            the end.
          - If indent is nil, then do not indent and
            do not write a newline."
  (when (or val (eq type 'boolean) (eq type 'symbol))
    (when indent
      (format stream "~&~V,0T" indent))
    (when name
      (format stream "~A: " name))
    (ecase type
      ((int32 uint32 int64 uint64 sint32 sint64 fixed32 sfixed32 fixed64 sfixed64)
       (format stream "~D" val))
      ((string)
       ;; TODO(cgay): This should be the inverse of parse-string.
       (format stream "\"~A\"" val))
      ((byte-vector)
       (format stream "~S" val))
      ((boolean)
       (format stream "~A" (if val "true" "false")))
      ((float double-float)
       (format stream "~D" val))
      ;; A few of our homegrown types
      ((symbol)
       (format stream "\"~A\"" (lisp-symbol-string val)))
      ((date time datetime timestamp)
       (format stream "~D" val)))
    (if indent
        (format stream "~%")
        (format stream " "))))

(defun print-enum (val enum name stream indent)
  "Print enum to stream

Parameters:
  VAL: The enum value.
  ENUM: The enum descriptor.
  NAME: The name to print before the value. If NIL, no name will be printed.
  STREAM: The stream to print to.
  INDENT: Either a number or nil.
          - If indent is a number, indent this print
            by (+ indent 2) and write a newline at
            the end.
          - If indent is nil, then do not indent and
            do not write a newline."
  (when val
    (when indent
      (format stream "~&~V,0T" indent))
    (when name
      (format stream "~A: " name))
    (let* ((e (find (keywordify val)
                    (enum-descriptor-values enum)
                    :key #'enum-value-descriptor-name))
           (value (and e (enum-value-descriptor-name e)))
           (proto-keyword-value (substitute #\_ #\- (string value))))
      (format stream "~A" proto-keyword-value)
      (if indent
          (format stream "~%")
          (format stream " ")))))

(defun print-message-brace (opening-p name pretty-print-p indent stream)
  "Print either the opening NAME { or closing }.

Parameters:
  OPENING-P: Is this an opening or closing brace.
  NAME: The name to print before the value. If NIL, no name will be printed.
  PRETTY-PRINT-P: When true, print newlines and indentation.
  INDENT: A set indentation to print to. Used only for pretty-print.
  STREAM: The stream to print to."
  (if opening-p
      (if pretty-print-p
          (format stream "~&~V,0T~A {~%" indent name)
          (format stream "~A { " name))
      (if pretty-print-p
          (format stream "~&~V,0T}~%" indent)
          (format stream "} "))))

;;; Parse objects that were serialized using the text format

(defun parse-text-format (type &key (stream *standard-input*))
  "Parses an object in stream STREAM of type TYPE written in text format."
  (declare (type symbol type)
           (type stream stream))
  (let ((message (find-message-descriptor type :error-p t)))
    (parse-text-format-impl message :stream stream)))

;;; TODO(cgay): replace all assertions here with something that signals a
;;; subtype of protobuf-error and shows current stream position.

(defun parse-text-format-impl
    (msg-desc &key (stream *standard-input*))
  "Parse a protobuf message with descriptor MSG-DESC from STREAM. This method
returns the parsed object."
  (declare (type message-descriptor msg-desc))
  (let ((object (funcall (get-constructor-name
                          (or (proto-alias-for msg-desc)
                              (proto-class msg-desc)))))
        ;; Repeated slot names, tracks which slots need to be nreversed.
        (rslots ()))
    (loop
      (skip-whitespace-comments-and-chars stream)
      (when (or (not (peek-char nil stream nil))
                (eql (peek-char nil stream nil) #\})
                (eql (peek-char nil stream nil) #\>))
        ;; We should respect the order of slots as
        ;; they were in the message.
        (dolist (slot rslots)
          (setf (proto-slot-value object slot)
                (nreverse (proto-slot-value object slot))))
        (return-from parse-text-format-impl object))
      (let* ((name  (parse-token stream))
             (field (and name (find-field-descriptor msg-desc name)))
             (type (and field (proto-class field)))
             (slot  (and field (proto-external-field-name field)))
             (repeated-p (and field (eql :repeated (proto-label field)))))
        (if (null field)
            (error 'unknown-field
                   :format-control "unknown field ~S, while parsing message of type ~A"
                   :format-arguments (list name msg-desc))
            (multiple-value-bind (val error-p)
                (parse-field type :stream stream :repeated-p repeated-p)
              (cond
                (error-p
                 (unknown-field-type type field msg-desc))
                (repeated-p
                 ;; If slot is NIL, then this field doesn't exist in the message
                 ;; so we skip it.
                 (when slot
                   (pushnew slot rslots)
                   ;; Brief note on val: VAL should be a list.
                   ;; In the case of repeated symbol slot, we may have
                   ;; symbol: nil
                   ;; in which case we want the symbol nil, which happens to
                   ;; also be a list...  since for a repeated field foo
                   ;; foo:  # no value defined for foo
                   ;; is invalid, we aren't going to have collisions.
                   (if (and (listp val) val)
                       (dolist (el val)
                         (push el (proto-slot-value object slot)))
                       (push val (proto-slot-value object slot)))))
                ((eq (proto-kind field) :map)
                 (dolist (pair val)
                   (setf (gethash (car pair) (proto-slot-value object slot))
                         (cdr pair))))
                (t
                 (when slot
                   (setf (proto-slot-value object slot) val))))))))))

(defun parse-field (type &key (stream *standard-input*) repeated-p)
  "Parse data of type TYPE from STREAM. This function returns
the object parsed. We need to know if hte field is REPEATED-P.
If the parsing fails, the function will
return T as a second value."
  (let ((desc (or (find-message-descriptor type)
                  (find-enum-descriptor type)
                  (find-map-descriptor type))))
    (flet ((parse-message ()
             (skip-whitespace-comments-and-chars stream)
             (let ((start-char (expect-char stream '(#\{ #\<))))
               (prog1
                   (parse-text-format-impl (find-message-descriptor type) :stream stream)
                 (skip-whitespace-comments-and-chars stream)
                 (expect-matching-end stream start-char))))
           (parse-scalar ()
             (case type
               ((float) (parse-float stream))
               ((double-float) (parse-double stream))
               ((string) (parse-string stream))
               ((symbol) (make-lisp-symbol (parse-string stream) t))
               ((boolean) (let ((token (parse-token stream)))
                            (cond ((string= token "true") t)
                                  ((string= token "false") nil)
                                  ;; Parsing failed, so return T as
                                  ;; a second value to indicate a
                                  ;; failure.
                                  (t (values nil t)))))
               (otherwise (parse-signed-int stream))))
           (parse (parse-function)
             (when (eql (peek-char nil stream nil) #\:)
               (read-char stream))
             (skip-whitespace-comments-and-chars stream)
             (if (and repeated-p
                      (eq (peek-char nil stream nil) #\[))
                 (progn
                   (read-char stream)
                   (skip-whitespace-comments-and-chars stream :chars #\,)
                   (let ((element-list (loop until (eq (peek-char nil stream nil) #\])
                                             collect (funcall parse-function)
                                             do
                                          (skip-whitespace-comments-and-chars stream :chars #\,))))
                     (read-char stream)
                     element-list))
                 (funcall parse-function))))

      (cond ((scalarp type)
             (parse #'parse-scalar))
            ((typep desc 'message-descriptor)
             (parse #'parse-message))
            ((typep desc 'enum-descriptor)
             (expect-char stream #\:)
             (let* ((name (parse-token stream))
                    (enum (find (keywordify name) (enum-descriptor-values desc)
                                :key #'enum-value-descriptor-name)))
               (and enum (enum-value-descriptor-name enum))))
            ((typep desc 'map-descriptor)
             (let ((key-type (proto-key-type desc))
                   (val-type (proto-value-type desc)))
               (flet ((parse-map-entry (key-type val-type stream)
                        (let (key val)
                          (expect-char stream #\{)
                          (assert (string= "key" (parse-token stream)))
                          (setf key (parse-field key-type :stream stream))
                          (skip-whitespace-comments-and-chars stream)
                          (assert (string= "value" (parse-token stream)))
                          (setf val (parse-field val-type :stream stream))
                          (skip-whitespace-comments-and-chars stream)
                          (expect-char stream #\})
                          (cons key val))))
                 (case (peek-char nil stream nil)
                   ((#\:)
                    (expect-char stream #\:)
                    (expect-char stream #\[)
                    (loop
                        with pairs = ()
                        do (skip-whitespace-comments-and-chars stream)
                           (push (parse-map-entry key-type val-type stream)
                                 pairs)
                           (if (eql (peek-char nil stream nil) #\,)
                               (read-char stream)
                               (progn
                                 (skip-whitespace-comments-and-chars stream)
                                 (expect-char stream #\])
                                 (return pairs)))))
                   (t
                    (skip-whitespace-comments-and-chars stream)
                    (list (parse-map-entry key-type val-type stream)))))))
            ;; Parsing failed, return t as a second vlaue to indicate failure.
            (t (values nil t))))))

(defun fmt (stream proto colon-p at-sign-p &optional width &rest other-args)
  "Format command for protobufs
   ~/cl-protobufs:fmt/ emits a non-pretty-printed protobuf of PROTO to STREAM.
   ~@/cl-protobufs:fmt/ emits a pretty-printed protobuf of PROTO to STREAM.
   COLON-P and AT-SIGN-P are the usual for format directives.
   WIDTH and OTHER-ARGS  is ignored."
  (declare (ignore width))
  (cond (other-args (error "FORMAT directive ~~/cl-protobufs:fmt/ takes only one argument."))
        (colon-p (error "FORMAT directive ~~/cl-protobufs:fmt/ does not take colons."))
        (t (print-text-format proto :stream stream :pretty-print-p at-sign-p))))
