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

;; Copyright (C) 2018  南優也

;; Author: 南優也 <yuyaminami@minamiyuuya-no-MacBook.local>
;; 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-buffer)
(require 'slack-dialog)

(define-derived-mode slack-dialog-buffer-mode fundamental-mode "Slack Dialog Buffer"
  (setq-local default-directory slack-default-directory)
  (setq-local buffer-read-only t))

(defvar slack-dialog-submit-button-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "RET") #'slack-dialog-buffer-submit)
    (define-key map [mouse-1] #'slack-dialog-buffer-submit)
    map))

(defvar slack-dialog-cancel-button-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "RET") #'slack-dialog-buffer-cancel)
    (define-key map [mouse-1] #'slack-dialog-buffer-cancel)
    map))

(defvar slack-dialog-select-element-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "RET") #'slack-dialog-buffer-select)
    (define-key map [mouse-1] #'slack-dialog-buffer-select)
    map))

(defface slack-dialog-element-placeholder-face
  '((t (:inherit font-lock-comment-face :slant normal
        ;; :box (:line-width 1 :color "#fff")
        )))
  "Used to dialog's element placeholder"
  :group 'slack)

(defface slack-dialog-element-error-face
  '((t (:inherit font-lock-warning-face)))
  "Used to dialog's element error message"
  :group 'slack)

(defface slack-dialog-element-hint-face
  '((t (:inherit font-lock-comment-face :slant italic)))
  "Used to dialog's element hint"
  :group 'slack)

(defface slack-dialog-element-label-face
  '((t (:weight bold)))
  "Used to dialog's element label"
  :group 'slack)

(defface slack-dialog-select-element-input-face
  '((t (:box (:line-width 1 :style released-button))))
  "Used to dialog's select element input"
  :group 'slack)

(defface slack-dialog-title-face
  '((t (:weight bold :height 1.2)))
  "Used to dialog's title"
  :group 'slack)

(defface slack-dialog-submit-button-face
  '((t (:box (:line-width 1 :style released-button)
        :color "#2aa198")))
  "Used to dialog's submit button"
  :group 'slack)

(defface slack-dialog-cancel-button-face
  '((t (:box (:line-width 1 :style released-button))))
  "Used to dialog's cancel button"
  :group 'slack)

(defclass slack-dialog-buffer (slack-buffer)
  ((dialog-id :initarg :dialog-id :type string)
   (dialog :initarg :dialog :type slack-dialog)))

(cl-defmethod slack-buffer-name ((this slack-dialog-buffer))
  (let ((team (slack-buffer-team this)))
    (with-slots (dialog-id dialog) this
      (with-slots (title) dialog
        (format "*slack-dialog: %s [%s] : %s*"
                title
                dialog-id
                (slack-team-name team))))))

(cl-defmethod slack-buffer-key ((_class (subclass slack-dialog-buffer)) dialog-id &rest _args)
  dialog-id)

(cl-defmethod slack-buffer-key ((this slack-dialog-buffer))
  (slack-buffer-key 'slack-dialog-buffer (oref this dialog-id)))

(cl-defmethod slack-team-buffer-key ((_class (subclass slack-dialog-buffer)))
  'slack-dialog-buffer)

(cl-defmethod slack-buffer-insert-label ((this slack-dialog-element))
  (with-slots (label optional) this
    (insert (propertize label
                        'face 'slack-dialog-element-label-face))
    (when optional
      (insert " (optional)"))))

(cl-defmethod slack-buffer-insert-hint ((this slack-dialog-text-element))
  (with-slots (hint) this
    (when hint
      (insert "\n")
      (insert (propertize hint
                          'face 'slack-dialog-element-hint-face))
      (insert "\n"))))

(defvar slack-dialog-element-edit-button-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "RET") #'slack-dialog-buffer-open-edit-element-buffer)
    (define-key map [mouse-1] #'slack-dialog-buffer-open-edit-element-buffer)
    map))

(defun slack-dialog-buffer-open-edit-element-buffer ()
  (interactive)
  (slack-if-let* ((element (get-text-property (point) 'slack-dialog-element))
                  (buffer slack-current-buffer)
                  (team (slack-buffer-team buffer))
                  (edit-buffer (slack-create-dialog-element-edit-buffer buffer element team)))
      (slack-buffer-display edit-buffer)))

(defun slack-create-dialog-element-edit-buffer (dialog-buffer element team)
  (slack-if-let*
      ((buf (slack-buffer-find 'slack-dialog-edit-element-buffer team dialog-buffer element)))
      buf
    (make-instance 'slack-dialog-edit-element-buffer
                   :dialog-buffer dialog-buffer
                   :element element
                   :team-id (oref team id))))

(cl-defmethod slack-buffer-insert-edit-button ((this slack-dialog-text-element))
  (insert (propertize " Edit "
                      'face '(:box (:line-width 1 :style released-button))
                      'keymap slack-dialog-element-edit-button-map
                      'slack-dialog-element this)))

(cl-defmethod slack-buffer-insert-placeholder ((this slack-dialog-text-element))
  (with-slots (placeholder) this
    (insert (propertize placeholder
                        'face 'slack-dialog-element-placeholder-face))))

(cl-defmethod slack-buffer-insert-errors ((this slack-dialog-element))
  (with-slots (errors) this
    (mapc #'(lambda (err)
              (insert (propertize (oref err error-message)
                                  'face 'slack-dialog-element-error-face))
              (insert "\n"))
          errors)))

(cl-defmethod slack-buffer-insert ((this slack-dialog-text-element))
  (with-slots (value placeholder errors) this
    (slack-buffer-insert-label this)
    (insert " ")
    (slack-buffer-insert-edit-button this)
    (insert "\n")
    (if value
        (insert value)
      (if placeholder
          (slack-buffer-insert-placeholder this)
        (insert "Click Edit")))
    (insert "\n")
    (slack-buffer-insert-errors this)
    (slack-buffer-insert-hint this)))

(cl-defmethod slack-buffer-insert ((this slack-dialog-textarea-element))
  (with-slots (value placeholder) this
    (slack-buffer-insert-label this)
    (insert "  ")
    (slack-buffer-insert-edit-button this)
    (insert "\n")
    (if value
        (insert value)
      (if placeholder
          (slack-buffer-insert-placeholder this)
        (insert "Click Edit")))
    (insert "\n")
    (slack-buffer-insert-errors this)
    (slack-buffer-insert-hint this)))

(defun slack-dialog-buffer-select ()
  (interactive)
  (slack-if-let* ((buffer slack-current-buffer)
                  (team (slack-buffer-team buffer))
                  (dialog (oref buffer dialog))
                  (dialog-id (oref buffer dialog-id))
                  (element-name (get-text-property (point) 'slack-dialog-element-name))
                  (dialog-element (cl-find-if #'(lambda (el) (string= element-name
                                                                      (oref el name)))
                                              (oref dialog elements)))
                  (selected (slack-dialog--execute dialog-element
                                                   dialog-id
                                                   team))
                  (label (car selected))
                  (value (cdr selected))
                  (option (make-instance 'slack-dialog-select-option
                                         :label label
                                         :value value)))
      (progn
        (oset dialog-element selected-options (list option))
        (oset dialog-element value value)
        (slack-dialog-buffer-redisplay buffer))))

(cl-defmethod slack-buffer-insert-select-button ((this slack-dialog-select-element))
  (let ((label (slack-if-let*
                   ((selected (slack-dialog-selected-option this)))
                   (slack-selectable-text selected)
                 "Choose an option...")))

    (insert (propertize (format " %s " label)
                        'face 'slack-dialog-select-element-input-face
                        'keymap slack-dialog-select-element-map
                        'slack-dialog-element-name (oref this name)))))

(cl-defmethod slack-buffer-insert ((this slack-dialog-select-element))
  (slack-buffer-insert-label this)
  (insert "\n")
  (slack-buffer-insert-select-button this)
  (insert "\n")
  (slack-buffer-insert-errors this))

(defun slack-dialog-buffer-submit ()
  (interactive)
  (slack-if-let*
      ((buffer slack-current-buffer))
      (slack-dialog-buffer--submit buffer)))

(cl-defmethod slack-dialog-buffer--submit ((this slack-dialog-buffer))
  (let* ((dialog (oref this dialog))
         (dialog-id (oref this dialog-id))
         (team (slack-buffer-team this))
         (elements (oref dialog elements)))
    (dolist (element elements)
      (let ((value (slack-dialog-element-value element)))
        (slack-dialog-element-validate element value)))
    (let ((params (mapcar #'(lambda (element)
                              (cons (oref element name)
                                    (slack-dialog-element-value element)))
                          elements)))
      (cl-labels
          ((create-dialog-element-error
            (payload)
            (make-instance #'slack-dialog-element-error
                           :name (plist-get payload :name)
                           :error-message (plist-get payload :error)))
           (set-dialog-element-error
            (dialog-error elements)
            (slack-if-let*
                ((element (cl-find-if #'(lambda (el)
                                          (string= (oref el name)
                                                   (oref dialog-error name)))
                                      elements))
                 (new-errors (cons dialog-error
                                   (cl-remove-if #'(lambda (e)
                                                     (string= (oref e name)
                                                              (oref dialog-error
                                                                    name)))
                                                 (oref element errors)))))
                (oset element errors new-errors)))
           (after-success
            (data)
            (slack-if-let* ((err (plist-get data :error)))
                (progn
                  (oset dialog error-message err)
                  (dolist (dialog-error (mapcar #'create-dialog-element-error
                                                (plist-get data :dialog_errors)))
                    (set-dialog-element-error dialog-error elements))

                  (slack-dialog-buffer-redisplay this))
              (slack-dialog-buffer-kill-buffer this))))
        (slack-dialog-clear-errors dialog)
        (slack-dialog--submit dialog dialog-id team params #'after-success)))))

(defun slack-dialog-buffer-cancel ()
  (interactive)
  (slack-if-let* ((buffer slack-current-buffer)
                  (team (slack-buffer-team buffer)))
      (with-slots (dialog dialog-id) buffer
        (slack-dialog-notify-cancel dialog dialog-id team)
        (slack-dialog-buffer-kill-buffer buffer))))

(cl-defmethod slack-dialog-buffer-kill-buffer ((this slack-dialog-buffer))
  (slack-if-let* ((buffer-name (slack-buffer-name this))
                  (buf (get-buffer buffer-name))
                  (win (get-buffer-window buf)))
      (progn
        (kill-buffer buf)
        (when (< 1 (count-windows))
          (delete-window win)))))

(cl-defmethod slack-buffer-insert ((this slack-dialog-buffer))
  (with-slots (dialog) this
    (with-slots (error-message title elements submit-label) dialog
      (let ((inhibit-read-only t))
        (insert (propertize title
                            'face 'slack-dialog-title-face))
        (when error-message
          (insert "\n")
          (insert (propertize error-message
                              'face 'slack-dialog-element-error-face)))
        (insert "\n\n")
        (mapc #'(lambda (el)
                  (slack-buffer-insert el)
                  (insert "\n"))
              elements)
        (insert "\n")
        (insert (propertize " Cancel "
                            'face 'slack-dialog-cancel-button-face
                            'keymap slack-dialog-cancel-button-map))
        (insert "\t")
        (insert (propertize (format " %s " submit-label)
                            'face 'slack-dialog-submit-button-face
                            'keymap slack-dialog-submit-button-map))
        (goto-char (point-min))))))

(cl-defmethod slack-buffer-init-buffer ((this slack-dialog-buffer))
  (let* ((buf (cl-call-next-method)))
    (with-current-buffer buf
      (slack-dialog-buffer-mode)
      (slack-buffer-set-current-buffer this)
      (slack-buffer-insert this))
    buf))

(defun slack-create-dialog-buffer (dialog-id dialog team)
  (slack-if-let*
      ((buf (slack-buffer-find 'slack-dialog-buffer team dialog-id dialog)))
      buf
    (make-instance 'slack-dialog-buffer
                   :dialog-id dialog-id
                   :dialog dialog
                   :team-id (oref team id))))

(cl-defmethod slack-dialog-buffer-save-element-value ((this slack-dialog-buffer) name value)
  (with-slots (dialog) this
    (with-slots (elements) dialog
      (let ((element (cl-find-if #'(lambda (el)
                                     (string= name
                                              (oref el name)))
                                 elements)))
        (oset element value value)
        (slack-dialog-buffer-redisplay this)))))

(cl-defmethod slack-dialog-buffer-redisplay ((this slack-dialog-buffer))
  (slack-if-let* ((bufname (slack-buffer-name this))
                  (buf (get-buffer bufname)))
      (with-current-buffer buf
        (let ((inhibit-read-only t)
              (cur-point (point)))
          (delete-region (point-min) (point-max))
          (slack-buffer-insert this)
          (when (and (< (point-min) cur-point)
                     (< cur-point (point-max)))
            (goto-char cur-point))))))

(defun slack-dialog-get (id team)
  (let ((url "https://slack.com/api/dialog.get")
        (params (list (cons "dialog_id" id))))
    (cl-labels
        ((on-success (&key data &allow-other-keys)
                     (slack-request-handle-error
                      (data "slack-dialog-get")
                      (slack-if-let*
                          ((payload (plist-get data :dialog))
                           (dialog (slack-dialog-create payload)))
                          ;; (slack-dialog-submit dialog id team)
                          (slack-buffer-display
                           (slack-create-dialog-buffer id
                                                       dialog
                                                       team))))))
      (slack-request
       (slack-request-create
        url
        team
        :type "POST"
        :params params
        :success #'on-success)))))

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