;;; nix.el -- run nix commands in Emacs -*- lexical-binding: t -*-

;; Author: Matthew Bauer <mjbauer95@gmail.com>
;; Homepage: https://github.com/NixOS/nix-mode
;; Keywords: nix

;; This file is NOT part of GNU Emacs.

;;; Commentary:

;; To use this just run:

;; M-x RET nix-shell RET

;; This will give you some

;;; Code:

(require 'pcomplete)
(require 'json)
(eval-when-compile
  (require 'let-alist))

(defgroup nix nil
  "Nix-related customizations."
  :group 'languages)

(defcustom nix-executable "nix"
  "Nix executable location."
  :group 'nix
  :type 'string)

(defcustom nix-build-executable "nix-build"
  "Nix-build executable location."
  :group 'nix
  :type 'string)

(defcustom nix-instantiate-executable "nix-instantiate"
  "Nix-instantiate executable location."
  :group 'nix
  :type 'string)

(defcustom nix-store-executable "nix-store"
  "Nix-store executable location."
  :group 'nix
  :type 'string)

(defcustom nix-shell-executable "nix-shell"
  "Location of ‘nix-shell’ executable."
  :group 'nix
  :type 'string)

(defcustom nix-store-dir "/nix/store"
  "Nix store directory."
  :group 'nix
  :type 'directory)

(defcustom nix-state-dir "/nix/var"
  "Nix state directory."
  :group 'nix
  :type 'directory)

(defun nix-system ()
  "Get the current system tuple."
  (nix--process-string "eval"
    "--raw"
    (if (nix-is-24) "--impure" )
    (if (nix-is-24) "--expr" )
    "(builtins.currentSystem)"))

(defvar nix-version nil)
(defun nix-version ()
  "Get the version of Nix."
  (or nix-version (nix--process-string "--version")))

(defun nix-show-config ()
  "Show nix config."
  (nix--process-json "show-config" "--json"))

(defconst nix-commands
  '("add-to-store"
    "build"
    "cat-nar"
    "cat-store"
    "copy"
    "copy-sigs"
    "dump-path"
    "edit"
    "eval"
    "hash-file"
    "hash-path"
    "log"
    "ls-nar"
    "ls-store"
    "optimise-store"
    "path-info"
    "ping-store"
    "repl"
    "run"
    "search"
    "show-config"
    "show-derivation"
    "sign-paths"
    "to-base16"
    "to-base32"
    "to-base64"
    "upgrade-nix"
    "verify"
    "why-depends"))

(defconst nix-toplevel-options
  '("-v"
    "--verbose"
    "-h"
    "--help"
    "--debug"
    "--help-config"
    "--option"
    "--version"))

(defconst nix-config-options
  '("allowed-uris"
    "allow-import-from-derivation"
    "allow-new-priveleges"
    "allowed-users"
    "auto-optimise-store"
    "builders"
    "builders-use-substitutes"
    "build-users-group"
    "compress-build-log"
    "connect-timeout"
    "cores"
    "extra-sandbox-paths"
    "extra-substituters"
    "fallback"
    "fsync-metadata"
    "hashed-mirrors"
    "http-connections"
    "keep-build-log"
    "keep-derivations"
    "keep-env-derivations"
    "keep-outputs"
    "max-build-log-size"
    "max-jobs"
    "max-silent-time"
    "netrc-file"
    "plugin-files"
    "pre-build-hook"
    "repeat"
    "require-sigs"
    "restrict-eval"
    "sandbox"
    "sandbox-dev-shm-size"
    "sandbox-paths"
    "secret-key-files"
    "show-trace"
    "substitute"
    "substituters"
    "system"
    "timeout"
    "trusted-public-keys"
    "trusted-subtituters"
    "trusted-users"))

(defun nix--pcomplete-flags (options)
  "Complete flags to the Nix command.
OPTIONS a list of options to accept."
  (while (pcomplete-match "^-" 0)
    (pcomplete-here options)
    (let ((last-arg (nth (1- pcomplete-index) pcomplete-args)))
      (cond
       ((string= "--option" last-arg)
        (pcomplete-here nix-config-options)
        (pcomplete-here))
       ((or (string= "-f" last-arg) (string= "--file" last-arg))
        (pcomplete-here (pcomplete-entries nil 'file-exists-p)))
       ((or (string= "--arg" last-arg) (string= "--argstr" last-arg))
        (pcomplete-here)
        (pcomplete-here))
       ((or (string= "-I" last-arg) (string= "--include" last-arg))
        (pcomplete-here (pcomplete-entries nil 'file-exists-p)))
       ((or (string= "-k" last-arg) (string= "--keep" last-arg))
        (pcomplete-here))
       ((or (string= "-u" last-arg) (string= "--unset" last-arg))
        (pcomplete-here))
       ((or (string= "-s" last-arg) (string= "--substituter" last-arg))
        (pcomplete-here))))))

(defun nix-is-24 ()
  "Whether Nix is a version with Flakes support."
  (let ((version (nix-version)))
    (save-match-data
      (when (string-match (rx bol "nix (Nix) " (group (+ digit) (?  "." (+ digit))))
                          version)
        (version<= "2.4" (match-string 1 version))))))

(defun nix-has-flakes ()
  "Whether Nix is a version with Flakes support."
  ;; earlier versions reported as 3, now it’s just nix-2.4
  (and (nix-is-24)
       (let-alist (nix-show-config)
	 (or
	  (seq-contains-p .experimental-features.value 2)
	  (seq-contains-p .experimental-features.value "flakes")))))

;;;###autoload
(defun pcomplete/nix ()
  "Completion for the nix command."
  (if (nix-is-24)
      (let ((stdout (generate-new-buffer "nix-completions"))
            (process-environment
             (cons (format "NIX_GET_COMPLETIONS=%s" (1- (length pcomplete-args)))
                   process-environment))
            result)
        (apply 'call-process nix-executable nil (list stdout nil) nil
               (cdr pcomplete-args))
        (with-current-buffer stdout (setq result (buffer-string)))
        (kill-buffer stdout)
        (let ((lines (split-string result "\n"))
              completions)
          (dolist (val (cdr lines))
            (unless (string= val "")
              (setq completions (cons (car (split-string val "\t")) completions))))
          (dolist (val (cddr pcomplete-args))
            (pcomplete-here))
          (pcomplete-here completions nil t)))
    (progn
      (nix--pcomplete-flags nix-toplevel-options)
      (pcomplete-here nix-commands)
      (pcase (nth (1- pcomplete-index) pcomplete-args)
        ("run"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr" "-c" "--command"
                                         "-f" "--file" "-i" "-I" "--include"
                                         "-k" "--keep" "-u" "--unset"))))
        ("build"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr" "--dry-run"
                                         "-f" "--file" "-I" "--include"
                                         "--no-link" "-o" "--out-link"))))
        ("add-to-store"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--dry-run" "-n" "--name"))))
        ("copy"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--all" "--arg" "--argstr"
                                         "-f" "--file" "--from"
                                         "-I" "--include" "--no-check-sigs"
                                         "--no-recursive" "-s" "--substitute"
                                         "--to"))))
        ("copy-sigs"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--all" "--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"
                                         "-r" "--recursive" "-s" "--substituter"))))
        ("dump-path"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"))))
        ("edit"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"))))
        ("eval"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"
                                         "--json" "--raw"))))
        ("hash-file"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--base16" "--base32"
                                         "--base64" "--type"))))
        ("hash-path"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--base16" "--base32"
                                         "--base64" "--type"))))
        ("log"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"
                                         "--json" "--raw"))))
        ("ls-nar"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("-d" "--directory"
                                         "--json" "-l" "--long"
                                         "-R" "--recursive"))))
        ("ls-store"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("-d" "--directory"
                                         "--json" "-l" "--long"
                                         "-R" "--recursive"))))
        ("repl"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr"
                                         "-I" "--include"))))
        ("search"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr"
                                         "-f" "--file"
                                         "-I" "--include"
                                         "--json" "--no-cache"
                                         "-u" "--update-cache"))))
        ("show-config"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--json"))))
        ("show-derivation"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--arg" "--argstr"
                                         "-f" "--file"
                                         "-I" "--include"
                                         "-r" "--recursive"))))
        ("sign-paths"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--all" "--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"
                                         "-k" "--key-file" "-r" "--recursive"))))
        ("upgrade-nix"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("-p" "--profile"))))
        ("verify"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("--all" "--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"
                                         "--no-contents" "--no-trust"
                                         "-r" "--recursive" "-n" "--sigs-needed"
                                         "-s" "--substuter"))))
        ("why-depends"
         (nix--pcomplete-flags
          (append nix-toplevel-options '("-a" "--all" "--arg" "--argstr"
                                         "-f" "--file" "-I" "--include"))))
        (_ (nix--pcomplete-flags nix-toplevel-options)))
      (pcomplete-here (pcomplete-entries)))))

(defun nix--process (&rest args)
  (with-temp-buffer
    (let* ((tmpfile  (make-temp-file "nix--process-stderr"))
	 (cleaned-args (seq-filter #'stringp args))
	 (exitcode (apply #'call-process `(,nix-executable nil (t ,tmpfile) nil ,@cleaned-args )))
	 (stderr (with-temp-buffer
		   (insert-file-contents tmpfile)
		   (buffer-string))))
      (delete-file tmpfile)
      (list (buffer-string) stderr exitcode))))

(defun nix--process-string (&rest args)
  (cl-multiple-value-bind (stdout stderr exitcode) (apply #'nix--process args)
    (if (not (eq exitcode 0))
      (error stderr))
    ;; cut-off the trailing newline
    (string-trim-right stdout)))

(defun nix--process-json (&rest args)
  (json-read-from-string
    (apply #'nix--process-string args)))

(defun nix--process-lines (&rest args)
  (seq-filter (lambda (el) (not (string= "" el)))
    (split-string
      (apply #'nix--process-string args) "\n")))

(defun nix--process-json-nocheck (&rest args)
  ;; No checking of exitcode is possible here until
  ;; https://github.com/NixOS/nix/issues/2474 is resolved
  (let ((result (apply #'nix--process args)))
    (json-read-from-string (car result))))

(provide 'nix)
;;; nix.el ends here
