;;; scala-mode-imenu.el - Major mode for editing scala -*- lexical-binding: t -*-
;;; Copyright (c) 2014 Heikki Vesalainen
;;; For information on the License, see the LICENSE file

;;; Code:

(require 'scala-mode-syntax)

(defcustom scala-imenu:should-flatten-index t
  "Controls whether or not the imenu index is flattened or hierarchical."
  :type 'boolean
  :safe #'booleanp
  :group 'scala)
(defcustom scala-imenu:build-imenu-candidate
  'scala-imenu:default-build-imenu-candidate
  "Controls whether or not the imenu index has definition type information."
  :type 'function
  :group 'scala)
(defcustom scala-imenu:cleanup-hooks nil
  "Functions that will be run after the construction of each imenu"
  :type 'hook
  :group 'scala)

(defun scala-imenu:flatten-list (incoming-list &optional predicate)
  (when (not predicate) (setq predicate 'listp))
  (cl-mapcan (lambda (x) (if (funcall predicate x)
			  (scala-imenu:flatten-list x predicate) (list x))) incoming-list))

(defun scala-imenu:flatten-imenu-index (index)
  (cl-mapcan (lambda (x) (if (listp (cdr x))
			  (scala-imenu:flatten-imenu-index (cdr x))
			(list x))) index))

(defun scala-imenu:create-imenu-index ()
  (let ((imenu-index (cl-mapcar 'scala-imenu:build-imenu-candidates
			     (scala-imenu:create-index))))
    (dolist (cleanup-hook scala-imenu:cleanup-hooks)
      (funcall cleanup-hook))
    (if scala-imenu:should-flatten-index
	(scala-imenu:flatten-imenu-index imenu-index)
      imenu-index)))

(defun scala-imenu:build-imenu-candidates (member-info &optional parents)
  (if (listp (car member-info))
      (let* ((current-member-info (car member-info))
	     (child-member-infos (cdr member-info))
	     (current-member-result
	      (scala-imenu:destructure-for-build-imenu-candidate
	       current-member-info parents))
	     (current-member-name (car current-member-result)))
	(if child-member-infos
	    (let ((current-member-members
		   (scala-imenu:build-child-members
		    (append parents `(,current-member-info))
		    (cdr member-info))))
	      `(,current-member-name .
	        ,(cons current-member-result current-member-members)))
	  current-member-result))
    (scala-imenu:destructure-for-build-imenu-candidate member-info parents)))

(defun scala-imenu:build-child-members (parents child-members)
  (cl-mapcar (lambda (child) (scala-imenu:build-imenu-candidates
			   child parents)) child-members))

(defun scala-imenu:destructure-for-build-imenu-candidate (member-info parents)
  (cl-destructuring-bind (member-name definition-type marker)
      member-info (funcall scala-imenu:build-imenu-candidate
			   member-name definition-type marker parents)))


(defun scala-imenu:default-build-imenu-candidate (member-name definition-type
							      marker parents)
  (let* ((all-names
	  (append (cl-mapcar (lambda (parent) (car parent)) parents)
		  `(,member-name)))
	 (member-string (mapconcat 'identity all-names ".")))
    `(,(format "(%s)%s" definition-type member-string) . ,marker)))

(defun scala-imenu:create-index ()
  (let ((class nil) (index nil))
    (goto-char (point-max))
    (while (setq class (scala-imenu:parse-nested-from-end))
      (setq index (cons class index)))
    index))

(defun scala-imenu:parse-nested-from-end ()
  (let ((last-point (point)) (class-name nil) (definition-type nil))
    (scala-syntax:beginning-of-definition)
    ;; We're done if scala-syntax:beginning-of-definition has no effect.
    (if (eq (point) last-point) nil
      (progn (looking-at scala-syntax:all-definition-re)
	     (setq class-name (match-string-no-properties 2))
	     (setq definition-type (match-string-no-properties 1)))
      `(,`(,class-name ,definition-type ,(point-marker)) .
	,(scala-imenu:nested-members)))))

(defun scala-imenu:parse-nested-from-beginning ()
  (scala-syntax:end-of-definition)
  (scala-imenu:parse-nested-from-end))

(defun scala-imenu:nested-members ()
  (let ((start-point (point)))
    (save-excursion
      (scala-syntax:end-of-definition)
      ;; This gets us inside of the class definition
      ;; It seems like there should be a better way
      ;; to do this.
      (backward-char)
      (reverse (scala-imenu:get-nested-members start-point)))))

(defvar scala-imenu:nested-definition-types '("class" "object" "trait"))

(defun scala-imenu:get-nested-members (parent-start-point)
  (scala-syntax:beginning-of-definition)
  (if (< parent-start-point (point))
      (cons (scala-imenu:get-member-info-at-point)
	    (scala-imenu:get-nested-members parent-start-point))
      nil))

(defun scala-imenu:get-member-info-at-point ()
  (looking-at scala-syntax:all-definition-re)
  (let* ((member-name (match-string-no-properties 2))
	 (definition-type (match-string-no-properties 1)))
    (if (member definition-type scala-imenu:nested-definition-types)
	(save-excursion (scala-imenu:parse-nested-from-beginning))
      `(,member-name ,definition-type ,(point-marker)))))


(provide 'scala-mode-imenu)
;;; scala-mode-imenu.el ends here -*- lexical-binding: t -*-
