(in-package #:mulk.protocols)


(defvar *protocols* (make-hash-table))


(defclass protocol (standard-class)
  ((methods :initarg :methods
            :reader protocol-methods)
   (strictness :initarg :strictness
               :reader protocol-strictness
               :initform '(nil)
               :documentation "Only tag classes with this protocol if they actually (rather than allegedly) conform to it.")))


(defun find-protocol (name &optional (errorp t))
  (find-class name errorp))


(defun (setf find-protocol) (protocol name &optional errorp)
  (declare (ignore errorp))
  (setf (find-class name) protocol))


(defun protocol-name (protocol)
  (class-name protocol))


(defvar *alleged-conformance* (make-hash-table :test 'equal))
(defvar *actual-conformance* (make-hash-table :test 'equal))


(defun conforms-to-p (class protocol)
  (let ((class (etypecase class
                 (class class)
                 (symbol (find-class class))))
        (protocol (etypecase protocol
                    (protocol protocol)
                    (symbol (find-protocol protocol)))))
    (or (gethash (cons class protocol) *alleged-conformance* nil)
        (some #'(lambda (x) (conforms-to-p x protocol))
              (class-direct-superclasses class)))))


(defun really-conforms-to-p (class protocol)
  (let ((class (etypecase class
                 (class class)
                 (symbol (find-class class))))
        (protocol (etypecase protocol
                    (protocol protocol)
                    (symbol (find-protocol protocol)))))
    (or (gethash (cons class protocol) *actual-conformance* nil)
        (some #'(lambda (x) (really-conforms-to-p x protocol))
              (class-direct-superclasses class)))))


(defgeneric %conforms-to-p (class protocol))

(defmethod %conforms-to-p ((class t) (protocol t))
  nil)


(defgeneric %really-conforms-to-p (class protocol))

(defmethod %really-conforms-to-p ((class t) (protocol t))
  nil)


(defmethod initialize-instance :around ((protocol protocol)
                                        &rest initargs
                                        &key (strictness
                                              '(nil)
                                              strictness-supplied-p)
                                             direct-superclasses
                                        &allow-other-keys)
  (declare (ignore strictness))
  (let ((new-initargs (copy-list initargs)))
    (unless strictness-supplied-p
      (setf (getf new-initargs :strictness)
            (list (some #'protocol-strictp direct-superclasses))))
    (apply #'call-next-method protocol new-initargs)))


(defun protocol-strictp (protocol)
  (car (protocol-strictness protocol)))


(defmethod validate-superclass (class (superclass protocol))
  (declare (ignore class))
  t)


(defmethod validate-superclass ((class standard-class) (superclass protocol))
  t)


(defmethod validate-superclass ((class protocol) (superclass standard-class))
  t)


(defmacro define-protocol (name superprotocols methods &rest options)
  "Define a new protocol.

superprotocols ::= (*name*\\*)

methods ::= ((method-name [\\* | class-name]\\*)\\*)"
  `(defclass ,name ,superprotocols
        ()
     (:metaclass protocol)
     (:methods ,@methods)
     ,@options))


(defun ensure-conformance (class-name protocol-designator)
  (let* ((protocol (typecase protocol-designator
                     (symbol (find-protocol protocol-designator))
                     (t protocol-designator)))
         (conforming-p nil))
    (with-accessors ((name protocol-name)
                     (superprotocols class-direct-superclasses)
                     (methods protocol-methods))
        protocol
      (loop for method in methods
            for (name . raw-argument-class-list) = method
            for argument-class-list = (substitute class-name
                                                  '*
                                                  raw-argument-class-list)
            for real-argument-class-list = (mapcar #'find-class
                                                   argument-class-list)
            for (applicable-methods methods-determinable-p) =
                (multiple-value-list
                 (funcall #'compute-applicable-methods-using-classes
                          (fdefinition name)
                          real-argument-class-list))
            when (and methods-determinable-p (null applicable-methods))
              collect method into missing-methods
              and do (warn (make-condition 'simple-style-warning
                                :format-control "Class ~A does not ~
                                                 implement method ~A ~
                                                 with argument types ~
                                                 ~A as required by ~
                                                 protocol ~A."
                                :format-arguments (list class-name
                                                        (first method)
                                                        real-argument-class-list
                                                        (protocol-name protocol))))
            unless methods-determinable-p
              do (warn (make-condition 'simple-style-warning
                            :format-control "Could not check whether ~
                                             class ~A implements ~
                                             method ~A with argument ~
                                             types ~A as required by ~
                                             protocol ~A.  Assuming it ~
                                             does."
                            :format-arguments (list class-name
                                                    (first method)
                                                    real-argument-class-list
                                                    (protocol-name protocol))))
            finally (setq conforming-p (null missing-methods)))
      (let ((really-conforming-p
             (and conforming-p
                  (every #'identity
                         ;; We have to use MAPCAR because we
                         ;; don't want short-circuiting.
                         (mapcar #'(lambda (superprotocol)
                                     (or (not (typep superprotocol 'protocol))
                                         (ensure-conformance class-name
                                                             superprotocol)))
                                 superprotocols)))))
        (setf (gethash (cons (find-class class-name) protocol)
                       *alleged-conformance*)
              t)
        (setf (gethash (cons (find-class class-name) protocol)
                       *actual-conformance*)
              really-conforming-p)
        (let ((original-class (find-class class-name)))
          (unless (or (subtypep original-class protocol)
                      (and (not really-conforming-p)
                           (protocol-strictp protocol)))
            (handler-case
                (let* ((new-class-name (gensym (symbol-name class-name)))
                       (temporary-class-name (gensym (symbol-name class-name)))
                       (new-class
                        (ensure-class
                             temporary-class-name
                             :direct-superclasses (list original-class protocol)
                             :metaclass (class-of original-class))))
                  (setf (class-name original-class) new-class-name)
                  (setf (class-name new-class) class-name)
                  (setf (find-class new-class-name) original-class)
                  (setf (find-class class-name) new-class))
              (serious-condition ()
                  (warn (make-condition
                             'simple-style-warning
                             :format-control "Could not add protocol ~A ~
                                              as a superclass of ~A. ~
                                              Most probably, the metaclasses ~
                                              are incompatible.  (See the MOP ~
                                              specification, specifically the ~
                                              part about VALIDATE-SUPERCLASS.)"
                             :format-arguments (list protocol
                                                     original-class)))))))))))


(defmacro implement-protocols (class protocols &body definitions)
  `(progn
     ,@definitions
     ,@(mapcar #'(lambda (protocol)
                   `(ensure-conformance ',class ',protocol))
               protocols)))
