;;; slack-dialog.el ---                              -*- lexical-binding: t; -*-

;; Copyright (C) 2018

;; Author:  <yuya373@yuya373>
;; Keywords:

;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:

;;

;;; Code:

(require 'eieio)
(require 'slack-util)
(require 'slack-request)
(require 'slack-selectable)
(require 'slack-team)
(require 'slack-room)

(defclass slack-dialog ()
  ((title :initarg :title :type string)
   (callback-id :initarg :callback_id :type string)
   (elements :initarg :elements :type list)
   (state :initarg :state :type (or null string) :initform nil)
   (submit-label :initarg :submit_label :type string :initform "Submit")
   (notify-on-cancel :initarg :notify_on_cancel :type boolean :initform nil)
   (error-message :initarg :error-message :type (or null string) :initform nil)
   ))

(defclass slack-dialog-element ()
  ((name :initarg :name :type string)
   (label :initarg :label :type string)
   (type :initarg :type :type string)
   (optional :initarg :optional :type boolean :initform nil)
   (placeholder :initarg :placeholder :type (or null string) :initform nil)
   (value :initarg :value :type (or null string) :initform nil)
   (errors :initarg :errors :type list :initform '())))

(defclass slack-dialog-element-error ()
  ((name :initarg :name :type string)
   (error-message :initarg :error-message :type string)))

(defclass slack-dialog-text-element (slack-dialog-element)
  ((max-length :initarg :max_length :type number :initform 150)
   (min-length :initarg :min_length :type number :initform 0)
   (hint :initarg :hint :type (or null string) :initform nil)
   ;; one of email, number, tel or url
   (subtype :initarg :subtype :type (or null string) :initform nil)))

(defclass slack-dialog-textarea-element (slack-dialog-text-element)
  ((max-length :initarg :max_length :type number :initform 3000)))

(defclass slack-dialog-select-element (slack-dialog-element slack-selectable)
  ((min-query-length :initarg :min_query_length :type (or null number) :initform nil)))

(defclass slack-dialog-select-option (slack-selectable-option)
  ((label :initarg :label :type string)))

(defclass slack-dialog-select-option-group (slack-selectable-option-group)
  ((label :initarg :label :type string)))

(cl-defmethod slack-dialog-element-value ((this slack-dialog-element))
  (or (oref this value) ""))

(cl-defmethod slack-dialog-element-value ((this slack-dialog-select-element))
  (with-slots (data-source selected-options) this
    (or (cond
         ((string= data-source "external")
          (and selected-options
               (car selected-options)
               (oref (car selected-options) value)))
         (t (oref this value)))
        "")))

(cl-defmethod slack-equalp ((this slack-dialog-element) other)
  (string= (oref this name)
           (oref other name)))

(cl-defmethod slack-dialog-selected-option ((this slack-dialog-select-element))
  (with-slots (data-source value options selected-options) this
    (if (string= data-source "static")
        (cl-find-if #'(lambda (op) (string= (oref op value)
                                            value))
                    options)
      (and selected-options (car selected-options)))))

(cl-defmethod slack-selectable-text ((this slack-dialog-select-option))
  (oref this label))

(cl-defmethod slack-selectable-text ((this slack-dialog-select-option-group))
  (oref this label))

(cl-defmethod slack-selectable-prompt ((this slack-dialog-select-element))
  (format "%s :"
          (oref this label)))

(defun slack-dialog-text-element-create (payload)
  (apply #'make-instance 'slack-dialog-text-element
         (slack-collect-slots 'slack-dialog-text-element payload)))

(defun slack-dialog-textarea-element-create (payload)
  (apply #'make-instance 'slack-dialog-textarea-element
         (slack-collect-slots 'slack-dialog-textarea-element payload)))

(defun slack-dialog-select-element-create (payload)
  (let ((options
         (mapcar #'(lambda (e)
                     (apply #'make-instance 'slack-dialog-select-option
                            (slack-collect-slots 'slack-dialog-select-option
                                                 e)))
                 (plist-get payload :options)))
        (option-groups
         (mapcar #'(lambda (e)
                     (apply #'make-instance
                            'slack-dialog-select-option-group
                            (slack-collect-slots 'slack-dialog-select-option-group
                                                 e)))
                 (plist-get payload :option_groups))))
    (setq payload (plist-put payload :options options))
    (setq payload (plist-put payload :option_groups option-groups))
    (apply #'make-instance 'slack-dialog-select-element
           (slack-collect-slots 'slack-dialog-select-element
                                payload))))

(defun slack-dialog-element-create (payload)
  (let ((type (plist-get payload :type)))
    (cond
     ((string= type "select")
      (slack-dialog-select-element-create payload))
     ((string= type "text")
      (slack-dialog-text-element-create payload))
     ((string= type "textarea")
      (slack-dialog-textarea-element-create payload))
     (t (error "Unknown dialog element type: %s" type)))))

(defun slack-dialog-create (payload)
  (let ((elements (mapcar #'slack-dialog-element-create
                          (plist-get payload :elements))))
    (setq payload (plist-put payload :elements elements))
    (apply #'make-instance 'slack-dialog
           (slack-collect-slots 'slack-dialog payload))))

(cl-defmethod slack-dialog-element-validate ((this slack-dialog-element) value)
  (with-slots (optional label) this
    (when (and (not optional)
               (or (null value)
                   (< (length value) 1)))
      (error "%s must not be empty" label))))

(cl-defmethod slack-dialog-element-validate ((_this slack-dialog-select-element) _value)
  (cl-call-next-method))

(cl-defmethod slack-dialog-element-validate ((this slack-dialog-text-element) value)
  (cl-call-next-method)
  (with-slots (min-length max-length label) this
    (when (< max-length (length value))
      (error "%s must be less than %s" label max-length))
    (when (< (length value) min-length)
      (error "%s must be greater than %s" label min-length))))

(cl-defmethod slack-dialog-execute ((this slack-dialog-text-element) _dialog-id _team)
  (with-slots (hint value placeholder label optional) this
    (let* ((prompt (format "%s%s%s : "
                           label
                           (if hint (format " (%s)" hint) "")
                           (if optional " (optional)" "")))
           (value (read-from-minibuffer prompt value)))
      (slack-dialog-element-validate this value)
      value)))

(cl-defmethod slack-dialog-execute ((_this slack-dialog-textarea-element) _dialog-id _team)
  (cl-call-next-method))

(cl-defmethod slack-dialog-select-element-get-suggestions ((this slack-dialog-select-element)
                                                           dialog-id team after-success)
  (let* ((url "https://slack.com/api/dialog.selectSuggestion")
         (min-query-length (oref this min-query-length))
         (prompt (format "Type hints to see options (minimum: %s) : " min-query-length))
         (params (list (cons "dialog_id" dialog-id)
                       (cons "name" (oref this name))
                       (cons "value" (read-from-minibuffer prompt)))))
    (cl-labels
        ((on-success (&key data &allow-other-keys)
                     (slack-request-handle-error
                      (data "slack-dialog-select-fetch-suggestion")
                      (funcall after-success data))))
      (slack-request
       (slack-request-create
        url
        team
        :type "POST"
        :params params
        :sync t
        :success #'on-success)))))

(cl-defmethod slack-dialog-execute ((this slack-dialog-select-element) dialog-id team)
  (slack-if-let* ((selected (slack-dialog--execute this dialog-id team)))
      (cdr selected)))

(cl-defmethod slack-dialog--execute ((this slack-dialog-select-element) dialog-id team)
  (with-slots (data-source) this
    (cond
     ((string= data-source "external")
      (let ((result-option nil))
        (cl-labels
            ((log-error (err)
                        (slack-log (format "Error: %s" err) team :level 'error))
             (select (options)
                     (slack-if-let*
                         ((selected (completing-read "Choose an Option..."
                                                     (mapcar #'(lambda (option)
                                                                 (plist-get option :label))
                                                             options)
                                                     nil t)))
                         (cl-find-if #'(lambda (option) (string= selected
                                                                 (plist-get option :label)))
                                     options)))
             (after-success (data)
                            (slack-request-handle-error
                             (data "slack-dialog-execute"
                                   #'log-error)
                             (let ((option-groups (plist-get data :option_groups))
                                   (options (plist-get data :options)))
                               (when option-groups
                                 (slack-if-let* ((selected-option-group (select option-groups)))
                                     (setq options (plist-get selected-option-group :options))))
                               (when options
                                 (slack-if-let* ((selected (select options)))
                                     (setq result-option selected)))))))
          (slack-dialog-select-element-get-suggestions this
                                                       dialog-id
                                                       team
                                                       #'after-success)
          (if result-option
              (cons (plist-get result-option :label)
                    (plist-get result-option :value))))))
     ((string= data-source "conversations")
      (slack-if-let* ((rooms (append (slack-team-channels team)
                                     (slack-team-groups team)
                                     (slack-team-ims team)))
                      (room (slack-room-select rooms team)))
          (cons (slack-room-name room team)
                (oref room id))))
     ((string= data-source "channels")
      (slack-if-let* ((channels (slack-team-channels team))
                      (channel (slack-room-select channels team)))
          (cons (slack-room-name channel team)
                (oref channel id))))
     ((string= data-source "users")
      (slack-if-let* ((id (plist-get (slack--user-select team) :id)))
          (cons (slack-user-name id team)
                id)))
     ((string= "static" data-source)
      (slack-if-let* ((selected (slack-selectable-select-from-static-data-source this)))
          (cons (oref selected label)
                (oref selected value))))
     (t (error "Unknown element's data-source: %s" data-source))
     )))

(cl-defmethod slack-dialog--submit ((_this slack-dialog)
                                    dialog-id team submission
                                    &optional after-success)
  (let ((url "https://slack.com/api/dialog.submit")
        (params (list (cons "submission" (json-encode-alist submission))
                      (cons "dialog_id" dialog-id))))
    (cl-labels
        ((on-success (&key data &allow-other-keys)
                     (when (functionp after-success)
                       (funcall after-success data))))
      (slack-request
       (slack-request-create
        url
        team
        :type "POST"
        :params params
        :success #'on-success)))))

(cl-defmethod slack-dialog-submit ((this slack-dialog) dialog-id team)
  (with-slots (elements) this
    (let ((submission (mapcar #'(lambda (element)
                                  (let ((value (slack-dialog-execute element dialog-id team)))
                                    (cons (oref element name) value)))
                              elements)))
      (slack-dialog--submit this dialog-id team submission))))

(cl-defmethod slack-dialog-notify-cancel ((this slack-dialog) dialog-id team)
  (slack-if-let* ((url "https://slack.com/api/dialog.notifyCancel")
                  (params (list (cons "dialog_id" dialog-id)))
                  (notify-cancelp (oref this notify-on-cancel)))
      (cl-labels
          ((on-success (&key data &allow-other-keys)
                       (slack-request-handle-error
                        (data "slack-dialog-notify-cancel"))))
        (slack-request
         (slack-request-create
          url
          team
          :type "POST"
          :params params
          :success #'on-success)))))

(cl-defmethod slack-dialog-clear-errors ((this slack-dialog))
  (oset this error-message nil)
  (dolist (element (oref this elements))
    (oset element errors nil)))

(provide 'slack-dialog)
;;; slack-dialog.el ends here
