(defpackage #:herecy-term
  (:use #:cl #:ettram #:trivial-gray-streams)
  (:export #:terminal #:move-cursor-relative #:clear-line
           #:make-terminal))

(in-package #:herecy-term)

(defun row-col-to-point (row col)
  (make-point (* (1- col) 8) (* (1- row) 16)))

(defclass cursor-state ()
  ((%row :accessor row :initform 1)
   (%col :accessor col :initform 1)
   (%blinkingp :accessor blinkingp :initform t)
   (%blink-phase :accessor blink-phase :initform 0)
   (%blink-state :accessor blink-state :initform t)
   (%visiblep :accessor visiblep :initform t)))

(defmethod cursor-picture-position ((cursor cursor-state))
  (with-accessors ((row row) (col col)) cursor
    (row-col-to-point row col)))

(defmethod set-cursor ((cursor cursor-state) row col)
  (setf (col cursor) col
        (row cursor) row))

(defclass input-buffer ()
  ((%array :accessor internal-array :initform (make-array 80 :element-type 'character :adjustable t :fill-pointer 0))
   (%write-index :accessor write-index :initform 0)
   (%readout-index :accessor readout-index :initform 0)
   (%finishedp :accessor finishedp :initform nil)))

#+(or)
(defmethod push-to-buffer ((buffer input-buffer) element)
  (vector-push-extend element (internal-array buffer)))

(defmethod insert-into-buffer ((buffer input-buffer) element)
  (let ((ia (internal-array buffer))
        (index (write-index buffer)))
    (loop :initially (vector-push-extend #\Null ia)
          :for index :from (- (length ia) 1) :downto index
          :do (setf (aref ia (1+ index)) (aref ia index)))
    (setf (aref ia index) element)
    (incf (write-index buffer))))

(defmethod rubout-from-buffer ((buffer input-buffer))
  (unless (zerop (write-index buffer))
    (let ((ia (internal-array buffer)))
      (decf (write-index buffer))
      (loop :for index :from (write-index buffer) :below (1- (length ia))
            :do (setf (aref ia index) (aref ia (1+ index)))
            :finally (pop-from-buffer buffer)))))

(defmethod consume-from-buffer ((buffer input-buffer))
  (cond ((< (readout-index buffer) (length (internal-array buffer)))
         (prog1
             (aref (internal-array buffer) (readout-index buffer))
           (incf (readout-index buffer))
           (when (>= (readout-index buffer) (length (internal-array buffer)))
             (reset-buffer buffer))))
        (t
         (prog1
             :eof
           (reset-buffer buffer)))))

(defmethod pop-from-buffer ((buffer input-buffer))
  (unless (buffer-empty-p buffer)
    (vector-pop (internal-array buffer))))

(defmethod finish-buffer ((buffer input-buffer))
  (setf (finishedp buffer) t))

(defmethod reset-buffer ((buffer input-buffer))
  (setf (finishedp buffer) nil
        (fill-pointer (internal-array buffer)) 0
        (readout-index buffer) 0
        (write-index buffer) 0))

(defmethod buffer-empty-p ((buffer input-buffer))
  (zerop (fill-pointer (internal-array buffer))))

(defclass terminal (fundamental-character-stream)
  ((%cursor-state :accessor cursor-state :initform (make-instance 'cursor-state))
   (%width :accessor width :initarg :columns :initarg :width)
   (%height :accessor height :initarg :rows :initarg :height)
   (%screen-grid :accessor screen-grid :initarg :grid)
   (%picture :accessor picture :initarg :picture)
   (%input-buffer :accessor input-buffer :initarg :input-buffer)
   (%overflowedp :accessor overflowedp :initform nil)
   (%command-state :accessor command-state :initform :normal)))

(defun make-terminal (rows columns picture)
  (make-instance 'terminal :picture picture :width columns :height rows
                 :grid (make-array (list rows columns) :element-type 'character
                                   :initial-element #\Space)
                 :input-buffer (make-instance 'input-buffer)))

(defmethod draw-screen-grid ((terminal terminal))
  (let ((original-row (row (cursor-state terminal)))
        (original-col (col (cursor-state terminal))))
    (loop :for col :from 1 :to (width terminal)
          :with cursor := (cursor-state terminal)
          :do (loop :for row :from 1 :to (height terminal)
                    :do (progn
                          (set-cursor cursor row col)
                          (print-to-picture (string (char-under-cursor terminal))
                                            (cursor-picture-position cursor)
                                            (picture terminal)))))
    (set-cursor (cursor-state terminal) original-row original-col)))

(defmethod update-cursor ((terminal terminal))
  (with-accessors ((cursor cursor-state)
                   (picture picture))
      terminal
    (incf (blink-phase cursor))
    (when (>= (blink-phase cursor) *blink-duration*)
      (setf (blink-phase cursor) 0
            (blink-state cursor) (not (blink-state cursor)))
      (let ((*fg-text-color* *fg-text-color*)
            (*bg-text-color* *bg-text-color*))
        (unless (blink-state cursor)
          (rotatef *fg-text-color* *bg-text-color*))
        (print-to-picture (string (char-under-cursor terminal))
                          (cursor-picture-position cursor) picture)))))

(defmethod reset-cursor-blinking ((terminal terminal))
  (let ((cursor (cursor-state terminal)))
    (setf (blink-state cursor) nil
          (blink-phase cursor) *blink-duration*)
    (update-cursor terminal)))

(defmethod scroll-down ((terminal terminal))
  ;; Scroll the screen grid
  (let ((grid (screen-grid terminal)))
    (loop :for y :from 0 :below (height terminal)
          :unless (= y 0)
            :do (dotimes (x (width terminal))
                  (setf (aref grid (1- y) x)
                        (aref grid y x))))
    (dotimes (x (width terminal))
      (setf (aref grid (1- (height terminal)) x)
            #\Space)))
  ;; Move cursor up a step
  (move-cursor-relative terminal -1 0)
  ;; Redisplay
  (draw-screen-grid terminal))

(defmethod move-cursor-absolute ((terminal terminal) row col)
  (let ((cursor (cursor-state terminal)))
    (setf row (max 1 row)
          col (max 1 col))
    (when (> col (width terminal))
      (setf col 1
            row (1+ row)))
    (cond ((> row (height terminal))
           (setf row (height terminal))
           (scroll-down terminal)
           (setf (overflowedp terminal) t))
          (t (setf (overflowedp terminal) nil)))
    ;; Ensure cursor blink is reset
    (reset-cursor-blinking terminal)
    ;; Perform the move
    (set-cursor cursor row col)
    (setf (blink-state cursor) t
          (blink-phase cursor) *blink-duration*)
    (update-cursor terminal)))

(defmethod move-cursor-relative ((terminal terminal) row col)
  (let ((cursor (cursor-state terminal)))
    (move-cursor-absolute terminal
                          (+ (row cursor) row)
                          (+ (col cursor) col))))

(defmethod char-under-cursor ((terminal terminal))
  (aref (screen-grid terminal)
        (1- (row (cursor-state terminal)))
        (1- (col (cursor-state terminal)))))

(defmethod put-character-at ((terminal terminal) row col char)
  (ettram:print-to-picture (string char)
                           (row-col-to-point row col)
                           (picture terminal))
  (setf (aref (screen-grid terminal) (1- row) (1- col)) char))

(defmethod put-character ((terminal terminal) char)
  (with-accessors ((row row) (col col))
      (cursor-state terminal)
    (put-character-at terminal row col char)
    (move-cursor-relative terminal
                          0
                          (if (= (character-width char) 8)
                              1
                              2))))

(defmethod clear-line ((terminal terminal))
  (dotimes (col (width terminal))
    (put-character-at terminal (row (cursor-state terminal)) (1+ col) #\Space)))

(defmethod stream-terpri ((stream terminal))
  (move-cursor-relative stream 1 -1000))

(defmethod stream-write-char ((stream terminal) character)
  (case (command-state stream)
    (:normal
     (case character
       (#\Newline (unless (overflowedp stream)
                    (terpri stream)))
       (#\Return (unless (overflowedp stream)
                    (terpri stream)))
       (#\Escape (setf (command-state stream) :escape))
       (t (put-character stream character))))
    (:escape
     (cerror "I also like to live dangerously." "Escape codes not implemented.")
     (case character
       (#\[ (setf (command-state stream) :csi))))
    (:csi
     (cerror "I also like to live dangerously." "CSI codes not implemented."))))

(defmethod stream-write-string ((stream terminal) string &optional start end)
  (let ((str string))
    (unless (null start)
      (setf str (subseq string start end)))
    (loop :for char :across str :do (write-char char stream))))

(defmethod stream-read-line ((stream terminal))
  (let ((buffer (input-buffer stream)))
    (loop :until (finishedp buffer)
          :do (sleep 1/30))
    (prog1
        (subseq (internal-array buffer)
                0 (1- (length (internal-array buffer))))
      (reset-buffer buffer))))

(defmethod stream-read-char ((stream terminal))
  (loop :until (finishedp (input-buffer stream))
        :do (sleep 1/30))
  (let ((popped (consume-from-buffer (input-buffer stream))))
    (when (buffer-empty-p (input-buffer stream))
      (reset-buffer (input-buffer stream)))
    popped))

(defmethod stream-force-output ((stream terminal)))

(defparameter *blink-duration* 30)

(defmethod update ((obj terminal) delta)
  ;; Get keyboard input
  (let ((events (copy-events)))
    (dolist (event events)
      (typecase event
        (char-type-event
         (let ((char (codepoint event))
               (ib (input-buffer obj))
               (cursor (cursor-state obj)))
           (insert-into-buffer ib char)
           (put-character obj char)
           (when (< (write-index ib) (length (internal-array ib)))
             (let ((row (row cursor))
                   (col (col cursor)))
               (loop :for char :across (subseq (internal-array ib) (write-index ib))
                     :do (put-character obj char))
               (move-cursor-absolute obj row col)))))
        (key-press-event
         (when (press-p event)
           (case (key-symbol event)
             (:enter
              (setf (write-index (input-buffer obj))
                    (length (internal-array (input-buffer obj))))
              (write-char #\Newline obj)
              (insert-into-buffer (input-buffer obj) #\Newline)
              (finish-buffer (input-buffer obj)))
             (:backspace
              (unless (zerop (write-index (input-buffer obj)))
                (move-cursor-relative obj 0 -1)
                (put-character obj #\Space)
                (move-cursor-relative obj 0 -1)
                (rubout-from-buffer (input-buffer obj))))
             (:left
              (unless (zerop (write-index (input-buffer obj)))
                (move-cursor-relative obj 0 -1)
                (decf (write-index (input-buffer obj)))))
             (:right
              (unless (>= (write-index (input-buffer obj))
                          (length (internal-array (input-buffer obj))))
                (move-cursor-relative obj 0 1)
                (incf (write-index (input-buffer obj)))))))))))
  ;; Update cursor
  (update-cursor obj))