;;;; Optparser package internals
;; more info: http://fraggod.net/prj/lisp_optparser/
;;
;; Example usage:
;;
;; (with-argv (argz concurrency verbose)
;; 	(*posix-argv* ("c" "concurrency" :value t) ("v" "verbose"))
;; 	(format t
;; 		"Arguments: ~s~%Optional values:~% verbose: ~s~% concurrency: ~s~%"
;; 		argz verbose concurrency))
;;
;; (parse-argv '("enc" "-c" "10" "--logging" "path1" "--timeout=20" "path2")
;; 	'(("v" :value nil :idx 1) ("verbose" :value nil :idx 1)
;; 		("l" :value nil :idx 2) ("logging" :value nil :idx 2) ("log-to" :value t :idx 2)
;; 		("c" :value t :idx :conc) ("concurrency" :value t :idx :conc)
;; 		("timeout" :value 10)))
;; => (values ("enc" "path1" "path2") ((1 . nil) ("timeout" . "20") (:conc . "10") (2 . t)))
;;


(in-package cl-user)
(defpackage optparser
	(:use :cl)
	(:export
		:argv-let
		:argv-bind
		:get-argv
		:getopt
		:parse-argv))
(in-package optparser)


;; Public macros
(defmacro argv-let ((args &rest specz) argv &body body)
	"Bind args and given vars to argz and each
subsequent option specifier, parsed from argv."
	`(argv-bind (,args ,@(mapcar #'car specz)) (,argv ,@(mapcar #'cadr specz)) ,@body))
	;; `(multiple-value-bind (,args ,@(mapcar #'car specz)) (get-argv ,argv ,@(mapcar #'cadr specz)) ,@body))

(defmacro argv-bind ((args &rest vars) (argv &rest specz) &body body)
	"Bind given vars to argz and each
subsequent option specifier, parsed from argv."
	`(multiple-value-bind (,args ,@vars) (get-argv ,argv ,@specz) ,@body))

(defmacro get-argv (argv &rest specz)
	"Yield multiple values for argz and each
subsequent option specifier, parsed from argv."
	`(multiple-value-bind (argz optz)
		(parse-argv ,argv
			',(loop with spec-forms for spec in specz for idx upfrom 0 do
				(loop with val = (when (eql (car (last spec 2)) :value) (car (last spec)))
					for opt in spec while (stringp opt)
					do (push `(,opt :value ,val :idx ,idx) spec-forms))
				finally (return (nreverse spec-forms))))
		(values-list (cons argz
			(loop for idx upto ,(1- (length specz)) collect (cdr (assoc idx optz)))))))

(defmacro getopt (argv &rest specz)
	"Yield argz list and optz alist, indexed by first element in spec,
skipping it as option alias if it's non-string."
	`(parse-argv ,argv
		',(loop with spec-forms for spec in specz do
			(loop
				with val = (when (eql (car (last spec 2)) :value) (car (last spec)))
				and idx = (car spec)
				for opt in spec while (or (stringp opt) (eql opt idx))
				if (stringp opt) do
				(push (list opt :value val :idx idx) spec-forms))
			finally (return (nreverse spec-forms)))))



;; Internal helper  macro
(defmacro aset (alist &rest valspec)
	(let (result)
		(loop while valspec do
			(let
				((var (pop valspec)) (val (pop valspec)))
				(push `(setf ,alist (acons ,var ,val ,alist)) result)))
		`(progn ,@result)))



;; Main worker function
(defun parse-argv (argv spec)
	"Return argz list and optz alist, parsed from argv,
according to alist of a given specz with value and
resulting opt index specification."
	(let (argz optz opt pos val idx)
		;; --- parse available argz/optz
		(loop for arg in argv do
			(if (and (not (eql val t)) (>= (length arg) 2) (eql (char arg 0) #\-))
				;; -- option
				(progn
					(setf opt (string-left-trim "-" arg))
					(if (eql (char arg 1) #\-)
						;; - long option
						(progn
							;; split arg into opt=val
							(if (setf pos (position #\= opt))
								(setf
									val (subseq opt (1+ pos))
									opt (subseq opt 0 pos))
								(setf val nil))
							;; get optspec for opt
							(when (not (setf pos (cdr (assoc opt spec :test #'string=))))
								(error (format nil "Invalid option: ~a" arg)))
							(setf idx (or (getf pos :idx) opt))
							;; push val or set flag to parse it, if necessary
							(if (getf pos :value)
								;; need val
								(if val
									(aset optz idx val)
									(setf val t)) ;; parse next arg as val
								;; true / false switch
								(when
									(or (not val)
										(find val ;; truth valz for --opt=VAL flag forms
											#("true" "enabled" "yes" "ok" "y" "1" "t")
											:test #'string=))
									(aset optz idx t))))
						;; - short option(s)
						(loop for arg across opt do
							(when (eql val t)
								(error (format nil "Option ~a requires an argument" opt)))
							(when (not (setf pos (cdr (assoc arg spec :test #'string=))))
								(error (format nil "Invalid option: ~a" arg)))
							(setf idx (or (getf pos :idx) opt))
							(if (getf pos :value)
								(setf opt arg val t)
								(aset optz idx t)))))
				;; -- arg / val
				(if (eql val t)
					(progn
						(aset optz idx arg)
						(setf val nil))
					(push arg argz)))
			finally (when (eql val t) ;; check that the last arg wasn't val-hungry option
				(error (format nil "Option ~a requires an argument" opt))))
		;; --- set defaults for unavailable optz w/ :value spec
		(dolist (arg spec)
			(setf
				opt (car arg)
				pos (cdr arg)
				idx (or (getf pos :idx) opt)
				val (getf pos :value))
			(when (not (assoc idx optz))
				(aset optz idx val)))
		;; --- return the stuff
		(values (nreverse argz) optz)))

