;;; 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:

(require 'dash)
(require 'ansi-color)
(require 'alchemist-utils)
(require 'alchemist-project)
(require 'alchemist-server)
(require 'alchemist-scope)
(require 'alchemist-goto)

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

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

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

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

(defvar alchemist-help-filter-output nil)

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

(defun alchemist-help-lookup-doc (search)
  "Lookup Elixir documentation for SEARCH."
  (setq alchemist-help-current-search-text search)
  (setq alchemist-help-filter-output nil)
  (if (not (alchemist-utils-empty-string-p search))
      (alchemist-server-complete-candidates
       (alchemist-help--completion-server-arguments search)
       #'alchemist-help-complete-filter-output)
    (message "No documentation for [%s] found." search)))

(defun alchemist-help-no-doc-available-p (string)
  "Return non-nil if STRING contains Elixir no documentation message."
  (or (string-match-p "No documentation for" string)
      (string-match-p "Could not load module" string)
      (string-match-p "it does not have Elixir-style docs" string)
      (alchemist-utils-empty-string-p string)))

(defun alchemist-help-store-search-in-history ()
  "Store the last `alchemist-help-current-search-text' in `alchemist-help-search-history'."
  (unless (memq 'alchemist-help-current-search-text alchemist-help-search-history)
    (add-to-list 'alchemist-help-search-history alchemist-help-current-search-text)))

(defun alchemist-help-display-doc (content)
  "Initialize the `alchemist-help-buffer-name' and insert CONTENT."
  (let ((default-directory (alchemist-project-root-or-default-dir))
        (buffer (get-buffer-create alchemist-help-buffer-name)))
    (cond
     ((alchemist-help-no-doc-available-p content)
      (message (format "No documentation for [%s] found."
                       alchemist-help-current-search-text)))
     (t
      (alchemist-help-store-search-in-history)
      (with-current-buffer buffer
        (let ((inhibit-read-only t))
          (goto-char (point-min))
          (erase-buffer)
          (insert content)
          (goto-char (point-min))
          (ansi-color-apply-on-region (point-min) (point-max))
          (alchemist-help-minor-mode)))
      (pop-to-buffer buffer)))))

(defun alchemist-help--search-at-point ()
  "Search through `alchemist-help' with the expression under the cursor"
  (let* ((expr (alchemist-scope-expression)))
    (alchemist-help-lookup-doc (alchemist-help--prepare-search-expr expr))))

(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."
  (let ((expr (buffer-substring-no-properties begin end)))
    (alchemist-help-lookup-doc (alchemist-help--prepare-search-expr expr))))

(defun alchemist-help--prepare-search-expr (expr)
  (let* ((module (alchemist-scope-extract-module expr))
         (module (alchemist-scope-alias-full-path module))
         (module (if module module ""))
         (function (alchemist-scope-extract-function expr))
         (function (if function function ""))
         (expr (cond
                ((and (not (alchemist-utils-empty-string-p module))
                      (not (alchemist-utils-empty-string-p function)))
                 (format "%s.%s" module function))
                ((not (alchemist-utils-empty-string-p module))
                 module)
                (t
                 expr))))
    expr))

(defun alchemist-help--elixir-modules-to-list (str)
  (let* ((str (replace-regexp-in-string "^Elixir\\." "" str))
         (modules (split-string str))
         (modules (delete nil modules))
         (modules (cl-sort modules 'string-lessp :key 'downcase))
         (modules (-distinct modules)))
    modules))

(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 "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--server-arguments (args)
  (if (and (not (equal major-mode 'alchemist-iex-mode))
           (not (bound-and-true-p alchemist-help-minor-mode)))
      (let* ((modules (alchemist-utils-prepare-modules-for-elixir
                       (alchemist-scope-all-modules))))
        (format "{ \"%s\", [ context: Elixir, imports: %s, aliases: [] ] }" args modules))
    (format "{ \"%s\", [ context: Elixir, imports: [], aliases: [] ] }" args)))

(defun alchemist-help--completion-server-arguments (args)
  "Build informations about the current context."
  (if (and (not (equal major-mode 'alchemist-iex-mode))
           (not (bound-and-true-p alchemist-help-minor-mode)))
      (let* ((modules (alchemist-utils-prepare-modules-for-elixir
                       (alchemist-scope-all-modules)))
             (aliases (alchemist-utils-prepare-aliases-for-elixir
                       (alchemist-scope-aliases))))
        (format "{ \"%s\", [ context: Elixir, imports: %s, aliases: %s ] }" args modules aliases))
    (format "{ \"%s\", [ context: Elixir, imports: [], aliases: [] ] }" args)))

(defun alchemist-help-complete-filter-output (_process output)
  (with-local-quit
    (setq alchemist-help-filter-output (cons output alchemist-help-filter-output))
    (if (alchemist-server-contains-end-marker-p output)
        (let* ((string (alchemist-server-prepare-filter-output alchemist-help-filter-output))
               (candidates (alchemist-complete--output-to-list
                            (ansi-color-filter-apply string)))
               (candidates (if (= (length candidates) 2)
                               nil
                             candidates)))
          (setq alchemist-help-filter-output nil)
          (if candidates
              (let* ((search (alchemist-complete--completing-prompt alchemist-help-current-search-text candidates)))
                (setq alchemist-help-current-search-text search)
                (alchemist-server-help (alchemist-help--server-arguments search) #'alchemist-help-filter-output))
            (alchemist-server-help (alchemist-help--server-arguments alchemist-help-current-search-text) #'alchemist-help-filter-output))))))

(defun alchemist-help-filter-output (_process output)
  (setq alchemist-help-filter-output (cons output alchemist-help-filter-output))
  (if (alchemist-server-contains-end-marker-p output)
      (let ((string (alchemist-server-prepare-filter-output alchemist-help-filter-output)))
        (alchemist-help-display-doc string)
        (setq alchemist-help-current-search-text nil)
        (setq alchemist-help-filter-output nil))))

(defun alchemist-help-modules-filter (_process output)
  (with-local-quit
    (setq alchemist-help-filter-output (cons output alchemist-help-filter-output))
    (if (alchemist-server-contains-end-marker-p output)
        (let* ((output (alchemist-server-prepare-filter-output alchemist-help-filter-output))
               (modules (alchemist-help--elixir-modules-to-list output))
               (search (completing-read
                        "Elixir help: "
                        modules
                        nil
                        nil
                        nil))
               (module (alchemist-scope-extract-module search))
               (function (alchemist-scope-extract-function search))
               (search (cond
                        ((and module function)
                         search)
                        ((and module
                              (not (string-match-p "[\/0-9]+$" module)))
                         (concat module "."))
                        (t
                         search))))
          (alchemist-help-lookup-doc (alchemist-utils-remove-dot-at-the-end search))))))

;; Public functions

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

If the buffer local variable `mark-active' is non-nil,
the actively marked region will be used for passing to `alchemist-help'."
  (interactive)
  (if mark-active
      (alchemist-help--search-marked-region (region-beginning) (region-end))
      (alchemist-help--search-at-point)))

(defvar alchemist-help-minor-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "q") #'quit-window)
    (define-key map (kbd "e") #'alchemist-help-search-at-point)
    (define-key map (kbd "s") #'alchemist-help)
    (define-key map (kbd "h") #'alchemist-help-history)
    (define-key map (kbd "M-.") #'alchemist-goto-definition-at-point)
    (define-key map (kbd "?") #'alchemist-help-minor-mode-key-binding-summary)
    map)
  "Keymap for `alchemist-help-minor-mode'.")

(define-minor-mode alchemist-help-minor-mode
  "Minor mode for displaying elixir help."
  :group 'alchemist-help
  :keymap alchemist-help-minor-mode-map
  (cond (alchemist-help-minor-mode
         (setq buffer-read-only t))
        (t
         (setq buffer-read-only nil))))

(defun alchemist-help ()
  "Load Elixir documentation for SEARCH."
  (interactive)
  (setq alchemist-help-filter-output nil)
  (alchemist-server-info "{ :type, :modules }" #'alchemist-help-modules-filter))

(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-lookup-doc search))

(provide 'alchemist-help)

;;; alchemist-help.el ends here
