;;; project-settings.el --- Define settings for project directories
;;
;; Author: Mark Triggs <mst@dishevelled.net>
;;
;; This file 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.
;;
;; This file 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 GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;
;; Commentary:
;;
;; Here's an example of how I use this:
;;
;;   (define-project bingrab ("~/projects/bingrab")
;;     (setq unit-test-file-fn
;;           (lambda (filename)
;;             (if (string-match "/test_" filename)
;;                 filename
;;               (prefix-file-name filename "tests/test_"))))
;;     (setq unit-test-command
;;           (lambda ()
;;             (test-with-compile
;;              "cd ~/projects/bingrab/; make"))))
;;
;;; Code:
;;


(defvar project-list '()
  "A list of currently defined projects.  Each element is of the form
 (PROJECT-NAME MATCH-FN CODE).  See `define-project' for more information.")


(defmacro define-project (name match-element &rest settings)
  "Define a new project called NAME.
MATCH-ELEMENT is used to decide which project a file/directory belongs to, and
should either be a list of paths, or a function which returns non-nil if a
given file belongs to this project.
SETTINGS should be a body of code that will be executed in the buffer of the
file/directory."
  `(add-new-project ',name ',match-element
                    (lambda () ,@settings)))


(defvar project-applied-hook '()
  "A hook run after a project is applied to a file/directory.")

(defmacro undefine-project (name)
  "Remove the project named by NAME."
  `(delete-project ',name))


(defun delete-project (name)
  (setq project-list (remove* name project-list :test 'eq :key 'car)))


(defun add-new-project (name match fn)
  (delete-project name)
  (push (list name match fn)
        project-list))


(defun project-normalise-path (path)
  (replace-regexp-in-string "\/*$" "" (expand-file-name path)))


(defun find-path-projects (path)
  (let ((path (project-normalise-path path)))
    (remove-if-not (lambda (project)
                     (destructuring-bind (project-name match fn) project
                       (project-matches match path)))
                   project-list)))


(defun project-matches (match path)
  (if (and (listp match) (every #'stringp match))
      (some #'(lambda (project-path)
                (eql (string-match (project-normalise-path project-path)
                                   path)
                     0))
            match)
    (funcall match path)))


(defun apply-project-settings ()
  (let ((projects (find-path-projects (or (buffer-file-name)
                                          default-directory))))
    (dolist (project (reverse projects))
      (destructuring-bind (name match fn) project
        (funcall fn)))
    (when projects
      (run-hooks 'project-applied-hook))))


(defun prefix-file-name (filename prefix)
  (let ((dir (file-name-directory filename))
        (name (file-name-nondirectory filename)))
    (concat dir prefix name)))

(defun unprefix-file-name (filename prefix)
  (let ((dir (file-name-directory filename))
        (name (file-name-nondirectory filename)))
    (concat dir (replace-regexp-in-string (format "^%s" prefix) "" name))))

(add-hook 'find-file-hook 'apply-project-settings)
(add-hook 'dired-mode-hook 'apply-project-settings)


(provide 'project-settings)
;;; project-settings.el ends here
