(ns cljcastr.player
  (:require [clojure.string :as str]))

(defn log [& args]
  (let [seq->vec #(if (seq? %) (vec %) %)]
    (apply js/console.log (map (comp clj->js seq->vec) args)))
  (last args))

(defn render
  "Given a string `s` and a context map `ctx`, replaces template variables of the
   form {{k}} with the value of the corresponding key in the context map. If
   the key `k` doesn't exist in the context map, the template variable won't be
   replaced (i.e. \"{{foo}}\" will remain \"{{foo}}\" in the interpolated
   string)."
  [s ctx]
  (str/replace s #"\{\{([^}]+)\}\}"
               (fn [[match k]] (get ctx (keyword k) match))))

(defn secs->ts
  "Given a number of seconds, returns a timestamp of the form h:mm:ss."
  [secs]
  (let [h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (if (pos? (int h))
           (str (int h) ":")
           "")
         (zero-pad mm) ":" (zero-pad ss))))

(defn get-el
  "Returns the element matching the specified CSS selector. If `selector` is
   already an element, just returns the element."
  [selector]
  (if (instance? js/Element selector)
    selector
    (js/document.querySelector selector)))

(defn get-els
  "Returns a sequence containing the elements matching the specified CSS
   selector."
  [selector]
  (->> (js/document.querySelectorAll selector)
       seq))

(defn get-attr
  "Returns the value of the specified attribute."
  [el attr]
  (let [el (get-el el)]
    (.getAttribute el (name attr))))

(defn set-attr!
  "Sets the value of the specified attribute."
  [el attr v]
  (let [el (get-el el)]
    (.setAttribute el (name attr) v)
    el))

(defn set-attrs! [el attrs]
  (let [el (get-el el)]
    (doseq [[k v] attrs]
      (.setAttribute el (name k) v))
    el))

(defn get-id
  "Returns the ID of an element."
  [el]
  (let [el (get-el el)]
    (.-id el)))

(defn set-id!
  "Sets the ID of an element."
  [el id]
  (let [el (get-el el)]
    (when-not (empty? id)
      (set-attr! el :id id))
    el))

(defn get-name
  "Returns the tag name of an element."
  [el]
  (let [el (get-el el)]
    (.-localName el)))

(defn get-text
  "Returns the inner text of an element."
  [el]
  (let [el (get-el el)]
    (or (.-textContent el)
        (.-innerText el))))

(defn set-text! [el text]
  (let [el (get-el el)]
    (when-not (empty? text)
      (set! (.-textContent el) text))
    el))

(defn get-value
  "Returns the value attribute of the specified element."
  [el]
  (let [el (get-el el)]
    (.-value el)))

(defn set-value!
  "Sets the value attribute of the specified element."
  [el v]
  (let [el (get-el el)]
    (set! (.-value el) v)))

(defn get-classes
  "Returns the classes of the specified element as a list of class names."
  [el]
  (let [el (get-el el)]
    (->> (str/split (.-classList el) #"\s")
         (remove empty?))))

(defn set-classes!
  "Sets the classes of the specified element. Classes may either be a list or a
   string.

   Example:

     ;; These two are equivalent
     (set-classes el [\"foo\" \"bar\"])
     (set-classes el \"foo bar\")"
  [el classes]
  (let [el (get-el el)]
    (when-not (empty? classes)
      (set! (.-classList el) (if (string? classes)
                               classes
                               (str/join " " classes))))
    el))

(defn get-children [el]
  (-> (get-el el)
      .-childNodes
      seq))

(defn add-child! [parent-el el]
  (let [parent-el (get-el parent-el)]
    (-> parent-el
        (.appendChild el))
    parent-el))

(defn clear-children! [el]
  (let [el (get-el el)]
    (.replaceChildren el)
    el))

(defn set-children! [parent-el els]
  (let [parent-el (get-el parent-el)]
    (clear-children! parent-el)
    (doseq [el els]
      (add-child! parent-el el))
    parent-el))

(defn get-styles
  "Returns the inline styles of an element as a map of style name to value."
  [el]
  (let [styles (-> (get-el el)
                   (.getAttribute "style")
                   (str/split #";\s+"))]
    (->> styles
         (remove empty?)
         (map #(let [[k v] (str/split % #":\s+")]
                 [(-> (str/trim k) keyword) (str/trim v)]))
         (into {}))))

(defn set-styles!
  "Sets the styles of an element from a map of style name to value."
  [el styles]
  (let [el (get-el el)]
    (when-not (empty? styles)
      (set! (.-style el) (->> styles
                              (map (fn [[k v]]
                                     (str (name k) ": " v)))
                              (str/join "; "))))
    el))

(defn add-styles!
  "Given a map of style name to value, adds the styles to the element."
  [el styles]
  (-> el
      (set-styles! (merge (get-styles el) styles))))

(defn add-style!
  "Adds a style to an element"
  [el style-name style-value]
  (-> el
      (add-styles! {style-name style-value})))

(defn create-el [{:keys [tag id classes styles text attrs] :as args}]
  (-> (js/document.createElement tag)
      (set-attrs! attrs)
      (set-id! id)
      (set-classes! classes)
      (set-styles! styles)
      (set-text! text)))

(defn add-event-listener! [el event-type f]
  (let [el (get-el el)]
    (.addEventListener el event-type
                       (fn [ev]
                         (log (str "Handling " event-type " on")
                              el ev)
                         (if f
                           (f ev)
                           (log "No event handler function defined"))))
    (log "Added listener for" event-type "events to" el)
    el))

(defn create-button
  ([button-name button]
   (create-button button-name button {}))
  ([button-name {:keys [label src click-handler]} ctx]
   (let [button-id (str "button-" (name button-name))]
     (-> (create-el {:tag "button"
                     :id button-id
                     :classes ["cljcastr"
                               (render "{{button-class}}" ctx)]
                     :attrs {:aria-label (render label ctx)}})
         (set-styles! {:padding 0
                       :margin 0
                       :border 0
                       :background "transparent"
                       :cursor "pointer"})
         (add-event-listener! "click" click-handler)
         (add-child! (create-el {:tag "img"
                                 :id (str button-id "-icon")
                                 :classes ["cljcastr"
                                           (render "{{img-class}}" ctx)]
                                 :attrs {:src src
                                         :draggable false}}))))))

(defn skip-forward! [audio-el sec]
  (let [cur (.-currentTime audio-el)]
    (set! (.-currentTime audio-el) (min (+ cur sec)
                                        (.-duration audio-el)))))

(defn skip-backward! [audio-el sec]
  (let [cur (.-currentTime audio-el)]
    (set! (.-currentTime audio-el) (max (- cur sec)
                                        0))))

(defn update-playback-position-indicators!
  "Given a current playback time and duration (both in seconds) and a map of
   elements, updates the elements to reflect the current playback position. The
   element map should contain the following keys:
   `:elapsed-time` - text element (for example: label, span, etc.)
   `:remaining-time` - text element (for example: label, span, etc.)
   `:progress-slider` - input element of type range
   `:progress-bar` - progress element"
  [current-secs duration-secs
   {:keys [elapsed-time remaining-time progress-slider progress-bar]}]
  (set-text! elapsed-time (secs->ts current-secs))
  (set-text! remaining-time (str "-" (secs->ts (- duration-secs current-secs))))
  (set-attr! progress-slider :aria-valuetext
             (str (secs->ts current-secs) " of " (secs->ts duration-secs)))
  (doseq [el [progress-slider progress-bar]]
    (set-value! el current-secs)))

(js/console.log "Ready to play!")

(comment

  (+ 1 2)
  ;; => 3
  
  (js/console.log (+ 1 2))
  ;; => nil
  
  (doto (+ 1 2)
    js/console.log)
  ;; => 3
  
  (doto (map inc (range 5))
    js/console.log)
  ;; => (1 2 3 4 5)
  
  (doto (->> (map inc (range 5))
             clj->js)
    js/console.log)
  ;; => #js [1 2 3 4 5]
  
  (let [res (map inc (range 5))]
    (js/console.log (clj->js res))
    res)
  ;; => (1 2 3 4 5)

  (log (map inc (range 5)))
  ;; => (1 2 3 4 5)
  
  ;; 1. Create an **h1** element.
  (def h1 (js/document.createElement "h1"))
  ;; => #'cljcastr.player/h1

  ;; 2. Set its inner text to "cljcastr player".
  (set! (.-textContent h1) "cljcastr player")
  ;; => "cljcastr player"

  ;; 3. Grab the **body** element that we helpfully put in our HTML page skeleton.
  (def body (js/document.querySelector "body"))
  ;; => #'cljcastr.player/body

  ;; 4. Add the new **h1** element as a child of the **body** element.
  (.appendChild body h1)
  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]
  
  (log h1)
  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]

  (def audio (js/document.createElement "audio"))
  ;; => #'cljcastr.player/audio

  (set! (.-controls audio) true)
  ;; => true

  (set! (.-src audio)
        "https://media.zencastr.com/audio-files/5d9048546bacd9001de88f1c/2abc6f33-6a0a-4658-a8e0-c811984c13a0.mp3")

  (->> audio
       (.appendChild body))
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (log audio)
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (def playback-controls
    {:play {:src "https://www.svgrepo.com/show/436852/play-circle-fill.svg"
            :label "Play"}
     :pause {:src "https://www.svgrepo.com/show/436829/pause-circle-fill.svg"
             :label "Pause"}
     :forward {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
               :label "Forward {{seek-dur-sec}} seconds"}
     :backward {:src "https://www.svgrepo.com/show/495036/backward-15-seconds.svg"
             :label "Back {{seek-dur-sec}} seconds"}
     :speed {:src "https://www.svgrepo.com/show/151785/speed-meter.svg"
             :label "Playback speed"}})
  ;; => #'cljcastr.player/playback-controls

  (def seek-dur-sec 15)
  ;; => #'cljcastr.player/seek-dur-sec

  (def playback-buttons
    (->> playback-controls
         (map (fn [[control-name {:keys [src label]}]]
                (let [el (js/document.createElement "img")]
                  (set! (.-id el) (str "button-" (name control-name)))
                  (set! (.-classList el) "cljcastr playback-icon")
                  (set! (.-src el) src)
                  (set! (.-draggable el) false)
                  el)))
         log))
  ;; => #'cljcastr.player/playback-buttons

  (def playback-controls-container
    (-> (let [el (js/document.createElement "div")]
          (set! (.-id el) "playback-controls")
          (set! (.-classList el) "cljcastr")
          el)
        log))
  ;; => #'cljcastr.player/playback-controls-container

  (doseq [el playback-buttons]
    (.appendChild playback-controls-container el))
  ;; => nil

  (.appendChild body playback-controls-container)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (doseq [el playback-buttons]
    (set! (.-style el) "width: 35px"))
  ;; => nil

  (doseq [el playback-buttons]
    (set! (.-style el)
          (str "width: 35px; filter: invert(59%) sepia(92%) saturate(611%) "
               "hue-rotate(343deg) brightness(97%) contrast(99%);")))
  ;; => nil

  ;; What do styles look like?
  (-> playback-buttons
      first
      (.getAttribute "style"))
  ;; => "width: 35px; filter: invert(59%) sepia(92%) saturate(611%)
  ;;     hue-rotate(343deg) brightness(97%) contrast(99%);"

  ;; Parse into a list of styles
  (-> playback-buttons
      first
      (.getAttribute "style")
      (str/split #";\s+"))
  ;; => ["width: 35px"
  ;;     "filter: invert(59%) sepia(92%) saturate(611%)
  ;;      hue-rotate(343deg) brightness(97%) contrast(99%);"]

  ;; Turn it into a map
  (let [styles (-> playback-buttons
                   first
                   (.getAttribute "style")
                   (str/split #";\s+"))]
    (->> styles
         (map #(let [[k v] (str/split % #":\s+")]
                 [(keyword k) v]))
         (into {})))
  ;; => {:width "35px",
  ;;     :filter "invert(59%) sepia(92%) saturate(611%)
  ;;              hue-rotate(343deg) brightness(97%) contrast(99%);"}

  (-> playback-buttons
      first
      get-styles)
  ;; => {:width "35px",
  ;;     :filter "invert(59%) sepia(92%) saturate(611%)
  ;;              hue-rotate(343deg) brightness(97%) contrast(99%);"}

  (->> {:width "35px", :height "35px"}
       (map (fn [[k v]]
              (str (name k) ": " v)))
       (str/join "; "))
  ;; => "width: 35px; height: 35px"

  ;; Back to black!
  (doseq [el playback-buttons]
    (set-styles! el {:width "35px"}))
  ;; => nil

  ;; Amber is the new black
  (doseq [el playback-buttons]
    (add-style! el :filter
                (str "invert(59%) sepia(92%) saturate(611%) "
                     "hue-rotate(343deg) brightness(97%) contrast(99%)")))
  ;; => nil

  (-> (js/document.querySelector "audio")
      log)
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (set! (.-classList audio) "cljcastr")
  ;; => "cljcastr"

  (-> (js/document.querySelector "audio.cljcastr")
      log)
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  ;; Grab the play button
  (def play-button (js/document.querySelector "#button-play"))
  ;; => #'cljcastr.player/play-button

  ;; Add a click handler
  (.addEventListener play-button "click"
                     (fn [ev]
                       (log ev)
                       (.play (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  ;; Grab the pause button
  (def pause-button (js/document.querySelector "#button-pause"))
  ;; => #'cljcastr.player/play-button

  ;; Add a click handler
  (.addEventListener pause-button "click"
                     (fn [ev]
                       (log ev)
                       (.pause (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.-currentTime audio)
  ;; => 29.396358

  (set! (.-currentTime audio) 15.0)
  ;; => 15

  ;; Forward
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (+ cur seek-dur-sec)))
  ;; => 30

  ;; Backward
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (- cur seek-dur-sec)))
  ;; => 15

  ;; Forward with guard
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (min (+ cur seek-dur-sec)
                                     (.-duration audio))))
  ;; => 30

  ;; The end is nigh!
  (set! (.-currentTime audio) (- (.-duration audio) 5))
  ;; => 5365.949958

  ;; Don't worry, all is well
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (min (+ cur seek-dur-sec)
                                     (.-duration audio))))
  ;; => 5370.949958

  ;; Hitting 88 miles per hour
  (set! (.-currentTime audio) 5)
  ;; => 5

  ;; Backward
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (max (- cur seek-dur-sec)
                                     0)))
  ;; => 0

  (skip-forward! audio seek-dur-sec)
  ;; => 15

  (skip-backward! audio seek-dur-sec)
  ;; => 0

  (def forward-button (js/document.querySelector "#button-forward"))
  ;; => #'cljcastr.player/forward-button

  (def backward-button (js/document.querySelector "#button-backward"))
  ;; => #'cljcastr.player/backward-button

  (.addEventListener forward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-forward! audio seek-dur-sec)))
  ;; => nil

  (.addEventListener backward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-backward! audio seek-dur-sec)))
  ;; => nil

  (let [button-name :play
        {:keys [src label]} {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                             :label "Forward {{seek-dur-sec}} seconds"}]
    (let [button-id (str "button-" (name button-name))
          button-el (js/document.createElement "button")
          img-el (js/document.createElement "img")]
      (set! (.-id button-el) button-id)
      (set! (.-classList button-el) "cljcastr")
      (set! (.-ariaLabel button-el) label)
      (set! (.-id img-el) (str button-id "-icon"))
      (set! (.-classList img-el) "cljcastr playback-icon")
      (set! (.-src img-el) src)
      (set! (.-draggable img-el) false)
      (.appendChild button-el img-el)
      (log button-el)))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  (let [button-name :play
        {:keys [src label]} {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                             :label "Forward {{seek-dur-sec}} seconds"}]
    (let [button-id (str "button-" (name button-name))
          button-el (create-el {:tag "button"
                                :attrs {:id button-id
                                        :class "cljcastr"
                                        :aria-label label}})
          img-el (create-el {:tag "img"
                             :attrs {:id (str button-id "-icon")
                                     :class "cljcastr playback-icon"
                                     :src src
                                     :draggable false}})]
      (.appendChild button-el img-el)
      (log button-el)))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  (let [s "Something {{something}} something"]
    (str/replace s "{{something}}" "SOMETHING"))
  ;; => "Something SOMETHING something"

  (let [s "Something {{something}} something"
        ctx {:something "SOMETHING"}]
    (str/replace s #"\{\{(something)\}\}" (fn [[_ k]] (get ctx (keyword k)))))
  ;; => "Something SOMETHING something"

  (let [s "It's easy as 123, ABC, baby you and me"]
    (str/replace s #"(\d)+.+([A-Z]+)"
                 (fn [match-vector] (str "<<<" (pr-str match-vector) ">>>"))))
  ;; => "It's easy as <<<[\"123, ABC\" \"3\" \"C\"]>>>, baby you and me"

  (render "foo {{bar}} baz {{blah}} {{barf}}" {:bar 27, :blah 42})
  ;; => "foo 27 baz 42 {{barf}}"

  (let [button-name :play
        {:keys [src label]} {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                             :label "Forward {{seek-dur-sec}} seconds"}]
    (let [button-id (str "button-" (name button-name))
          button-el (create-el {:tag "button"
                                :attrs {:id button-id
                                        :class "cljcastr"
                                        :aria-label (render label {:seek-dur-sec seek-dur-sec})}})
          img-el (create-el {:tag "img"
                             :attrs {:id (str button-id "-icon")
                                     :class "cljcastr playback-icon"
                                     :src src
                                     :draggable false}})]
      (.appendChild button-el img-el)
      (log button-el)))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  (->> (create-button :forward
                      {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                       :label "Forward {{seek-dur-sec}} seconds"}
                      {:seek-dur-sec seek-dur-sec
                       :button-class "playback-button"
                       :img-class "playback-icon"})
       log)
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  ;; Throw away the current crappy buttons
  (.replaceChildren playback-controls-container)
  ;; => nil

  ;; Let's fix the order of our buttons whilst we're at it
  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :button-class "playback-button"
                                                :img-class "playback-icon"})]
                  (-> button-el
                      .-firstChild
                      (set-styles! {:width "35px"
                                    :filter "invert(59%) sepia(92%) saturate(611%) hue-rotate(343deg) brightness(97%) contrast(99%)"}))
                  button-el)))
         log))
  ;; => #'cljcastr.player/playback-buttons

  (doseq [el playback-buttons]
    (.appendChild playback-controls-container el))
  ;; => nil

  ;; Grab the new buttons
  (def play-button (js/document.querySelector "#button-play"))
  ;; => #'cljcastr.player/play-button

  (def pause-button (js/document.querySelector "#button-pause"))
  ;; => #'cljcastr.player/play-button

  (def forward-button (js/document.querySelector "#button-forward"))
  ;; => #'cljcastr.player/forward-button

  (def backward-button (js/document.querySelector "#button-backward"))
  ;; => #'cljcastr.player/backward-button

  ;; Attach the click handlers to them
  (.addEventListener play-button "click"
                     (fn [ev]
                       (log ev)
                       (.play (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener pause-button "click"
                     (fn [ev]
                       (log ev)
                       (.pause (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener forward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-forward! (js/document.querySelector "audio.cljcastr")
                                      seek-dur-sec)))
  ;; => nil

  (.addEventListener backward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-backward! (js/document.querySelector "audio.cljcastr")
                                       seek-dur-sec)))
  ;; => nil

  (.replaceChildren playback-controls-container)
  ;; => nil

  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :button-class "playback-button"
                                                :img-class "playback-icon"})]
                  (-> button-el
                      .-firstChild
                      (set-styles! {:width "35px"
                                    :filter (str "invert(59%) sepia(92%) saturate(611%) "
                                                 "hue-rotate(343deg) brightness(97%) contrast(99%)")}))
                  button-el)))
         log))
  ;; => #'cljcastr.player/playback-buttons

  (doseq [el playback-buttons]
    (.appendChild playback-controls-container el))
  ;; => nil

  (def play-button (js/document.querySelector "#button-play"))
  ;; => #'cljcastr.player/play-button

  (def pause-button (js/document.querySelector "#button-pause"))
  ;; => #'cljcastr.player/play-button

  (def forward-button (js/document.querySelector "#button-forward"))
  ;; => #'cljcastr.player/forward-button

  (def backward-button (js/document.querySelector "#button-backward"))
  ;; => #'cljcastr.player/backward-button

  (.addEventListener play-button "click"
                     (fn [ev]
                       (log ev)
                       (.play (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener pause-button "click"
                     (fn [ev]
                       (log ev)
                       (.pause (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener forward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-forward! audio seek-dur-sec)))
  ;; => nil

  (.addEventListener backward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-backward! audio seek-dur-sec)))
  ;; => nil

  (log "Something happened!" {:foo 1, :bar 2})
  ;; => "Something happened!"

  (log "Something happened!" {:foo 1, :bar 2})
  ;; => {:foo 1, :bar 2}

  (get-el "#button-play")
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  (get-els "#playback-controls > button")
  ;; => (#object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]])

  (-> (get-children "#playback-controls")
      log)
  ;; => (#object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]])

  (-> (get-el "#playback-controls")
      .-childNodes
      log)
  ;; => #object[NodeList [object NodeList]]

  (-> (get-el "#playback-controls")
      .-childNodes
      count)
  ;; => "#error {:message \"No protocol method ICounted.-count defined for type object: [object NodeList]\", ...}"

  (-> (get-children "#playback-controls")
      vec
      log)
  ;; => [#object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]]

  (-> (get-children "#playback-controls")
      log)
  ;; => (#object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]])

  (->> (add-child! "#playback-controls"
                   (create-el {:tag "span"
                               :text "Some nonsense"}))
       (log "I got a new child"))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (->> (clear-children! "#playback-controls")
       (log "Empty nest"))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (->> (set-children! "#playback-controls" playback-buttons)
       log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (add-event-listener! "#button-speed" "click"
                       #(log "Should probably do something with this event, eh?" %))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  (def playback-controls
    {:play {:src "https://www.svgrepo.com/show/436852/play-circle-fill.svg"
            :label "Play"
            :click-handler #(.play (get-el "audio.cljcastr"))}
     :pause {:src "https://www.svgrepo.com/show/436829/pause-circle-fill.svg"
             :label "Pause"
             :click-handler #(.pause (get-el "audio.cljcastr"))}
     :forward {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
               :label "Forward {{seek-dur-sec}} seconds"
               :click-handler #(skip-forward! (get-el "audio.cljcastr") seek-dur-sec)}
     :backward {:src "https://www.svgrepo.com/show/495036/backward-15-seconds.svg"
                :label "Back {{seek-dur-sec}} seconds"
                :click-handler #(skip-backward! (get-el "audio.cljcastr") seek-dur-sec)}
     :speed {:src "https://www.svgrepo.com/show/151785/speed-meter.svg"
             :label "Playback speed"}})
  ;; => #'cljcastr.player/playback-controls

  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :class "playback-control"})]
                  (-> (get-children button-el)
                      first
                      (set-styles! {:width "35px"
                                    :filter "invert(59%) sepia(92%) saturate(611%) hue-rotate(343deg) brightness(97%) contrast(99%)"}))
                  button-el)))
         log))
  ;; => #'cljcastr.player/playback-buttons

  (set-children! "#playback-controls" playback-buttons)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (add-style! audio :display "none")
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :button-class "playback-button"
                                                :img-class "playback-icon"})]
                  (-> (get-children button-el)
                      first
                      (set-styles! {:width "35px"
                                    :filter (str "invert(59%) sepia(92%) saturate(611%) "
                                                 "hue-rotate(343deg) brightness(97%) contrast(99%)")}))
                  button-el)))
         log))
  ;; => #'cljcastr.player/playback-buttons

  (set-children! "#playback-controls" playback-buttons)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (add-style! audio :display "none")
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :button-class "playback-button"
                                                :img-class "playback-icon"})]
                  (-> (get-children button-el)
                      first
                      (set-styles! {:width "35px"
                                    :filter (str "invert(59%) sepia(92%) saturate(611%) "
                                                 "hue-rotate(343deg) brightness(97%) contrast(99%)")}))
                  button-el)))
         log))
  ;; => #'cljcastr.player/playback-buttons

  (set-children! "#playback-controls" playback-buttons)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (def elapsed-time (create-el {:tag "span"
                                :text (str (.-currentTime audio))
                                :classes ["cljcastr"]
                                :id "elapsed-time"}))
  ;; => #'cljcastr.player/cur-time

  (def remaining-time (create-el {:tag "span"
                                  :text (str (- (.-duration audio)
                                                (.-currentTime audio)))
                                  :classes ["cljcastr"]
                                  :id "remaining-time"}))
  ;; => #'cljcastr.player/remaining-time

  (doseq [el [elapsed-time remaining-time]]
    (add-child! "#playback-controls" el))
  ;; => nil

  (set! (.-currentTime audio) 3645)
  ;; => 3645

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)]
    (str (int h) ":" (int mm) ":" (int ss)))
  ;; => "1:0:45"

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (int h) ":" (zero-pad mm) ":" (zero-pad ss)))
  ;; => "1:00:45"

  (set! (.-currentTime audio) 46)
  ;; => 46

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (int h) ":" (zero-pad mm) ":" (zero-pad ss)))
  ;; => "0:00:46"

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (if (pos? (int h))
           (str (int h) ":")
           "")
         (zero-pad mm) ":" (zero-pad ss)))
  ;; => "00:46"

  ;; Elapsed time
  (secs->ts (.-currentTime audio))
  ;; => "00:46"

  ;; Remaining time
  (secs->ts (- (.-duration audio) (.-currentTime audio)))
  ;; => "1:28:44"

  (-> elapsed-time
      (set-text! (secs->ts (.-currentTime audio)))
      log)
  ;; => #object[HTMLSpanElement [object HTMLSpanElement]]

  (-> remaining-time
      (set-text! (str "-" (secs->ts (- (.-duration audio) (.-currentTime audio)))))
      log)
  ;; => #object[HTMLSpanElement [object HTMLSpanElement]]

  (doseq [el [elapsed-time remaining-time]]
    (add-styles! el {:margin "0 5px 0 5px"
                     :font-size "0.7rem"
                     :color "grey"}))
  ;; => nil

  (let [el (-> (create-el {:tag "span"
                           :id "elapsed-time"
                           :classes ["cljcastr"]
                           :text (secs->ts 123)
                           :styles {:margin "0 5px 0 5px"
                                    :font-size "0.7rem"
                                    :color "grey"}})
               log)]
    (add-child! "#playback-controls" el))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (def episode-container (create-el {:tag "div"
                                     :id "episode-container"
                                     :classes ["cljcastr"]
                                     :styles {:width "100%"
                                              :display "flex"
                                              :flex-direction "column"
                                              :align-items "center"}}))
  ;; => #'cljcastr.player/episode-container

  (def episode-info
    (-> (create-el {:tag "div"
                    :id "episode-info"
                    :classes ["cljcastr"]
                    :styles {:max-width "420px"
                             :text-align "center"}})
        (set-children! [(create-el {:tag "h3"
                                    :id "podcast-title"
                                    :classes ["cljcastr"]
                                    :text "defn"})
                        (create-el {:tag "h2"
                                    :id "episode-title"
                                    :classes ["cljcastr"]
                                    :text "876b4c306172 Cora Sutton"})
                        (create-el {:tag "span"
                                    :id "episode-date"
                                    :classes ["cljcastr"]
                                    :styles {:color "grey"}
                                    :text "July 9, 2025"})])
        log))
  ;; => #'cljcastr.player/episode-info

  (def episode-art
    (-> (create-el {:tag "div"
                    :id "episode-art-container"
                    :classes ["cljcastr"]})
        (add-child! (create-el {:tag "img"
                                :id "episode-art"
                                :classes ["cljcastr"]
                                :styles {:width "400px"
                                         :border-radius "5%"
                                         :padding "10px"}
                                :attrs {:src "https://media.zencastr.com/image-files/5d9048546bacd9001de88f1c/a5b922ac-7294-4d58-9a33-4308f5d4f50b.png"}}))
        log))
  ;; => #'cljcastr.player/episode-art

  (def progress (create-el {:tag "div"
                            :id "progress"
                            :classes ["cljcastr"]
                            :styles {:width "390px"
                                     :margin "1.5em auto 0 auto"}}))
  ;; => #'cljcastr.player/progress

  (def playback-controls-container (-> (create-el {:tag "div"
                                                   :id "playback-controls"
                                                   :classes ["cljcastr"]})
                                       (set-children! playback-buttons)
                                       log))
  ;; => #'cljcastr.player/playback-controls-container

  (def playback-speed (create-el {:tag "div"
                                  :id "playback-speed"
                                  :classes ["cljcastr"]}))
  ;; => #'cljcastr.player/playback-speed

  (-> episode-container
      (set-children! [episode-info
                      episode-art
                      (-> audio
                          (set-styles! {:display "none"}))
                      progress
                      playback-controls-container
                      playback-speed])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (set-children! "body" [episode-container])
  ;; => #object[HTMLBodyElement [object HTMLBodyElement]]

  (def progress-bar
    (create-el {:tag "progress"
                :id "progress-bar"
                :classes ["cljcastr"]
                :attrs {:min 0
                        :max (.-duration audio)
                        :value (.-currentTime audio)}
                :styles {:appearance "none"
                         :height "8px"
                         :width "100%"
                         :border "0"
                         :border-radius "4px"
                         :margin "0"
                         :overflow "hidden"
                         :background-color "lightgray"
                         :color "#f7931e"}}))
  ;; => #'cljcastr.player/progress-bar

  (-> progress
      (set-children! [(-> elapsed-time
                          (add-styles! {:float "left"}))
                      (-> remaining-time
                          (add-styles! {:float "right"}))
                      progress-bar])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (set! (.-value progress-bar) (.-currentTime audio))
  ;; => 370.277385

  (def progress-slider
    (create-el {:tag "input"
                :id "progress-slider"
                :classes ["cljcastr"]
                :attrs {:type "range"
                        :min 0
                        :max (.-duration audio)
                        :value (.-currentTime audio)
                        :step "any"
                        :aria-label "Seek"
                        :aria-valuetext (str (secs->ts (.-currentTime audio))
                                             " of "
                                             (secs->ts (.-duration audio)))
                        :aria-valuemax (.-duration audio)}
                :styles {:appearance "none"
                         :cursor "pointer"
                         :height "28px"
                         :width "100%"
                         :border "0"
                         :margin "0"
                         :touch-action "manipulation"}}))
  ;; => #'cljcastr.player/progress-slider

  (-> progress
      (set-children! [(-> elapsed-time
                          (add-styles! {:float "left"}))
                      (-> remaining-time
                          (add-styles! {:float "right"}))
                      progress-slider
                      progress-bar])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (doseq [el [progress-slider progress-bar]]
    (set! (.-value el) (.-currentTime audio)))
  ;; => nil

  (add-event-listener! progress-slider "input"
                       #(->> % .-target .-value (set! (.-currentTime audio))))
  ;; => #object[HTMLInputElement [object HTMLInputElement]]

  (let [secs (.-currentTime audio)
        duration-secs (.-duration audio)]
    (set-text! elapsed-time (secs->ts secs))
    (set-text! remaining-time (str "-" (secs->ts (- duration-secs secs))))
    (.setAttribute progress-slider "aria-valuetext"
                   (str (secs->ts secs) " of " (secs->ts duration-secs)))
    (doseq [el [progress-slider progress-bar]]
      (set! (.-value el) (.-currentTime audio))))
  ;; => nil

  (update-playback-position-indicators! (.-currentTime audio) (.-duration audio)
                                        {:elapsed-time elapsed-time
                                         :remaining-time remaining-time
                                         :progress-slider progress-slider
                                         :progress-bar progress-bar})
  ;; => nil

  (add-event-listener! audio "timeupdate"
                       #(update-playback-position-indicators!
                         (.-currentTime audio) (.-duration audio)
                         {:elapsed-time elapsed-time
                          :remaining-time remaining-time
                          :progress-slider progress-slider
                          :progress-bar progress-bar}))
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (def progress-container
    (create-el {:tag "div"
                :id "progress-container"
                :classes ["cljcastr"]
                :styles {:position "relative"
                         :height "28px"
                         :clear "both"}}))
  ;; => #'cljcastr.player/progress-container

  (-> progress-container
      (set-children! [(-> progress-bar
                          (add-styles! {:position "absolute"
                                        :top "10px"
                                        :left "0px"
                                        :z-index 0}))
                      (-> progress-slider
                          (add-styles! {:position "absolute"
                                        :top "0px"
                                        :left "0px"
                                        :z-index 1
                                        :background-color "transparent"}))])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (-> progress
      (add-styles! {:position "relative"})
      (set-children! [(-> elapsed-time
                          (add-styles! {:float "left"}))
                      (-> remaining-time
                          (add-styles! {:float "right"}))
                      progress-container])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )
