;;; alchemist-help.el --- Functionality for Elixir documentation lookup -*- 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:

;; Functionality for Elixir documentation lookup.

;;; Code:

(defgroup alchemist-help nil
  "Functionality for Elixir documentation lookup."
  :prefix "alchemist-help-"
  :group 'alchemist)

;; Variables

(defcustom alchemist-help-ansi-color-docs t
  "If t, `alchemist-help' will present ansi colored documentation."
  :type 'boolean
  :group 'alchemist-help)

(defcustom alchemist-help-buffer-name "*elixir help*"
  "Name of the Elixir help buffer."
  :type 'string
  :group 'alchemist-help)

(defvar alchemist-help-mix-run-command "mix run"
  "The shell command for 'mix run'.")

(defvar alchemist-help-search-history '()
  "Storage for the search history.")

(defvar alchemist-help-current-search-text '()
  "Stores the current search.")

;; Faces

(defface alchemist-help--key-face
  '((t (:inherit font-lock-variable-name-face :bold t :foreground "red")))
  "Fontface for the letter keys in the summary."
  :group 'alchemist-help)

(defun alchemist-help--load-ansi-color-setting ()
  (let ((config (gethash "ansi-color-docs" (alchemist-project-config))))
    (if config
        (intern config)
      alchemist-help-ansi-color-docs)))

(defun alchemist-help--exp-at-point ()
  "Return the expression under the cursor."
  (let (p1 p2)
    (save-excursion
      (skip-chars-backward "-_A-Za-z0-9.?!:")
      (setq p1 (point))
      (skip-chars-forward "-_A-Za-z0-9.?!:")
      (setq p2 (point))
      (buffer-substring-no-properties p1 p2))))

(defun alchemist-help--start-help-process (exp callback)
  (let* ((buffer (get-buffer-create "alchemist-help-buffer"))
         (command (alchemist-help--eval-string-command (alchemist-help--build-code-for-search exp)))
         (proc (start-process-shell-command "alchemist-help-proc" buffer command)))
    (set-process-sentinel proc (lambda (process signal)
                                 (when (equal signal "finished\n")
                                   (funcall callback (alchemist-utils--get-buffer-content (process-buffer process))))
                                 (alchemist-utils--erase-buffer (process-buffer process))))))

(defun alchemist-help--execute (search)
  (let ((last-directory default-directory)
        (last-buffer (current-buffer)))
    (alchemist-complete search (lambda (candidates)
                                 (if candidates
                                     (let* ((search (alchemist-complete--completing-prompt search candidates)))
                                       (setq alchemist-help-current-search-text search)
                                       (alchemist-help--start-help-process search (lambda (output)
                                                                                    (alchemist-help--initialize-buffer output)
                                                                                    (with-current-buffer last-buffer
                                                                                      (cd last-directory)))))
                                   (message "No documentation found for '%s'" search))))))

(defun alchemist-help--execute-without-complete (search)
  (setq alchemist-help-current-search-text search)
  (let ((last-directory default-directory)
        (last-buffer (current-buffer)))
    (alchemist-help--start-help-process search (lambda (output)
                                                 (alchemist-help--initialize-buffer output)
                                                 (with-current-buffer last-buffer
                                                   (cd last-directory))))))

(defun alchemist-help--build-code-for-search (string)
  (format "import IEx.Helpers

Application.put_env(:iex, :colors, [enabled: %s])

h(%s)" (if (alchemist-help--load-ansi-color-setting) "true" "false") string))

(defun alchemist-help--eval-string-command (string)
  (when (alchemist-project-p)
    (alchemist-project--establish-root-directory))
  (let* ((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 string)
                    (format "%s -e \"%s\"" alchemist-execute-command string))))
    command))

(defun alchemist-help--bad-search-output-p (string)
  (let ((match (or (string-match-p "No documentation for " string)
                   (string-match-p "Invalid arguments for h helper" string)
                   (string-match-p "** (TokenMissingError)" string)
                   (string-match-p "** (SyntaxError)" string)
                   (string-match-p "** (FunctionClauseError)" string)
                   (string-match-p "** (CompileError)" string)
                   (string-match-p "Could not load module" string))))
    (if match
        t
      nil)))

(defun alchemist-help--initialize-buffer (content)
  (pop-to-buffer alchemist-help-buffer-name)
  (setq buffer-undo-list nil)
  (let ((inhibit-read-only t)
        (buffer-undo-list t))
    (cond ((alchemist-help--bad-search-output-p content)
           (message (propertize
                     (format "No documentation for [ %s ] found." alchemist-help-current-search-text)
                     'face 'alchemist-help--key-face)))
          (t
           (erase-buffer)
           (insert content)
           (unless (memq 'alchemist-help-current-search-text alchemist-help-search-history)
             (add-to-list 'alchemist-help-search-history alchemist-help-current-search-text))))
    (delete-matching-lines "do not show this result in output" (point-min) (point-max))
    (delete-matching-lines "^Compiled lib\\/" (point-min) (point-max))
    (ansi-color-apply-on-region (point-min) (point-max))
    (toggle-read-only 1)
    (alchemist-help-minor-mode 1)))

(defun alchemist-help-minor-mode-key-binding-summary ()
  (interactive)
  (message
   (concat "[" (propertize "q" 'face 'alchemist-help--key-face)
           "]-quit ["
           (propertize "e" 'face 'alchemist-help--key-face)
           "]-search-at-point ["
           (propertize "m" 'face 'alchemist-help--key-face)
           "]-search-marked-region ["
           (propertize "s" 'face 'alchemist-help--key-face)
           "]-search ["
           (propertize "h" 'face 'alchemist-help--key-face)
           "]-history ["
           (propertize "?" 'face 'alchemist-help--key-face)
           "]-keys")))

(defun alchemist-help-search-at-point ()
  "Search through `alchemist-help' with the expression under the cursor."
  (interactive)
  (alchemist-help--execute (alchemist-help--exp-at-point)))

(defun alchemist-help-search-marked-region (begin end)
  "Run `alchemist-help' with the marked region.
Argument BEGIN where the mark starts.
Argument END where the mark ends."
  (interactive "r")
  (let ((region (filter-buffer-substring begin end)))
    (alchemist-help--execute region)))

(defun alchemist-help--elixir-modules-to-list (str)
  (let* ((modules (split-string str))
         (modules (mapcar (lambda (m)
                            (when (string-match-p "Elixir\\." m)
                              (replace-regexp-in-string "Elixir\\." "" m))) modules))
         (modules (delete nil modules))
         (modules (cl-sort modules 'string-lessp :key 'downcase))
         (modules (delete-dups modules)))
    modules)
  )

(defun alchemist-help--get-modules ()
  (let* ((elixir-code "
defmodule AlchemistModule do
  def get_modules do
    modules = Enum.map(:code.all_loaded, fn({m, _}) -> Atom.to_string(m) end)

    if :code.get_mode() === :interactive do
      modules ++ get_modules_from_applications()
    else
      modules
    end
  end

  defp get_modules_from_applications do
    for {app, _, _} <- :application.loaded_applications,
        {_, modules} = :application.get_key(app, :modules),
             module <- modules,
             has_doc = Code.get_docs(module, :moduledoc), elem(has_doc, 1) do
      Atom.to_string(module)
    end
  end
end

AlchemistModule.get_modules |> Enum.map &IO.puts/1
")
         (command (if (alchemist-project-p)
                      (format "%s -e \"%s\"" alchemist-help-mix-run-command elixir-code)
                    (format "%s -e \"%s\"" alchemist-execute-command elixir-code))))
    (when (alchemist-project-p)

      (alchemist-project--establish-root-directory))
    (alchemist-help--elixir-modules-to-list (shell-command-to-string command))))

(define-minor-mode alchemist-help-minor-mode
  "Minor mode for displaying elixir help."
  :group 'alchemist-help
  :keymap '(("q" . quit-window)
            ("e" . alchemist-help-search-at-point)
            ("m" . alchemist-help-search-marked-region)
            ("s" . alchemist-help)
            ("h" . alchemist-help-history)
            ("?" . alchemist-help-minor-mode-key-binding-summary)))

(defun alchemist-help (search)
  "Load Elixir documentation for SEARCH."
  (interactive
   (list (completing-read
          "Elixir help: "
          (alchemist-help--get-modules)
          nil
          nil
          nil)))
  (alchemist-help--execute (if (string-match-p "\\.$" search)
                               search
                             (concat search "."))))

(defun alchemist-help-history (search)
  "Load Elixir from the documentation history for SEARCH."
  (interactive
   (list
    (completing-read "Elixir help history: " alchemist-help-search-history nil nil "")))
  (alchemist-help--execute-without-complete search))

(provide 'alchemist-help)

;;; alchemist-help.el ends here
