(uiop:define-package :st-buchberger/src/parser
(:nicknames :parser)
(:export #:polynomial #:term #:term-monomial #:*zero-polynomial*
 #:term-coefficient #:monomial-indeterminates #:polynomial-terms
 #:parse-polynomial #:parse-term #:parse-coefficient
 #:parse-monomial #:parse-expt-symbol))

(in-package :st-buchberger/src/parser)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Grammar
;; =======
;;
;; polynomial  : term
;;             | "(" "-" term+ ")"
;;             | "(" "+" term* ")"
;;
;; term        : coefficient
;;             | monomial
;;             | "(" "*" coefficient monomial ")"
;;             | "(" "-" term ")"
;;
;; coefficient : rational
;;
;; monomial    : expt-symbol
;;             | "(" expt-symbol+ ")"
;;
;; expt-symbol : symbol
;;             | "(" "expt" symbol non-negative-integer ")"
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defstruct polynomial
 terms)

(defstruct term
 coefficient
 monomial)

(defstruct monomial
 "Association list whose keys are indeterminates (symbols) and the
corresponding values are the powers."
 indeterminates)

(defparameter *zero-polynomial*
 (make-polynomial :terms (list (make-term :coefficient 0
                                          :monomial (make-monomial
                                                     :indeterminates nil))))
 "Representation of the zero polynomial.")

(defun parse-polynomial (sexp)
 "Parse polynomial in SEXP."
 (cond
   ((null sexp)
    (error "Invalid expression: ~S." sexp))
   ((and (consp sexp) (eq (first sexp) '+))
    (if (zerop (length (rest sexp)))
        *zero-polynomial*
        (make-polynomial :terms (loop :for x :in (rest sexp)
                                      :collect (parse-term x)))))
   ((and (consp sexp) (eq (first sexp) '-))
    (case (length (rest sexp))
      (0 *zero-polynomial*)
      (1 (make-polynomial :terms (list (parse-term sexp))))
      (t (make-polynomial :terms (loop :for x :in (rest sexp)
                                       :for term := (parse-term x)
                                       :for i :from 0
                                       :when (plusp i) :do
                                         (setf (term-coefficient term)
                                               (- (term-coefficient term)))
                                       :collect term)))))
   (t (make-polynomial :terms (list (parse-term sexp))))))

(defun parse-term (sexp)
 "Parse term in SEXP."
 (cond
   ((atom sexp)
    (if (numberp sexp)
        (make-term :coefficient (parse-coefficient sexp)
                   :monomial (make-monomial :indeterminates nil))
        (make-term :coefficient 1 :monomial (parse-monomial sexp))))
   ((consp sexp)
    (destructuring-bind (first . rest) sexp
      (case first
        (* (let ((coeff (car rest)))
             (if (numberp coeff)
                 (make-term :coefficient (parse-coefficient coeff)
                            :monomial (parse-monomial (cdr rest)))
                 (make-term :coefficient 1
                            :monomial (parse-monomial rest)))))
        (- (let ((term (apply #'parse-term rest)))
             (setf (term-coefficient term) (- (term-coefficient term)))
             term))
        (t (make-term :coefficient 1
                      :monomial (parse-monomial sexp))))))))

(defun parse-coefficient (sexp)
 "Parse coefficient in SEXP."
 (unless (and (numberp sexp) (typep sexp 'rational))
   (error "Invalid coefficient: ~S." sexp))
 sexp)

(defun parse-monomial (sexp)
 "Parse monomial in SEXP."
 (make-monomial
  :indeterminates (if (atom sexp)
                      (list (parse-expt-symbol sexp))
                      (if (eq (car sexp) 'expt)
                          (list (parse-expt-symbol sexp))
                          (loop :for x :in sexp
                                :collect (parse-expt-symbol x))))))

(defun parse-expt-symbol (sexp)
 "Parse (possibly exponentiated) symbol in SEXP."
 (flet ((error-invalid-expt-symbol (sexp)
          (error "Invalid (possibly exponentiated) symbol: ~S." sexp)))
   (cond
     ((atom sexp)
      (unless (symbolp sexp)
        (error-invalid-expt-symbol sexp))
      (cons sexp 1))
     (t
      (destructuring-bind (op symbol power) sexp
        (unless (and (eq op 'expt)
                     (symbolp symbol)
                     (typep power '(integer 0)))
          (error-invalid-expt-symbol sexp))
        (cons symbol power))))))