;;
;; GNU-lisp functions for editing SM macro files
;;
;; The following should be put in your .emacs file to automatically
;; load these functions when editing files *.m or *.sav
;;
;;(setq auto-mode-alist
;;      (cons (cons "\\.m$" 'SM-mode)
;;	    (cons (cons "\\.sav$" 'SM-mode) auto-mode-alist)))
;;(autoload 'SM-mode "~lupton/sm/sm.el" nil t)
;; 
(defvar SM-indent 3 "*Indentation for blocks.")
(defvar SM-indent-continue 4 "*Extra indentation for continuation lines.")
(defvar SM-comment-column 40 "*Comments start in this column")
(defvar SM-tab-always-indent nil
  "*Non-nil means TAB in SM mode should always reindent the current line,
regardless of where in the line point is when the TAB command is used.")

(defvar SM-mode-syntax-table nil
  "Syntax table in use in SM-mode buffers.")

(if SM-mode-syntax-table
    ()
  (setq SM-mode-syntax-table (make-syntax-table))
  (modify-syntax-entry ?_ "w" SM-mode-syntax-table)
  (modify-syntax-entry ?. "w" SM-mode-syntax-table)
  (modify-syntax-entry ?+ "." SM-mode-syntax-table)
  (modify-syntax-entry ?- "." SM-mode-syntax-table)
  (modify-syntax-entry ?* "." SM-mode-syntax-table)
  (modify-syntax-entry ?/ "." SM-mode-syntax-table)
  (modify-syntax-entry ?% "." SM-mode-syntax-table)
  (modify-syntax-entry ?\" "\"" SM-mode-syntax-table)
  (modify-syntax-entry ?\\ "/" SM-mode-syntax-table)
  (modify-syntax-entry ?{ "(}" SM-mode-syntax-table)
  (modify-syntax-entry ?} "){" SM-mode-syntax-table)
  (modify-syntax-entry ?# "<" SM-mode-syntax-table)
  (modify-syntax-entry ?\n ">" SM-mode-syntax-table))

(defvar SM-mode-map () 
  "Keymap used in SM mode.")

(if SM-mode-map
    ()
  (setq SM-mode-map (make-sparse-keymap))
  (define-key SM-mode-map "\t" 'SM-tab)
  (define-key SM-mode-map "\C-m" 'SM-newline-and-indent)
  (define-key SM-mode-map "}" 'SM-insert-and-indent)
  (define-key SM-mode-map "\e\C-a" 'beginning-of-SM-macro)
  (define-key SM-mode-map "\e\C-e" 'end-of-SM-macro)
  (define-key SM-mode-map "\e\C-h" 'mark-SM-macro)
  (define-key SM-mode-map "\e\C-m" 'SM-indent-macro)
  (define-key SM-mode-map "\e\t" 'SM-indent-line)
  (define-key SM-mode-map "\e;" 'SM-add-comment)

(defun SM-mode () "Major mode for editing SM code.
Lines starting anywhere but the left margin are indented by 2 tabs,
then all further indentation is done with spaces.

   Macro declarations are followed by a tab, then the number of args,
then another tab.

Variables controlling indentation style and extra features:

 SM-indent                                (default: 3)
    Indentation for blocks.
 SM-indent-continue                       (default: 4)
    Extra indentation for continuation lines.
 SM-comment-column                        (default: 40)
    Starting column for comments following code.
 SM-tab-always-indent                     (default: nil)
    Non-nil means TAB in SM mode should always reindent the current line,
    regardless of where in the line point is when the TAB command is used.

Turning on SM mode calls the value of the variable SM-mode-hook 
with no args, if it is non-nil.
\\{SM-mode-map}"
  (interactive)
  (kill-all-local-variables)
  (set-syntax-table SM-mode-syntax-table)
  (make-local-variable 'indent-line-function)
  (setq indent-line-function 'SM-indent-line)
  (setq indent-tabs-mode nil)
  (make-local-variable 'comment-indent-hook)
  (setq comment-indent-hook nil)
  (make-local-variable 'comment-start-skip)
  (setq comment-start-skip "[^\\\\]#+[ \t]*")
  (make-local-variable 'comment-start)
  (setq comment-start "# ")
  (setq comment-column SM-comment-column)
  (make-local-variable 'require-final-newline)
  (setq require-final-newline t)
  (use-local-map SM-mode-map)
  (setq mode-name "SM")
  (setq major-mode 'SM-mode)
  (run-hooks 'SM-mode-hook))

(defun SM-comment-hook ()
  (save-excursion
    (skip-chars-backward " \t")
    (max (+ 1 (current-column))
	 comment-column)
))

(defun beginning-of-SM-macro (num)
  "Moves point to the beginning of the current SM macro."
  (interactive "p")
  (while (not (= num 0))
    (setq num (- num 1))
    (re-search-backward "^[^ \t\n]" nil 'move)))

(defun end-of-SM-macro (num)
  "Moves point to the end of the current SM macro."
  (interactive "p")
  (while (not (= num 0))
    (setq num (- num 1))
    (end-of-line)
    (re-search-forward "^[^ \t\n]" nil 'move)
    (beginning-of-line)))

(defun mark-SM-macro ()
  "Put mark at end of SM macro, point at beginning. Marks are pushed."
  (interactive)
  (end-of-SM-macro 1)
  (push-mark (point))
  (beginning-of-SM-macro 1)
  (buffer-substring (point)
		  (save-excursion (skip-chars-forward "^ \t\n") (point))))

(defun SM-indent-line ()
  "Indent current SM line based on its contents and on previous lines.
Deal with first lines of macros correctly."
  (interactive)
  (let ((cfi (calculate-SM-indent)))
    (save-excursion (SM-indent-to-column cfi)
      (beginning-of-line)
      (if (looking-at "[^ \t\n]")	; macro declaration
	  (progn
	    (skip-chars-forward "^ \t")
	    (cond ((looking-at "[ \t]*$") (delete-horizontal-space))
		  ((looking-at "[ \t]*[^ \t0-9]") ; No arguments
		   (if (< (current-column) 8)
		       (if (not (looking-at "\t\t[^ \t]"))
			   (progn
			     (delete-horizontal-space)
			     (insert "\t\t")))
		       (if (not (looking-at "\t[^ \t]"))
			   (progn
			     (delete-horizontal-space)
			     (insert "\t")))))
		  (t			; Macro has arguments
		   (if (< (current-column) 8)
		       (if (not (looking-at "\t[0-9]+\t"))
			   (progn
			     (delete-horizontal-space)
			     (insert "\t")
			     (skip-chars-forward "0-9")
			     (delete-horizontal-space)
			     (insert "\t")))
		       (if (not (looking-at " [0-9]+\t"))
			   (progn
			     (delete-horizontal-space)
			     (insert " ")
			     (skip-chars-forward "0-9")
			     (delete-horizontal-space)
			     (insert "\t")))))))))
	    (if (< (current-column) cfi) (move-to-column cfi))))

(defun SM-newline-and-indent ()
  "Insert a newline, and indent. When splitting lines, preserve indentation"
  (interactive)
  (if (looking-at "$")
      (newline-and-indent)
    (progn (newline) (insert " ") (SM-indent-line))))

(defun SM-indent-macro ()
  "Properly indents the SM macro which contains point."
  (interactive)
  (save-excursion
    (mark-SM-macro)
    (let ((name (mark-SM-macro)))
      (message (format "Indenting macro %s..." name))
      (indent-region (point) (mark) nil)
      (message (format "Indenting macro %s... done." name))))
  (let ((col (calculate-SM-indent)))
    (if (< (current-column) col)
	(move-to-column col)))
  )

(defun calculate-SM-indent ()
  "Calculates the SM indent column based on previous lines."
  (let (icol)
    (save-excursion
      (if (SM-previous-statement)
	  (setq icol 16)		; minimum indentation (2 tabs)
	(progn
	  (if (= (point) (point-min))
	      (setq icol 16)
	    (setq icol (SM-current-line-indentation))))))
    (save-excursion
      (beginning-of-line)
      (cond
       ((looking-at "[^ \t\n]") (setq icol 0))
       ((save-excursion
	  (forward-line -1)
	  (if (looking-at ".*\\\\$") (setq icol (+ icol SM-indent-continue)))))
       ((save-excursion
	  (skip-paired-braces)
	(looking-at "[^#\n]*}")) (setq icol (- icol SM-indent)))
      )
      (if (looking-at "[ \t\n]")
	  (progn
	    (previous-line 1)
	    (skip-paired-braces)
	    (if (looking-at "[^#\n]*{")
		(setq icol (+ icol SM-indent))))))
      (max (if (= icol 0) 0 16) icol)))

(defun skip-paired-braces () "Skip pairs of braces {...}"
  (interactive)
  (while
      (re-search-forward "{[^#}]*}" (save-excursion (end-of-line) (point)) t)
    (goto-char (match-end 0))))

(defun SM-current-line-indentation ()
  "Indentation of current line."
  (save-excursion
    (beginning-of-line)
    ;; Move past whitespace.
    (skip-chars-forward " \t")
    (current-column)))

(defun SM-indent-to-column (col)
  "Indents current line with up to 2 tabs then spaces to column COL."
  (save-excursion
    (beginning-of-line)
    (if (not (and (progn (skip-chars-forward "\t ") (= (current-column) col))
	      (looking-at "\t\t")))
	(progn
	  (beginning-of-line)
	  (delete-horizontal-space)
	  (SM-indent-to col)))
	  (skip-chars-forward "\t ")
      ;; Indent any comment following code on the same line
	  (if (and
	       (not (looking-at comment-start))
;	       (save-excursion
;		 (beginning-of-line) (looking-at "[ \t\n]"))
	       (re-search-forward comment-start-skip
				  (save-excursion (end-of-line) (point)) t))
	      (progn (goto-char (match-beginning 0))
		     (if (not (= (+ 1 (current-column)) (SM-comment-hook)))
			 (progn
			   (delete-horizontal-space)
			   (SM-indent-to (SM-comment-hook))))))))

(defun SM-indent-to (arg)
  "Like indent-to, but use tabs for first 16 columns"
   (interactive)
   (indent-to arg)
   (save-excursion
     (beginning-of-line)
     (if (looking-at "                ") ; 16 spaces
         (progn (delete-char 16) (insert "\t\t")))))

(defun SM-previous-statement ()
  "Moves point to beginning of the previous SM statement.
Returns 'first-statement if that statement is the first
non-comment SM statement in the file, and nil otherwise.
Skip to first line of multiple line-statements (ending in \\)."
  (interactive)
  (cond ((or
	  (not (= (forward-line -1) 0))
	  (looking-at "[^ \t\n]"))
	      'first-statement)
	(t
	 (beginning-of-line)
	 (while (and (= (forward-line -1) 0)
		     (or (looking-at "[ \t]*$") (looking-at ".*\\\\$"))
		     ))
	 (forward-line 1)
	 nil
	 )))

(defun SM-add-comment ()
  (interactive)
  (beginning-of-line)
  (if (re-search-forward comment-start-skip
		     (save-excursion (end-of-line) (point)) t)
      (progn (if (not (=
	    (progn (goto-char (+ 1 (match-beginning 0)))
		   (current-column)) (SM-comment-hook)))
	  (progn
	    (delete-horizontal-space)
	    (SM-indent-to (SM-comment-hook))))
	     (if (looking-at "#* ")	; move to start of text
		 (skip-chars-forward "# ")
	       (progn (end-of-line) (insert " "))))
    (progn
      (end-of-line)
      (delete-horizontal-space)
      (SM-indent-to (SM-comment-hook))
      (insert comment-start))))

(defun SM-insert-and-indent ()
  "Insert a key, and indent the line"
  (interactive)
  (insert last-command-char)
  (SM-indent-line)
  (blink-matching-open))

(defun SM-tab (a)
  "With a prefix argument simply insert a tab (or spaces as appropriate).
If SM-tab-always-indent is nil indent the current line if dot is to the
left of any text, otherwise insert an (SM) tab. If SM-tab-always-indent
is non-nil, simply reindent line"
  (interactive "p")
;  (debug)
  (if (not (= 1 a))			; insert an (SM) tab
      (SM-insert-tab)
    (if SM-tab-always-indent
	(SM-indent-line)
      (let ((before-all-text
	     (save-excursion (skip-chars-backward " \t") (looking-at "^"))))
	(if before-all-text
	    (SM-indent-line)
	  (SM-insert-tab))))))
  
(defun SM-insert-tab ()
  "insert spaces to the next tab stop, except at the start of a line where
up to two real tabs are allowed"
  (interactive)
  (if (or (looking-at "^") (save-excursion (backward-char) (looking-at "^")))
	  (insert ?\C-i) (insert-tab)))
)