;;; specifier.el --- Lisp interface to specifiers
;; Keywords: internal

;; Copyright (C) 1995 Ben Wing

;; Author: Ben Wing <wing@netcom.com>

;; This file is part of XEmacs.

;; XEmacs 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 2, or (at your option)
;; any later version.

;; XEmacs 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 XEmacs; see the file COPYING.  If not, write to the Free
;; Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

(defun make-specifier-and-init (type spec-list &optional dont-canonicalize)
  "Create and initialize a new specifier.

This is a front-end onto `make-specifier' that allows you to create a
specifier and add specs to it at the same time.  TYPE specifies the
specifier type.  SPEC-LIST supplies the specification(s) to be added
to the specifier. Normally, almost any reasonable abbreviation of the
full spec-list form is accepted, and is converted to the full form;
however, if optional argument DONT-CANONICALIZE is non-nil, this
conversion is not performed, and the SPEC-LIST must already be in full
form.  See `canonicalize-spec-list'."
  (let ((sp (make-specifier type)))
    (if (not dont-canonicalize)
	(setq spec-list (canonicalize-spec-list spec-list type)))
    (add-spec-list-to-specifier sp spec-list)
    sp))

;; God damn, do I hate dynamic scoping.

(defun map-specifier (ms-specifier ms-func &optional ms-locale ms-maparg)
  "Apply MS-FUNC to the specification(s) for MS-LOCALE in MS-SPECIFIER.

If MS-LOCALE is a locale, MS-FUNC will be called for that locale.
If MS-LOCALE is a locale type, MS-FUNC will be mapped over all locales
of that type.  If MS-LOCALE is 'all or nil, MS-FUNC will be mapped
over all locales in MS-SPECIFIER.

MS-FUNC is called with four arguments: the MS-SPECIFIER, the locale
being mapped over, the inst-list for that locale, and the
optional MS-MAPARG.  If any invocation of MS-FUNC returns non-nil,
the mapping will stop and the returned value becomes the
value returned from `map-specifier'.  Otherwise, `map-specifier'
returns nil."
  (let ((ms-specs (specifier-spec-list ms-specifier ms-locale))
	ms-result)
    (while (and ms-specs (not ms-result))
      (let ((ms-this-spec (car ms-specs)))
	(setq ms-result (funcall ms-func ms-specifier (car ms-this-spec)
			      (cdr ms-this-spec) ms-maparg))
	(setq ms-specs (cdr ms-specs))))
    ms-result))

(defun canonicalize-inst-pair (inst-pair specifier-type &optional noerror)
  "Canonicalize the given INST-PAIR.

SPECIFIER-TYPE specifies the type of specifier that this SPEC-LIST
will be used for.

Canonicalizing means converting to the full form for an inst-pair, i.e.
`(TAG-SET . INSTANTIATOR)'.  A single, untagged instantiator is given
a tag set of nil (the empty set), and a single tag is converted into
a tag set consisting only of that tag.

If NOERROR is non-nil, signal an error if the inst-pair is invalid;
otherwise return t."
  ;; OK, the possibilities are:
  ;;
  ;; a) a single instantiator
  ;; b) a cons of a tag and an instantiator
  ;; c) a cons of a tag set and an instantiator
  (cond ((valid-instantiator-p inst-pair specifier-type)
	 ;; case (a)
	 (cons nil inst-pair))

	((not (consp inst-pair))
	 ;; not an inst-pair
	 (if noerror t
	   ;; this will signal an appropriate error.
	   (check-valid-instantiator inst-pair specifier-type)))

	((and (valid-specifier-tag-p (car inst-pair))
	      (valid-instantiator-p (cdr inst-pair) specifier-type))
	 ;; case (b)
	 (cons (list (car inst-pair)) (cdr inst-pair)))

	((and (valid-specifier-tag-set-p (car inst-pair))
	      (valid-instantiator-p (cdr inst-pair) specifier-type))
	 ;; case (c)
	 inst-pair)
	 
	(t
	 (if noerror t
	   (signal 'error (list "Invalid specifier tag set"
				(car inst-pair)))))))

(defun canonicalize-inst-list (inst-list specifier-type &optional noerror)
  "Canonicalize the given INST-LIST (a list of inst-pairs).

SPECIFIER-TYPE specifies the type of specifier that this INST-LIST
will be used for.

Canonicalizing means converting to the full form for an inst-list, i.e.
`((TAG-SET . INSTANTIATOR) ...)'.  This function accepts a single
inst-pair or any abbrevation thereof or a list of (possibly
abbreviated) inst-pairs. (See `canonicalize-inst-pair'.)

If NOERROR is non-nil, signal an error if the inst-list is invalid;
otherwise return t."

  ;; OK, the possibilities are:
  ;;
  ;; a) an inst-pair or various abbrevations thereof
  ;; b) a list of (a)
  (let ((result (canonicalize-inst-pair inst-list specifier-type t)))
    (if (not (eq result t))
	;; case (a)
	(list result)

      (if (not (consp inst-list))
	  ;; not an inst-list.
	  (if noerror t
	   ;; this will signal an appropriate error.
	    (check-valid-instantiator inst-list specifier-type))

	;; case (b)
	(catch 'cann-inst-list
	  ;; don't use mapcar here; we need to catch the case of
	  ;; an invalid list.
	  (let ((rest inst-list)
		(result nil))
	    (while rest
	      (if (not (consp rest))
		  (if noerror (throw 'cann-inst-list t)
		    (signal 'error (list "Invalid list format" inst-list)))
		(let ((res2 (canonicalize-inst-pair (car rest) specifier-type
						    noerror)))
		  (if (eq res2 t)
		      ;; at this point, we know we're noerror because
		      ;; otherwise canonicalize-inst-pair would have
		      ;; signalled an error.
		      (throw 'cann-inst-list t)
		    (setq result (cons res2 result)))))
	      (setq rest (cdr rest)))
	    (nreverse result)))))))

(defun canonicalize-spec (spec specifier-type &optional noerror)
  "Canonicalize the given SPEC (a specification).

SPECIFIER-TYPE specifies the type of specifier that this SPEC-LIST
will be used for.

Canonicalizing means converting to the full form for a spec, i.e.
`(LOCALE (TAG-SET . INSTANTIATOR) ...)'.  This function accepts a
possibly abbreviated inst-list or a cons of a locale and a possibly
abbreviated inst-list. (See `canonicalize-inst-list'.)

If NOERROR is nil, signal an error if the specification is invalid;
otherwise return t."
  ;; OK, the possibilities are:
  ;;
  ;; a) an inst-list or some abbrevation thereof
  ;; b) a cons of a locale and an inst-list
  (let ((result (canonicalize-inst-list spec specifier-type t)))
    (if (not (eq result t))
	;; case (a)
	(cons 'global result)

      (if (not (consp spec))
	  ;; not a spec.
	  (if noerror t
	    ;; this will signal an appropriate error.
	    (check-valid-instantiator spec specifier-type))

	(if (not (valid-specifier-locale-p (car spec)))
	    ;; invalid locale.
	    (if noerror t
	      (signal 'error (list "Invalid specifier locale" (car spec))))

	  ;; case (b)
	  (let ((result (canonicalize-inst-list (cdr spec) specifier-type
						noerror)))
	    (if (eq result t)
		;; at this point, we know we're noerror because
		;; otherwise canonicalize-inst-list would have
		;; signalled an error.
		t
	      (cons (car spec) result))))))))

(defun canonicalize-spec-list (spec-list specifier-type &optional noerror)
  "Canonicalize the given SPEC-LIST (a list of specifications).

SPECIFIER-TYPE specifies the type of specifier that this SPEC-LIST
will be used for.

Canonicalizing means converting to the full form for a spec-list, i.e.
`((LOCALE (TAG-SET . INSTANTIATOR) ...) ...)'.  This function accepts
a possibly abbreviated specification or a list of such things. (See
`canonicalize-spec'.) This is the function used to convert spec-lists
accepted by `set-specifier' and such into a form suitable for
`add-spec-list-to-specifier'.

This function tries extremely hard to resolve any ambiguities,
and the built-in specifier types (font, image, toolbar, etc.) are
designed so that there won't be any ambiguities.

If NOERROR is nil, signal an error if the spec-list is invalid;
otherwise return t."
  ;; OK, the possibilities are:
  ;;
  ;; a) a spec or various abbrevations thereof
  ;; b) a list of (a)
  (let ((result (canonicalize-spec spec-list specifier-type t)))
    (if (not (eq result t))
	;; case (a)
	(list result)

      (if (not (consp spec-list))
	  ;; not a spec-list.
	  (if noerror t
	   ;; this will signal an appropriate error.
	    (check-valid-instantiator spec-list specifier-type))

	;; case (b)
	(catch 'cann-spec-list
	  ;; don't use mapcar here; we need to catch the case of
	  ;; an invalid list.
	  (let ((rest spec-list)
		(result nil))
	    (while rest
	      (if (not (consp rest))
		  (if noerror (throw 'cann-spec-list t)
		    (signal 'error (list "Invalid list format" spec-list)))
		(let ((res2 (canonicalize-spec (car rest) specifier-type
					       noerror)))
		  (if (eq res2 t)
		      ;; at this point, we know we're noerror because
		      ;; otherwise canonicalize-spec would have
		      ;; signalled an error.
		      (throw 'cann-spec-list t)
		    (setq result (cons res2 result)))))
	      (setq rest (cdr rest)))
	    (nreverse result)))))))

(defun set-specifier (specifier value &optional how-to-add)
  "Add some specifications to SPECIFIER.
VALUE can be a single instantiator or tagged instantiator (added as a
global specification), a list of tagged and/or untagged instantiators
(added as a global specification), a cons of a locale and instantiator
or locale and instantiator list, a list of such conses, or nearly any
other reasonable form.  More specifically, VALUE can be anything
accepted by `canonicalize-spec-list'.

HOW-TO-ADD is the same as in `add-spec-to-specifier'.

Note that `set-specifier' is exactly complementary to `specifier-specs'
except in the case where SPECIFIER has no specs at all in it but nil
is a valid instantiator (in that case, `specifier-specs' will return
nil (meaning no specs) and `set-specifier' will interpret the `nil'
as meaning \"I'm adding a global instantiator and its value is `nil'\"),
or in strange cases where there is an ambiguity between a spec-list\n\
and an inst-list, etc. (The built-in specifier types are designed\n\
in such a way as to avoid any such ambiguities.)

If you want to work with spec-lists, you should probably not use these
functions, but should use the lower-level functions `specifier-spec-list'
and `add-spec-list-to-specifier'.  These functions always work with
fully-qualified spec-lists; thus, there is no possibility for
ambiguity and no need to go through the function `canonicalize-spec-list',
which is potentially time-consuming."
  (add-spec-list-to-specifier
    specifier
    (canonicalize-spec-list value (specifier-type specifier))
    how-to-add))
