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

;; Copyright (C) 2021, 2022, 2024, 2025, 2026  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)
(require 'jao-notify)

(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))
         (r (shell-command-to-string (format "mpc -p %s %s" port cmd))))
    (replace-regexp-in-string "^\\(warning: \\)?MPD .+\n" "" r)))

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

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

(defconst jao-mpc--stfmt
  (jao-mpc--fformat
   '(artist album composer originaldate genre title track name file)))

(defconst jao-mpc--stfmtt
  (jao-mpc--fformat '(currenttime totaltime percenttime songpos length)))

(defmacro jao-mpc--parse-fields (res-str res)
  `(dolist (s (split-string ,res-str "\n" t " ") ,res)
     (when (string-match "\\(.+\\):::\\(.+\\)" s)
       (push (cons (intern (match-string 1 s)) (match-string 2 s)) ,res))))

(defun jao-mpc--current (&optional port)
  (let ((s (jao-mpc--cmd (format "-f '%s' current" jao-mpc--stfmt) port))
        (st (jao-mpc--cmd (format "status '%s'" jao-mpc--stfmtt)))
        (res))
    (jao-mpc--parse-fields s res)
    (jao-mpc--parse-fields st res)))

(defsubst jao-mpc-status (&optional port)
  (string-trim (jao-mpc--cmd "status %state%" port)))

(defsubst jao-mpc-playing-p (&optional port)
  (string-prefix-p "playing" (jao-mpc-status port)))

(defsubst jao-mpc--queue-len (&optional port)
  (string-to-number (jao-mpc--cmd "status %length%" port)))

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

(defun jao-mpc--current-timestr (playing-times &optional current)
  (let* ((current (or current (jao-mpc--current)))
         (time (alist-get 'totaltime current "")))
    (if playing-times
        (format "%s/%s%s"
                (alist-get 'currenttime current "")
                time
                (alist-get 'percenttime current ""))
      (format "%s" time))))

(defun jao-mpc--current-str (&optional port times)
  (if-let* ((current (jao-mpc--current port))
            (title (alist-get 'title current (alist-get 'name current))))
    (let ((len (alist-get 'length current "0"))
          (album (alist-get 'album current))
          (artist (alist-get 'artist current))
          (composer (alist-get 'composer current))
          (no (string-to-number (alist-get 'songpos current "0")))
          (tims (concat " [" (jao-mpc--current-timestr times current) "]")))
      (format "%s%s %s%s%s%s" ;; 
              (jao--put-face (if (zerop no) "" (format "%d/%s " no len))
                             'jao-themes-f02)
              (jao--put-face (or title "") 'jao-themes-f00)
              (jao--put-face (or 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)
              (jao--put-face tims (if times 'jao-themes-f00 'jao-themes-dimm))))
    ""))

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

(defun jao-mpc--set-current-str (&optional port)
  (let ((status (or (jao-mpc-status port) "")))
    (setq jao-mpc-minibuffer-str
          (if (string= "playing" status) (jao-mpc--current-str port) ""))
    (when (and jao-random-album-active
               (or (string= status "stopped") (string= status "paused"))
               (string= "0\n" (jao-mpc--cmd "status %songpos%" port)))
      (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)))))

(defvar jao-mpc--browser-port nil)

(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" jao-mpc--browser-port))
  (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 port idp)
  (interactive)
  (let ((a (or album (string-trim (thing-at-point 'line))))
        (p (or port jao-mpc--browser-port)))
    (jao-mpc--cmd "clear" p)
    (jao-mpc--cmd (if idp (concat "add " a) (format "findadd album \"%s\"" a))
                  p)
    (jao-mpc--cmd "play" p)))

(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)))

(defun jao-mpc--with-delayed-random-album (cmd port)
  (let ((st jao-random-album-active))
    (setq jao-random-album-active nil)
    (jao-mpc--cmd cmd port)
    (accept-process-output nil 0.5)
    (setq jao-random-album-active st)))

;;;###autoload
(defun jao-mpc-stop (&optional port)
  (interactive)
  (jao-mpc--with-delayed-random-album "stop" port))

;;;###autoload
(defun jao-mpc-toggle (&optional port)
  (interactive)
  (jao-mpc--with-delayed-random-album "toggle" port))

;;;###autoload
(defun jao-mpc-pause (&optional port)
  (interactive)
  (jao-mpc--with-delayed-random-album "pause" 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)
  (message "%s" (jao-mpc--current-str port t)))

;;;###autoload
(defun jao-mpc-echo-current-times (&optional port)
  (interactive)
  (message "Playing time: %s" (jao-mpc--current-timestr t)))

;;;###autoload
(defun jao-mpc-notify (&optional port)
  (interactive)
  (when-let* ((current (jao-mpc--current)))
    (let* ((artist (or (alist-get 'artist current) "Anonymous"))
           (title (or (alist-get 'title current)
                      (when-let* ((tl (alist-get 'file current)))
                        (file-name-base tl))
                      ""))
           (album (or (alist-get 'album current) ""))
           (track (or (alist-get 'track current) ""))
           (times (jao-mpc--current-timestr t current)))
      (jao-notify (format "%s -- %s" artist times)
                  (format "%s %s" track title)
                  jao-notify-audio-icon
                  album))))

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

;;;###autoload
(defun jao-mpc-add-or-play-url (url &optional play)
  "Add the given URL to mpc's playing list, or just play it."
  (let ((p (or play (yes-or-no-p (format "Play %s right now?" url)))))
    (when p (jao-mpc-clear))
    (jao-mpc-add-url url)
    (if p (jao-mpc-play) (message "%s added to mpc queue" url))))

(defvar jao-mpc-stream-urls
  '(("classic fm" . "http://media-ice.musicradio.com:80/ClassicFMMP3")
    ("wcpe" . "http://audio-mp3.ibiblio.org:8000/wcpe.mp3")
    ("davide of mimic" . "http://streaming01.zfast.co.uk:8018/stream")
    ("cinemix" . "http://94.23.51.96:8000") ;; 209.9.238.4:6022 209.9.238.4:6046
    ("bbc gold" . "http://media-ice.musicradio.com:80/GoldMP3")
    ("irish gold" . "http://icecast2.rte.ie/gold")))

;;;###autoload
(defun jao-mpc-play-stream ()
  "Select a predefined stream URL and add or play it in mpc."
  (interactive)
  (let ((s (completing-read "Stream: " jao-mpc-stream-urls)))
    (jao-mpc-add-or-play-url (cdr (assoc s jao-mpc-stream-urls)) t)))

;;;###autoload
(defun jao-mpc-show-albums (&optional port)
  "Show album list."
  (interactive)
  (setq jao-mpc--browser-port port)
  (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)
  (let ((jao-random-album-active nil)) (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)))))

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

;;;###autoload
(defun jao-mpc-select-album (&optional port)
  (interactive)
  (let* ((albums-str (jao-mpc--cmd jao-mpc--albums-cmd port))
         (albums-str (if (string= "" albums-str)
                         (jao-mpc--cmd jao-mpc--simple-albums-cmd port)
                       albums-str))
         (albums (split-string albums-str "\n" t)))
    (when-let* ((album (completing-read "Play album: " albums nil t)))
      (jao-mpc--add-and-play (car (split-string album "-" t " ")) port))))

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