;;;; state.lisp -- functions for dealing with client state

;; Copyright (C) 2022  Colin Okay

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

(in-package :oneliners.cli)

;;; Config Struct

(defplist config
  (handle "")
  (api-token "")
  (host "")
  (shell "bash"))

;;; CONFIG VAR AND OPERATIONS

(defvar *config* nil
  "Holds a config struct instance.")

(defun api-token ()
  (a:if-let (token (config-api-token *config*))
    token
    (error () "No API TOKEN")))

(defun (setf api-token) (newvalue)
  (setf (config-api-token *config*) newvalue))

(defun handle ()
  (config-handle *config*))

(defun (setf handle) (newvalue)
  (setf (config-handle *config*) newvalue))

(defun host ()
  (config-host *config*))

(defun shell ()
  (config-shell *config*))


;;; CACHE VAR AND OPERATIONS

(defvar *cache* nil
  "Holds cached oneliners as a list.")

(defun merge-oneliners (new)
  "Modifies *CACHE*. Merge updated oneliners into the *cache*, ensuring to remove old versions."
  (setf *cache* 
        (nconc
         new
         (delete-if
          (lambda (old-oneliner)
            (find (oneliner-id old-oneliner)
                  new
                  :key #'oneliner-id
                  :test #'equal))
          *cache*))))

(defun get-cached (id-or-name)
  "Looks up a oneliner instance by ID-OR-NAME using the current binding of *cache*. "
  (find id-or-name
        *cache*
        :key (etypecase id-or-name
               (integer #'oneliner-id)
               (string #'oneliner-name))
        :test #'equal))

(defun remove-from-cache (id-or-name)
  "Removes an item from the contents of *cache*."
  (a:when-let (found (get-cached id-or-name))
    (setf *cache* (delete found *cache*))))

;;; DRAFTS VAR AND OPERATIONS

(defvar *drafts* nil
  "Holds a list of oneliner drafts yet to be sent to the server.")

(defun fetch-draft (name)
  "Fetch a draft by name form the *DRAFTS* association list."
  (cdr (assoc name *drafts* :test #'string-equal)))

(defun drop-draft (name)
  "Drop a draft by NAME from the *DFRAFTS* association list."
  (setf *DRAFTS* (delete (assoc name *DRAFTS* :test #'string-equal) *DRAFTS*)))

(defun put-draft (name draft)
  "Modifies *DRAFTS*, adding a new DRAFT associated with NAME. If NAME
is already associated, that old association is deleted."
  (drop-draft name)
  (push (cons name draft) *drafts*))

;;; LOADING AND SAVING STATE

(defun config-file ()
  "Returns the pahtname holding the location of the config file."
  (merge-pathnames ".config/oneliners.config" (user-homedir-pathname)))

(defun cached-oneliners-file ()
  "Returns the pathname holding the location of the cache."
  (merge-pathnames ".cache/oneliners.cache" (user-homedir-pathname)))

(defun drafts-file ()
  "Returns the pathame holding the location of the oneliner drafts file."
  (merge-pathnames ".cache/oneliners.drafts" (user-homedir-pathname)))

(defun wipe-cache ()
  "Deletes the cache, if present."
  (uiop:delete-file-if-exists (cached-oneliners-file)))

(defun write-config-to-disk ()
  (print-to-file
   *config*
   (config-file)))

(defun write-cache-to-disk ()
  (print-to-file *cache* (cached-oneliners-file)))

(defun read-config-file ()
  "Read a configuration from the location returned by CONFIG-FILE. NIL
if there is no such file"
  (read-from-file (config-file)))

(defun read-cache-file ()
  "Read the cache from the location returned by
CACHED-ONELINERS-FILE. NIL if there is no such file."
  (read-from-file (cached-oneliners-file)))

(defun make-fresh-config ()
  "Prompts the user to supply some values for a config file."
  (format t "It seems you are calling `ol` for the first time. Running Setup~%~%")
  (make-config
   :host (prompt "Oneliner Server Host: "
                 :prefill "https://api.oneliners.wiki")
   :shell (prompt "With which shell should oneliners be run? "
                  :prefill "bash")))

(defun read-drafts-file ()
  (read-from-file (drafts-file)))

(defun write-drafts-to-disk ()
  (print-to-file *drafts* (drafts-file)))

(defun ensure-config ()
  "Ensures that a configuration file exists on disk, prompting the
user for some input if it does not."
  (if (uiop:file-exists-p (config-file))
      (read-config-file)
      (make-fresh-config)))


;;; STATE LOADING MACRO

(defmacro with-local-state (&body body)
  "Binds the *config* and *cache* dynamic variables from disk, and
sets the api's *host* variable. If BODY produces no errors, the "
  `(let* ((*config* (ensure-config))
          (*cache* (read-cache-file))
          (*drafts* (read-drafts-file))
          (api:*host* (config-host *config*))
          (errorsp nil))
     (unwind-protect 
          (handler-case
              (progn
                (assert api:*host* () "ol must be configured with a server host.")
                (set-term-width)
                ,@body)
            (dexador.error:http-request-failed (e)
              (setf errorsp t)
              (format *error-output* "Operation failed. The server at ~a returned with ~a~%"
                      api:*host*
                      (dexador.error:response-status e)))
            (error (e)
              (setf errorsp t)
              (format *error-output* "Unknown Error: ~a~%" e)))
       (unless errorsp
                (write-drafts-to-disk)
                (write-cache-to-disk) 
                (write-config-to-disk)))))