(in-package :remote-snake-server-game)

(defclass game ()
  ((initial-size
    :initarg :initial-size
    :initform 3)
   (initial-position
    :initarg :initial-position
    :initform (list 0 0))
   (snake
    :accessor snake)
   (dir
    :initarg :initial-direction
    :initform :right)
   (grid-size
    :initarg :grid-size
    :initform (list 30 30))
   (food
    :initform (make-array 0))))

;;; Class constructor to initialize the snake
(defmethod initialize-instance :after ((g game) &key)
  (with-slots (snake initial-size initial-position) g
  (setf snake (make-array initial-size :initial-element initial-position))))

;;; Pretty-print a game
(defmethod print-object ((g game) stream)
  (with-slots (snake dir food) g
    (format t "Snake:      ")
    (dotimes (i (first (array-dimensions snake)))
      (let ((elem (aref snake i)))
        (unless (eql i 0)
          (format t "<="))
        (format t "(~a,~a)" (first elem) (second elem))))
    (format t "~%Size:       ~a" (first (array-dimensions snake)))
    (format t "~%Direction:  ~a" dir)
    (format t "~%Food:  ~a" food)))

;;; Note that there is no waranty that nb food are added (ex: if food position collide with snake position)
(defgeneric add-food (g nb)
  (:documentation "Add food on the game grid."))

(defgeneric refresh (g &key)
  (:documentation "Refresh the game g (move forward or change direction)."))

;;; Return true when doing a legal move (ex: snake can goto left when it is in the right direction)
(defun legal-move (active-dir dir)
  (or
   (eq active-dir dir) ; Goto same direction
   (and (or (eq dir :up) (eq dir :down)) ; Got up or down only if the snake is on the left or right direction
        (or (eq active-dir :left) (eq active-dir :right)))
   (and (or (eq dir :left) (eq dir :right)) ; Goto left or right only if the snake is on the up or down direction
        (or (eq active-dir :up) (eq active-dir :down)))))

;;; Grow snake of grow-size amount (snake is growing by the tail)
(defun grow-snake (snake grow-size)
  (let* ((old-size (first (array-dimensions snake)))
         (new-size (+ old-size grow-size))
         (tail (aref snake (- old-size 1)))
         (new-tail (coerce (make-array grow-size :initial-element tail) 'list))
         (new-snake (make-array new-size :initial-contents `(,@(coerce snake 'list) ,@new-tail))))
    new-snake))
         
    

(defmethod refresh ((g game) &key (dir nil dir-supplied-p))
  ;; First, update direction
  (with-slots ((active-dir dir)) g
      (when dir-supplied-p
        (if (and
             (or (eq dir :up) (eq dir :down) (eq dir :left) (eq dir :right))
             (legal-move active-dir dir))
                 (setf (slot-value g 'dir) dir)
                 (error "Invalid direction supplied"))))
  ;; Then, move the snake
  (with-slots (snake dir) g
    (let ((last-old-x nil) (last-old-y nil))
      (dotimes (i (first (array-dimensions snake)))
        (let ((elem (aref snake i)))
          (let ((x (first elem)) (y (second elem)))
            ;; Move snake
            (if (eql i 0) ; Move head
                (progn
                  ;; Update last-old-x and last-old-y (to move the body when i>0)
                  (setf last-old-x x)
                  (setf last-old-y y)
                  (cond ((eq dir :up) (incf y))
                        ((eq dir :down) (decf y))
                        ((eq dir :left) (decf x))
                        ((eq dir :right) (incf x))))
              (progn ; Move body
                  (rotatef x last-old-x)
                  (rotatef y last-old-y)))
            (format t "l:(~a,~a) n:(~a,~a) ~%" last-old-x last-old-y x y)
            (setf (aref snake i) (list x y))))))) ; Apply new snake location (update snake slot)
              
  ;; Check if we loose
  )

;;; Function to compare two list of two elements
(defun equal-coord (c1 c2)
  (let ((x1 (car c1))
        (x2 (car c2))
        (y1 (car (cdr c1)))
        (y2 (car (cdr c2))))
    (and (eql x1 x2) (eql y1 y2))))

(defmethod add-food ((g game) nb)
  (with-slots (snake grid-size food) g
    (let ((snake-list (coerce snake 'list)) ; To be able to use the function member later
          (food-list (coerce food 'list))
          (size-x (first grid-size))
          (size-y (second grid-size))
          (food-size (first (array-dimensions food))))
      (dotimes (i nb)
        (let ((x (random size-x))
              (y (random size-y)))
          (when (eq (member (list x y) snake-list :test #'equal-coord) nil) ; Add if there is no conflict between snake and food position
            (setf food (make-array (1+ food-size) :initial-contents `(,@food-list ,(list x y))))))))))