(defmodule lutil-list
  (export all))

(defun ->dict (list-data)
  "'list-data' is a list of implicit pairs:
    * the odd elements are keys of type 'atom'
    * the even elemnts are the values.

  This list is partitioned. zipped to tuples, and then converted to a dict."
  (let (((tuple keys values) (partition list-data)))
    (dict:from_list
     (lists:zip keys values))))

(defun ->tuple (list-data)
  (let ((quoted (lists:map (lambda (x) `',x) list-data)))
    (eval `(tuple ,@quoted))))

(defun chunks (list-data count)
  (chunks list-data count #(by-parts)))

(defun chunks
  ((list-data len #(by-length))
   (chunks-by-length list-data len))
  ((list-data count #(by-parts))
   (chunks-by-parts list-data count)))

(defun chunks-by-length (list-data len)
  (let*((leader-len (case (rem (length list-data) len)
                      (0 0)
                      (n (- len n))))
        (leader (lists:duplicate leader-len 'undefined)))
    (chunks-by-length (++ leader (lists:reverse list-data)) '() 0 len)))

;; The following is adapted from an Erlang answer given by user chops on
;; StackOverflow, here:
;; * https://stackoverflow.com/questions/12534898/splitting-a-list-in-equal-sized-chunks-in-erlang
(defun chunks-by-length
  (('() acc _ _)
   acc)
  ((`(,h . ,t) acc pos max) (when (== pos max))
   (chunks-by-length t (cons (list h) acc) 1 max))
  ((`(,h . ,t) `(,h-acc . ,t-acc) pos max)
   (chunks-by-length t (cons (cons h h-acc) t-acc) (+ 1 pos) max))
  ((`(,h . ,t) '() pos max)
   (chunks-by-length t (list (list h)) (+ 1 pos) max)))

(defun chunks-by-parts (list-data count)
  (let ((length (round (/ (length list-data) count))))
    (chunks-by-length list-data length)))
  
(defun partition (list-data)
  "This function takes lists of even length with an implicit key (atom) value
  pairing and generates a list of two lists: one with all the keys, and the
  other with all the values."
  (lists:partition #'is_atom/1 list-data))

;; The zip/2, zip/3, and zip/4 implementations are for kicks; probably *much*
;; better to use Erlang's lists:zip/2 and lists:zip3/3. There's no zip/4, so
;; hey -- have fun. The zip/1 function is what's most interesting: it can be
;; used for lists of lists of any length -- no need for special-case arities.
;; In particular, the zip/1 function was needed for the lmatrix library.
;;
;; Note that, unlike the Erlang zip functions, these functions *do not* return
;; a list of tuples; they return a list of lists.
(defun zip (lists)
  (zip-any lists '()))

(defun zip (list-1 list-2)
  (zip-2 list-1 list-2 '()))

(defun zip (list-1 list-2 list-3)
  (zip-3 list-1 list-2 list-3 '()))

(defun zip (list-1 list-2 list-3 list-4)
  (zip-4 list-1 list-2 list-3 list-4 '()))

(defun zip-2
  ((_ '() acc)
    acc)
  (('() _ acc)
    acc)
  (((cons h1 t1) (cons h2 t2) acc)
    (zip-2 t1 t2 (++ acc `((,h1 ,h2))))))

(defun zip-3
  ((_ _ '() acc)
    acc)
  ((_ '() _ acc)
    acc)
  (('() _ _ acc)
    acc)
  (((cons h1 t1) (cons h2 t2) (cons h3 t3) acc)
    (zip-3 t1 t2 t3 (++ acc `((,h1 ,h2 ,h3))))))

(defun zip-4 (list-1 list-2 list-3 list-4 acc)
  (cond
    ((lists:any
        (lambda (x) (== x '()))
        (list list-1 list-2 list-3 list-4))
     acc)
    ('true
      (let (((list (cons h1 t1) (cons h2 t2) (cons h3 t3) (cons h4 t4))
             (list list-1 list-2 list-3 list-4)))
        (zip-4 t1 t2 t3 t4 (++ acc `((,h1 ,h2 ,h3 ,h4))))))))

;; An alternate implementation:
;; (defun zip-4 (list-1 list-2 list-3 list-4 acc)
;;   ((_ _ _ '() acc)
;;     acc)
;;   ((_ _ '() _ acc)
;;     acc)
;;   ((_ '() _ _ acc)
;;     acc)
;;     (('() _ _ _ acc)
;;     acc)
;;   (((cons h1 t1) (cons h2 t2) (cons h3 t3) (cons h4 t4) acc)
;;     (zip-4 t1 t2 t3 t4 (++ acc `((,h1 ,h2 ,h3 ,h4))))))

(defun zip-any (lists acc)
  (cond
    ((lists:any
        (lambda (x) (== x '()))
        lists)
     acc)
    ('true
      (let ((heads (lists:map #'cl:car/1 lists))
            (tails (lists:map #'cl:cdr/1 lists)))
        (zip-any tails (++ acc `(,heads)))))))
