;;; python-environment.el --- virtualenv API for Emacs Lisp

;; Copyright (C) 2015 Takafumi Arakaki

;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
;; Keywords: applications, tools
;; Version: 0.0.2
;; Package-Requires: ((deferred "0.3.1"))

;; This file is NOT part of GNU Emacs.

;; python-environment.el 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.

;; python-environment.el 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 python-environment.el.
;; If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;;

;;; Code:

(require 'deferred)

(defconst python-environment-version "0.0.2alpha0")

(defcustom python-environment-directory
  (locate-user-emacs-file ".python-environments")
  "Path to directory to store all Python virtual environments.  A string.

If you want to change the location to, say ``~/.python-environments``,
then set it like this in your Emacs setup file::

    (setq python-environment-directory \"~/.python-environments\")"
  :group 'python-environment)

(defcustom python-environment-default-root-name "default"
  "Default Python virtual environment name.  A string.

This is a name of directory relative to `python-environment-directory'
where default virtual environment locates.
Thus, typically the default virtual environment path is
``~/.emacs.d/.python-environments/default``."
  :group 'python-environment)

(defcustom python-environment-virtualenv
  (list "virtualenv" "--system-site-packages" "--quiet")
  ;; --quiet is required for Windows.  Without it, virtualenv raises
  ;; UnicodeEncodeError
  ;; See: https://github.com/tkf/emacs-jedi/issues/148#issuecomment-38290546
  "``virtualenv`` command to use, including command options.  List of strings.

For example, if you want to use specific Python executable (to
specify Python version), append ``--python`` option like this::

    (setq python-environment-virtualenv
          (append python-environment-virtualenv
                  '(\"--python\" \"PATH/TO/bin/python\")))

I added ``--system-site-packages`` as default, but this is not
mandatory.  If you don't like it, removing does not break
anything (well, theoretically).  For reason why it is default,
see discussion here:
https://github.com/tkf/emacs-python-environment/issues/3"
  :group 'python-environment)

(defvar python-environment--verbose nil)

(defun python-environment--deferred-process (msg command)
  (message "%s..." msg)
  (deferred:$
    (apply #'deferred:process command)
    (deferred:watch it
      (apply-partially
       (lambda (msg output)
         (message "%s...Done" msg)
         (when python-environment--verbose
           (message output)))
       msg))))

(defun python-environment--blocking-process (msg command)
  (message "%s (SYNC)..." msg)
  (let (exit-code output)
    (with-temp-buffer
      (setq exit-code
            (apply #'call-process (car command)
                   nil                ; INFILE (no input)
                   t                  ; BUFFER (output to this buffer)
                   nil                ; DISPLAY (no refresh is needed)
                   (cdr command)))
      (setq output (buffer-string)))
    (when (or python-environment--verbose
              (not (= exit-code 0)))
      (message output))
    (message "%s (SYNC)...Done" msg)
    (unless (= exit-code 0)
      (error "Command %S exits with error code %S." command exit-code))))


(defun python-environment-root-path (&optional root)
  (expand-file-name (or root python-environment-default-root-name)
                    python-environment-directory))

(defun python-environment--make-with-runner (proc-runner root virtualenv)
  (let ((path (convert-standard-filename
               (python-environment-root-path root)))
        (virtualenv (append (or virtualenv python-environment-virtualenv)
                            (when python-environment--verbose
                              (list "--verbose")))))
    (unless (executable-find (car virtualenv))
      (error "Program named %S does not exist." (car virtualenv)))
    (funcall proc-runner
             (format "Making virtualenv at %s" path)
             (append virtualenv (list path)))))

(defun python-environment-make (&optional root virtualenv)
  "Make virtual environment at ROOT asynchronously.

This function does not wait until ``virtualenv`` finishes.
Instead, it returns a deferred object [#]_.  So, if you want to
do some operation after the ``virtualenv`` command finishes, do
something like this::

    (deferred:$
     (python-environment-make)
     (deferred:nextc it (lambda (output) DO-SOMETHING-HERE)))

If ROOT is specified, it is used instead of
`python-environment-default-root-name'.  ROOT can be a relative
path from `python-environment-virtualenv' or an absolute path.

If VIRTUALENV (list of string) is specified, it is used instead of
`python-environment-virtualenv'.

.. [#] https://github.com/kiwanami/emacs-deferred"
  (python-environment--make-with-runner
   #'python-environment--deferred-process
   root virtualenv))

(defun python-environment-make-block (&optional root virtualenv)
  "Blocking version of `python-environment-make'.
I recommend NOT to use this function in interactive commands.
For reason, see `python-environment-run-block'"
  (python-environment--make-with-runner
   #'python-environment--blocking-process
   root virtualenv))

(defun python-environment-exists-p (&optional root)
  "Return non-`nil' if virtualenv at ROOT exists.
See `python-environment-make' for how ROOT is interpreted."
  (let ((bin (python-environment-bin "python" root)))
    (and bin (file-exists-p bin))))

(defun python-environment--existing (root &rest paths)
  (when paths
    (let ((full-path (expand-file-name (car paths)
                                       (python-environment-root-path root))))
      (if (file-exists-p full-path)
          full-path
        (apply #'python-environment--existing root (cdr paths))))))

(defun python-environment-bin (path &optional root)
  "Return full path to \"ROOT/bin/PATH\" or \"ROOT/Scripts/PATH\" if exists.
``Scripts`` is used instead of ``bin`` in typical Windows case.
In Windows, path with extension \".ext\" may be returned.
See `python-environment-make' for how ROOT is interpreted."
  (python-environment--existing root
                                (concat "bin/" path)
                                (concat "Scripts/" path)
                                (concat "Scripts/" path ".exe")))

(defun python-environment-lib (path &optional root)
  "Return full path to \"ROOT/lib/PATH\" or \"ROOT/Lib/PATH\" if exists.
``Lib`` is used instead of ``lib`` in typical Windows case.
See `python-environment-make' for how ROOT is interpreted."
  (python-environment--existing root
                                (concat "lib/" path)
                                (concat "Lib/" path)))

(defun python-environment--run-with-runner (proc-runner command root)
  (funcall proc-runner
           (format "Running: %s" (mapconcat 'identity command " "))
           (cons (python-environment-bin (car command) root)
                 (cdr command))))

(defun python-environment--run-1 (&optional command root)
  (python-environment--run-with-runner
   #'python-environment--deferred-process
   command root))

(defun python-environment--run-block-1 (command root)
  (python-environment--run-with-runner
   #'python-environment--blocking-process
   command root))

(defun python-environment-run (command &optional root virtualenv)
  "Run COMMAND installed in Python virtualenv located at ROOT
asynchronously.

Instead of waiting for COMMAND to finish, a deferred object [#]_
is returned so that you can chain operations.

See `python-environment-make' for how ROOT and VIRTUALENV are
interpreted and how to work with deferred object.

Use `python-environment-run-block' if you want to wait until
the command exit (NOT recommended in interactive command).

.. [#] https://github.com/kiwanami/emacs-deferred"
  (if (python-environment-exists-p root)
      (python-environment--run-1 command root)
    (deferred:$
      (python-environment-make root virtualenv)
      (deferred:nextc it
        (apply-partially
         (lambda (command root _)
           (python-environment--run-1 command root))
         command root)))))

(defun python-environment-run-block (command &optional root virtualenv)
  "Blocking version of `python-environment-run'.
I recommend NOT to use this function in interactive commands.
Emacs users have more important things to than waiting for some
command to finish."
  (unless (python-environment-exists-p root)
    (python-environment-make-block root virtualenv))
  (python-environment--run-block-1 command root))

(provide 'python-environment)

;; Local Variables:
;; coding: utf-8
;; indent-tabs-mode: nil
;; End:

;;; python-environment.el ends here
