;;; guix-hydra.el --- Common code for interacting with Hydra  -*- lexical-binding: t -*-

;; Copyright © 2015 Alex Kost <alezost@gmail.com>

;; This file is part of GNU Guix.

;; GNU Guix 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.

;; GNU Guix 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:

;; This file provides some general code for 'list'/'info' interfaces for
;; Hydra (Guix build farm).

;;; Code:

(require 'json)
(require 'guix-buffer)
(require 'guix-entry)
(require 'guix-utils)
(require 'guix-help-vars)

(guix-define-groups hydra)

(defvar guix-hydra-job-regexp
  (concat ".*\\." (regexp-opt guix-help-system-types) "\\'")
  "Regexp matching a full name of Hydra job (including system).")

(defun guix-hydra-message (entries search-type &rest _)
  "Display a message after showing Hydra ENTRIES."
  ;; XXX Add more messages maybe.
  (when (null entries)
    (if (eq search-type 'fake)
        (message "The update is impossible due to lack of Hydra API.")
      (message "Hydra has returned no results."))))

(defun guix-hydra-list-describe (ids)
  "Describe 'hydra' entries with IDS (list of identifiers)."
  (guix-buffer-display-entries
   (guix-entries-by-ids ids (guix-buffer-current-entries))
   'info (guix-buffer-current-entry-type)
   ;; Hydra does not provide an API to receive builds/jobsets by
   ;; IDs/names, so we use a 'fake' search type.
   '(fake)
   'add))


;;; Readers

(defvar guix-hydra-projects
  '("gnu" "guix")
  "List of available Hydra projects.")

(guix-define-readers
 :completions-var guix-hydra-projects
 :single-reader guix-hydra-read-project
 :single-prompt "Project: ")

(guix-define-readers
 :single-reader guix-hydra-read-jobset
 :single-prompt "Jobset: ")

(guix-define-readers
 :single-reader guix-hydra-read-job
 :single-prompt "Job: ")

(guix-define-readers
 :completions-var guix-help-system-types
 :single-reader guix-hydra-read-system
 :single-prompt "System: ")


;;; Defining URLs

(defvar guix-hydra-url "http://hydra.gnu.org"
  "URL of the Hydra build farm.")

(defun guix-hydra-url (&rest url-parts)
  "Return Hydra URL."
  (apply #'concat guix-hydra-url "/" url-parts))

(defun guix-hydra-api-url (type args)
  "Return URL for receiving data using Hydra API.
TYPE is the name of an allowed method.
ARGS is alist of (KEY . VALUE) pairs.
Skip ARG, if VALUE is nil or an empty string."
  (declare (indent 1))
  (let* ((fields (mapcar
                  (lambda (arg)
                    (pcase arg
                      (`(,key . ,value)
                       (unless (or (null value)
                                   (equal "" value))
                         (concat (guix-hexify key) "="
                                 (guix-hexify value))))
                      (_ (error "Wrong argument '%s'" arg))))
                  args))
         (fields (mapconcat #'identity (delq nil fields) "&")))
    (guix-hydra-url "api/" type "?" fields)))


;;; Receiving data from Hydra

(defun guix-hydra-receive-data (url)
  "Return output received from URL and processed with `json-read'."
  (with-temp-buffer
    (url-insert-file-contents url)
    (goto-char (point-min))
    (let ((json-key-type 'symbol)
          (json-array-type 'list)
          (json-object-type 'alist))
      (json-read))))

(defun guix-hydra-get-entries (entry-type search-type &rest args)
  "Receive ENTRY-TYPE entries from Hydra.
SEARCH-TYPE is one of the types defined by `guix-hydra-define-interface'."
  (unless (eq search-type 'fake)
    (let* ((url         (apply #'guix-hydra-search-url
                               entry-type search-type args))
           (raw-entries (guix-hydra-receive-data url))
           (entries     (guix-hydra-filter-entries
                         raw-entries
                         (guix-hydra-filters entry-type))))
      entries)))


;;; Filters for processing raw entries

(defun guix-hydra-filter-entries (entries filters)
  "Filter ENTRIES using FILTERS.
Call `guix-modify' on each entry from ENTRIES."
  (mapcar (lambda (entry)
            (guix-modify entry filters))
          entries))

(defun guix-hydra-filter-names (entry name-alist)
  "Replace names of ENTRY parameters using NAME-ALIST.
Each element of NAME-ALIST is (OLD-NAME . NEW-NAME) pair."
  (mapcar (lambda (param)
            (pcase param
              (`(,name . ,val)
               (let ((new-name (guix-assq-value name-alist name)))
                 (if new-name
                     (cons new-name val)
                   param)))))
          entry))

(defun guix-hydra-filter-boolean (entry params)
  "Convert number PARAMS (0/1) of ENTRY to boolean values (nil/t)."
  (mapcar (lambda (param)
            (pcase param
              (`(,name . ,val)
               (if (memq name params)
                   (cons name (guix-number->bool val))
                 param))))
          entry))


;;; Wrappers for defined variables

(defvar guix-hydra-entry-type-data nil
  "Alist with hydra entry type data.
This alist is filled by `guix-hydra-define-entry-type' macro.")

(defun guix-hydra-entry-type-value (entry-type symbol)
  "Return SYMBOL's value for ENTRY-TYPE from `guix-hydra'."
  (symbol-value (guix-assq-value guix-hydra-entry-type-data
                                 entry-type symbol)))

(defun guix-hydra-search-url (entry-type search-type &rest args)
  "Return URL to receive ENTRY-TYPE entries from Hydra."
  (apply (guix-assq-value (guix-hydra-entry-type-value
                           entry-type 'search-types)
                          search-type)
         args))

(defun guix-hydra-filters (entry-type)
  "Return a list of filters for ENTRY-TYPE."
  (guix-hydra-entry-type-value entry-type 'filters))


;;; Interface definers

(defmacro guix-hydra-define-entry-type (entry-type &rest args)
  "Define general code for ENTRY-TYPE.
Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...

Required keywords:

  - `:search-types' - default value of the generated
    `guix-ENTRY-TYPE-search-types' variable.

Optional keywords:

  - `:filters' - default value of the generated
    `guix-ENTRY-TYPE-filters' variable.

  - `:filter-names' - if specified, a generated
    `guix-ENTRY-TYPE-filter-names' function for filtering these
    names will be added to `guix-ENTRY-TYPE-filters' variable.

  - `:filter-boolean-params' - if specified, a generated
    `guix-ENTRY-TYPE-filter-boolean' function for filtering these
    names will be added to `guix-ENTRY-TYPE-filters' variable.

The rest keyword arguments are passed to
`guix-define-entry-type' macro."
  (declare (indent 1))
  (let* ((entry-type-str     (symbol-name entry-type))
         (prefix             (concat "guix-" entry-type-str))
         (search-types-var   (intern (concat prefix "-search-types")))
         (filters-var        (intern (concat prefix "-filters")))
         (get-fun            (intern (concat prefix "-get-entries"))))
    (guix-keyword-args-let args
        ((search-types-val   :search-types)
         (filters-val        :filters)
         (filter-names-val   :filter-names)
         (filter-bool-val    :filter-boolean-params))
      `(progn
         (defvar ,search-types-var ,search-types-val
           ,(format "\
Alist of search types and according URL functions.
Functions are used to define URL to receive '%s' entries."
                    entry-type-str))

         (defvar ,filters-var ,filters-val
           ,(format "\
List of filters for '%s' parameters.
Each filter is a function that should take an entry as a single
argument, and should also return an entry."
                    entry-type-str))

         ,(when filter-bool-val
            (let ((filter-bool-var (intern (concat prefix
                                                   "-filter-boolean-params")))
                  (filter-bool-fun (intern (concat prefix
                                                   "-filter-boolean"))))
              `(progn
                 (defvar ,filter-bool-var ,filter-bool-val
                   ,(format "\
List of '%s' parameters that should be transformed to boolean values."
                            entry-type-str))

                 (defun ,filter-bool-fun (entry)
                   ,(format "\
Run `guix-hydra-filter-boolean' with `%S' variable."
                            filter-bool-var)
                   (guix-hydra-filter-boolean entry ,filter-bool-var))

                 (setq ,filters-var
                       (cons ',filter-bool-fun ,filters-var)))))

         ;; Do not move this clause up!: name filtering should be
         ;; performed before any other filtering, so this filter should
         ;; be consed after the boolean filter.
         ,(when filter-names-val
            (let* ((filter-names-var (intern (concat prefix
                                                     "-filter-names")))
                   (filter-names-fun filter-names-var))
              `(progn
                 (defvar ,filter-names-var ,filter-names-val
                   ,(format "\
Alist of '%s' parameter names returned by Hydra API and names
used internally by the elisp code of this package."
                            entry-type-str))

                 (defun ,filter-names-fun (entry)
                   ,(format "\
Run `guix-hydra-filter-names' with `%S' variable."
                            filter-names-var)
                   (guix-hydra-filter-names entry ,filter-names-var))

                 (setq ,filters-var
                       (cons ',filter-names-fun ,filters-var)))))

         (defun ,get-fun (search-type &rest args)
           ,(format "\
Receive '%s' entries.
See `guix-hydra-get-entries' for details."
                    entry-type-str)
           (apply #'guix-hydra-get-entries
                  ',entry-type search-type args))

         (guix-alist-put!
          '((search-types . ,search-types-var)
            (filters      . ,filters-var))
          'guix-hydra-entry-type-data ',entry-type)

         (guix-define-entry-type ,entry-type
           :parent-group guix-hydra
           :parent-faces-group guix-hydra-faces
           ,@%foreign-args)))))

(defmacro guix-hydra-define-interface (buffer-type entry-type &rest args)
  "Define BUFFER-TYPE interface for displaying ENTRY-TYPE entries.

This macro should be called after calling
`guix-hydra-define-entry-type' with the same ENTRY-TYPE.

ARGS are passed to `guix-BUFFER-TYPE-define-interface' macro."
  (declare (indent 2))
  (let* ((entry-type-str  (symbol-name entry-type))
         (buffer-type-str (symbol-name buffer-type))
         (get-fun         (intern (concat "guix-" entry-type-str
                                          "-get-entries")))
         (definer         (intern (concat "guix-" buffer-type-str
                                          "-define-interface"))))
    `(,definer ,entry-type
       :get-entries-function ',get-fun
       :message-function 'guix-hydra-message
       ,@args)))

(defmacro guix-hydra-info-define-interface (entry-type &rest args)
  "Define 'info' interface for displaying ENTRY-TYPE entries.
See `guix-hydra-define-interface'."
  (declare (indent 1))
  `(guix-hydra-define-interface info ,entry-type
     ,@args))

(defmacro guix-hydra-list-define-interface (entry-type &rest args)
  "Define 'list' interface for displaying ENTRY-TYPE entries.
Remaining arguments (ARGS) should have a form [KEYWORD VALUE] ...

Optional keywords:

  - `:describe-function' - default value of the generated
    `guix-ENTRY-TYPE-list-describe-function' variable (if not
    specified, use `guix-hydra-list-describe').

The rest keyword arguments are passed to
`guix-hydra-define-interface' macro."
  (declare (indent 1))
  (guix-keyword-args-let args
      ((describe-val :describe-function))
    `(guix-hydra-define-interface list ,entry-type
       :describe-function ,(or describe-val ''guix-hydra-list-describe)
       ,@args)))


(defvar guix-hydra-font-lock-keywords
  (eval-when-compile
    `((,(rx "(" (group (or "guix-hydra-define-entry-type"
                           "guix-hydra-define-interface"
                           "guix-hydra-info-define-interface"
                           "guix-hydra-list-define-interface"))
            symbol-end)
       . 1))))

(font-lock-add-keywords 'emacs-lisp-mode guix-hydra-font-lock-keywords)

(provide 'guix-hydra)

;;; guix-hydra.el ends here