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

(defparameter *varvec* (make-label :name "VARVEC" :address #x8100))
(defparameter *argvec* (make-label :name "ARGVEC" :address #xF0))

(defvar *instruction-source* nil)
(defvar *compile-result*)

(defun produce-instruction (instruction-class &optional operand)
  (push (make-instance instruction-class :operand operand :source *instruction-source*)
        *compile-result*))

(defun produce-label (&optional label)
  (when (null label)
    (setf label (make-label)))
  (push label *compile-result*)
  label)

(defun produce-comment (text)
  (push text *compile-result*))

(defmacro format-inst (destination control-string &rest format-arguments)
  `(format ,destination "~C~A~%" #\Tab (format nil ,control-string ,@format-arguments)))

(defclass reference () ())

(defgeneric ref-constantp (reference) (:method (reference) nil))

(defclass reference-constant (reference)
  ((%value :accessor ref-value :initarg :value)))

(defmethod ref-constantp ((reference reference-constant))
  t)

(defmethod print-object ((object reference-constant) stream)
  (print-unreadable-object (object stream :type t)
    (format stream "~D" (ref-value object))))

(defmethod dereference ((ref reference-constant))
  (produce-instruction 'inst-lda-immediate (ref-value ref)))

(defclass reference-variable (reference)
  ((%index :accessor ref-index :initarg :index)))

(defmethod print-object ((object reference-variable) stream)
  (print-unreadable-object (object stream :type t)
    (format stream "@~D" (ref-index object))))

(defmethod dereference ((ref reference-variable))
  (produce-instruction 'inst-lda-absolute (make-offset-label *varvec* (ref-index ref))))

(defclass node ()
  ((%next :accessor next :accessor normal-next :initform nil)))

(defmethod generate-code :before ((node node))
  (produce-comment (format nil "~A" node)))

(defmethod generate-code :after ((node node))
  (terpri))

(defmethod compile-node ((node node))
  (generate-code node)
  (unless (null (next node))
    (compile-node (next node))))

(defclass node-call (node)
  ((%callee :accessor callee :initarg :callee)
   (%arguments :accessor arguments :initarg :arguments)))

(defmethod print-object ((object node-call) stream)
  (print-unreadable-object (object stream :type t :identity t)
    (format stream "~A~A" (callee object) (arguments object))))

(defmethod generate-code ((node node-call))
  (let ((*instruction-source* node))
    (loop :for ref :in (arguments node)
          :for index :from 0
          :do (dereference ref)
          :do (produce-instruction 'inst-sta-absolute (make-offset-label *argvec* index)))
    (produce-instruction 'inst-jsr-absolute (callee node))))

(defclass node-branch (node)
  ((%branch-next :accessor branch-next :initarg :branch-next)))

(defmethod generate-code ((node node-branch))
  (let ((*instruction-source* node)
        (else-label (make-label :name-prefix "ELSE")))
    (produce-instruction 'inst-lda-absolute "RESULT")
    (produce-instruction 'inst-bne-relative else-label)
    ;; The THEN branch
    (compile-node (branch-next node))
    ;; The ELSE branch
    (produce-label else-label)))

(defclass node-dotimes (node)
  ((%stop-ref :accessor stop-ref :initarg :stop-ref
              :documentation "A reference giving a value of how many times to run the loop.")
   (%loopee-node :accessor loopee-node :initarg :loopee-node)))

(defmethod generate-code ((node node-dotimes))
  (let ((*instruction-source* node)
        (loop-label (make-label :name-prefix "LOOPBACK")))
    (produce-instruction 'inst-txa-implied)
    (produce-instruction 'inst-pha-implied)

    (dereference (stop-ref node))
    (produce-instruction 'inst-tax-implied)
    (produce-label loop-label)
    (compile-node (loopee-node node))
    (produce-instruction 'inst-dex-implied)
    (produce-instruction 'inst-bne-relative loop-label)

    (produce-instruction 'inst-pla-implied)
    (produce-instruction 'inst-tax-implied)))

(defmethod compile-starting-at ((node node))
  (let ((*compile-result* '())
        (*label-counter* 0))
    (compile-node node)
    (nreverse *compile-result*)))

(defun make-call (callee args)
  (let ((arguments
          (loop :for (constp value) :in args
                :with index := -1
                :if constp
                  :collect (make-instance 'reference-constant :value value)
                :else
                  :collect (make-instance 'reference-variable :index (incf index)))))
    (make-instance 'node-call :callee callee
                              :arguments arguments)))