
(cl:in-package :py-configparser)

;; The conditions (errors)

(define-condition configparser-error (error) ())

;; Errors for the configuration management side
(define-condition config-error (configparser-error) ())
(define-condition no-section-error (config-error) ())
(define-condition duplicate-section-error (config-error) ())
(define-condition no-option-error (config-error) ())
(define-condition interpolation-error (config-error) ())
(define-condition interpolation-depth-error (interpolation-error) ())
(define-condition interpolation-missing-option-error (interpolation-error) ())
(define-condition interpolation-syntax-error (interpolation-error) ())


;;
;; Configuration storage and management routines
;;


;; The structures
;;   Note: because ABCL has issues with its CLOS support
;;         (as per 1-1-2008), we use structures below to
;;         be maximally portable.


(defstruct section
  name
  options)

(defstruct config
  (defaults (make-section :name "DEFAULT"))
  sections
  (option-name-transform-fn #'string-downcase)
  (section-name-transform-fn #'identity))

(defun norm-option-name (config option-name)
  (funcall (config-option-name-transform-fn config) option-name))

(defun norm-section-name (config section-name)
  (funcall (config-section-name-transform-fn config) section-name))

(defun %validate-section-name (name)
  (when (or (= 0 (length name))
            (find #\] name)
            (find #\Newline name)
            (find #\Return name))
    (error 'no-section-error)) ;; Invalid section name, signal so.
  name)

(defun %validate-option-name (name)
  (when (or (= 0 (length name))
            (eql (aref name 0) #\[)
            (find #\Space name)
            (find #\Tab name)
            (find #\Return name)
            (find #\Newline name))
    (error 'no-option-error));; No such option error
  name)

;; non-API
(defun %get-section (config section-name)
  (if (string= "DEFAULT" section-name)
      (config-defaults config)
      (let* ((norm-section-name (norm-section-name config section-name))
             (section (find norm-section-name (config-sections config)
                            :key #'section-name
                            :test #'string=)))
        (unless section
          (error 'no-section-error)) ;; no-such-section error
        section)))

;; non-API
(defun %get-option (config section-name option-name if-does-not-exist)
  (let* ((section (%get-section config section-name))
         (norm-option (norm-option-name config option-name))
         (option (assoc norm-option
                        (section-options section)
                        :test #'string=)))
    (if (null option)
        (if (eq if-does-not-exist :error)
            (error 'no-option-error) ;; no such option error
            (values (car (push (list (%validate-option-name option-name))
                               (section-options section)))
                    section))
        (values option section))))

;;
;; The API
;;

(defun defaults (config)
  "Returns an alist containing instance wide defaults, where the
elements are 2-element dotted lists: the CDR is the value
associated with the key."
  (section-options (config-defaults config)))

(defun sections (config)
  "Returns a list of names of defined sections."
  (mapcar #'section-name (config-sections config)))

(defun has-section-p (config section-name)
  "Returns `NIL' when the section is not added to the config yet,
some other value if it is."
  (handler-case
      (%get-section config section-name)
    (no-section-error () nil)))

(defun add-section (config section-name)
  "Adds a new section to the config.

If the section exists, the `duplicate-section-error' is raised."
  (%validate-section-name section-name)
  (let ((norm-section-name (funcall (config-section-name-transform-fn config)
                                    section-name)))
    (when (has-section-p config section-name)
      (error 'duplicate-section-error))
    (car (push (make-section :name norm-section-name)
               (config-sections config)))))

(defun options (config section-name)
  "Returns a list of option names which are defined in the given section."
  (let ((section (%get-section config section-name)))
    (mapcar #'first (section-options section))))

(defun has-option-p (config section-name option-name)
  "Returns a generalised boolean with a value of `NIL' when
the specified option does not exist in the specified section
and some other value otherwise."
  (handler-case
      (%get-option config section-name option-name :error)
    (no-option-error () nil)))

;; non-API
(defun %extract-replacement (option-value)
  ;; Returns: (VALUES replacement-option start end) or NIL
  (let ((%-pos (position #\% option-value)))
    (when (and %-pos
             (< (+ 3 %-pos) (length option-value))
             (eql (aref option-value (1+ %-pos)) #\( ))
        (let ((paren-pos (position #\) option-value :start %-pos)))
          (unless (and paren-pos
                       (< (1+ paren-pos) (length option-value))
                       (eql (aref option-value (1+ paren-pos)) #\s))
            (error 'interpolation-syntax-error))
            ;; syntax error: %(..)s is minimally required
          (when (<= 0 (- paren-pos %-pos 2))
            (let ((replacement-name
                   (make-array (- paren-pos %-pos 2)
                               :element-type (array-element-type option-value)
                               :displaced-to option-value
                               :displaced-index-offset (+ 2 %-pos))))
              (when (= 0 (length replacement-name))
                ;; some preconditions on replacement-name
                (error 'interpolation-syntax-error))
              (values replacement-name %-pos (1+ paren-pos))))))))
        
;; non-API
(defun %option-value (config section option-name &key defaults)
  (if (string= option-name "__name__")
      (section-name section)
      (let* ((norm-option-name (norm-option-name config option-name))
             (option (has-option-p config (section-name section) option-name)))
        (if option
          (cdr option)
          (labels ((get-value (repositories)
                     (when (null repositories)
                       (error 'interpolation-missing-option-error))
                     ;; no such option error
                     (let ((value (assoc norm-option-name (car repositories)
                                         :test #'string=)))
                       (if value
                         (cdr value)
                         (get-value (cdr repositories))))))
          (get-value (list (section-options section)
                           defaults
                           (defaults config))))))))

;; non-API
(defun %expand-option-value (config section option-value defaults
                            &optional dependees)
  (multiple-value-bind
        (replacement-name start end)
      (%extract-replacement option-value)
    (unless replacement-name
      ;; nothing to do here...
      (return-from %expand-option-value option-value))

    (let ((norm-replacement (norm-option-name config replacement-name))
          (replacement-value (%option-value config section
                                            replacement-name
                                            :defaults defaults)))
      (when (member norm-replacement dependees :test #'string=)
        (error 'interpolation-depth-error)) ;; recursive dependency...
      (%expand-option-value
       config
       section
       (concatenate 'string
                   (subseq option-value 0 start)
                   (%expand-option-value config
                                         section
                                         replacement-value
                                         defaults
                                         (cons norm-replacement dependees))
                   (subseq option-value (1+ end) (length option-value)))
       defaults
       dependees))))

(defun get-option (config section-name option-name
                   &key (expand t) defaults type)
  "Returns the value of the specified option in the specified section.

If `expand' is `NIL', any options which depend on other options
won't be expanded and the raw configuration value is returned.

When `defaults' is an alist of which the elements are dotted lists of
key/value pairs, these values are used in the expansion of option values.

`type' may be one of `:boolean', `:number' or it may remain unspecified."
  (multiple-value-bind
        (option section)
      (%get-option config section-name option-name :error)
    (flet ((convert-boolean (v)
             (cond
               ((member v '("1" "yes" "true" "on") :test #'string=)
                T)
               ((member v '("0" "no" "false" "off") :test #'string=)
                NIL)
               (t
                (error 'not-a-boolean))))
           (convert-number (v)
             (parse-number:parse-number v)))
      (let ((string-value
             (if expand
                 (%expand-option-value config
                                       section (cdr option)
                                       defaults
                                       (list option-name))
                 (cdr option))))
        (cond
          ((eq type :boolean)
           (convert-boolean string-value))
          ((eq type :number)
           (convert-number string-value))
          ((null type)
           string-value)
          (t
           (error "Illegal `type' parameter value.")))))))

(defun set-option (config section-name option-name value)
  "Sets the value of the specified option in the specified section.

If the section does not exist, a `no-section-error' is raised. If the
option does not exist, it is created."
  (let ((option (%get-option config section-name option-name :create)))
    (setf (cdr option) value)))

(defun items (config section-name &key (expand t) defaults)
  "Returns an alist of which the items are dotted lists of key/value
pairs being the option names and values specified in the given section.

When `expand' is `NIL', options are returned in raw form. Otherwise
option values are expanded.

The definition of `defaults' is the same as for `get-option'."
  (let ((section (%get-section config section-name)))
    (if expand
        (mapcar #'(lambda (x)
                    (cons (car x) (get-option config section-name
                                              (car x) ;; option-name
                                              :expand expand
                                              :defaults defaults)))
                (section-options section))
        (section-options section))))

(defun remove-option (config section-name option-name)
  "Remove the specified option from the given section."
  (multiple-value-bind
        (option section)
      (%get-option config section-name option-name :error)
    (setf (section-options section)
          (remove option (section-options section)))))

(defun remove-section (config section-name)
  "Remove the specified section.

In case the section name equals the magic name `DEFAULT',
an error is raised, since this section can't be removed."
  (when (string= section-name "DEFAULT")
    (error 'no-section-error)) ;; no such section error
  (let ((section (%get-section config section-name)))
    (setf (config-sections config)
          (remove section (config-sections config)))))

