;;  Author: Robert Dodier

;;  This program is free software; you can redistribute it and/or
;;  modify it under the terms of the GNU General Public License,
;;  http://www.gnu.org/copyleft/gpl.html.

;;  This program has NO WARRANTY, not even the implied warranty of
;;  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

(in-package :maxima)

;; Read functions:
;;   M: read_matrix (file_name, sep_ch_flag)$
;;   read_lisp_array (file_name, A, sep_ch_flag)$
;;   read_maxima_array (file_name, A, sep_ch_flag)$
;;   read_hashed_array (file_name, A, sep_ch_flag)$
;;   L: read_nested_list (file_name, sep_ch_flag)$
;;   L: read_list (file_name, sep_ch_flag)$
;;
;; Write function:
;;   write_data (X, file_name, sep_ch_flag)$

;; See numericalio.texi for a lengthier description.

;; -------------------- read functions --------------------

(defun $read_matrix (file-name &optional sep-ch-flag)
  `(($matrix) ,@(cdr ($read_nested_list file-name sep-ch-flag))))


(defun $read_lisp_array (file-name A &optional sep-ch-flag)
  ($fillarray A ($read_list file-name sep-ch-flag))
  '$done)


(defun $read_maxima_array (file-name A &optional sep-ch-flag)
  ($fillarray A ($read_list file-name sep-ch-flag))
  '$done)


(defun $read_hashed_array (stream-or-filename A &optional sep-ch-flag)
  (if (streamp stream-or-filename)
    (read-hashed-array-from-stream stream-or-filename A sep-ch-flag)
    (let ((file-name (require-string stream-or-filename)))
      (with-open-file (in file-name :if-does-not-exist nil)
        (if (not (null in))
          (read-hashed-array-from-stream in A sep-ch-flag)
          (merror "read_hashed_array no such file `~a'" file-name))))))

(defun read-hashed-array-from-stream (in A sep-ch-flag)
  (let (key L (sep-ch (get-input-sep-ch sep-ch-flag (truename in))))
    (loop
      (setq L (read-line in nil 'eof))
      (if (eq L 'eof) (return))
      (setq L (make-mlist-from-string L sep-ch))
      (cond
        ((> ($length L) 0)
         (setq key ($first L))
         (if (= ($length L) 1)
           (arrstore (list (list A 'array) key) nil)
           (arrstore (list (list A 'array) key) ($rest L)))))))
  A)

(defun $read_nested_list (stream-or-filename &optional sep-ch-flag)
  (if (streamp stream-or-filename)
    (read-nested-list-from-stream stream-or-filename sep-ch-flag)
    (let ((file-name (require-string stream-or-filename)))
      (with-open-file (in file-name :if-does-not-exist nil)
        (if (not (null in))
          (read-nested-list-from-stream in sep-ch-flag)
          (merror "read_nested_list: no such file `~a'" file-name))))))

(defun read-nested-list-from-stream (in sep-ch-flag)
  (let (A L (sep-ch (get-input-sep-ch sep-ch-flag (truename in))))
    (loop
      (setq L (read-line in nil 'eof))
      (if (eq L 'eof)
        (return (cons '(mlist simp) (nreverse A))))
      (setq A (cons (make-mlist-from-string L sep-ch) A)))))


(defun $read_list (stream-or-filename &optional sep-ch-flag)
  (if (streamp stream-or-filename)
    (read-list-from-stream stream-or-filename sep-ch-flag)
    (let ((file-name (require-string stream-or-filename)))
      (with-open-file (in file-name :if-does-not-exist nil)
        (if (not (null in))
          (read-list-from-stream in sep-ch-flag)
          (merror "read_list: no such file `~a'" file-name))))))

(defun read-list-from-stream (in sep-ch-flag)
  (let (A L (sep-ch (get-input-sep-ch sep-ch-flag (truename in))))
    (loop
      (setq L (read-line in nil 'eof))
      (if (eq L 'eof)
        (return (cons '(mlist simp) (nreverse A))))
      ;; use nreconc accumulation to avoid n^2 cons's
      (setq A (nreconc (cdr (make-mlist-from-string L sep-ch)) A)))))

;; Usage: (make-mlist-from-string "1 2 3 foo bar baz")

(defun make-mlist-from-string (s sep-ch)
  ; scan-one-token-g isn't happy with symbol at end of string.
  (setq s (concatenate 'string s " "))

  (with-input-from-string (*parse-stream* s)
    (let ((token) (L) (LL) (sign))
      (loop
        (setq token (scan-one-token-g t 'eof))
        (cond
          ((eq token 'eof)
           (cond
             ((not (null sign))
              (format t "numericalio: trailing sign (~S) at end of line; strange, but just eat it.~%" sign)))
           (cond
             ((eq sep-ch #\space)
              (return (cons '(mlist) LL)))
             (t
               (return (cons '(mlist) (appropriate-append L LL)))))))
        (cond
          ((or (eq token '$-) (eq token '$+))
           (setq sign (cond ((eq token '$-) -1) (t 1))))
          (t
            (cond
              ((not (null sign))
               (setq token (m* sign token))
               (setq sign nil)))
            (cond
              ((eq sep-ch #\space)
               (setq LL (append LL (list token))))
              (t
                (cond
                  ((eq token sep-ch)
                   (setq L (appropriate-append L LL))
                   (setq LL nil))
                  (t
                    (setq LL (append LL (list token)))))))))))))


(defun appropriate-append (L LL)
  (cond
    ((null LL) (append L '(nil)))
    ((= (length LL) 1) (append L LL))
    (t (append L (list (append '((mlist)) LL))))))


;; -------------------- write functions -------------------

(defun open-file-appropriately (file-name)
  (open file-name
        :direction :output
        :if-exists (if (or (eq $file_output_append '$true) (eq $file_output_append t)) :append :supersede)
        :if-does-not-exist :create))

(defun $write_data (X stream-or-filename &optional sep-ch-flag)
  (let
    ((out
       (if (streamp stream-or-filename)
         stream-or-filename
         (open-file-appropriately (require-string stream-or-filename)))))
    (cond
      (($matrixp X)
        (write-matrix X out sep-ch-flag))
      ((arrayp X)
        (write-lisp-array X out sep-ch-flag))
      ((mget X 'array)
        (write-maxima-array X out sep-ch-flag))
      ((mget X 'hashar)
        (write-hashed-array X out sep-ch-flag))
      (($listp X)
        (write-list X out sep-ch-flag))
      (t (merror "write_data: don't know what to do with a ~M" (type-of X))))
    (if (not (streamp stream-or-filename))
      (close out))
    '$done))

(defun write-matrix (M out sep-ch-flag)
  (let ((sep-ch (get-output-sep-ch sep-ch-flag (truename out))))
    (mapcar #'(lambda (x) (write-list-lowlevel (cdr x) out sep-ch)) (cdr M))))

(defun write-lisp-array (A out sep-ch-flag)
  (let ((sep-ch (get-output-sep-ch sep-ch-flag (truename out))) (d (array-dimensions A)))
    (write-lisp-array-helper A d '() out sep-ch)))

(defun write-lisp-array-helper (A d indices out sep-ch)
  (cond ((equalp (length d) 1)
      (let ((L '()))
        (sloop for i from 0 to (- (car d) 1) do
          (let ((x (apply 'aref (append (list A) (reverse (cons i indices))))))
            (setq L (cons x L))))
        (write-list-lowlevel (reverse L) out sep-ch)))
    (t
      (sloop for i from 0 to (- (car d) 1) do
        (write-lisp-array-helper A (cdr d) (cons i indices) out sep-ch)
        (cond ((> (length d) 2) (terpri out)))))))

(defun write-maxima-array (A out sep-ch-flag)
  (write-lisp-array (symbol-array (mget A 'array)) out sep-ch-flag))

(defun write-hashed-array (A out sep-ch-flag)
  (let
    ((keys (cdddr (meval (list '($arrayinfo) A))))
     (sep-ch (get-output-sep-ch sep-ch-flag (truename out)))
     L)
    (loop
      (if (not keys) (return))
      (setq L ($arrayapply A (car keys)))
      (cond ((listp L) (pop L))
            (t (setq L (list L))))
      (write-list-lowlevel (append (cdr (pop keys)) L) out sep-ch))))

(defun write-list (L out sep-ch-flag)
  (let ((sep-ch (get-output-sep-ch sep-ch-flag (truename out))))
    (write-list-lowlevel (cdr L) out sep-ch)))

(defun write-list-lowlevel (L out sep-ch)
  (setq sep-ch (cond ((symbolp sep-ch) (cadr (exploden sep-ch))) (t sep-ch)))
  (cond ((not (null L))
      (loop 
        (if (not L) (return))
        (let ((e (pop L)))
          (cond (($listp e)
              (write-list-lowlevel (cdr e) out sep-ch))
            (t (mgrind e out)
              (cond ((null L) (terpri out))
                (t (write-char sep-ch out)))))))))
  (finish-output out))

(defun get-input-sep-ch (sep-ch-flag file-name)
  (cond
    ((eq sep-ch-flag '$tab)
     (format t "numericalio: separator flag ``tab'' not recognized for input; assume ``space'' instead.~%")
     #\space)
    (t (get-output-sep-ch sep-ch-flag file-name))))

(defun get-output-sep-ch (sep-ch-flag file-name)
  (cond
    ((eq sep-ch-flag '$space) #\space)
    ((eq sep-ch-flag '$tab) #\tab)
    ((or (eq sep-ch-flag '$comma) (eq sep-ch-flag '$csv)) '$\,) ; '$csv is backwards compatibility ... sigh
    ((eq sep-ch-flag '$pipe) '$\|)
    ((eq sep-ch-flag '$semicolon) '$\;)

    ((null sep-ch-flag)
      (cond ((equal (pathname-type file-name) "csv") '$\,)
        (t #\space)))
    (t
      (format t "numericalio: separator flag ~S not recognized; assume ``space''.~%" (stripdollar sep-ch-flag))
      #\space)))

(defun require-string (s)
  (cond
    ((stringp s)
     s)
    ((mstringp s)
     (print-invert-case (stripdollar s)))
    (t
      (merror "numericalio: expected a string, instead found a ~:M" (type-of s)))))
