;;; alchemist-scope.el --- Provides information about Elixir source code context   -*- lexical-binding: t -*-

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

;; Provides information about the Elixir source code context.

;;; Code:

(require 'dash)

(defgroup alchemist-scope nil
  "Provides information about the Elixir source code context."
  :prefix "alchemist-scope-"
  :group 'alchemist)

(defconst alchemist-scope-defmodule-regex "defmodule \\([A-Za-z\._]+\\)\s+"
  "The regex for matching Elixir defmodule macro.")

(defconst alchemist-scope-alias-regex
  "^\s+alias\s+\\([-:_A-Za-z0-9,\.\?!\]+\\)\\(\s*,\s*as:\s*\\)?\\([-_A-Za-z0-9,\.\?!\]+\\)?\n"
  "The regex for matching Elixir alias definitions.
   Example:
   alias Phoenix.Router.Resource, as: Special")

(defconst alchemist-scope-alias-regex-two
  "^\s+alias\s+\\([-:_A-Za-z0-9,\.\?!\]+\\)\.{\\([-:_A-Za-z0-9\s,\.\?!\]+\\)}\n"
  "The regex for matching Elixir alias definitions.
   Example:
   alias List.Chars.{Atom, Float}")

(defconst alchemist-scope-use-regex
  "^\s+use\s+\\([A-Za-z0-9\.]+\\)"
  "The regex for matching Elixir use definitions.")

(defconst alchemist-scope-import-regex
  "^\s+import\s+\\([A-Za-z0-9\.]+\\)"
  "The regex for matching Elixir import definitions.")

(defun alchemist-scope-inside-string-p ()
  "Return non-nil if `point' is inside a string or heredoc."
  (let* ((pos (point))
         (parse-info (syntax-ppss pos)))
    (or (and (nth 3 parse-info)
             (nth 8 parse-info))
        (and (looking-at "\"\"\"\\|'''\\|\"\\|\'")
             (match-beginning 0)))))

(defun alchemist-scope-inside-module-p ()
  "Return non-nil if `point' is currently inside a module."
  (save-excursion
    (end-of-line)
    (let ((found-flag-p nil)
          (found-p nil))
      (while (and (not found-flag-p)
                  (re-search-backward alchemist-scope-defmodule-regex nil t))
        (when (not (alchemist-scope-inside-string-p))
          (setq found-flag-p t)
          (setq found-p t)))
      found-p)))

(defun alchemist-scope-module ()
  "Return name from the current defmodule."
  (save-excursion
    (let ((found-flag-p nil)
          (module-name ""))
      (save-match-data
        (while (and (not found-flag-p)
                    (re-search-backward alchemist-scope-defmodule-regex nil t))
          (when (not (alchemist-scope-inside-string-p))
            (setq module-name (match-string 1))
            (setq found-flag-p t))
          (when (equal 1 (line-number-at-pos (point)))
            (setq found-flag-p t)))
        module-name))))

(defun alchemist-scope-aliases ()
  "Return aliases from the current module."
  (let* ((aliases '())
         (context (alchemist-scope-module)))
    (save-excursion
      (when (alchemist-scope-inside-module-p)
        (end-of-line)
        ;; alias definition like:
        ;;
        ;;   alias Phoenix.Router.Resource, as: Special
        (while (re-search-backward alchemist-scope-alias-regex nil t)
          (when (and
                 (not (alchemist-scope-inside-string-p))
                 (equal context (alchemist-scope-module)))
            (let* ((alias (match-string 1))
                   (as (if (match-string 3) (match-string 3) nil))
                   (as (if as as (car (last (split-string alias "\\."))))))
              (setq aliases (append aliases (list (list (alchemist-utils-remove-dot-at-the-end alias)
                                                        (alchemist-utils-remove-dot-at-the-end as))))))))
        ;; alias definition like:
        ;;
        ;;   alias List.Chars.{Atom, Float}
        (while (re-search-backward alchemist-scope-alias-regex-two nil t)
          (when (and
                 (not (alchemist-scope-inside-string-p))
                 (equal context (alchemist-scope-module)))
            (let* ((prefix (match-string 1))
                   (alias-collection (if (match-string 2) (split-string (match-string 2) ",") nil)))
              (-map (lambda (alias)
                      (let* ((alias (replace-regexp-in-string "\s+" "" alias))
                             (namespace (format "%s.%s" prefix alias)))
                        (setq aliases (append aliases (list (list (alchemist-utils-remove-dot-at-the-end namespace)
                                                                  (alchemist-utils-remove-dot-at-the-end alias)))))))
                    alias-collection))))))
    aliases))

(defun alchemist-scope--modules (regex)
  (let ((modules '())
        (context (alchemist-scope-module)))
    (save-excursion
      (when (not (alchemist-utils-empty-string-p context))
        (while (re-search-backward regex nil t)
          (when (and (match-string 1)
                     (not (alchemist-scope-inside-string-p))
                     (equal context (alchemist-scope-module)))
            (cl-pushnew (substring-no-properties (match-string 1)) modules))))
      modules)))

(defun alchemist-scope-use-modules ()
  "Return `use' introduced module names from the current module."
  (alchemist-scope--modules alchemist-scope-use-regex))

(defun alchemist-scope-import-modules ()
  "Return `import' introduced module names from the current module."
  (alchemist-scope--modules alchemist-scope-import-regex))

(defun alchemist-scope-all-modules ()
  "Return `use' and `import' introduced modules from the current module."
  (let ((current (alchemist-scope-module))
        (use (alchemist-scope-use-modules))
        (import (alchemist-scope-import-modules))
        (modules '()))
    (push current modules)
    (push use modules)
    (push import modules)
    (-flatten modules)))

(defun alchemist-scope-extract-module (expr)
  "Extract module from EXPR."
  (let* ((parts (split-string expr "\\."))
         (function (car (last parts)))
         (case-fold-search nil))
    (when (string-match-p "^[a-z_\?!]+" function)
      (delete function parts))
    (unless (string-match-p "^[a-z_\?!]+" (car parts))
      (alchemist-utils-remove-dot-at-the-end (mapconcat 'concat parts ".")))))

(defun alchemist-scope-extract-function (expr)
  "Extract function from EXPR."
  (let* ((parts (split-string expr "\\."))
         (function (car (last parts)))
         (case-fold-search nil))
    (when (and function
               (string-match-p "^[a-z_\?!]+" function))
      function)))

(defun alchemist-scope-alias-full-path (module)
  "Solve the full path for the MODULE alias."
  (if (not (alchemist-utils-empty-string-p module))
      (let* ((aliases (-map (lambda (m)
                              (when (string-match-p (format "^%s" (car (cdr m))) module)
                                (replace-regexp-in-string (format "^%s" (car (cdr m))) (car m) module t)))
                            (alchemist-scope-aliases)))
             (aliases (delete nil aliases)))
        (if aliases
            (car aliases)
          module))))

(defun alchemist-scope-expression ()
  "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))))

(provide 'alchemist-scope)

;;; alchemist-scope.el ends here
