;;; ob-ipython.el --- org-babel functions for IPython evaluation

;; Author: Greg Sexton <gregsexton@gmail.com>
;; Keywords: literate programming, reproducible research
;; Homepage: http://www.gregsexton.org
;; Package-Version: 20260324.859
;; Package-Revision: 35414dccffe4
;; Package-Requires: ((s "1.9.0") (dash "2.10.0") (dash-functional "1.2.0") (f "0.17.2") (emacs "24"))

;; The MIT License (MIT)

;; Copyright (c) 2015 Greg Sexton

;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:

;; The above copyright notice and this permission notice shall be included in
;; all copies or substantial portions of the Software.

;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
;; THE SOFTWARE.

;;; Commentary:

;; Org-Babel support for evaluating Python source code using IPython.

;;; Code:

(require 'ob)
(require 'ob-python)
(require 'dash)
(require 'dash-functional)
(require 's)
(require 'f)
(require 'json)
(require 'python)
(require 'cl)

;; variables

(defcustom ob-ipython-kernel-extra-args '()
  "List of extra args to pass when creating a kernel."
  :group 'ob-ipython)

(defcustom ob-ipython-client-path
  (f-expand "./client.py"
            (or (-when-let (f load-file-name) (f-dirname f)) default-directory))
  "Path to the client script."
  :group 'ob-ipython)

(defcustom ob-ipython-command
  "jupyter"
  "Command to launch ipython. Usually ipython or jupyter."
  :group 'ob-ipython)

(defcustom ob-ipython-resources-dir "./obipy-resources/"
  "Directory where resources (e.g images) are stored so that they
can be displayed.")

;; utils

(defun ob-ipython--write-string-to-file (file string)
  (if string
      (with-temp-buffer
        (let ((require-final-newline nil))
          (insert string)
          (write-file file)))
    (error "No output was produced to write to a file.")))

(defun ob-ipython--write-base64-string (file b64-string)
  (if b64-string
      (with-temp-buffer
        (let ((buffer-file-coding-system 'binary)
              (require-final-newline nil))
          (insert b64-string)
          (base64-decode-region (point-min) (point-max))
          (write-file file)))
    (error "No output was produced to write to a file.")))

(defun ob-ipython--create-traceback-buffer (traceback)
  (let ((buf (get-buffer-create "*ob-ipython-traceback*")))
    (with-current-buffer buf
      (special-mode)
      (let ((inhibit-read-only t))
        (erase-buffer)
        (-each traceback
          (lambda (line) (insert (format "%s\n" line))))
        (ansi-color-apply-on-region (point-min) (point-max))))
    (pop-to-buffer buf)))

(defun ob-ipython--create-inspect-buffer (doc)
  (let ((buf (get-buffer-create "*ob-ipython-inspect*")))
    (with-current-buffer buf
      (special-mode)
      (let ((inhibit-read-only t))
        (erase-buffer)
        (insert doc)
        (ansi-color-apply-on-region (point-min) (point-max))
        (whitespace-cleanup)
        (goto-char (point-min))))
    (pop-to-buffer buf)))

(defun ob-ipython--clear-output-buffer ()
  (let ((buf (get-buffer-create "*ob-ipython-out*")))
    (with-current-buffer buf
      (let ((inhibit-read-only t))
        (erase-buffer)))))

(defun ob-ipython--output (output append-p)
  (when (not (s-blank? output))
    (let ((buf (get-buffer-create "*ob-ipython-out*")))
      (with-current-buffer buf
        (special-mode)
        (let ((inhibit-read-only t))
          (unless append-p (erase-buffer))
          (when (s-blank? (buffer-string)) (pop-to-buffer buf))
          (let ((p (point)))
            (if (= p (point-max))     ;allow tailing
                (progn (insert output)
                       (-when-let (w (get-buffer-window buf 'visible))
                         (set-window-point w (point-max))))
              (save-excursion
                (goto-char (point-max))
                (insert output)))
            (ansi-color-apply-on-region p (point-max))
            ;; this adds some support for control chars
            (comint-carriage-motion p (point-max)))
          (unless append-p (goto-char (point-min))))))))

(defun ob-ipython--dump-error (err-msg)
  (with-current-buffer (get-buffer-create "*ob-ipython-debug*")
    (special-mode)
    (let ((inhibit-read-only t))
      (erase-buffer)
      (insert err-msg)
      (goto-char (point-min))))
  (error "There was a fatal error trying to process the request. See *ob-ipython-debug*"))

(defun ob-ipython--generate-file-name (suffix)
  (s-concat (make-temp-name ob-ipython-resources-dir) suffix))

;; process management

(defun ob-ipython--kernel-file (name)
  (if (s-ends-with-p ".json" name)
      name
    (format "emacs-%s.json" name)))

(defun ob-ipython--kernel-repl-cmd (name)
  (list ob-ipython-command "console" "--simple-prompt" "--existing"
        (ob-ipython--kernel-file name)))

;;; TODO: could setup a default sentinel that outputs error on process
;;; early termination
(defun ob-ipython--create-process (name cmd)
  (let ((buf (get-buffer-create (format "*ob-ipython-%s*" name))))
    (with-current-buffer buf (erase-buffer))
    (apply 'start-process name buf (car cmd) (cdr cmd))))

(defun ob-ipython--get-python ()
  (locate-file (if (eq system-type 'windows-nt)
                   "python.exe"
                 (or python-shell-interpreter "python"))
               exec-path))

(defun ob-ipython--create-kernel (name &optional kernel)
  (when (and (not (ignore-errors (process-live-p (get-process (format "kernel-%s" name)))))
             (not (s-ends-with-p ".json" name)))
    (ob-ipython--create-process
     (format "kernel-%s" name)
     (append
      (list ob-ipython-command "console" "--simple-prompt")
      (list "-f" (ob-ipython--kernel-file name))
      (if kernel (list "--kernel" kernel) '())
      ;;should be last in the list of args
      ob-ipython-kernel-extra-args))
    (sleep-for 1)))

(defun ob-ipython--get-kernel-processes ()
  (let ((procs (-filter (lambda (p)
                          (s-starts-with? "kernel-" (process-name p)))
                        (process-list))))
    (-zip (-map (-compose (-partial 's-replace "kernel-" "")
                          'process-name)
                procs)
          procs)))

(defun ob-ipython--create-repl (name)
  (let ((python-shell-completion-native-enable nil)
        (cmd (s-join " " (ob-ipython--kernel-repl-cmd name))))
    (if (string= "default" name)
        (progn
          (run-python cmd nil nil)
          (format "*%s*" python-shell-buffer-name))
      (let ((process-name (format "Python:%s" name)))
        (get-buffer-process
         (python-shell-make-comint cmd process-name nil))
        (format "*%s*" process-name)))))

;; kernel management

(defun ob-ipython--choose-kernel ()
  (let ((procs (ob-ipython--get-kernel-processes)))
    (-> (ido-completing-read "kernel? " (-map 'car procs) nil t)
        (assoc procs)
        cdr
        list)))

;;; TODO: make this work on windows
;;; NOTE: interrupting remote kernel not currently possible, cf https://github.com/jupyter/jupyter_console/issues/150
(defun ob-ipython-interrupt-kernel (proc)
  "Interrupt a running kernel. Useful for terminating infinite
loops etc. If things get really desparate try `ob-ipython-kill-kernel'."
  (interactive (ob-ipython--choose-kernel))
  (when proc
    ;; send SIGINT to "python -m ipykernel_launcher", a child of proc
    (let ((proc-name (process-name proc)))
      (accept-process-output
       ;; get the child pid with pgrep -P
       ;; NOTE assumes proc has only 1 child (seems to be true always)
       (make-process
        :name (concat proc-name "-child")
        :command (list "pgrep" "-P" (number-to-string
                                     (process-id proc)))
        ;; send SIGINT to child-proc
        :filter
        (lambda (proc child-proc-id)
          (make-process
           :name (concat "interrupt-" proc-name)
           :command (list "kill" "-2"
                          (string-trim child-proc-id)))))))))

(defun ob-ipython-kill-kernel (proc)
  "Kill a kernel process. If you then re-evaluate a source block
a new kernel will be started."
  (interactive (ob-ipython--choose-kernel))
  (when proc
    (delete-process proc)
    (message (format "Killed %s" (process-name proc)))))

;; evaluation

(defvar ob-ipython--async-queue nil)

(defun ob-ipython--enqueue (q x)
  (set q (append (symbol-value q) (list x))))

(defun ob-ipython--dequeue (q)
  (let ((ret (car (symbol-value q))))
    (set q (cdr (symbol-value q)))
    ret))

(defun ob-ipython--collect-json ()
  ;; this function assumes that we're in a buffer with the json lines
  (let ((json-array-type 'list))
    (let (acc)
      (while (not (= (point) (point-max)))
        (setq acc (cons (json-read) acc))
        (forward-line))
      (nreverse acc))))

(defun ob-ipython--running-p ()
  (get-process "execute"))

(defun ob-ipython--run-async (code name callback args)
  (let ((proc (ob-ipython--create-process
               "execute"
               (list (ob-ipython--get-python)
                     "--" ob-ipython-client-path "--conn-file" name "--execute"))))
    ;; TODO: maybe add a way of disabling streaming output?
    ;; TODO: cleanup and break out - we parse twice, can we parse once?
    (set-process-filter
     proc
     (lexical-let ((parse-pos 0))
       (lambda (proc output)
         ;; not guaranteed to be given lines - we need to handle buffering
         (with-current-buffer (process-buffer proc)
           (goto-char (point-max))
           (insert output)
           (let ((json-array-type 'list))
             (goto-char parse-pos)
             (while (not (= (point) (point-max)))
               (condition-case nil
                   (progn (-> (json-read)
                              list
                              ob-ipython--extract-output
                              (ob-ipython--output t))
                          (forward-line)
                          (setq parse-pos (point)))
                 (error (goto-char (point-max))))))))))
    (set-process-sentinel
     proc
     (lexical-let ((callback callback)
                   (args args))
       (lambda (proc state)
         (when (not (process-live-p proc))
           (with-current-buffer (process-buffer proc)
             (goto-char (point-min))
             (apply callback (-> (ob-ipython--collect-json)
                                 ob-ipython--eval
                                 (cons args))))
           (ob-ipython--maybe-run-async)))))
    (process-send-string proc code)
    (process-send-string proc "\n")
    (process-send-eof proc)))

(defun ob-ipython--maybe-run-async ()
  (when (not (ob-ipython--running-p))
    (when-let (val (ob-ipython--dequeue 'ob-ipython--async-queue))
      (cl-destructuring-bind (code name callback args) val
        (ob-ipython--run-async code name callback args)))))

(defun ob-ipython--execute-request-async (code name callback args)
  (ob-ipython--enqueue 'ob-ipython--async-queue (list code name callback args))
  (ob-ipython--maybe-run-async))

(defun ob-ipython--execute-request (code name)
  (with-temp-buffer
    (let ((ret (apply 'call-process-region code nil
                      (ob-ipython--get-python) nil t nil
                      (list "--" ob-ipython-client-path "--conn-file" name "--execute"))))
      (if (> ret 0)
          (ob-ipython--dump-error (buffer-string))
        (goto-char (point-min))
        (ob-ipython--collect-json)))))

(defun ob-ipython--extract-output (msgs)
  (->> msgs
       (-filter (lambda (msg) (string= "stream" (cdr (assoc 'msg_type msg)))))
       (-filter (lambda (msg) (-contains? '("stdout" "stderr")
                                          (->> msg (assoc 'content)
                                               (assoc 'name)
                                               cdr))))
       (-map (lambda (msg) (->> msg (assoc 'content) (assoc 'text) cdr)))
       (-reduce 's-concat)))

(defun ob-ipython--extract-result (msgs)
  `((:value . ,(->> msgs
                    (-filter (lambda (msg)
                               (s-equals? "execute_result"
                                          (cdr (assoc 'msg_type msg)))))
                    (-mapcat (lambda (msg)
                               (->> msg (assoc 'content) (assoc 'data) cdr)))))
    (:display . ,(->> msgs
                      (-filter (lambda (msg)
                                 (s-equals? "display_data"
                                            (cdr (assoc 'msg_type msg)))))
                      (-mapcat (lambda (msg)
                                 (->> msg (assoc 'content) (assoc 'data) cdr)))))))

(defun ob-ipython--extract-error (msgs)
  (let ((error-content
         (->> msgs
              (-filter (lambda (msg) (-contains? '("execute_reply" "inspect_reply")
                                                 (cdr (assoc 'msg_type msg)))))
              car
              (assoc 'content)
              cdr)))
    ;; TODO: this doesn't belong in this abstraction
    (ob-ipython--create-traceback-buffer (cdr (assoc 'traceback error-content)))
    (format "%s: %s" (cdr (assoc 'ename error-content)) (cdr (assoc 'evalue error-content)))))

(defun ob-ipython--extract-status (msgs)
  (->> msgs
       (-filter (lambda (msg) (-contains? '("execute_reply" "inspect_reply" "complete_reply")
                                          (cdr (assoc 'msg_type msg)))))
       car
       (assoc 'content)
       (assoc 'status)
       cdr))

(defun ob-ipython--extract-execution-count (msgs)
  (->> msgs
       (-filter (lambda (msg) (-contains? '("execute_reply")
                                          (cdr (assoc 'msg_type msg)))))
       car
       (assoc 'content)
       (assoc 'execution_count)
       cdr))

(defun ob-ipython--eval (service-response)
  (let ((status (ob-ipython--extract-status service-response)))
    (cond ((string= "ok" status) `((:result . ,(ob-ipython--extract-result service-response))
                                   (:output . ,(ob-ipython--extract-output service-response))
                                   (:exec-count . ,(ob-ipython--extract-execution-count service-response))))
          ((string= "abort" status) (error "Kernel execution aborted."))
          ((string= "error" status) (error (ob-ipython--extract-error service-response))))))

;; inspection

(defun ob-ipython--inspect-request (code &optional pos detail)
  (let ((input (json-encode `((code . ,code)
                              (pos . ,(or pos (length code)))
                              (detail . ,(or detail 0)))))
        (args (list "--" ob-ipython-client-path
                    "--conn-file"
                    (ob-ipython--get-session-from-edit-buffer (current-buffer))
                    "--inspect")))
    (with-temp-buffer
      (let ((ret (apply 'call-process-region input nil
                        (ob-ipython--get-python) nil t nil
                        args)))
        (if (> ret 0)
            (ob-ipython--dump-error (buffer-string))
          (goto-char (point-min))
          (ob-ipython--collect-json))))))

(defun ob-ipython--inspect (code pos)
  "Given a piece of code and a point position, return inspection results."
  (let* ((resp (ob-ipython--inspect-request code pos 0))
         (status (ob-ipython--extract-status resp)))
    (if (string= "ok" status)
        (->> resp
             (-filter (lambda (msg)
                        (-contains? '("execute_result" "display_data" "inspect_reply")
                                    (cdr (assoc 'msg_type msg)))))
             (-mapcat (lambda (msg)
                        (->> msg
                             (assoc 'content)
                             (assoc 'data)
                             cdr))))
      (error (ob-ipython--extract-error resp)))))

(defun ob-ipython-inspect (buffer pos)
  "Ask a kernel for documentation on the thing at POS in BUFFER."
  (interactive (list (current-buffer) (point)))
  (let ((code (with-current-buffer buffer
                (buffer-substring-no-properties (point-min) (point-max)))))
    (-if-let (result (->> (ob-ipython--inspect code pos)
                          (assoc 'text/plain)
                          cdr))
        (ob-ipython--create-inspect-buffer result)
      (message "No documentation was found."))))

;; completion

(defun ob-ipython--complete-request (code &optional pos)
  (let ((input (json-encode `((code . ,code)
                              (pos . ,(or pos (length code))))))
        (args (list "--" ob-ipython-client-path "--conn-file"
                    (ob-ipython--get-session-from-edit-buffer (current-buffer))
                    "--complete")))
    (with-temp-buffer
      (let ((ret (apply 'call-process-region input nil
                        (ob-ipython--get-python) nil t nil
                        args)))
        (if (> ret 0)
            (ob-ipython--dump-error (buffer-string))
          (goto-char (point-min))
          (ob-ipython--collect-json))))))

(defun ob-ipython-completions (buffer pos)
  "Ask a kernel for completions on the thing at POS in BUFFER."
  (let* ((code (with-current-buffer buffer
                 (buffer-substring-no-properties (point-min) (point-max))))
         (resp (ob-ipython--complete-request code pos))
         (status (ob-ipython--extract-status resp)))
    (if (not (string= "ok" status))
        '()
      (->> resp
           (-filter (lambda (msg)
                      (-contains? '("complete_reply")
                                  (cdr (assoc 'msg_type msg)))))
           (-mapcat (lambda (msg)
                      (->> msg
                           (assoc 'content)
                           cdr)))))))

(defun ob-ipython--company-doc-buffer (doc)
  "Make company-suggested doc-buffer with ansi-color support."
  (let ((buf (company-doc-buffer doc)))
    (with-current-buffer buf
      (ansi-color-apply-on-region (point-min) (point-max)))
    buf))

(defun company-ob-ipython (command &optional arg &rest ignored)
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-ob-ipython))
    (prefix (and ob-ipython-mode
                 (let ((res (ob-ipython-completions (current-buffer) (1- (point)))))
                   (substring-no-properties (buffer-string)
                                            (cdr (assoc 'cursor_start res))
                                            (cdr (assoc 'cursor_end res))))))
    (candidates (cons :async (lambda (cb)
                               (let ((res (ob-ipython-completions
                                           (current-buffer) (1- (point)))))
                                 (funcall cb (cdr (assoc 'matches res)))))))
    (sorted t)
    (doc-buffer (ob-ipython--company-doc-buffer
                 (cdr (assoc 'text/plain (ob-ipython--inspect arg (length arg))))))))

;; mode

(define-minor-mode ob-ipython-mode
  ""
  nil
  " ipy"
  '())

;; babel framework

(add-to-list 'org-src-lang-modes '("ipython" . python))
(add-hook 'org-mode-hook 'ob-ipython-auto-configure-kernels)

(defvar ob-ipython-configured-kernels nil)

(defun ob-ipython--get-kernels ()
  "Return a list of available jupyter kernels and their corresponding languages.
The elements of the list have the form (\"kernel\" \"language\")."
  (and ob-ipython-command
       (let ((kernelspecs (cdar (json-read-from-string
                                 (shell-command-to-string
                                  (s-concat ob-ipython-command " kernelspec list --json"))))))
         (-map (lambda (spec)
                 (cons (symbol-name (car spec))
                       (->> (cdr spec)
                            (assoc 'spec)
                            cdr
                            (assoc 'language)
                            cdr)))
               kernelspecs))))

(defun ob-ipython--configure-kernel (kernel-lang)
  "Configure org mode to use specified kernel."
  (let* ((kernel (car kernel-lang))
         (language (cdr kernel-lang))
         (jupyter-lang (concat "jupyter-" language))
         (mode (intern (or (cdr (assoc language org-src-lang-modes))
                           (replace-regexp-in-string "[0-9]*" "" language))))
         (header-args (intern (concat "org-babel-default-header-args:" jupyter-lang))))
    (add-to-list 'org-src-lang-modes `(,jupyter-lang . ,mode))
    ;; Only set defaults if the corresponding variable is nil or does not
    ;; exist yet.
    (unless (and (boundp header-args) (symbol-value header-args))
      (set (intern (concat "org-babel-default-header-args:" jupyter-lang))
           `((:session . ,language)
             (:kernel . ,kernel))))
    (defalias (intern (concat "org-babel-execute:" jupyter-lang))
      'org-babel-execute:ipython)
    (defalias (intern (concat "org-babel-" jupyter-lang "-initiate-session"))
      'org-babel-ipython-initiate-session)
    kernel-lang))

(defun ob-ipython-auto-configure-kernels (&optional replace)
  "Auto-configure kernels for use with org-babel based on the
available kernelspecs of the current jupyter installation. If
REPLACE is non-nil, force configuring the kernels even if they
have previously been configured."
  (interactive (list t))
  (when (or replace (not ob-ipython-configured-kernels))
    (setq ob-ipython-configured-kernels
          (-map 'ob-ipython--configure-kernel (ob-ipython--get-kernels)))))

(defvar org-babel-default-header-args:ipython '())

(defun org-babel-edit-prep:ipython (info)
  ;; TODO: based on kernel, should change the major mode
  (ob-ipython--create-kernel (->> info (nth 2) (assoc :session) cdr
                                  ob-ipython--normalize-session)
                             (->> info (nth 2) (assoc :kernel) cdr))
  (ob-ipython-mode +1))

(defun ob-ipython--normalize-session (session)
  (if (string= "default" session)
      (error "default is reserved for when no name is provided. Please use a different session name.")
    (or session "default")))

(defun ob-ipython--get-session-from-edit-buffer (buffer)
  (with-current-buffer buffer
    (->> org-src--babel-info
         (nth 2)
         (assoc :session)
         cdr
         ob-ipython--normalize-session)))

(defun org-babel-execute:ipython (body params)
  "Execute a block of IPython code with Babel.
This function is called by `org-babel-execute-src-block'."
  (ob-ipython--clear-output-buffer)
  (if (cdr (assoc :async params))
      (ob-ipython--execute-async body params)
    (ob-ipython--execute-sync body params)))

(defun ob-ipython--execute-async (body params)
  (let* ((file (cdr (assoc :ipyfile params)))
         (session (cdr (assoc :session params)))
         (result-type (cdr (assoc :result-type params)))
         (sentinel (ipython--async-gen-sentinel)))
    (ob-ipython--create-kernel (ob-ipython--normalize-session session)
                               (cdr (assoc :kernel params)))
    (ob-ipython--execute-request-async
     (org-babel-expand-body:generic (encode-coding-string body 'utf-8)
                                    params (org-babel-variable-assignments:python params))
     (ob-ipython--normalize-session session)
     (lambda (ret sentinel buffer file result-type)
       (let ((replacement (ob-ipython--process-response ret file result-type)))
         (ipython--async-replace-sentinel sentinel buffer replacement)))
     (list sentinel (current-buffer) file result-type))
    (format "%s - %s" (length ob-ipython--async-queue) sentinel)))

(defun ob-ipython--execute-sync (body params)
  (let* ((file (cdr (assoc :ipyfile params)))
         (session (cdr (assoc :session params)))
         (result-type (cdr (assoc :result-type params))))
    (ob-ipython--create-kernel (ob-ipython--normalize-session session)
                               (cdr (assoc :kernel params)))
    (-when-let (ret (ob-ipython--eval
                     (ob-ipython--execute-request
                      (org-babel-expand-body:generic (encode-coding-string body 'utf-8)
                                                     params (org-babel-variable-assignments:python params))
                      (ob-ipython--normalize-session session))))
      (ob-ipython--process-response ret file result-type))))

(defun ob-ipython--process-response (ret file result-type)
  (let ((result (cdr (assoc :result ret)))
        (output (cdr (assoc :output ret))))
    (if (eq result-type 'output)
        output
      (ob-ipython--output output nil)
      (s-concat
       (format "# Out[%d]:\n" (cdr (assoc :exec-count ret)))
       (s-join "\n" (->> (-map (-partial 'ob-ipython--render file)
                               (list (cdr (assoc :value result))
                                     (cdr (assoc :display result))))
                         (remove-if-not #'identity)))))))

(defun ob-ipython--render (file-or-nil values)
  (let ((org (lambda (value) value))
        (png (lambda (value)
               (let ((file (or file-or-nil (ob-ipython--generate-file-name ".png"))))
                 (ob-ipython--write-base64-string file value)
                 (format "[[file:%s]]" file))))
        (svg (lambda (value)
               (let ((file (or file-or-nil (ob-ipython--generate-file-name ".svg"))))
                 (ob-ipython--write-string-to-file file value)
                 (format "[[file:%s]]" file))))
        (html (lambda (value)
                ;; ((eq (car value) 'text/html)
                ;;  (let ((pandoc (executable-find "pandoc")))
                ;;    (and pandoc (with-temp-buffer
                ;;                  (insert value)
                ;;                  (shell-command-on-region
                ;;                   (point-min) (point-max)
                ;;                   (format "%s -f html -t org" pandoc) t t)
                ;;                  (s-trim (buffer-string))))))
                ))
        (txt (lambda (value)
               (let ((lines (s-lines value)))
                 (if (cdr lines)
                     (->> lines
                          (-map 's-trim)
                          (s-join "\n  ")
                          (s-concat "  ")
                          (format "#+BEGIN_EXAMPLE\n%s\n#+END_EXAMPLE"))
                   (s-concat ": " (car lines)))))))
    (or (-when-let (val (cdr (assoc 'text/org values))) (funcall org val))
        (-when-let (val (cdr (assoc 'image/png values))) (funcall png val))
        (-when-let (val (cdr (assoc 'image/svg+xml values))) (funcall svg val))
        (-when-let (val (cdr (assoc 'text/plain values))) (funcall txt val)))))

(defun org-babel-prep-session:ipython (session params)
  "Prepare SESSION according to the header arguments in PARAMS.
VARS contains resolved variable references"
  ;; c-u c-c c-v c-z
  (error "Currently unsupported."))

(defun org-babel-load-session:ipython (session body params)
  "Load BODY into SESSION."
  ;; c-c c-v c-l
  (error "Currently unsupported."))

(defun org-babel-ipython-initiate-session (&optional session params)
  "Create a session named SESSION according to PARAMS."
  (if (string= session "none")
      (error "ob-ipython currently only supports evaluation using a session.
Make sure your src block has a :session param.")
    (when (not (s-ends-with-p ".json" session))
      (ob-ipython--create-kernel (ob-ipython--normalize-session session)
                                 (cdr (assoc :kernel params))))
    (ob-ipython--create-repl (ob-ipython--normalize-session session))))

;; async

(defun ipython--async-gen-sentinel ()
  ;; lifted directly from org-id. thanks.
  (let ((rnd (md5 (format "%s%s%s%s%s%s%s"
                          (random)
                          (current-time)
                          (user-uid)
                          (emacs-pid)
                          (user-full-name)
                          user-mail-address
                          (recent-keys)))))
    (format "%s-%s-4%s-%s%s-%s"
            (substring rnd 0 8)
            (substring rnd 8 12)
            (substring rnd 13 16)
            (format "%x"
                    (logior
                     #b10000000
                     (logand
                      #b10111111
                      (string-to-number
                       (substring rnd 16 18) 16))))
            (substring rnd 18 20)
            (substring rnd 20 32))))

(defun ipython--async-replace-sentinel (sentinel buffer replacement)
  (save-window-excursion
    (save-excursion
      (save-restriction
        (with-current-buffer buffer
          (goto-char (point-min))
          (re-search-forward sentinel)
          (re-search-backward "\\(call\\|src\\)_\\|^[ \t]*#\\+\\(BEGIN_SRC\\|CALL:\\)")
          (org-babel-remove-result)
          (org-babel-insert-result
           replacement
           (cdr (assoc :result-params (nth 2 (org-babel-get-src-block-info)))))
          (org-redisplay-inline-images))))))

;; lib

(provide 'ob-ipython)

;;; ob-ipython.el ends here
