;;; core-customization.el --- Spacemacs Core File -*- lexical-binding: t -*-
;;
;; Copyright (c) 2012-2022 Sylvain Benner & Contributors
;;
;; Author: Eugene "JAremko" Yaremenko <w3techplayground@gmail.com>
;; URL: https://github.com/syl20bnr/spacemacs
;;
;; This file is not part of GNU Emacs.
;;
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU 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 General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.


(require 'validate)
(require 'seq)
(eval-when-compile (require 'cl-lib))

(defalias 'spacemacs-customization//validate 'validate-value)

(defvar spacemacs-customization--current-group nil
  "Used to auto-set `spacemacs|defc' group.")

(defgroup spacemacs-layers nil
  "Spacemacs layers customizations."
  :group 'spacemacs
  :prefix 'spacemacs-layers-)

(defgroup spacemacs--uncustomizable nil
  "Dummy group that contains variables that can't be customized."
  :group 'spacemacs--phony-group)

(defgroup spacemacs-dotspacemacs-init nil
  "Dotspacemacs init customizations."
  :group 'spacemacs--uncustomizable)

(defgroup spacemacs-dotspacemacs-layers nil
  "Dotspacemacs layers customizations."
  :group 'spacemacs--uncustomizable)

(defconst spacemacs-customization-uncustomizable-groups
  '(spacemacs--uncustomizable
    spacemacs-dotspacemacs-init
    spacemacs-dotspacemacs-layers)
  "List of variable groups that can't be customized.")

(defmacro spacemacs|defc (symbol standard doc type &optional group-override safe)
  "Spacemacs flavored `defcustom' for .spacemacs configurations.
SYMBOL         is the variable name; it should not be quoted.
STANDARD       is an expression specifying the variable's standard value.
DOC            is a doc-string.
TYPE           should be a widget type for editing the symbol's value.
               See Info node `(elisp) Customization Types' for a list of
               base types and useful composite types.
GROUP-OVERRIDE should be provided if you don't want Spacemacs to infer the
               configuration group from the currently configured layer name.
SAFE           should either be a function or t to be set to
               safe-local-variable property. When it's t, use TYPE to determine
               the safety.

NOTE: Use interactive function `spacemacs/customization-valid-p' to test if a
      variable has a proper type. In interactive mode it will also `message'
      variable's symbol, value and type - so you can call this function with a
      similar .spacemacs variable and use its type as an example.
NOTE: Spacemacs checks variables using validate.el package. Currently it
      doesn't support: `:inline', `plist', `coding-system', `color', `hook',
      `restricted-sexp' types so more general ones should be used instead.
NOTE: Variables defined with a group listed in
      `spacemacs-customization-uncustomizable-groups' won't appear in
      `spacemacs' customization subgroups. Also their doc-string won't provide
      customization  menu link when viewed via `describe-variable'."
  (declare (indent defun) (doc-string 3) (debug (name body)))
  `(let ((group (or ,group-override
                    spacemacs-customization--current-group
                    'spacemacs--uncustomizable)))
     (put ',symbol 'spacemacs-customization--variable t)
     (custom-declare-variable
      ',symbol
      ;; Took this from the `defcustom' implementation.
      ,(if lexical-binding
           ``(funcall #',(lambda () ,standard))
         `',standard)
      ,(format "%s\n\nTYPE: %s\n" doc type)
      :type ,type
      :group group)
     (pcase ,safe
       ('t (put ',symbol 'safe-local-variable
                (apply-partially 'spacemacs-customization//get-variable-validator
                                 ',symbol)))
       ((pred functionp) (put ',symbol 'safe-local-variable ,safe)))
     (when (memq group spacemacs-customization-uncustomizable-groups)
       ;; HACK: This will make `custom-variable-p' return nil
       ;; so the `describe-variable' function won't add customization
       ;; link. Will there be the reckoning? Will see!
       (put ',symbol 'standard-value nil)
       (put ',symbol 'custom-autoload nil))))

(defun spacemacs/customization-valid-p (var-symbol)
  "Return t if VAR-SYMBOL is a spacemacs custom variable with valid value.
Emits message with the result when called interactively."
  (interactive "v")
  (let* ((defc? (get var-symbol 'spacemacs-customization--variable))
         (val (symbol-value var-symbol))
         (type (custom-variable-type var-symbol))
         (valid? (and defc? (validate-value val type t))))
    (when (called-interactively-p 'interactive)
      (if valid?
          (message "symbol: \"%s\" value: \"%s\" type: \"%s\" is valid."
                   var-symbol val type)
        (if defc?
            (condition-case err (validate-value val type) (error (message err)))
          (message "%s is not Spacemacs customization variable" var-symbol))))
    valid?))

(defun spacemacs-customization//get-variable-validator (var-symbol val)
  "Check the validity of VAL for the spacemacs custom variable VAR-SYMBOL.

Return t if VAL is valid, and return an error when VAL is invalid or when
VAR-SYMBOL is not a spacemacs custom variable.

This function should be used with `apply-partially', and be set to the
`safe-local-variable' perperty of VAR-SYMBOL. `apply-partially' returns a
validation function that takes one argument and validate it against the scheme
of VAR-SYMBOL.

When loading local variables, `safe-local-variable-p' would call the validation
function, and it would demote any error to a message and a nil return value.
Thus it returns t when VAL is valid and nil otherwise."
  (if-let* ((_ (get var-symbol 'spacemacs-customization--variable))
            (type (custom-variable-type var-symbol)))
      (or (validate-value val type nil) t)
    (user-error "%s is not a Spacemacs customization variable" var-symbol)))

(defun spacemacs-customization//group-variables (group-symbol)
  "Given customization group symbol get its variables."
  (let (ret-val)
    (cl-labels ((rec (gs)
                     (cl-dolist (el (get gs 'custom-group))
                       (cl-case (cadr el)
                         (custom-variable (push (car el) ret-val))
                         (custom-group (rec (car el)))))))
      (rec group-symbol))
    ret-val))

(defun spacemacs-customization//validate-group-vars (group-symbol)
  "Given customization group symbol validate its variables."
  (dolist (var (spacemacs-customization//group-variables group-symbol))
    (let ((val (symbol-value var))
          (type (custom-variable-type var)))
      (condition-case err (validate-value val type)
        (error (error (concat "Variable: \"%s\" "
                              "has value: \"%s\" "
                              "that doesn't match its type: \"%s\". "
                              "Validator message: \"%s\"")
                      var val type err))))))

(defun spacemacs-customization//validate-dotspacemacs-init-vars ()
  "Validate variables set in `dotspacemacs/init' function."
  (spacemacs-customization//validate-group-vars 'spacemacs-dotspacemacs-init))

(defun spacemacs-customization//validate-dotspacemacs-layers-vars ()
  "Validate variables set in `dotspacemacs/layers' function."
  (spacemacs-customization//validate-group-vars 'spacemacs-dotspacemacs-layers))

(defun spacemacs-customization//create-layer-group (layer-name category-name)
  "Create customization group heirarchy for the LAYER-NAME configurations.
Layers customization group symbol is returned."
  (let* ((category-group-name (format "spacemacs-layers-%s" category-name))
         (layer-group-name (format "%s-%s" category-group-name layer-name))
         (category-group-symbol (intern category-group-name)))
    (custom-declare-group category-group-symbol
                          nil
                          (format "Spacemacs %s category customizations."
                                  category-name)
                          :group 'spacemacs-layers
                          :prefix (intern (concat category-group-name "-")))
    (custom-declare-group
     (intern layer-group-name)
     nil
     (format "Spacemacs %s layer customizations." layer-name)
     :group category-group-symbol
     :prefix (intern (concat category-group-name "-"
                             layer-group-name "-")))))

(provide 'core-customization)
