;;; zotelo.el --- Manage Zotero collections from emacs
;;
;; Filename: zotelo.el
;; Author: Spinu Vitalie
;; Maintainer: Spinu Vitalie
;; Copyright (C) 2011-2012, Spinu Vitalie, all rights reserved.
;; Created: Oct 2 2011
;; Version: 1.3.9000
;; URL: https://github.com/vitoshka/zotelo
;; Keywords: zotero, emacs, reftex, bibtex, MozRepl, bibliography manager
;; Package-Requires: ((cl-lib "0.5"))
;;
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; 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, 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; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;; Floor, Boston, MA 02110-1301, USA.
;;
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Commentary:
;;
;; Zotelo helps you efficiently export and synchronize local databases (bib,
;; rdf, html, json etc) and [Zotero](http://www.zotero.org) collections directly
;; from emacs.
;;
;; Zotelo can be used in conjunction with any emacs mode but is primarily
;; intended for bibtex and RefTeX users.
;;
;; zotelo-mode-map lives on  C-c z prefix.
;;
;;   *Installation*
;;
;;   (add-hook 'TeX-mode-hook 'zotelo-minor-mode)
;;
;;  See https://github.com/vspinu/zotelo for more
;;
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Code:

(require 'cl-lib)

(defgroup zotelo nil "Customization for zotelo"
  :group 'convenience)

(defcustom zotelo-default-translator 'BibTeX
  "The name of the default zotero-translator to use (a symbol).
Must correspond to one of the labels of the translators in
Zotero. You can set this variable interactively with
`zotelo-set-translator'."
  :type 'symbol
  :group 'zotelo)

(defcustom zotelo-translator-charsets
  '((BibTeX . "Western")
    (Default . "Unicode"))
  "Default charsets for exporting bibliography.
Alist where the car of each element is a name of a
translator (symbol) and the cdr is the name of the character
set (string) that should be used by default for this translator
to export the bibliography. The special `Default' translator sets
the character set for all other translators not listed here."
  :group 'zotelo
  :type '(repeat
          (cons :tag ""
                (symbol :tag "Translator")
                (string :tag "   Charset"))))

(defcustom zotelo-charset nil
  "Charset used for exporting bibliography.
If nil (default), the charset will be determined by the current
translator and `zotelo-translator-charsets'. You can set the
buffer local value of this variable interactively with
`zotelo-set-charset'."
  :group 'zotelo
  :type '(string :tag "Charset")
  :safe 'string-or-null-p)

(defcustom zotelo-use-journal-abbreviation nil
  "If non-nil, use journal abbreviations for exporting bibliography.
See https://www.zotero.org/support/kb/journal_abbreviations"
  :group 'zotelo
  :type '(boolean :tag "Use journal abbreviation")
  :safe 'booleanp)

(defcustom zotelo-bibliography-commands '("bibliography" "nobibliography" "zotelo" "addbibresource")
  "List of commands which specify databases to use.
For example \\bibliography{file1,file2} or \\zotelo{file1,file2}
both specify that file1 is a primary database and file2 is the
secondary one."
  :group 'zotelo
  :type 'list)

(defvar zotelo--check-timer nil
  "Global timer executed at `zotelo-check-interval' seconds. ")

(defvar zotelo-check-interval 5
  "Seconds between checks for zotero database changes.
Note that zotelo uses idle timer. Yeach time emacs is idle for
this number of seconds zotelo checks for an update.")

(defvar zotelo-auto-update-all nil
  "If t zotelo checks for the change in zotero database every
`zotelo-check-interval' seconds and auto updates all buffers with
active `zotelo-minor-mode'. If nil the only updated files are
those with non-nil file local variable `zotelo-auto-update'. See
`zotelo-mark-for-auto-update'. ")

(defvar zotelo--auto-update-is-on nil
  "If t zotelo auto updates the collection on changes in zotero database.
  You can toggle it with 'C-c z T'")

(defvar zotelo--ignore-files (list "_region_.tex"))

(defvar zotelo--verbose nil)

(defun zotelo-verbose ()
  "Toggle zotelo debug messages (all printed in *message* buffer)"
  (interactive)
  (message "zotelo verbose '%s'" (setq zotelo--verbose (not zotelo--verbose))))

(defun zotelo--message (str)
  (when zotelo--verbose
    (with-current-buffer "*Messages*"
      (let ((inhibit-read-only t))
	(goto-char (point-max))
	(insert (format "\n zotelo message [%s]\n %s\n" (current-time-string) str))))))



;;; JAVASCRIPT

(defconst zotelo--get-zotero-database-js
  "var zotelo_zotero = Components.classes['@zotero.org/Zotero;1'].getService(Components.interfaces.nsISupports).wrappedJSObject;
zotelo_zotero.getZoteroDatabase().path;")

(defconst zotelo--get-zotero-storage-js
  "var zotelo_zotero = Components.classes['@zotero.org/Zotero;1'].getService(Components.interfaces.nsISupports).wrappedJSObject;
zotelo_zotero.getStorageDirectory().path;")

(defconst zotelo--render-collection-js
  "
var zotelo_render_collection = function() {
var R=%s;
var Zotero = Components.classes['@zotero.org/Zotero;1'].getService(Components.interfaces.nsISupports).wrappedJSObject;
var print_names = function(collections, prefix){
    for (c in collections) {
        var fullname = prefix + '/' + collections[c].name;
        R.print(collections[c].id + ' ' + fullname);
        if (collections[c].hasChildCollections) {
            var subcol = Zotero.getCollections(collections[c].id);
            print_names(subcol, fullname); 
        }}};
print_names(Zotero.getCollections(), '');
var groups = Zotero.Groups.getAll();        
for (g in groups){
    print_names(groups[g].getCollections(), '/*groups*/'+groups[g].name);
}};
")

(defconst zotelo--render-translators-js
  "
var zotelo_render_translators = function() {
var R=%s;
var Zotero = Components.classes['@zotero.org/Zotero;1'].getService(Components.interfaces.nsISupports).wrappedJSObject;
var translator = new Zotero.Translate.Export();
for each (var w in translator.getTranslators()) {
    R.print(\"'\" + w.label + \"' \" +
            w.translatorID + \" '\" +
            w.target + \"'\");
}};
")

(defconst zotelo--render-charsets-js
  "
var R = %s;
Components.utils.import(\"resource://gre/modules/CharsetMenu.jsm\");
zoteloAllCharsets = CharsetMenu.getData().pinnedCharsets.concat(CharsetMenu.getData().otherCharsets);
for each (var cs in zoteloAllCharsets) {
    R.print(\"'\" + cs.label + \"' '\" + cs.value + \"'\");
};
")

;;;; moz-repl splits long commands. Need to send it partially, but then errors
;;;; in first parts are not visible ... :(
;;;; todo: insert the check dirrectly in moz-command ??? 
(defconst zotelo--export-collection-js
  "
var zotelo_filename=('%s');
var zotelo_id = %s;
var zotelo_translator_id = '%s';
var charset = '%s';
var jabrev = %s;
var zotelo_prefs = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefService).getBranch('extensions.zotero.');
var zotelo_file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile);
var zotelo_recColl = zotelo_prefs.getBoolPref('recursiveCollections');
zotelo_file.initWithPath(zotelo_filename);
//split
var zotelo_zotero = Components.classes['@zotero.org/Zotero;1'].getService(Components.interfaces.nsISupports).wrappedJSObject;
var zotelo_collection = true;
var zotelo_translator = new zotelo_zotero.Translate.Export();
if (zotelo_id != 0){ //not all collections
    zotelo_collection = zotelo_zotero.Collections.get(zotelo_id);
    zotelo_translator.setCollection(zotelo_collection);
} else {
    zotelo_translator.setLibraryID(null);
}
//split
if(zotelo_collection){
    zotelo_translator.setLocation(zotelo_file);
    zotelo_translator.setTranslator(zotelo_translator_id);
    zotelo_prefs.setBoolPref('recursiveCollections', true);
    zotelo_translator.setDisplayOptions({'exportCharset': charset, 'useJournalAbbreviation': jabrev});
    zotelo_translator.translate();
    zotelo_prefs.setBoolPref('recursiveCollections', zotelo_recColl);
    zotelo_out=':MozOK:';
}else{
    zotelo_out='Collection with the id ' + zotelo_id + ' does not exist.';
};
//split
zotelo_out;
"
  "Command sent to zotero for export request.")

(defconst zotelo--dateModified-js
  "var zotelo_zotero = Components.classes['@zotero.org/Zotero;1'].getService(Components.interfaces.nsISupports).wrappedJSObject;
var zotelo_id = %s;
var zotelo_collection = zotelo_zotero.Collections.get(zotelo_id);
if(zotelo_collection){
   ':MozOK:' + zotelo_collection.dateModified;
}else{
   'Collection with the id ' + zotelo_id + ' does not exist.';
}"
  "Command to get last modification date of the collection.")



;;; ZOTELO MINOR MODE

(defvar zotelo-minor-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map "\C-czu" 'zotelo-update-database)
    (define-key map "\C-cze" 'zotelo-export-secondary)
    (define-key map "\C-czs" 'zotelo-set-collection)
    (define-key map "\C-czc" 'zotelo-set-collection)
    (define-key map "\C-czC" 'zotelo-set-charset)
    (define-key map "\C-czm" 'zotelo-mark-for-auto-update)
    (define-key map "\C-czr" 'zotelo-reset)
    (define-key map "\C-czt" 'zotelo-set-translator)
    (define-key map "\C-czT" 'zotelo-toggle-auto-update)
    map))

;;;###autoload
(define-minor-mode zotelo-minor-mode
  "zotelo minor mode for interaction with Firefox.
With no argument, this command toggles the mode. Non-null prefix
argument turns on the mode. Null prefix argument turns off the
mode.

When this minor mode is enabled, `zotelo-set-collection' prompts
for zotero collection and stores it as file local variable . To
manually update the BibTeX data base call
`zotelo-update-database'. The \"file_name.bib\" file will be
created with the exported zotero items. To specify the file_name
just insert insert \\bibliography{file_name} anywhere in the
buffer.

This mode is designed mainly for latex modes and works in
conjunction with RefTex, but it can be used in any other mode
such as org-mode.

\\{zotelo-minor-mode-map}"
  nil
  (zotelo--auto-update-is-on " ZX" " zx")
  :keymap zotelo-minor-mode-map
  :group 'zotelo
  (if zotelo-minor-mode
      (progn
        (unless (timerp zotelo--check-timer)
          (setq zotelo--check-timer
                (run-with-idle-timer 5 zotelo-check-interval 'zotelo--check-and-update-all))))
    (unless
        (cl-loop for b in (buffer-list)
                 for is-zotelo-mode = (buffer-local-value 'zotelo-minor-mode b)
                 until is-zotelo-mode
                 finally return is-zotelo-mode)
      ;; if no more active zotelo mode, cancel the timer and kill the process
      (when (timerp zotelo--check-timer)
        (cancel-timer zotelo--check-timer)
        (setq zotelo--check-timer nil)
        (delete-process (zotelo--moz-process))
        (kill-buffer zotelo--moz-buffer)))))

(defun zotelo--check-and-update-all ()
  "Function run with `zotelo--check-timer'."
  (when zotelo--auto-update-is-on
    (let ( out id any-z-buffer-p z-buffer-p)
      (zotelo--message  "zotelo checking for updates.")
      (dolist (b  (buffer-list)) ;iterate through zotelo buffers
        (setq z-buffer-p (buffer-local-value 'zotelo-minor-mode b))
        (when z-buffer-p
          (setq any-z-buffer-p t))
        (when (and
               ;; zotelo buffer?
               z-buffer-p
               ;; exclusion reg-exp  matched?,
               (not (delq nil (mapcar (lambda (reg)
                                        (string-match reg (buffer-name b)))
                                      zotelo--ignore-files)))
               ;; collection is set?,
               (assoc 'zotero-collection (buffer-local-value 'file-local-variables-alist b))
               ;; auto-update-all?, auto-update?
               (let ((auto-update
                      (assoc 'zotelo-auto-update (buffer-local-value 'file-local-variables-alist b))))
                 (if (and zotelo-auto-update-all (null auto-update))
                     (setq auto-update '(t . t)))
                 (cdr auto-update)))
          (with-current-buffer b
            (ignore-errors
              (setq id (zotelo-update-database t))))
          (when id
            (setq out
                  (append (list (buffer-name b)) out)))))
      (if (> (length out) 0)
          (message "Bibliography updated in %s buffers: %s." (length out) out))
      (when (and (not any-z-buffer-p)
                 (timerp zotelo--check-timer))
        ;; stop timer if no more zotelo buffers
        (cancel-timer zotelo--check-timer)
        (setq zotelo--check-timer nil)
        (delete-process (zotelo--moz-process))
        (kill-buffer zotelo--moz-buffer)))))

;;;###autoload
(defun zotelo-export-secondary ()
  "Export zotero collection into  secondary BibTeX database.
Before export, ask for a secondary database and zotero collection
to be exported into the database. Secondary databases are those
in \\bibliography{file1, file2, ...}, except the first one.

Throw error if there is only one (primary) file listed in
\\bibliography{...}. Throw error if zotero collection is not
found by MozRepl"
  (interactive)
  (let* ((files (zotelo--locate-bibliography-files))
         (bibfile (cond
                   ((< (length files) 2)
                    (error "No secondary databases (\\bibliography{...} lists contain less than 2 files)."))
                   ((= (length files) 2)
                    (cadr files))
                   (t (completing-read "File to update: " (cdr files)))))
         (collection (zotelo-set-collection
                      (format "Export into '%s': " (file-name-nondirectory bibfile))
                      'no-update 'no-set)))
    (zotelo-update-database nil bibfile (get-text-property 0 'zotero-id collection))))

(defun zotelo--get-translators ()
  "Get translators from running Zotero instance.
In case that no default extension is provided for the translator
by Zotero, use `txt'"
  (let ((buf (get-buffer-create "*moz-command-output*"))
	translators)
    ;; set up the translator list
    (moz-command (format zotelo--render-translators-js
			 (process-get (zotelo--moz-process) 'moz-prompt)))
    (moz-command "zotelo_render_translators()" buf)
    (with-current-buffer buf
      (goto-char (point-min))
      (zotelo--message (format "Translators:\n %s"
			       (buffer-substring-no-properties (point-min) (min 500 (point-max)))))
      (while (re-search-forward "^'\\(.+\\)' \\(.*\\) '\\(.*\\)'$" nil t)
	(let* ((label (intern (match-string-no-properties 1)))
               (id (match-string-no-properties 2))
               (ext-from-zotero (match-string-no-properties 3))
               (extension (if (string= ext-from-zotero "")
                              "txt"
                            ext-from-zotero)))
          (setq translators (cons (cons label (cons id (cons extension nil))) translators)))))
    (if (null translators)
        (error "No translators found or error occured see *moz-command-output* buffer for clues.")
      translators)))

(defun zotelo-set-translator ()
  "Ask to choose from available translators and set `zotelo-default-translator'."
  (interactive)
  (let ((tnames (mapcar (lambda (el) (symbol-name (car el)))
                        (zotelo--get-translators))))
    (setq zotelo-default-translator
          (intern (completing-read "Choose translator: " tnames nil nil nil nil 
                                   (symbol-name zotelo-default-translator))))
    (message "Translator set to %s" zotelo-default-translator)))

(defvar zotelo--cached-charsets nil)
(defun zotelo--get-charsets ()
  "Get charsets (character encoding) for export from running Zotero instance."
  (or zotelo--cached-charsets
      (let ((buf (get-buffer-create "*moz-command-output*"))
	    charsets)
	(moz-command (format zotelo--render-charsets-js
			     (process-get (zotelo--moz-process) 'moz-prompt))
		     buf)
	(with-current-buffer buf
	  (goto-char (point-min))
	  (zotelo--message (format "Charsets:\n %s"
				   (buffer-substring-no-properties (point-min) (min 500 (point-max)))))
	  (while (re-search-forward "^'\\(.+\\)' '\\(.*\\)'$" nil t)
	    (let ((label (match-string-no-properties 1))
		  (value (match-string-no-properties 2)))
	      (setq charsets (cons (list label value) charsets)))))
	(if (null charsets)
	    (error "No charsets found or error occured see *moz-command-output* buffer for clues.")
	  (setq zotelo--cached-charsets (nreverse charsets))))))

;;;###autoload
(defun zotelo-set-charset ()
  "Ask to choose from available character sets for exporting the bibliography.
This function sets the variable `zotelo-charset'."
  (interactive)
  (let ((charsets (mapcar (lambda (el) (car el))
                          (zotelo--get-charsets))))
    (setq-local zotelo-charset
                (completing-read "Choose Charset: " charsets))
    (message "Charset was set to %s" zotelo-charset)))

;;;###autoload
(defun zotelo-update-database (&optional check-zotero-change bibfile id)
  "Update the primary BibTeX database associated with the current buffer.
Primary database is the first file in \\bibliography{file1, file2,
...}, list. If you want to export into a different file use
`zotelo-update-database-secondary'.

When BIBFILE is supplied, use it instead of the file in
\\bibliography{...}. If ID is supplied, use it instead of the id
from file local variables. Through an error if zotero collection
has not been found by MozRepl"
  (interactive)
  (let ((bibfile (or bibfile
		     (car (zotelo--locate-bibliography-files))))
        (proc  (zotelo--moz-process))
        (id (or id (zotelo--get-local-collection-id)))
        (file-name (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))
        (translator (assoc zotelo-default-translator (zotelo--get-translators)))
        all-colls-p bib-last-change zotero-last-change)
    (unless translator
      (error "Cannot find %s in Zotero's translators" zotelo-default-translator))

    (unless bibfile
      ;; (setq file-name (concat file-name "."))
      (setq bibfile file-name)
      (message "Using '%s' filename for %s export." file-name zotelo-default-translator))

    (let ((extension (nth 2 translator)))
      (if (string-match (concat "\\." extension "$") bibfile)
          (setq bibfile (expand-file-name bibfile))
        (setq bibfile (concat (expand-file-name bibfile) "." extension))))
    (setq bib-last-change (nth 5 (file-attributes bibfile))) ;; nil if bibfile does not exist
    (setq bibfile (replace-regexp-in-string "\\\\" "\\\\"
					    (convert-standard-filename bibfile) nil 'literal))
    (unless (file-exists-p (file-name-directory bibfile))
      (error "Directory '%s' does not exist; create it first." (file-name-directory bibfile)))
    ;; Add cygwin support.
    ;; "C:\\foo\\test.bib" workes with javascript.
    ;; while "/foo/test.bib" "C:\cygwin\foo\test.bib" and "C:/cygwin/foo/test.bib" don't
    (when (eq system-type 'cygwin)
      (setq bibfile
            (replace-regexp-in-string
             "/" "\\\\\\\\" (substring
                             (shell-command-to-string (concat "cygpath -m '" bibfile "'")) 0 -1))))
    (when (and (called-interactively-p 'any) (null id))
      (zotelo-set-collection "Zotero collection is not set. Choose one: " 'no-update)
      (setq id (zotelo--get-local-collection-id)))
    
    (when check-zotero-change
      (set-time-zone-rule t)
      (with-current-buffer (moz-command (format zotelo--dateModified-js id))
        (goto-char (point-min))
        (when (re-search-forward ":MozOK:" nil t) ;; ingore the error it is  cought latter
          (setq zotero-last-change (date-to-time
                                    (buffer-substring-no-properties (point) (point-max)))))))
    (when (and id
               (or (null check-zotero-change)
                   (null bib-last-change)
                   (time-less-p bib-last-change zotero-last-change)))

      (let* ((charset (or zotelo-charset
			  (cdr (assoc (car translator) zotelo-translator-charsets))
			  (cdr (assoc 'Default zotelo-translator-charsets))))
	     (charset (cadr (assoc charset (zotelo--get-charsets))))
	     (journal-abbr (if zotelo-use-journal-abbreviation
			       "true"
			     "false"))
	     (cstr (format zotelo--export-collection-js
			   bibfile id (cadr translator) charset journal-abbr))
	     (msg  (format "Executing command: \n\n (moz-command (format zotelo--export-collection-js '%s' %s %s %s %s))\n\n translated as:\n %s\n"
			   bibfile id (cadr translator) charset journal-abbr cstr))
	     (com (split-string cstr "//split" t))
	     (com1))
	(zotelo--message msg)
	(message "Updating '%s' ..." (file-name-nondirectory bibfile))
	(while (setq com1 (pop com))
	  (when com ;; append to all except the last one
	    (setq com1 (concat com1 "\":MozOK:\"")))
	  (with-current-buffer (moz-command com1)
	    (goto-char (point-min))
	    (unless (re-search-forward ":MozOK:" nil t)
	      (error "MozError: \n%s" (buffer-substring-no-properties (point) (point-max)))))))
      
      (let ((buf (get-file-buffer bibfile)))
	(when buf (with-current-buffer buf (revert-buffer 'no-auto 'no-conf))))
      (message "'%s' updated successfully (%s)" (file-name-nondirectory bibfile) zotelo-default-translator)
      id)))

(defun zotelo--locate-bibliography-files ()
  ;; Scan buffer for bibliography macro and return as a list.
  ;; Modeled after the corresponding reftex function
  (save-excursion
    (goto-char (point-max))
    (if (re-search-backward
         (concat
	  ;; "\\(\\`\\|[\n\r]\\)[^%]*\\\\\\("
          "\\(^\\)[^%\n\r]*\\\\\\("
          (mapconcat 'identity zotelo-bibliography-commands "\\|")
          "\\){[ \t]*\\([^}]+\\)") nil t)
        (split-string   (when (match-beginning 3)
                          (buffer-substring-no-properties (match-beginning 3) (match-end 3)))
                        "[ \t\n\r]*,[ \t\n\r]*"))))

;;;###autoload
(defun zotelo-set-collection (&optional prompt no-update no-file-local)
  "Ask for a zotero collection.
Ido interface is used by default. If you don't like it set
`zotelo-use-ido' to nil.  In `ido-mode' use \"C-s\" and \"C-r\"
for navigation. See ido-mode emacs wiki for many more details.

If no-update is t, don't update after setting the collecton. If
no-file-local is non-nill don't set file-local variable. Return
the properized collection name."
  (interactive)
  (let ((buf (get-buffer-create "*moz-command-output*"))
        colls)
    ;; set up the collection list
    (moz-command (format zotelo--render-collection-js
                         (process-get (zotelo--moz-process) 'moz-prompt)))
    (moz-command "zotelo_render_collection()" buf)
    (with-current-buffer buf
      (goto-char (point-min))
      (zotelo--message (format "Collections:\n %s" 
                               (buffer-substring-no-properties (point-min) (min 500 (point-max)))))
      (let (name id)
        (while (re-search-forward "^\\([0-9]+\\) /\\(.*\\)$" nil t)
          (setq id (match-string-no-properties 1)
                name (match-string-no-properties 2))
          (setq colls (cons (cons name id) colls)))))

    (if (null colls)
        (error "No collections found or error occured see *moz-command-output* buffer for clues.")
      ;; (setq colls (mapcar 'remove-text-properties colls))
      (let* ((colls (cons (cons "*ALL*" "0") (nreverse colls)))
             (name (completing-read (or prompt "Collection: ") (mapcar #'car colls)))
             (id (or (cdr (assoc name colls))
                     (error "Null id for collection '%s'. Please see *moz-command-output* for clues." name))))
        (unless no-file-local
          (save-excursion
            (add-file-local-variable 'zotero-collection (propertize id 'name name))
            (hack-local-variables))
          (unless no-update
            (zotelo-update-database)))
       name))))

(defun zotelo-mark-for-auto-update (&optional unmark)
  "Mark current file for auto-update.
If the file is marked for auto-update zotelo runs
`zotelo-update-database' on it whenever the zotero data-base is
updated.

File is marked by adding file local variable
'zotelo-auto-update'. To un-mark the file call this function with
an argument or just delete or set to nil the local variable at
the end of the file."
  (interactive "P")
  (save-excursion
    (if unmark
        (progn
          (delete-file-local-variable 'zotelo-auto-update)
          (setq file-local-variables-alist
                (assq-delete-all 'zotelo-auto-update file-local-variables-alist)))
      (add-file-local-variable 'zotelo-auto-update t)
      (hack-local-variables))))

;;;###autoload
(defun zotelo-reset ()
  "Reset zotelo."
  (interactive)
  (delete-process (zotelo--moz-process))
  (kill-buffer zotelo--moz-buffer)
  (message "Killed moz process"))

(defun zotelo-toggle-auto-update ()
  "Togles auto-updating in all buffers.
Note that once toggled in your firefox and MozRepl must be
started, otherwise you will start getting error screens. "
  (interactive)
  (setq zotelo--auto-update-is-on (not zotelo--auto-update-is-on)))

(defun zotelo--get-local-collection-id ()
  (or (and (boundp 'zotero-collection) zotero-collection)
      (cdr (assoc 'zotero-collection file-local-variables-alist))))


;;; MOZ UTILITIES

(defvar zotelo--moz-host "localhost")
(defvar zotelo--moz-port 4242)
(defvar zotelo--moz-buffer nil)
(defvar zotelo--startup-error-count 0)
(defvar zotelo--max-errors 10)

(defun zotelo--moz-process ()
  "Return inferior MozRepl process.  Start it if necessary."
  (or (if (buffer-live-p zotelo--moz-buffer)
          (get-buffer-process zotelo--moz-buffer))
      (progn
        (zotelo--moz-start-process)
        (zotelo--moz-process))))

(defun zotelo--moz-start-process ()
  "Start mozrepl process and connect to Firefox.
Note that you have to start the MozRepl server from Firefox."
  (interactive)
  (setq zotelo--moz-buffer (get-buffer-create "*zoteloMozRepl*"))
  (condition-case err
      (let ((proc (make-network-process :name "zoteloMozRepl" :buffer zotelo--moz-buffer
					:host zotelo--moz-host :service zotelo--moz-port
					:filter 'moz-ordinary-insertion-filter)))
        (sleep-for 0 100)
	(set-process-query-on-exit-flag proc nil)
        (with-current-buffer zotelo--moz-buffer
          (set-marker (process-mark proc) (point-max)))
        (setq zotelo--startup-error-count 0))
    (file-error
     (let ((buf (get-buffer-create "*MozRepl Error*")))
       (setq zotelo--startup-error-count (1+ zotelo--startup-error-count))
       (with-current-buffer buf
         (erase-buffer)
         (insert "Can't start MozRepl, the error message was:\n\n     "
                 (error-message-string err)
                 "\n"
                 "\nA possible reason is that you have not installed"
                 "\nthe MozRepl add-on to Firefox or that you have not"
                 "\nstarted it.  You start it from the menus in Firefox:"
                 "\n\n     Tools / MozRepl / Start"
                 "\n"
                 "\nSee ")
         (insert-text-button
          "MozRepl home page"
          'action (lambda (button)
                    (browse-url
                     "http://hyperstruct.net/projects/mozrepl")
                    )
          'face 'button)
         (insert
          " for more information."
          "\n"
          "\nMozRepl is also available directly from Firefox add-on"
          "\npages, but is updated less frequently there.\n\n"
          (format "zotelo Error Count: %s\n\n%s"
                  zotelo--startup-error-count
                  (if (not (and (>= zotelo--startup-error-count 10)
                                zotelo--auto-update-is-on))
                      "If zotelo auto-update is on, press \"C-c z t\" to turn it off."
                    (setq zotelo--auto-update-is-on nil)
                    (setq zotelo--startup-error-count 0)
                    "Too many errors. zotelo auto-update was turned off!\nUse [C-c z t] to switch it on.")))
         )
       (kill-buffer "*zoteloMozRepl*")
       (display-buffer buf t)
       (error "zotelo cannot start MozRepl")))))

(defun moz-ordinary-insertion-filter (proc string)
  "simple filter for command execution"
  (with-current-buffer (process-buffer proc)
    (let ((ready (string-match "\\(\\w+\\)> \\'" string))
	  moving)
      (when ready
	(process-put proc 'moz-prompt (match-string-no-properties 1 string)))
      (process-put proc 'busy (not ready))
      (setq moving (= (point) (process-mark proc)))
      (save-excursion
        ;; Insert the text, moving the process-marker.
        (goto-char (process-mark proc))
        (insert string)
        (set-marker (process-mark proc) (point)))
      (if moving (goto-char (process-mark proc))))))

(defvar moz-verbose nil
  "If t print informative statements.")

;;;###autoload
(defun moz-command (com &optional buf)
  "Send the moz-repl  process command COM and delete the output
from the MozRepl process buffer.  If an optional second argument BUF
exists, it must be a string or an existing buffer object. The
output is inserted in that buffer. BUF is erased before use.

New line is automatically appended.
"
  (if buf
      (setq buf (get-buffer-create buf))
    (setq buf (get-buffer-create "*moz-command-output*")))
  (let ((proc (zotelo--moz-process))
        oldpb oldpf oldpm)
    (save-excursion
      ;; (set-buffer sbuffer)
      (when (process-get proc 'busy)
        (process-send-string proc ";\n") ;; clean up unfinished
        (sleep-for 0 100)
        (when (process-get proc 'busy)
          (error
           "MozRepl process is not ready. Try latter or reset.")))
      (setq oldpf (process-filter proc))
      (setq oldpb (process-buffer proc))
      (setq oldpm (marker-position (process-mark proc)))
      ;; need the buffer-local values in result buffer "buf":
      (unwind-protect
	  (progn
	    (set-process-buffer proc buf)
	    (set-process-filter proc 'moz-ordinary-insertion-filter)
	    ;; Output is now going to BUF:
            (with-current-buffer buf
	      (erase-buffer)
	      (set-marker (process-mark proc) (point-min))
	      (process-put proc 'busy t)
	      (process-send-string proc (concat com "\n"))
	      (moz-wait-for-process proc)
	      ;;(delete-region (point-at-bol) (point-max))
	      )
	    (zotelo--message "Moz-command finished"))
	;; Restore old values for process filter
	(set-process-buffer proc oldpb)
	(set-process-filter proc oldpf)
	;; need oldpb here!!! otherwise it is not set for some reason
	(set-marker (process-mark proc) oldpm oldpb))))
  buf)


(defun moz-wait-for-process (proc &optional wait)
  "Wait for 'busy property of the process to become nil.
If SEC-PROMPT is non-nil return if secondary prompt is detected
regardless of whether primary prompt was detected or not.  If
WAIT is non-nil wait for WAIT seconds for process output before
the prompt check, default 0.01s. "
  ;; (unless (eq (process-status proc) 'run)
  ;;   (error "MozRepl process has died unexpectedly."))
  (setq wait (or wait 0.01))
  (save-excursion
    (while (or (accept-process-output proc wait)
	       (process-get proc 'busy)))))


;; (defun inferior-moz-track-proc-busy (comint-output)
;;   "track if process returned the '>' prompt and mark it as busy if not."
;;   (if (string-match "\\(\\w+\\)> \\'" comint-output)
;;       (process-put (get-buffer-process (current-buffer)) 'busy nil)
;;     (process-put (get-buffer-process (current-buffer)) 'busy t)))

;; (defun zotelo-insert-busy-hook ()
;;   "Add `inferior-moz-track-proc-busy' to comint-outbut-filter hook "
;;   (add-hook 'comint-output-filter-functions 'inferior-moz-track-proc-busy nil t)
;;   )

;; (add-hook 'inferior-moz-hook 'zotelo-insert-busy-hook)

(provide 'zotelo)
;;; zotelo.el ends here.