;;; alchemist-complete.el --- Complete functionality for Elixir source code -*- lexical-binding: t -*-

;; Copyright © 2014-2015 Samuel Tonini

;; Author: Samuel Tonini <tonini.samuel@gmail.com

;; 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 of the License, 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. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Complete functionality for Elixir and Erlang source code.

;;; Code:

(defgroup alchemist-complete nil
  "Complete functionality for Elixir source code."
  :prefix "alchemist-complete-"
  :group 'alchemist)

(defvar alchemist-complete-debug-mode t)

(defun alchemist-complete-debug-mode ()
  "Enables the debug mode for completion if `alchemist-complete-debug-mode'
is `nil', otherwise it disable it."
  (interactive)
  (setq alchemist-complete-debug-mode (not alchemist-complete-debug-mode))
  (let ((state (if alchemist-complete-debug-mode
                   "ENABLED"
                 "DISABLED")))
    (message "Alchemist complete debug mode is: %s" state)))

(defun alchemist-complete--concat-prefix-with-functions (prefix functions &optional add-prefix)
  (let* ((prefix (mapconcat 'concat (butlast (split-string prefix "\\.") 1) "."))
         (candidates (mapcar (lambda (c) (concat prefix "." c)) (cdr functions))))
    (if add-prefix
        (push prefix candidates)
      candidates)))

(defun alchemist-complete--add-prefix-to-function (prefix function)
  (let* ((prefix (mapconcat 'concat (butlast (split-string prefix "\\.") 1) "."))
         (candidate (concat prefix "." function)))
    candidate))

(defun alchemist-complete--build-candidates (a-list)
  (let* ((search-term (car a-list))
         (candidates (mapcar (lambda (f)
                               (let* ((candidate f)
                                      (meta (if (string-match-p "^.+/" f)
                                                (replace-regexp-in-string "^.+/" "/" f)
                                              "")))
                                 (cond
                                  ((and (string-match-p "^:" search-term)
                                        (not (string-match-p "\\.$" search-term)))
                                   (propertize (concat ":" candidate)))
                                  ((string-match-p "\\." search-term)
                                   (propertize (alchemist-complete--add-prefix-to-function search-term
                                                                                           (replace-regexp-in-string "/[0-9]$" "" candidate)) 'meta meta))
                                  (t (propertize (replace-regexp-in-string "/[0-9]$" "" candidate) 'meta meta)))))
                             (cdr a-list))))
    candidates))

(defun alchemist-complete--build-help-candidates (a-list)
  (let* ((search-term (car a-list))
         (candidates (cond ((> (alchemist-utils--count-char-in-str "\\." search-term) 1)
                            (let ((search (if (string-match-p "\\.[a-z0-9_\?!]+$" search-term)
                                              (list (replace-regexp-in-string "\\.[a-z0-9_\?!]+$" "" search-term))
                                            (list (replace-regexp-in-string "\\.$" "" search-term))))
                                  (candidates (mapcar (lambda (c)
                                                        (if (string-match-p "\\.[a-z0-9_\?!]+$" search-term)
                                                            (concat (replace-regexp-in-string "\\.[a-z0-9_\?!]+$" "." search-term) c)
                                                          (concat search-term c)))
                                                      (cdr a-list))))
                              (append search candidates)))
                            ((string-match-p "\\.$" search-term)
                            (alchemist-complete--concat-prefix-with-functions search-term a-list t))
                           ((string-match-p "\\.[a-z0-9_\?!]+$" search-term)
                            (alchemist-complete--concat-prefix-with-functions search-term a-list))
                           (t
                            a-list))))
    (delete-dups candidates)))

(defun alchemist-complete--output-to-list (output)
  (let* ((output (replace-regexp-in-string "^cmp:" "" output))
         (output (split-string output))
         (output (delete nil output)))
    output)
  )

(defun alchemist-complete--clear-buffer (buffer)
  "Clears the BUFFER from not used lines."
  (with-current-buffer buffer
    (delete-non-matching-lines "^cmp:" (point-min) (point-max))))

(defun alchemist-complete--elixir-complete-code (exp)
  (format "
defmodule Alchemist do
  def expand(exp) do
    {status, result, list } = IEx.Autocomplete.expand(Enum.reverse(exp))

    case { status, result, list } do
      { :no, _, _ }  -> ''
      { :yes, [], _ } -> List.insert_at(list, 0, exp)
      { :yes, _,  _ } -> expand(exp ++ result)
    end
  end
end

Alchemist.expand('%s') |> Enum.map fn (f) -> IO.puts('cmp:' ++ f) end
" exp))

(defun alchemist-complete--command (exp)
  (let* ((elixir-code (alchemist-complete--elixir-complete-code exp))
         (compile-option (if (and (alchemist-project-p)
                                  (alchemist-project--load-compile-when-needed-setting))
                             ""
                           "--no-compile"))
         (command (if (alchemist-project-p)
                      (format "%s %s -e \"%s\"" alchemist-help-mix-run-command compile-option elixir-code)
                    (format "%s -e \"%s\"" alchemist-execute-command elixir-code)))
         )
    (when (alchemist-project-p)
      (alchemist-project--establish-root-directory))
    command))

(defun alchemist-complete--sentinel (proc callback &optional format-function)
  (set-process-sentinel proc (lambda (process signal)
                               (cond ((equal signal "finished\n")
                                      (alchemist-complete--clear-buffer (process-buffer process))
                                      (let* ((candidates (alchemist-complete--output-to-list
                                                          (alchemist--utils-clear-ansi-sequences
                                                           (alchemist-utils--get-buffer-content (process-buffer process)))))
                                             (candidates (if format-function
                                                             (funcall format-function candidates)
                                                           candidates)))
                                        (funcall callback candidates)))
                                     (t
                                      (when alchemist-complete-debug-mode
                                        (alchemist-complete--debug-message (alchemist-utils--get-buffer-content (process-buffer process))))
                                      (funcall callback '())))
                               (alchemist-utils--erase-buffer (process-buffer process)))))

(defun alchemist-complete--debug-message (content)
  (alchemist-message (format "== ALCHEMIST COMPLETION FAILED ==\n== OUTPUT BEGIN:\n%s== OUTPUT END:"
                             content)))

(defun alchemist-complete--completing-prompt (initial completing-collection)
  (let* ((completing-collection (alchemist-complete--build-help-candidates completing-collection)))
    (cond ((equal (length completing-collection) 1)
           (car completing-collection))
          (completing-collection
           (completing-read
            "Elixir help: "
            completing-collection
            nil
            nil
            (replace-regexp-in-string "\\.$" "" initial)))
          (t initial))))

(defun alchemist-complete (exp callback)
  (let* ((buffer (get-buffer-create "alchemist-complete-buffer"))
         (command (alchemist-complete--command exp))
         (proc (start-process-shell-command "alchemist-complete-proc" buffer command)))
    (alchemist-complete--sentinel proc callback)))

(defun alchemist-complete-candidates (exp callback)
  (let* ((buffer (get-buffer-create "alchemist-complete-buffer"))
         (command (alchemist-complete--command exp))
         (proc (start-process-shell-command "alchemist-complete-proc" buffer command)))
    (alchemist-complete--sentinel proc callback #'alchemist-complete--build-candidates)))

(provide 'alchemist-complete)

;;; alchemist-complete.el ends here
