;;; editorconfig.el --- EditorConfig Emacs extension

;; Copyright (C) 2011-2014 EditorConfig Team

;; Author: EditorConfig Team <editorconfig@googlegroups.com>
;; Version: 0.4
;; URL: http://github.com/editorconfig/editorconfig-emacs#readme

;; See
;; http://github.com/editorconfig/editorconfig-emacs/graphs/contributors
;; or the CONTRIBUTORS file for the list of contributors.

;; Redistribution and use in source and binary forms, with or without
;; modification, are permitted provided that the following conditions are met:
;;
;; 1. Redistributions of source code must retain the above copyright notice,
;;    this list of conditions and the following disclaimer.
;; 2. Redistributions in binary form must reproduce the above copyright notice,
;;    this list of conditions and the following disclaimer in the documentation
;;    and/or other materials provided with the distribution.

;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
;; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;; ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
;; LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
;; CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
;; POSSIBILITY OF SUCH DAMAGE.

;;; Commentary:

;; EditorConfig helps developers define and maintain consistent
;; coding styles between different editors and IDEs.

;; The EditorConfig project consists of a file format for defining
;; coding styles and a collection of text editor plugins that enable
;; editors to read the file format and adhere to defined styles.
;; EditorConfig files are easily readable and they work nicely with
;; version control systems.

;;; Code:

(defcustom edconf-exec-path
  "editorconfig"
  "EditorConfig command"
  :type 'string
  :group 'editorconfig)

(defcustom edconf-indentation-alist
  '((emacs-lisp-mode lisp-indent-offset)
    (lisp-mode lisp-indent-offset)
    (c-mode c-basic-offset)
    (c++-mode c-basic-offset)
    (objc-mode c-basic-offset)
    (java-mode c-basic-offset)
    (idl-mode c-basic-offset)
    (pike-mode c-basic-offset)
    (awk-mode c-basic-offset)
    (cmake-mode cmake-tab-width)
    (coffee-mode coffee-tab-width)
    (cperl-mode cperl-indent-level)
    (css-mode css-indent-offset)
    (haskell-mode haskell-indent-spaces
                  haskell-indent-offset
                  shm-indent-spaces)
    (js-mode js-indent-level)
    (json-mode js-indent-level)
    (js2-mode js2-basic-offset)
    (js3-mode js3-indent-level)
    (perl-mode perl-indent-level)
    (python-mode . edconf-set-indentation/python-mode)
    (ruby-mode ruby-indent-level)
    (sh-mode sh-basic-offset sh-indentation)
    (nxml-mode nxml-child-indent (nxml-attribute-indent . 2))
    (sgml-mode sgml-basic-offset)
    (livescript-mode livescript-tab-width)
    (mustache-mode mustache-basic-offset)
    (scala-mode scala-indent:step)
    (groovy-mode c-basic-offset)
    (latex-mode . edconf-set-indentation/latex-mode)
    (web-mode (web-mode-indent-style . (lambda (size) 2))
              web-mode-markup-indent-offset
              web-mode-css-indent-offset
              web-mode-code-indent-offset
              web-mode-script-padding
              web-mode-style-padding))
  "Alist of indentation setting methods by modes.

Each element looks like (MODE . FUNCTION) or (MODE . INDENT-SPEC-LIST).

If FUNCTION is provided, it will be called when setting the indentation.  The
indent size will be passed.

If INDENT-SPEC-LIST is provided, each element of it must have one of the
following forms:

 1. VARIABLE

    It means (VARIABLE . 1).

 2. (VARIABLE . SPEC)

    Setting VARIABLE according to the type of SPEC:

      - Integer

        The value is (* SPEC INDENT-SIZE);

      - Function

        The value is (funcall SPEC INDENT-SIZE);

      - Any other type.

        The value is SPEC.

NOTE: Only the **buffer local** value of VARIABLE will be set."
  :type '(alist :key-type symbol :value-type sexp)
  :risky t
  :group 'editorconfig)

(defun edconf-string-integer-p (string)
  "Whether a string representing integer"
  (if (stringp string)
    (string-match-p "\\`[0-9]+\\'" string)
    nil))

(defun edconf-set-indentation/python-mode (size)
  (set (make-local-variable (if (or (> emacs-major-version 24)
                                    (and (= emacs-major-version 24)
                                         (>= emacs-minor-version 3)))
                                'python-indent-offset
                              'python-indent))
       size)
  ;; For https://launchpad.net/python-mode
  (when (boundp 'py-indent-offset)
    (set (make-local-variable 'py-indent-offset) size)))

(defun edconf-set-indentation/latex-mode (size)
  (set (make-local-variable 'tex-indent-basic) size)
  (set (make-local-variable 'tex-indent-item) size)
  (set (make-local-variable 'tex-indent-arg) (* 2 size))
  ;; For AUCTeX
  (when (boundp 'TeX-brace-indent-level)
    (set (make-local-variable 'TeX-brace-indent-level) size))
  (when (boundp 'LaTeX-indent-level)
    (set (make-local-variable 'LaTeX-indent-level) size))
  (when (boundp 'LaTeX-item-indent)
    (set (make-local-variable 'LaTeX-item-indent) (- size))))

(defun edconf-set-indentation (style &optional size tab_width)
  "Set indentation type from given style and size"
  (make-local-variable 'indent-tabs-mode)
  (make-local-variable 'tab-width)
  (if (edconf-string-integer-p size)
    (setq size (string-to-number size))
    (when (not (equal size "tab")) (setq size nil)))
  (setq tab-width (cond (tab_width (string-to-number tab_width))
                        ((numberp size) size)
                        (t tab-width)))
  (when (equal size "tab")
    (setq size tab-width))
  (cond ((equal style "space")
         (setq indent-tabs-mode nil))
        ((equal style "tab")
         (setq indent-tabs-mode t)))
  (when size
    (let ((parent major-mode)
          entry)
      ;; Find the closet parent mode of `major-mode' in
      ;; `edconf-indentation-alist'.
      (while (and (not (setq entry (assoc parent edconf-indentation-alist)))
                  (setq parent (get parent 'derived-mode-parent))))
      (when entry
        (let ((fn-or-list (cdr entry)))
          (cond ((functionp fn-or-list) (funcall fn-or-list size))
                ((listp fn-or-list)
                 (dolist (elem fn-or-list)
                   (cond ((symbolp elem) (set (make-local-variable elem) size))
                         ((consp elem)
                          (let ((spec (cdr elem)))
                            (set (make-local-variable (car elem))
                                 (cond ((functionp spec) (funcall spec size))
                                       ((integerp spec) (* spec size))
                                       (t spec))))))))))))))

(defun edconf-set-line-ending (end-of-line)
  "Set line ending style to CR, LF, or CRLF"
  (set-buffer-file-coding-system
   (cond
    ((equal end-of-line "lf") 'undecided-unix)
    ((equal end-of-line "cr") 'undecided-mac)
    ((equal end-of-line "crlf") 'undecided-dos)
    (t 'undecided))
   nil t))

(defun edconf-set-trailing-nl (final-newline)
  (cond
   ((equal final-newline "true")
    ;; keep prefs around how/when the nl is added, if set - otherwise add on save
    (set      (make-local-variable 'require-final-newline)      (or require-final-newline t))
    (set      (make-local-variable 'mode-require-final-newline) (or mode-require-final-newline t)))
   ((equal final-newline "false")
    ;; FIXME: Add functionality for actually REMOVING any trailing newlines here!
    ;;        (rather than just making sure we don't automagically ADD a new one)
    (set      (make-local-variable 'require-final-newline) nil)
    (set      (make-local-variable 'mode-require-final-newline) nil))))

(defun edconf-set-trailing-ws (trim-trailing-ws)
  "set up trimming of trailing whitespace at end of lines"
  (make-local-variable 'write-file-functions) ;; just current buffer
  (when (equal trim-trailing-ws "true")
    ;; when true we push delete-trailing-whitespace (emacs > 21)
    ;; to write-file-functions
    (add-to-list
     'write-file-functions
     'delete-trailing-whitespace))
  (when (equal trim-trailing-ws "false")
    ;; when false we remove every delete-trailing-whitespace
    ;; from write-file-functions
    (setq
     write-file-functions
     (delete
      'delete-trailing-whitespace
      write-file-functions))))

(defun edconf-set-line-length (length)
  "set the max line length (fill-column)"
  (when (edconf-string-integer-p length)
    (set-fill-column (string-to-number length))))

(defun edconf-get-properties ()
  "Call EditorConfig core and return output"
  (let ((oldbuf (current-buffer)))
    (call-process edconf-exec-path nil "ecbuffer" nil (buffer-file-name oldbuf))
    (set-buffer (get-buffer "ecbuffer"))
    (let (props-string)
      (setq props-string (buffer-string))
      (set-buffer oldbuf)
      (kill-buffer (get-buffer "ecbuffer"))
      props-string)))

(defun edconf-parse-properties (props-string)
  "Create properties hash table from string of properties"
  (let (props-list properties)
    (setq props-list (split-string props-string "\n")
          properties (make-hash-table :test 'equal))
    (dolist (prop props-list properties)
      (let ((key-val (split-string prop " *= *")))
        (when (> (length key-val) 1)
          (let ((key (intern (car key-val)))
                (val (mapconcat 'identity (cdr key-val) "")))
            (puthash key val properties)))))))

;;;###autoload
(defun edconf-find-file-hook ()
  (when (executable-find edconf-exec-path)
    (let ((props (edconf-parse-properties (edconf-get-properties))))
      (edconf-set-indentation (gethash 'indent_style props)
                              (gethash 'indent_size props)
                              (gethash 'tab_width props))
      (edconf-set-line-ending (gethash 'end_of_line props))
      (edconf-set-trailing-nl (gethash 'insert_final_newline props))
      (edconf-set-trailing-ws (gethash 'trim_trailing_whitespace props))
      (edconf-set-line-length (gethash 'max_line_length props)))))

;;;###autoload
(add-hook 'find-file-hook 'edconf-find-file-hook)

(provide 'editorconfig)

;;; editorconfig.el ends here
