(in-package #:user-side-compiler)

(defgeneric check-arguments (instruction))

(defclass instruction ()
  ((%mnemonic :allocation :class :reader mnemonic :initarg :mnemonic)
   (%operand :accessor operand :initarg :operand)
   (%next :accessor next :accessor normal-next :initarg :next)
   (%source :accessor source :initarg :source :initform nil)))

(defmethod print-object ((object instruction) stream)
  (cond ((some (lambda (type)
                 (typep object type))
               '(immediate-mixin accumulator-mixin zero-page-mixin absolute-mixin))
         (format stream "#<~A ~A~A~A>"
                 (mnemonic object)
                 (typecase object
                   (immediate-mixin "#")
                   ((member indirect-y-mixin indirect-x-mixin) "(")
                   (t ""))
                 (typecase object
                   (accumulator-mixin "A")
                   (t (operand object)))
                 (typecase object
                   (indirect-x-mixin ", X)")
                   (indirect-y-mixin "),Y")
                   (t ""))))
        (t
         (format stream "#<~A~A>" (mnemonic object)
                 (if (or (typep object 'implied-mixin)
                         (typep object 'accumulator-mixin))
                     ""
                     (format nil " ~S" (operand object)))))))

(defclass complete-instruction-mixin ()
  ((%opcode :allocation :class :reader opcode :initarg :opcode)))

(defmethod bytesquash ((object complete-instruction-mixin))
  (append (list (opcode object))
          (when (> (instruction-length object) 1)
            (bytesquash (operand object)))))

(defclass immediate-mixin () ())
(defclass implied-mixin () ())
(defclass accumulator-mixin () ())
(defclass zero-page-mixin () ())

(defmethod shared-initialize :after ((instance zero-page-mixin) slot-names &rest initargs &key &allow-other-keys)
  (declare (ignore slot-names initargs))
  (when (numberp (operand instance))
    (assert (< (operand instance) #x100))))

(defclass zero-page-x-mixin (zero-page-mixin) ())
(defclass absolute-mixin () ())
(defclass absolute-x-mixin (absolute-mixin) ())
(defclass absolute-y-mixin (absolute-mixin) ())
(defclass indirect-x-mixin (zero-page-mixin) ())
(defclass indirect-y-mixin (zero-page-mixin) ())
(defclass relative-mixin () ())

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun addressing-mode-to-class-name (mode-keyword)
    (cadr (assoc mode-keyword
                 '((:implied implied-mixin)
                   (:immediate immediate-mixin)
                   (:accumulator accumulator-mixin)
                   (:zero-page zero-page-mixin)
                   (:zero-page-x zero-page-x-mixin)
                   (:zero-page-y zero-page-y-mixin)
                   (:relative relative-mixin)
                   (:absolute absolute-mixin)
                   (:absolute-x absolute-x-mixin)
                   (:absolute-y absolute-y-mixin)
                   (:indirect-x indirect-x-mixin)
                   (:indirect-y indirect-y-mixin))))))

(defgeneric instruction-length (object)
  (:method ((obj immediate-mixin)) 2)
  (:method ((obj implied-mixin)) 1)
  (:method ((obj accumulator-mixin)) 1)
  (:method ((obj zero-page-mixin)) 2)
  (:method ((obj relative-mixin)) 2)
  (:method ((obj absolute-mixin)) 3))

(defclass branching-mixin ()
  ((%branch-next :accessor branch-next :initarg :branch-next)))

(defmacro define-instruction (mnemonic branching-p &rest modes-and-codes)
  (let* ((name (string-upcase mnemonic))
         (base-name (intern (format nil "INST-~A" name))))
    `(progn
       (defclass ,base-name (instruction)
         ((%mnemonic :allocation :class :initform ',mnemonic)))
       (setf (slot-value (make-instance ',base-name) '%mnemonic) ',mnemonic)
       ,@(loop :for (mode code) :in modes-and-codes
               :for class-name := (intern (format nil "INST-~A-~A" name mode))
               :collect `(progn
                           (defclass ,class-name
                               ,(append
                                 `(,base-name
                                   ,(addressing-mode-to-class-name mode)
                                   complete-instruction-mixin)
                                 (when branching-p
                                   '(branching-mixin)))
                             ((%opcode :allocation :class :initform ,code)))
                           (setf (slot-value (allocate-instance (find-class ',class-name)) '%opcode) ,code))))))

(defun fix-label-addresses-in-instruction-list (instruction-list origin)
  (loop :with address := origin
        :with cell := instruction-list
        :for obj :in instruction-list
        :if (typep obj 'label)
          :do (setf (address obj) address)
        :else
          :do (when (typep obj 'instruction)
                (incf address (instruction-length obj)))
        :unless (null (rest cell))
          :do (setf cell (rest cell))
        :finally (setf (cdr cell)
                       (list (make-label :name "PROGRAM_END" :address address)))))

(defun bytesquash-instruction-list (instruction-list origin)
  (let* ((end-label (find "PROGRAM_END" (remove-if-not #'labelp instruction-list)
                          :key #'name :test #'equalp))
         (byte-vector (make-array (- (address end-label) origin)
                                  :element-type '(unsigned-byte 8)
                                  :fill-pointer 0)))
    (loop :for thing :in instruction-list
          :when (typep thing 'instruction)
            :do (dolist (byte (bytesquash thing))
                  (vector-push byte byte-vector)))
    byte-vector))

;;; Testing

(define-instruction "TXA" nil (:implied #x8a))
(define-instruction "PHA" nil (:implied #x48))
(define-instruction "PLA" nil (:implied #x68))
(define-instruction "TAX" nil (:implied #xaa))
(define-instruction "DEX" nil (:implied #xca))
(define-instruction "BNE" t (:relative #xd0))
(define-instruction "LDA" nil
  (:immediate #xa9)
  (:zero-page #xa5)
  (:absolute #xad)
  (:absolute-y #xb9))
(define-instruction "LDY" nil
  (:immediate #xa0)
  (:absolute #xac))
(define-instruction "STA" nil
  (:absolute #x8d)
  (:absolute-y #x99))
(define-instruction "JSR" nil
  (:absolute #x20))