;;; jao-mpc.el --- Using mpc to interact with mpd   -*- lexical-binding: t; -*-

;; Copyright (C) 2021, 2022  jao

;; Author: jao <mail@jao.io>
;; Keywords: convenience
;; Version: 0.1
;; Package-requires: ((emacs "27.1"))
;; URL: https://codeberg.org/jao/lib/media

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

;; Simple mpd interaction using mpc.

;;; Code:

(require 'jao-themes)
(require 'jao-lyrics)
(require 'jao-random-album)

(defconst jao-mpc--albums "*MPC Albums*")
(defconst jao-mpc--playlist "*MPC Playlist*")

(defvar jao-mpc-port 6600)
(defvar-local jao-mpc--port nil)

(defun jao-mpc--cmd (cmd &optional port)
  (let ((port (or port jao-mpc--port jao-mpc-port)))
    (shell-command-to-string (format "mpc -p %s %s" port cmd))))

(defconst jao-mpc--fields
  '(artist album composer originaldate genre title track position time name))

(defconst jao-mpc--stfmt
  (mapconcat (lambda (f) (format "%s:::%%%s%%" f f)) jao-mpc--fields "\n"))

(defun jao-mpc--current (&optional port)
  (let ((s (jao-mpc--cmd (format "-f '%s' current" jao-mpc--stfmt) port))
        (res))
    (dolist (s (split-string s "\n" t " ") res)
      (when (string-match "\\(.+\\):::\\(.+\\)" s)
        (push (cons (intern (match-string 1 s)) (match-string 2 s)) res)))))

(defun jao-mpc--playing-p (&optional port)
  (not (string-blank-p (jao-mpc--cmd "status|grep '\\[playing\\]'" port))))

(defun jao-mpc--queue-len (&optional port)
  (string-to-number (jao-mpc--cmd "playlist|wc -l" port)))

(defsubst jao--put-face (str face)
  (put-text-property 0 (length str) 'face face str)
  str)

(defun jao-mpc--current-str (&optional port current len)
  (let* ((current (or current (jao-mpc--current port)))
         (len (or len (jao-mpc--queue-len port)))
         (title (alist-get 'title current (alist-get 'name current "")))
         (album (alist-get 'album current))
         (artist (alist-get 'artist current))
         (composer (alist-get 'composer current))
         (no (string-to-number (alist-get 'position current "0")))
         (time (alist-get 'time current "")))
    (format "> %s%s %s%s%s%s" ;; 
            (jao--put-face (if (zerop no) "" (format "%02d/%s " no len))
                           'jao-themes-f02)
            (jao--put-face title 'jao-themes-f00)
            (jao--put-face artist 'jao-themes-f01)
            (jao--put-face (if composer (format " [%s]" composer) "")
                           'jao-themes-f01)
            (jao--put-face (if album (format " (%s)" album) "") 'jao-themes-f11)
            (if (string-blank-p time)
                ""
              (jao--put-face (format " [%s]" time) 'jao-themes-dimm)))))

(defvar jao-mpc-minibuffer-str "")

(defun jao-mpc--set-current-str (&optional port)
  (setq jao-mpc-minibuffer-str
        (if (jao-mpc--playing-p port)
            (jao-mpc--current-str port)
          (when (and (null port) jao-random-album-p (not (jao-mpc--current)))
            (jao-random-album-next))
          ""))
  (jao-minibuffer-refresh))

(defvar jao-mpc--idle-procs nil)

(defun jao-mpc--idle-loop (&optional port)
  (when-let (proc (alist-get port jao-mpc--idle-procs))
    (ignore-errors (kill-process proc)))
  (setf (alist-get port jao-mpc--idle-procs nil t)
        (make-process :name (format "jao-mpc-idleloop (%s)" port)
                      :buffer nil
                      :noquery t
                      :command `("mpc" "-p" ,(format "%s" (or port jao-mpc-port))
                                 "idleloop" "player")
                      :filter (lambda (_p _s) (jao-mpc--set-current-str port)))))

(define-derived-mode jao-mpc-albums-mode fundamental-mode "MPC Albums"
  "Mode to display the list of albums known by mpd."
  (read-only-mode -1)
  (delete-region (point-min) (point-max))
  (insert (jao-mpc--cmd "list album"))
  (goto-char (point-min))
  (read-only-mode 1))

(defun jao-mpc--album-buffer ()
  (if-let (b (get-buffer jao-mpc--albums))
      b
    (with-current-buffer (get-buffer-create jao-mpc--albums)
      (jao-mpc-albums-mode)
      (current-buffer))))

(defun jao-mpc--add-and-play (&optional album)
  (interactive)
  (let ((album (or album (string-trim (thing-at-point 'line)))))
    (jao-mpc--cmd "clear")
    (jao-mpc--cmd (format "findadd album \"%s\"" album))
    (jao-mpc--cmd "play")))

(define-key jao-mpc-albums-mode-map (kbd "n") #'next-line)
(define-key jao-mpc-albums-mode-map (kbd "p") #'previous-line)
(define-key jao-mpc-albums-mode-map (kbd "RET") #'jao-mpc--add-and-play)
(define-key jao-mpc-albums-mode-map (kbd "q") #'bury-buffer)

(define-derived-mode jao-mpc-playlist-mode nil "MPC Playlist"
  "Mode to display the list of playlist known by mpd."
  (read-only-mode -1)
  (delete-region (point-min) (point-max))
  (setq-local jao-mpc--port jao-mpc-port)
  (insert (jao-mpc--cmd "playlist"))
  (goto-char (point-min))
  (display-line-numbers-mode 1)
  (read-only-mode 1))

(defun jao-mpc--playlist-goto-current ()
  (interactive)
  (let ((c (string-trim (or (jao-mpc--cmd "current") ""))))
    (unless (string-blank-p c)
      (goto-char (point-min))
      (when (re-search-forward (regexp-quote c) nil t)
        (beginning-of-line)))))

(defun jao-mpc--playlist-play ()
  (interactive)
  (jao-mpc--cmd (format "play %s" (line-number-at-pos))))

(define-key jao-mpc-playlist-mode-map (kbd "n") #'next-line)
(define-key jao-mpc-playlist-mode-map (kbd "p") #'previous-line)
(define-key jao-mpc-playlist-mode-map (kbd "q") #'bury-buffer)
(define-key jao-mpc-playlist-mode-map (kbd ".") #'jao-mpc--playlist-goto-current)
(define-key jao-mpc-playlist-mode-map (kbd "RET") #'jao-mpc--playlist-play)
(define-key jao-mpc-playlist-mode-map (kbd "C") #'jao-mpc-clear)

(defun jao-mpc--playlist-buffer (&optional port)
  (with-current-buffer (get-buffer-create jao-mpc--playlist)
    (let ((jao-mpc-port (or port jao-mpc-port))) (jao-mpc-playlist-mode))
    (current-buffer)))

;;;###autoload
(defun jao-mpc-stop (&optional port)
  (interactive)
  (jao-mpc--cmd "stop" port))

;;;###autoload
(defun jao-mpc-toggle (&optional port)
  (interactive)
  (jao-mpc--cmd "toggle" port))

;;;###autoload
(defun jao-mpc-play (&optional port)
  (interactive)
  (jao-mpc--cmd "play" port))

;;;###autoload
(defun jao-mpc-next (&optional port)
  (interactive)
  (jao-mpc--cmd "next" port))

;;;###autoload
(defun jao-mpc-previous (&optional port)
  (interactive)
  (jao-mpc--cmd "prev" port))

;;;###autoload
(defun jao-mpc-seek (delta &optional port)
  (interactive "nDelta: ")
  (jao-mpc--cmd (format "seek %s%s" (if (> delta 0) "+" "") delta) port))

;;;###autoload
(defun jao-mpc-clear (&optional port)
  (interactive)
  (jao-mpc--cmd "clear" port))

;;;###autoload
(defun jao-mpc-echo-current (&optional port)
  (interactive)
  (jao-notify (jao-mpc--current-str port)))

;;;###autoload
(defun jao-mpc-add-url (url)
  (interactive "sURL: ")
  (jao-mpc--cmd (format "add %s" url)))

;;;###autoload
(defun jao-mpc-show-albums ()
  "Show album list."
  (interactive)
  (pop-to-buffer (jao-mpc--album-buffer)))

;;;###autoload
(defun jao-mpc-show-playlist (&optional port)
  "Show current playlist."
  (interactive)
  (pop-to-buffer (jao-mpc--playlist-buffer port))
  (jao-mpc--playlist-goto-current))

;;;###autoload
(defun jao-mpc-lyrics-track-data (&optional port)
  (let ((c (string-trim (jao-mpc--cmd "current" port))))
    (unless (string-blank-p c)
      (when (string-match "\\(.+\\) - \\(.+\\)" c)
        (cons (match-string 1 c) (match-string 2 c))))))

;;;###autoload
(defun jao-mpc-connect (&optional port)
  (interactive)
  (jao-mpc--idle-loop port)
  (when (jao-mpc--playing-p port) (jao-mpc--set-current-str port)))

;;;###autoload
(defun jao-mpc-setup (&optional secondary-port priority)
  (setq jao-lyrics-info-function #'jao-mpc-lyrics-track-data)
  (jao-random-album-setup #'jao-mpc--album-buffer
                          #'jao-mpc--add-and-play
                          #'jao-mpc-stop
                          jao-notify-audio-icon)
  (jao-mpc-connect)
  (when secondary-port (jao-mpc-connect secondary-port))
  (when priority
    (if (> priority 0)
        (jao-minibuffer-add-variable 'jao-mpc-minibuffer-str priority)
      (jao-minibuffer-add-msg-variable 'jao-mpc-minibuffer-str (- priority)))))

(defvar jao-mpc--album-titles nil)
(defconst jao-mpc--albums-cmd
  "-f '%album% - %artist%' find \"(ALBUM =~ '.*')\" | uniq")

;;;###autoload
(defun jao-mpc-select-album (refresh)
  (interactive "P")
  (let ((albums (or (and (not refresh) jao-mpc--album-titles)
                    (setq jao-mpc--album-titles
                          (split-string (jao-mpc--cmd jao-mpc--albums-cmd)
                                        "\n" t)))))
    (when-let (album (completing-read "Play album: " albums nil t))
      (jao-mpc--add-and-play (car (split-string album "-" t " "))))))

(provide 'jao-mpc)
;;; jao-mpc.el ends here