;;; js2r-functions.el --- Function manipulation functions for js2-refactor    -*- lexical-binding: t; -*-

;; Copyright (C) 2012-2014 Magnar Sveen
;; Copyright (C) 2015-2016 Magnar Sveen and Nicolas Petton

;; Author: Magnar Sveen <magnars@gmail.com>,
;;         Nicolas Petton <nicolas@petton.fr>
;; Keywords: conveniences

;; 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/>.

;;; Code:

(require 'dash)
(require 'yasnippet)

(require 'js2r-helpers)

(defun js2r-localize-parameter ()
  "Turn parameter into local var in local function."
  (interactive)
  (js2r--guard)
  (js2r--wait-for-parse
   (if (js2-name-node-p (js2-node-at-point))
       (js2r--localize-parameter-pull)
     (js2r--localize-parameter-push))))

(defun js2r--localize-parameter-push ()
  (let* ((node (js2-node-at-point))
         (arg-node (or (js2r--closest-node-where 'js2r--parent-is-call-node node)
                       (error "Place cursor on argument to localize")))
         (call-node (js2-node-parent arg-node))
         (value (js2-node-string arg-node))
         (target (js2-call-node-target call-node))
         (fn (if (js2-name-node-p target)
                 (js2r--local-fn-from-name-node target)
               (error "Can only localize parameter for local functions")))
         (usages (js2r--function-usages fn))
         (index (car (--keep (when (eq arg-node it) it-index)
                             (js2-call-node-args call-node))))
         (name (js2-name-node-name (nth index (js2-function-node-params fn)))))
    (js2r--localize-parameter fn usages index name value)))

(defun js2r--localize-parameter-pull ()
  (let* ((name-node (js2-node-at-point))
         (name (if (js2-name-node-p name-node)
                   (js2-name-node-name name-node)
                 (error "Place cursor on parameter to localize")))
         (fn (or (js2r--closest-node-where #'js2r--is-local-function name-node)
                 (error "Can only localize parameter in local functions")))
         (index (or (js2r--param-index-for name fn)
                    (error "%S isn't a parameter to this function" name)))
         (usages (js2r--function-usages fn))
         (examples (-distinct (--map (js2r--argument index it) usages)))
         (value (js2r--choose-one "Value: " examples)))
    (js2r--localize-parameter fn usages index name value)))

(defun js2r--localize-parameter (fn usages index name value)
  (save-excursion
    (js2r--goto-fn-body-beg fn)
    (save-excursion
      (--each usages (js2r--remove-argument-at-index index it)))
    (newline-and-indent)
    (insert "var " name " = " value ";")
    (js2r--remove-parameter-at-index index fn)))

(defun js2r--parent-is-call-node (node)
  (js2-call-node-p (js2-node-parent node)))

(defun js2r--local-fn-from-name-node (name-node)
  (->> name-node
    (js2r--local-usages-of-name-node)
    (-map #'js2-node-parent)
    (-first #'js2-function-node-p)))

(defun js2r--param-index-for (name fn)
  (car (--keep (when (equal name (js2-name-node-name it)) it-index)
               (js2-function-node-params fn))))

(defun js2r--argument (index call-node)
  (js2-node-string (nth index (js2-call-node-args call-node))))

(defun js2r--remove-parameter-at-index (index fn)
  (js2r--delete-node-in-params (nth index (js2-function-node-params fn))))

(defun js2r--remove-argument-at-index (index call-node)
  (js2r--delete-node-in-params (nth index (js2-call-node-args call-node))))

(defun js2r--delete-node-in-params (node)
  (goto-char (js2-node-abs-pos node))
  (delete-char (js2-node-len node))
  (if (and (looking-back "(")
           (looking-at ", "))
      (delete-char 2)
    (when (looking-back ", ")
      (delete-char -2))))

(defun js2r--choose-one (prompt options)
  (when options
    (if (cdr options)
        (completing-read prompt options)
      (car options))))

(defun js2r-introduce-parameter ()
  "Introduce a parameter in a local function."
  (interactive)
  (js2r--guard)
  (js2r--wait-for-parse
   (if (use-region-p)
       (js2r--introduce-parameter-between (region-beginning) (region-end))
     (let ((node (js2r--closest-extractable-node)))
       (js2r--introduce-parameter-between (js2-node-abs-pos node)
					  (js2-node-abs-end node))))))

(defun js2r--introduce-parameter-between (beg end)
  (unless (js2r--single-complete-expression-between-p beg end)
    (error "Can only introduce single, complete expressions as parameter"))

  (let ((fn (js2r--closest-node-where #'js2r--is-local-function (js2-node-at-point))))
    (unless fn
      (error "Can only introduce parameter in local functions"))
    (save-excursion
      (let ((name (read-string "Parameter name: "))
            (val (buffer-substring beg end))
            (usages (js2r--function-usages fn)))
        (goto-char beg)
        (save-excursion
          (-each usages (-partial #'js2r--add-parameter val)))
        (delete-char (- end beg))
        (insert name)
        (js2r--add-parameter name fn)
        (query-replace val name nil (js2-node-abs-pos fn) (js2r--fn-body-end fn))))))

(defun js2r--function-usages (fn)
  (-map #'js2-node-parent (js2r--function-usages-name-nodes fn)))

(defun js2r--function-usages-name-nodes (fn)
  (let ((name-node (or (js2-function-node-name fn)
                       (js2-var-init-node-target (js2-node-parent fn)))))
    (remove name-node (js2r--local-usages-of-name-node name-node))))

(defun js2r--add-parameter (name node)
  (save-excursion
    (js2r--goto-closing-paren node)
    (unless (looking-back "(")
      (insert ", "))
    (insert name)))

(defun js2r--goto-closing-paren (node)
  (goto-char (js2-node-abs-pos node))
  (search-forward "(")
  (forward-char -1)
  (forward-list)
  (forward-char -1))

(defun js2r--goto-fn-body-beg (fn)
  (goto-char (js2-node-abs-pos fn))
  (search-forward "{"))

(defun js2r--fn-body-end (fn)
  (save-excursion
    (js2r--goto-fn-body-beg fn)
    (forward-char -1)
    (forward-list)
    (point)))

(defun js2r--is-local-function (node)
  (or (js2r--is-var-function-expression node)
      (js2r--is-function-declaration node)))

(defun js2r--is-method (node)
  (and (js2-function-node-p node)
       (js2-object-prop-node-p (js2-node-parent node))))

(defun js2r--is-var-function-expression (node)
  (and (js2-function-node-p node)
       (js2-var-init-node-p (js2-node-parent node))))

(defun js2r--is-assigned-function-expression (node)
  (and (js2-function-node-p node)
       (js2-assign-node-p (js2-node-parent node))))

(defun js2r--is-function-declaration (node)
  (let ((parent (js2-node-parent node)))
    (and (js2-function-node-p node)
         (not (js2-assign-node-p parent))
         (not (js2-var-init-node-p parent))
         (not (js2-object-prop-node-p parent)))))

(defun js2r-arguments-to-object ()
  "Change from a list of arguments to a parameter object."
  (interactive)
  (js2r--guard)
  (js2r--wait-for-parse
   (let ((node (js2-node-at-point)))
     (unless (and (looking-at "(")
		  (or (js2-function-node-p node)
		      (js2-call-node-p node)
		      (js2-new-node-p node)))
       (error "Place point right before the opening paren in the call or function"))

     (-when-let* ((target (js2r--node-target node))
		  (fn (and (js2-name-node-p target)
			   (js2r--local-fn-from-name-node target))))
       (setq node fn))
     (if (js2-function-node-p node)
	 (js2r--arguments-to-object-for-function node)
       (js2r--arguments-to-object-for-args-with-unknown-function (js2r--node-args node))))))

(defun js2r--arguments-to-object-for-function (function-node)
  (let ((params (js2-function-node-params function-node)))
    (when (null params)
      (error "No params to convert"))
    (save-excursion
      (js2r--execute-changes
       (-concat
        ;; change parameter list to just (params)
        (list
         (list :beg (+ (js2-node-abs-pos function-node) (js2-function-node-lp function-node))
               :end (+ (js2-node-abs-pos function-node) (js2-function-node-rp function-node) 1)
               :contents "(params)"))

        ;; add params. in front of function local param usages
        (let* ((local-param-name-nodes (--mapcat (-> it
                                                   (js2-node-abs-pos)
                                                   (js2r--local-name-node-at-point)
                                                   (js2r--local-usages-of-name-node))
                                                 params))
               (local-param-name-usages (--remove (js2-function-node-p (js2-node-parent it))
                                                  local-param-name-nodes))
               (local-param-name-positions (-map #'js2-node-abs-pos local-param-name-usages)))
          (--map
           (list :beg it :end it :contents "params.")
           local-param-name-positions))

        ;; update usages of function
        (let ((names (-map #'js2-name-node-name params))
              (usages (js2r--function-usages function-node)))
          (--map
           (js2r--changes/arguments-to-object it names)
           usages)))))))

(defun js2r--changes/arguments-to-object (node names)
  (let ((args (js2r--node-args node)))
    (list :beg (+ (js2-node-abs-pos node) (js2r--node-lp node))
          :end (+ (js2-node-abs-pos node) (js2r--node-rp node) 1)
          :contents (js2r--create-object-with-arguments names args))))

(defun js2r--arguments-to-object-for-args-with-unknown-function (args)
  (when (null args)
    (error "No arguments to convert"))
  (let ((names (--map-indexed
                (format "${%d:%s}"
                        (1+ it-index)
                        (if (js2-name-node-p it)
                            (js2-name-node-name it)
                          "key"))
                args)))
    (yas-expand-snippet (js2r--create-object-with-arguments names args)
                        (point)
                        (save-excursion (forward-list) (point)))))

(defun js2r--create-object-with-arguments (names args)
  (let (arg key result)
    (--dotimes (length args)
      (setq arg (nth it args))
      (setq key (nth it names))
      (setq result
            (concat result
                    (format "    %s: %s,\n"
                            key
                            (buffer-substring (js2-node-abs-pos arg)
                                              (js2-node-abs-end arg))))))
    (concat "({\n" (substring result 0 -2) "\n})")))

(defun js2r-extract-function (name)
  "Extract a function from the closest statement expression from the point."
  (interactive "sName of new function: ")
  (js2r--extract-fn
   name
   (lambda ()
       (unless (js2r--looking-at-function-declaration)
         (goto-char (js2-node-abs-pos (js2r--closest #'js2-expr-stmt-node-p)))))
   "%s(%s);"
   "function %s(%s) {\n%s\n}\n\n"))

(defun js2r-extract-method (name)
  "Extract a method from the closest statement expression from the point."
  (interactive "sName of new method: ")
  (let ((class-node (js2r--closest #'js2-class-node-p)))
    (js2r--extract-fn
     name
     (unless class-node
       (lambda ()
         (goto-char (js2-node-abs-pos (js2r--closest #'js2-object-prop-node-p)))))
     "this.%s(%s);"
     (if class-node
         "%s(%s) {\n%s\n}\n\n"
       "%s: function (%s) {\n%s\n},\n\n"))))

(defun js2r--extract-fn (name goto-position call-template function-template)
  (js2r--guard)
  (js2r--wait-for-parse
   (unless (use-region-p)
     (error "Mark the expressions to extract first"))
   (save-excursion
     (let* ((parent (js2r--first-common-ancestor-in-region (region-beginning) (region-end)))
	    (block (js2r--closest-node-where #'js2-block-node-p parent))
	    (fn (js2r--closest-node-where #'js2-function-node-p block))
	    (exprs (js2r--marked-expressions-in-block block))
	    (vars (-mapcat #'js2r--name-node-decendants exprs))
	    (local (--filter (js2r--local-to-fn-p fn it) vars))
	    (names (-distinct (-map 'js2-name-node-name local)))
	    (declared-in-exprs (-map #'js2r--var-init-node-target-name (-mapcat #'js2r--var-init-node-decendants exprs)))
	    (outside-exprs (-difference (js2-block-node-kids block) exprs))
	    (outside-var-uses (-map #'js2-name-node-name (-mapcat #'js2r--name-node-decendants outside-exprs)))
	    (declared-in-but-used-outside (-intersection declared-in-exprs outside-var-uses))
	    (export-var (car declared-in-but-used-outside))
	    (params (-difference names declared-in-exprs))
	    (params-string (mapconcat #'identity (reverse params) ", "))
	    (first (car exprs))
	    (last (car (last exprs)))
	    (beg (js2-node-abs-pos (car exprs)))
	    (end (js2-node-abs-end last))
	    (contents (buffer-substring beg end)))
       (goto-char beg)
       (delete-region beg end)
       (when (js2-return-node-p last)
	 (insert "return "))
       (when export-var
	 (setq contents (concat contents "\nreturn " export-var ";"))
	 (insert "var " export-var " = "))
       (insert (format call-template name params-string))
       (goto-char (js2-node-abs-pos fn))
       (when goto-position (funcall goto-position))
       (let ((start (point)))
	 (insert (format function-template name params-string contents))
	 (indent-region start (1+ (point))))))))

(defun js2r--var-init-node-target-name (node)
  (js2-name-node-name
   (js2-var-init-node-target node)))

(defun js2r--function-around-region ()
  (or
   (js2r--closest-node-where #'js2-function-node-p
                             (js2r--first-common-ancestor-in-region
                              (region-beginning)
                              (region-end)))
   (error "This only works when you mark stuff inside a function")))

(defun js2r--marked-expressions-in-block (fn)
  (-select #'js2r--node-is-marked (js2-block-node-kids fn)))

(defun js2r--node-is-marked (node)
  (and
   (<= (region-beginning) (js2-node-abs-end node))
   (>= (region-end) (js2-node-abs-pos node))))

(defun js2r--name-node-decendants (node)
  (-select #'js2-name-node-p (js2r--decendants node)))

(defun js2r--var-init-node-decendants (node)
  (-select #'js2-var-init-node-p (js2r--decendants node)))

(defun js2r--decendants (node)
  (let (vars)
    (js2-visit-ast node
                   (lambda (node end-p)
                      (unless end-p
                        (setq vars (cons node vars)))))
    vars))

(defun js2r--local-to-fn-p (fn name-node)
  (let* ((name (js2-name-node-name name-node))
         (scope (js2-node-get-enclosing-scope name-node))
         (scope (js2-get-defining-scope scope name)))
    (eq fn scope)))


(defun js2r-toggle-arrow-function-and-expression ()
  "Toggle between function expression to arrow function."
  (interactive)
  (save-excursion
    (js2r--find-closest-function)
    (cond ((js2r--arrow-function-p)
           (js2r--transform-arrow-function-to-expression))
          ((and (js2r--function-start-p) (not (js2r--looking-at-function-declaration)))
           (js2r--transform-function-expression-to-arrow))
          (t (error "Can only toggle between function expressions and arrow function")))))


;; Toggle between function name() {} and var name = function ();

(defun js2r-toggle-function-expression-and-declaration ()
  (interactive)
  (save-excursion
    (js2r--find-closest-function)
    (cond
     ((js2r--looking-at-var-function-expression)
      (when (js2r--arrow-function-p) (js2r--transform-arrow-function-to-expression))
      (js2r--transform-function-expression-to-declaration))
     ((js2r--looking-at-function-declaration)
      (js2r--transform-function-declaration-to-expression))
     (t (error "Can only toggle between function declarations and free standing function expressions")))))


(defun js2r--arrow-function-p ()
  (interactive)
  (save-excursion
    (ignore-errors
      (js2r--find-closest-function)
      (and (looking-at "(?[,[:space:][:word:]]*)?[[:space:]]*=>")
           (not (js2r--point-inside-string-p))))))

(defun js2r--transform-arrow-function-to-expression ()
  (when (js2r--arrow-function-p)
    (let (has-parenthesis)
      (save-excursion
        (js2r--find-closest-function)
        (let ((end (make-marker)))
          (save-excursion
            (search-forward "=>")
            (set-marker end (js2-node-abs-end (js2-node-at-point))))
          (setq has-parenthesis (looking-at "\\s-*("))
          (insert "function ")
          (if has-parenthesis
              (forward-list)
            (insert "("))
          (search-forward "=>")
          (delete-char -2)
          (js2r--ensure-just-one-space)
          (unless has-parenthesis
            (backward-char 1)
            (insert ")"))
          (unless (looking-at "\\s-*{")
            (js2r--ensure-just-one-space)
            (insert "{ return ")
            (js2r--ensure-just-one-space)
            (goto-char (marker-position end))
            (insert "; }")))))))

(defun js2r--transform-function-expression-to-arrow ()
  (when (not (js2r--arrow-function-p))
    (save-excursion
      (js2r--find-closest-function)
      (let ((pos (point))
             (params
              (js2-function-node-params (js2-node-at-point)))
             parenthesis-start
             parenthesis-end)
        (when (js2r--looking-at-function-declaration)
          (error "Can not convert function declarations to arrow function"))
        (search-forward "(")
        (backward-char 1)
        (delete-region pos (point))
        (setq parenthesis-start (point))
        (forward-list)
        (setq parenthesis-end (point))
        (insert " => ")
        (js2r--ensure-just-one-space)
        (when (and (= 1 (length params))
                   (not js2r-always-insert-parens-around-arrow-function-params))
          (goto-char parenthesis-end)
          (backward-delete-char 1)
          (goto-char parenthesis-start)
          (delete-char 1))))))


(defun js2r--function-start-p()
  (let* ((fn (js2r--closest #'js2-function-node-p)))
    (and fn
    (= (js2-node-abs-pos fn) (point)))))

(defun js2r--find-closest-function ()
  (when (not (js2r--function-start-p))
    (let* ((fn (js2r--closest #'js2-function-node-p)))
      (goto-char (js2-node-abs-pos fn)))))

(defun js2r--looking-at-method ()
  (and (js2r--function-start-p)
       (looking-back ": ?")))

(defun js2r--looking-at-function-declaration ()
  (and (js2r--function-start-p)
       (looking-back "^ *")))

(defun js2r--looking-at-var-function-expression ()
  (and (js2r--function-start-p)
       (looking-back "^ *var[\s\n]*[a-z_$]+[\s\n]*=[\s\n]*")))

(defun js2r--transform-function-expression-to-declaration ()
  (when (js2r--looking-at-var-function-expression)
    (delete-char 9)
    (forward-list)
    (forward-list)
    (delete-char 1)
    (backward-list)
    (backward-list)
    (delete-backward-char 3)
    (back-to-indentation)
    (delete-char 4)
    (insert "function ")))

(defun js2r--transform-function-declaration-to-expression ()
  (when (js2r--looking-at-function-declaration)
    (delete-char 9)
    (insert "var ")
    (search-forward "(")
    (backward-char 1)
    (insert " = function ")
    (forward-list)
    (forward-list)
    (insert ";")))

(defun js2r-toggle-function-async ()
  "Toggle the innermost function from sync to async."
  (interactive)
  (save-excursion
    (js2r--find-closest-function)
    (if (looking-back "async[[:space:]\n]+")
        (delete-region (match-beginning 0) (match-end 0))
      (insert "async "))))

(provide 'js2r-functions)
;;; js2-functions.el ends here
