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

(defonce state (atom {}))

(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
  "If `selector` is a string, returns the element matching CSS selector.
   If `selector` is a `Node`, returns it. Otherwise, throws an exception."
  [selector]
  (cond
    (string? selector)
    (js/document.querySelector selector)

    (instance? js/Node selector)
    selector

    :else
    (let [cls (-> selector .-constructor .-name)]
      (throw (ex-info (str "Expecting a string or a Node; got: " cls)
                      {:dom/error :dom/error.invalid-class
                       :js/class cls})))))

(defn descendant?
  "Returns truthy if the specified element is an ancestor of the element named by
   the specified CSS selector. NOTE: `ancestor-sel` must be a string containing
   a CSS selector, not an element!"
  [el ancestor-sel]
  (-> (get-el el)
      (.closest ancestor-sel)))

(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 attr->clj
  "Given an entry in an attribute map (i.e. `Attr` in a `NamedNodeMap` that is
   the value of the `.-attributes` property of an element), returns a vector of
   the name (as a keyword) and value of the attribute."
  [attr]
  [(-> (.-name attr) keyword) (.-value attr)])

(defn get-attrs
  "Returns the attributes of an element as a map of attribute names (as keywords)
   to attribute values."
  [el]
  (let [el (get-el el)
        attrs (.-attributes el)]
    (->> (range (.-length attrs))
         (map #(->> % (.item attrs) attr->clj))
         (into {}))))

(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 add-attrs! [el attrs]
  (let [el (get-el el)]
    (set-attrs! el (merge (get-attrs el) attrs))))

(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)]
    (if (instance? js/CharacterData el)
      (.-data 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 has-class?
  "Returns true if the element's list of classes contains the specified class."
  [el cls]
  (-> (get-classes el)
      set
      (contains? cls)))

(defn get-children
  "Returns the children of the specified element. Accepts an `opts` map with the
   following keys:

     `:filter-children` - predicate passed to `filter` on list of children
     `:remove-children` - predicate passed to `remove` on list of children

   `filter` is called before `remove`."
  ([el]
   (get-children el {}))
  ([el {:keys [filter-children remove-children]
        :or {filter-children (constantly true)
             remove-children (constantly false)} :as opts}]
   (->> (get-el el)
        .-childNodes
        seq
        (filter filter-children)
        (remove remove-children))))

(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 el->clj
  "Given an HTML element, returns a map containing :tag, :id, :classes, :styles,
   :text, and :attrs."
  [el]
  (if (map? el)
    el
    (let [el (get-el el)]
      (->> {:tag (get-name el)
            :id (get-id el)
            :classes (get-classes el)
            :styles (get-styles el)
            :text (get-text el)
            :attrs (get-attrs el)}
           (remove (fn [[_ v]] (empty? v)))
           (into {})))))

(defn group-styles
  "Given a collection of DOM elements, returns a map keyed by styles. The value
   at each key will be a vector of the corresponding elements, in the order they
   appeared in the original list.

   Example:

     (let [els [(create-el {:tag \"div\", :id \"el1\", :styles {:color \"red\"}})
                (create-el {:tag \"div\", :id \"el2\", :styles {:color \"blue\"}})
                (create-el {:tag \"div\", :id \"el3\"})
                (create-el {:tag \"div\", :id \"el4\", :styles {:color \"red\"}})]]
       (->> (group-styles els)
            (map (fn [[styles els]]
                   [styles (map #(select-keys % [:id]) els)]))))
     ;; => ([{:color \"red;\"} ({:id \"el1\"} {:id \"el4\"})]
     ;;     [{:color \"blue;\"} ({:id \"el2\"})])"
  [els]
  (->> els
       (map el->clj)
       (remove (comp empty? :styles))
       (group-by :styles)))

(defn attrs->str
  "Given a map of attributes, returns a string of k=\"v\"..."
  [attrs]
  (->> attrs
       (map (fn [[k v]] (str (name k) "=\"" v "\"")))
       (str/join " ")))

(defn indent
  "Returns `indent-str` repeated `indent-level` times."
  [indent-level indent-str]
  (->> (repeat indent-level indent-str)
       (str/join "")))

(defn el->html
  "Given an element (or the Clojure representation of an element), returns an
   HTML string."
  ([el]
   (el->html el {}))
  ([el {:keys [indent-level indent-str exclude-styles?]
        :or {indent-level 0, indent-str "  "}
        :as opts}]
   (if (instance? js/CharacterData el)
     (str/trim (get-text el))
     (let [{:keys [tag attrs]} (if (map? el) el (el->clj el))
           attrs (if exclude-styles? (dissoc attrs :style) attrs)
           attrs-str (attrs->str attrs)
           ;; Reset indentation for <head> and <body> tags
           indent-level (if (#{"head" "body"} (str/lower-case tag)) 0 indent-level)
           children-html (->> (get-children el opts)
                              (map #(el->html % (assoc opts :indent-level
                                                       (inc indent-level))))
                              (str/join ""))
           tag-open (str (indent indent-level indent-str)
                         "<" tag
                         (if (empty? attrs-str) "" (str " " attrs-str))
                         ">"
                         (if (str/ends-with? children-html "\n")
                           "\n"
                           ""))
           tag-close (str (if (str/ends-with? children-html "\n")
                            (indent indent-level indent-str)
                            "")
                          "</" tag ">\n")]
       (str tag-open children-html tag-close)))))

(defn format-css
  "Returns a CSS string with the specified selector and styles"
  [sel styles]
  (str sel " {\n  "
       (->> styles
            (map (fn [[k v]]
                   (str (name k) ": " v)))
            (str/join ";\n  "))
       "\n}"))

(defn el->css
  "Given an element (or the Clojure representation of an element), returns a CSS
   rule corresponding to the element's ID."
  ([el]
   (let [el (if (map? el) el (-> (get-el el) el->clj))
         {:keys [styles id tag classes]} el
         sel (if (not-empty id)
               (str "#" id)
               (str tag (if (seq classes)
                          (str "." (first classes)))))]
     (el->css el sel)))
  ([el sel]
   (format-css sel (:styles el))))

(defn els->css
  "Given an collection of elements (or Clojure representations of elements),
   returns CSS rules corresponding to the elements' IDs."
  [els]
  (->> els
       (map el->clj)
       (remove (comp empty? :styles))
       (map el->css)
       (str/join "\n\n")))

(defn create-el [{:keys [tag id classes styles text attrs children] :as args}]
  (-> (js/document.createElement tag)
      (set-attrs! attrs)
      (set-id! id)
      (set-classes! classes)
      (set-styles! styles)
      (set-children! children)
      (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 save-file! [filename content content-type]
  (let [blob-url (-> (js/Blob. [content] (clj->js {:type content-type}))
                     js/URL.createObjectURL)]
    (-> (create-el {:tag "a"
                    :attrs {:download filename
                            :href blob-url}})
        .click)))

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

(defn keep-child? [el]
  (cond
    (instance? js/Text el)
    (-> (get-text el)
        (str/replace #"[\n\s]+" "")
        not-empty)
    
    (instance? js/Element el)
    (or (contains? #{"html" "head" "body"}
                   (get-name el))
        (descendant? el "head")
        (and (descendant? el "body")
             (has-class? el "cljcastr")))))

(defn dom->html
  ([]
   (dom->html {}))
  ([opts]
   (el->html "html"
             (merge opts
                    {:filter-children keep-child?}))))

(defn save-html!
  ([]
   (save-html! {}))
  ([{:keys [filename html]
     :or {filename "index.html"}
     :as opts}]
   (let [html (or html (dom->html opts))]
     (save-file! filename html "text/html"))))

(defn dom->css []
  (->> (get-els ".cljcastr")
       group-styles
       (map (fn [[_styles [el & rest-els]]]
              (if (seq rest-els)
                (let [sel (str "." (->> (:classes el)
                                        (remove (partial = "cljcastr"))
                                        first))]
                  (el->css el sel))
                (el->css el))))
       (str/join "\n\n")
       log))

(defn save-css!
  ([]
   (save-css! "main.css"))
  ([filename]
   (save-css! filename (dom->css)))
  ([filename css]
   (save-file! filename css "text/css")))

(def seek-dur-sec 15)

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

(defn init-ui! []
  (doseq [[el-id {:keys [click-handler]}] playback-controls]
    (add-event-listener! (str "#button-" (name el-id)) "click"
                         click-handler))
  (let [audio (get-el "audio.cljcastr")]
    (add-event-listener! "#progress-slider" "input"
                         #(->> % .-target .-value (set! (.-currentTime audio))))
    (add-event-listener! audio "timeupdate"
                         #(update-playback-position-indicators!
                           (.-currentTime audio) (.-duration audio)
                           (->> [:elapsed-time
                                 :remaining-time
                                 :progress-slider
                                 :progress-bar]
                                (map (fn [el-id]
                                       [el-id (str "#" (name el-id))]))
                                (into {}))))
    audio)
  (log "UI initialised"))

(when-not (:initialised? @state)
  (init-ui!)
  (swap! state assoc :initialised? true))

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

(comment

  (get-el "body")
  ;; => "#error {:message \"Unable to resolve symbol: get-el\",
  ;;             :data {:type :sci/error, :line 1, :column 1, ...}}

  (set-children! "body"
                 [(create-el {:tag "h1"
                              :classes ["cljcastr"]
                              :text "cljcastr player"})
                  (create-el {:tag "audio"
                              :classes ["cljcastr"]
                              :attrs {:controls true
                                      :src "https://media.zencastr.com/audio-files/5d9048546bacd9001de88f1c/2abc6f33-6a0a-4658-a8e0-c811984c13a0.mp3"}})])
  ;; => #object[HTMLBodyElement [object HTMLBodyElement]]

  (-> (get-el "body")
      log)
  ;; => #object[HTMLBodyElement [object HTMLBodyElement]]

  (def children (-> (get-children "body")
                    log))
  ;; => #'cljcastr.player/children

  (count children)
  ;; => 3

  (def children (-> (js/document.querySelectorAll "body > .cljcastr")
                    seq
                    log))
  ;; => #'cljcastr.player/children

  (count children)
  ;; => 2

  (->> children
       (map get-name)
       log)
  ;; => ("h1" "audio")
  
  (->> children
       (map #(.-attributes %))
       log)
  ;; => (#object[NamedNodeMap [object NamedNodeMap]]
  ;;     #object[NamedNodeMap [object NamedNodeMap]])
  
  (->> children
       (map (fn [el]
              (let [attrs (.-attributes el)]
                (->> (range (.-length attrs))
                     (map #(let [attr (.item attrs %)]
                             [(-> (.-name attr) keyword) (.-value attr)]))
                     (into {})))))
       log)
  ;; => ({:class "cljcastr"}
  ;;     {:controls "true",
  ;;      :src "https://media.zencastr.com/audio-files/...",
  ;;      :class "cljcastr"})

  (->> children
       (map get-attrs)
       log)
  ;; => ({:class "cljcastr"}
  ;;     {:controls "true",
  ;;      :src "https://media.zencastr.com/audio-files/...",
  ;;      :class "cljcastr"})
  
  (->> children
       (map (fn [el]
              {:tag (get-name el)
               :attrs (get-attrs el)}))
       log)
  ;; => ({:tag "h1", :attrs {:class "cljcastr"}}
  ;;     {:tag "audio",
  ;;      :attrs {:controls "true",
  ;;              :src "https://media.zencastr.com/audio-files/...",
  ;;              :class "cljcastr"}})
  
  (->> children
       (map (fn [el]
              (let [text (get-text el)]
                (merge {:tag (get-name el)
                        :attrs (get-attrs el)}
                       (when-not (empty? text)
                         {:text text})))))
       log)
  ;; => ({:tag "h1", :attrs {:class "cljcastr"}, :text "cljcastr player"}
  ;;     {:tag "audio",
  ;;      :attrs {:controls "true",
  ;;              :src "https://media.zencastr.com/audio-files/...",
  ;;              :class "cljcastr"}})
  
  (->> children
       (map el->clj)
       log)
  ;; => ({:tag "h1",
  ;;      :classes ("cljcastr"),
  ;;      :text "cljcastr player",
  ;;      :attrs {:class "cljcastr"}}
  ;;     {:tag "audio",
  ;;      :classes ("cljcastr"),
  ;;      :attrs {:controls "true",
  ;;              :src "https://media.zencastr.com/audio-files/...",
  ;;              :class "cljcastr"}})

  (->> (create-el {:tag "button"
                   :id "button-play"
                   :classes ["cljcastr" "playback-button"]
                   :styles {:padding "0px"
                            :margin "0px"
                            :border "0px"
                            :background "transparent"
                            :cursor "pointer"}
                   :attrs {:aria-label "Play"}})
       el->clj
       create-el
       log)
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]
  
  (->> children
       (map el->clj)
       (map (fn [{:keys [tag attrs text]}]
              (let [attrs-str (if (empty? attrs)
                                ""
                                (str " "
                                     (->> attrs
                                          (map (fn [[k v]] (str (name k) "=\"" v "\"")))
                                          (str/join " "))))]
                (str "<" tag attrs-str ">" text "</" tag ">"))))
       (str/join "\n")
       log)
  ;; => "<h1 class=\"cljcastr\">cljcastr player</h1>\n
  ;;     <audio controls=\"true\"
  ;;       src=\"https://media.zencastr.com/audio-files/...\"
  ;;       class=\"cljcastr\"></audio>"
  
  (->> children
       (map el->html)
       (str/join "\n")
       log)
  ;; => "<h1 class=\"cljcastr\">cljcastr player</h1>\n
  ;;     <audio controls=\"true\"
  ;;       src=\"https://media.zencastr.com/audio-files/...\"
  ;;       class=\"cljcastr\"></audio>"

  (-> (create-el {:tag "h1", :attrs {:class "cljcastr"}, :text "cljcastr player"})
      log)
  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]

  (->> (get-els "body > .cljcastr")
       (map el->html)
       (str/join "\n")
       log)
  ;; => "<h1 class=\"cljcastr\">cljcastr player</h1>\n
  ;;     <audio controls=\"true\"
  ;;       src=\"https://media.zencastr.com/audio-files/...\"
  ;;       class=\"cljcastr\"></audio>"

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

  (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

  (->> (create-el {:tag "div"
                   :id "episode-container"
                   :classes ["cljcastr"]
                   :styles {:width "100%"
                            :display "flex"
                            :flex-direction "column"
                            :align-items "center"}
                   :children
                   [
                    ;; episode-info
                    (create-el {:tag "div"
                                :id "episode-info"
                                :classes ["cljcastr"]
                                :styles {:max-width "420px"
                                         :text-align "center"}
                                :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"})]})

                    ;;episode-art
                    (create-el {:tag "div"
                                :id "episode-art-container"
                                :classes ["cljcastr"]
                                :children
                                [(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"}})]})

                    ;;audio
                    (-> (create-el {:tag "audio"
                                    :classes ["cljcastr"]
                                    :attrs {:controls true
                                            :src "https://media.zencastr.com/audio-files/5d9048546bacd9001de88f1c/2abc6f33-6a0a-4658-a8e0-c811984c13a0.mp3"}
                                    :styles {:display "none"}})
                        (add-event-listener! "timeupdate"
                                             (fn [ev]
                                               (let [audio (.-target ev)]
                                                 (update-playback-position-indicators!
                                                  (.-currentTime audio) (.-duration audio)
                                                  {:elapsed-time "#elapsed-time"
                                                   :remaining-time "#remaining-time"
                                                   :progress-slider "#progress-slider"
                                                   :progress-bar "#progress-bar"})))))

                    ;;progress
                    (create-el {:tag "div"
                                :id "progress"
                                :classes ["cljcastr"]
                                :styles {:width "390px"
                                         :margin "1.5em auto 0 auto"}
                                :children
                                [(create-el {:tag "span"
                                             :text (secs->ts 0)
                                             :classes ["cljcastr"]
                                             :id "elapsed-time"
                                             :styles {:margin "0 5px 0 5px"
                                                      :font-size "0.7rem"
                                                      :color "grey"
                                                      :float "left"}})
                                 (create-el {:tag "span"
                                             :text (secs->ts 0)
                                             :classes ["cljcastr"]
                                             :id "remaining-time"
                                             :styles {:margin "0 5px 0 5px"
                                                      :font-size "0.7rem"
                                                      :color "grey"
                                                      :float "right"}})
                                 (create-el {:tag "div"
                                             :id "progress-container"
                                             :classes ["cljcastr"]
                                             :styles {:position "relative"
                                                      :height "28px"
                                                      :clear "both"}
                                             :children
                                             [(create-el {:tag "progress"
                                                          :id "progress-bar"
                                                          :classes ["cljcastr"]
                                                          :attrs {:min 0
                                                                  :max 0
                                                                  :value 0}
                                                          :styles {:appearance "none"
                                                                   :height "8px"
                                                                   :width "100%"
                                                                   :border "0"
                                                                   :border-radius "4px"
                                                                   :margin "0"
                                                                   :overflow "hidden"
                                                                   :background-color "lightgray"
                                                                   :color "#f7931e"
                                                                   :position "absolute"
                                                                   :top "10px"
                                                                   :left "0px"
                                                                   :z-index 0}})
                                              (create-el {:tag "input"
                                                          :id "progress-slider"
                                                          :classes ["cljcastr"]
                                                          :attrs {:type "range"
                                                                  :min 0
                                                                  :max 0
                                                                  :value 0
                                                                  :step "any"
                                                                  :aria-label "Seek"
                                                                  :aria-valuetext (str (secs->ts 0)
                                                                                       " of "
                                                                                       (secs->ts 0))
                                                                  :aria-valuemax 0}
                                                          :styles {:appearance "none"
                                                                   :cursor "pointer"
                                                                   :height "28px"
                                                                   :width "100%"
                                                                   :border "0"
                                                                   :margin "0"
                                                                   :touch-action "manipulation"
                                                                   :position "absolute"
                                                                   :top "0px"
                                                                   :left "0px"
                                                                   :z-index 1
                                                                   :background-color "transparent"}})]})]})

                    ;;playback-controls
                    (create-el {:tag "div"
                                :id "playback-controls"
                                :classes ["cljcastr"]
                                :children
                                (->> [: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)})]})
       log
       list
       (set-children! "body"))
  ;; => #object[HTMLBodyElement [object HTMLBodyElement]]

  (let [audio (get-el "audio.cljcastr")]
    (set-text! "#elapsed-time" (secs->ts (.-currentTime audio)))
    (set-text! "#remaining-time" (secs->ts (- (.-duration audio)
                                              (.-currentTime audio))))
    (add-attrs! "#progress-bar" {:max (.-duration audio)
                                 :value (.-currentTime audio)})
    (add-attrs! "#progress-slider" {:max (.-duration audio)
                                    :value (.-currentTime audio)
                                    :aria-valuetext (str (secs->ts (.-currentTime audio))
                                                         " of "
                                                         (secs->ts (.-duration audio)))
                                    :aria-valuemax (.-duration audio)}))
  ;; => #object[HTMLInputElement [object HTMLInputElement]]

  (->> (get-els "body > .cljcastr")
       (map el->html)
       (str/join "\n")
       log)
  ;; => "<div id=\"episode-container\" class=\"cljcastr\"
  ;;       style=\"width: 100%; display: flex; flex-direction: column;
  ;;       align-items: center;\">
  ;;       defn876b4c306172 Cora SuttonJuly 9, 202500:001:29:30</div>"

  (->> (get-els "body > .cljcastr")
       (map (fn [el]
              (let [{:keys [tag attrs text]} (if (map? el) el (el->clj el))
                    attrs-str (attrs->str attrs)
                    tag-open (str "  <" tag (if (empty? attrs-str) "" (str " " attrs-str)) ">")
                    tag-close (str "</" tag ">")
                    children (get-children el)]
                (if (seq children)
                  (str/join "\n"
                            (concat [tag-open]
                                    (map el->html children)
                                    [tag-close]))
                  (str tag-open text tag-close)))))
       (str/join "\n")
       log)
  ;; => "<div id=\"episode-container\" class=\"cljcastr\" style=\"...\">\n
  ;;       <div id=\"episode-info\" class=\"cljcastr\" style=\"...\">defn876b4c306172 Cora SuttonJuly 9, 2025</div>\n
  ;;       <div id=\"episode-art-container\" class=\"cljcastr\"></div>\n
  ;;       <audio controls=\"true\" src=\"https://media.zencastr.com/audio-files/...\" class=\"cljcastr\" style=\"display: none;\"></audio>\n
  ;;       <div id=\"progress\" class=\"cljcastr\" style=\"...\">00:37-1:28:53</div>\n
  ;;       <div id=\"playback-controls\" class=\"cljcastr\"></div>\n
  ;;       </div>"

  (->> (get-els "body > .cljcastr")
       (map el->html)
       (str/join "\n")
       log)
  ;; => "#error {:message \"Failed to execute 'querySelector' on 'Document':
  ;;                        '[object Text]' is not a valid selector.\",
  ;;             ...}"

  (get-children "#episode-info")
  ;; => (#object[HTMLHeadingElement [object HTMLHeadingElement]]
  ;;     #object[HTMLHeadingElement [object HTMLHeadingElement]]
  ;;     #object[HTMLSpanElement [object HTMLSpanElement]])

  (-> (get-children "#episode-info")
      first
      el->html)
  ;; => "#error {:message \"Failed to execute 'querySelector' on 'Document':
  ;;                        '[object Text]' is not a valid selector.\",
  ;;             ...}"

  (-> (get-children "#episode-info")
      first
      get-children
      log)
  ;; => (#object[Text [object Text]])

  (-> (get-children "#episode-info")
      first
      get-children
      first
      log)
  ;; => #object[Text [object Text]]

  (-> (get-children "#episode-info")
      first
      get-children
      first
      (.-data))
  ;; => "defn"

  (-> (get-children "#episode-info")
      first
      get-children
      first
      get-text)
  ;; => "#error {:message \"Failed to execute 'querySelector' on 'Document':
  ;;                        '[object Text]' is not a valid selector.\",
  ;;             ...}"

  (-> (get-children "#episode-info")
      first
      get-children
      first
      get-el)
  ;; => "#error {:message \"Failed to execute 'querySelector' on 'Document':
  ;;                        '[object Text]' is not a valid selector.\",
  ;;             ...}"

  (-> (get-children "#episode-info")
      first
      get-children
      first
      get-text)
  ;; => "defn"

  (get-el "#podcast-title")
  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]

  (get-el (js/document.querySelector "#podcast-title"))
  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]

  (get-el 42)
  ;; => "#error {:message \"Expecting a string or a Node; got: Number\", ...}"

  (try
    (get-el 42)
    (catch js/Error e
      (ex-data e)))
  ;; => {:dom/error :dom/error.invalid-class, :js/class "Number"}

  (-> (get-children "#episode-info")
      first
      get-children
      first
      get-text)
  ;; => "defn"

  (el->html "#episode-info")
  ;; => "#error {:message \"Could not find instance method: getAttribute\", ...}"

  (el->html (js/Text. "foo"))
  ;; => "foo"

  (-> (create-el {:tag "h3", :text "defn"})
      el->html)
  ;; => "  <h3>\ndefn\n</h3>"

  (el->html "#episode-info")
  ;; => "  <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n
  ;;       <h3 id=\"podcast-title\" class=\"cljcastr\">\ndefn\n</h3>\n
  ;;       <h2 id=\"episode-title\" class=\"cljcastr\">\n876b4c306172 Cora Sutton\n</h2>\n
  ;;       <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">\nJuly 9, 2025\n</span>\n
  ;;       </div>"

  (-> (el->html "html")
      log)
  ;; => "  <html xmlns=\"http://www.w3.org/1999/xhtml\">\n
  ;;       <head>\n\n
  ;;       \n
  ;;       <title>\ncljcastr episode player\n</title>\n\n
  ;;       \n
  ;;       <link rel=\"stylesheet\" href=\"main.css\"></link>\n\n\n
  ;;       \n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.js\" type=\"application/javascript\"></script>\n\n
  ;;       \n
  ;;       <script>\nvar SCITTLE_NREPL_WEBSOCKET_PORT = 1340;\n</script>\n\n
  ;;       \n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.nrepl.js\" type=\"application/javascript\"></script>\n\n\n\n
  ;;       <script type=\"application/x-scittle\" src=\"player.cljs\"></script>\n\n\n</head>\n\n\n\n
  ;;       <body>\n
  ;;       <div class=\"translate-tooltip-mtz blue sm-root translate hidden_translate\">
  ;;       <div class=\"header-wrapper\">
  ;;       <div class=\"header-controls\">
  ;;       ...
  ;;       </div>
  ;;       <span class=\"translate-button-mtz hidden_translate blue\"></span>\n
  ;;       <audio class=\"audio-for-speech\" src=\"\"></audio>\n
  ;;       <div id=\"episode-container\" class=\"cljcastr\" style=\"width: 100%; display: flex; flex-direction: column; align-items: center;\">\n
  ;;       <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n
  ;;       <h3 id=\"podcast-title\" class=\"cljcastr\">\ndefn\n</h3>\n
  ;;       <h2 id=\"episode-title\" class=\"cljcastr\">\n876b4c306172 Cora Sutton\n</h2>\n
  ;;       <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">\nJuly 9, 2025\n</span>\n
  ;;       </div>\n
  ;;       ...
  ;;       <button aria-label=\"Playback speed\" id=\"button-speed\" class=\"cljcastr playback-button\" style=\"padding: 0px; margin: 0px; border: 0px; background: transparent; cursor: pointer;\">\n
  ;;       <img src=\"https://www.svgrepo.com/show/151785/speed-meter.svg\" draggable=\"false\" id=\"button-speed-icon\" class=\"cljcastr playback-icon\" style=\"width: 35px; filter: invert(59%) sepia(92%) saturate(611%) hue-rotate(343deg) brightness(97%) contrast(99%);\"></img>\n
  ;;       </button>\n
  ;;       </div>\n
  ;;       </div>\n
  ;;       </body>\n
  ;;       </html>"

  (->> (get-children "#episode-info")
       log)
  ;; => (#object[Text [object Text]]
  ;;     #object[HTMLHeadingElement [object HTMLHeadingElement]]
  ;;     #object[Text [object Text]]
  ;;     #object[HTMLHeadingElement [object HTMLHeadingElement]]
  ;;     #object[Text [object Text]]
  ;;     #object[HTMLSpanElement [object HTMLSpanElement]]
  ;;     #object[Text [object Text]])

  (->> (get-children "#episode-info"
                     {:filter-children (partial instance? js/HTMLHeadingElement)})
       log
       (map get-text))
  ;; => ("\ndefn\n" "\n876b4c306172 Cora Sutton\n")

  (->> (get-children "#episode-info"
                     {:remove-children (partial instance? js/Text)})
       log
       (map get-text))
  ;; => ("\ndefn\n" "\n876b4c306172 Cora Sutton\n" "\nJuly 9, 2025\n")

  (el->html "#episode-info")
  ;; => "  <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n\n
  ;;       \n
  ;;       <h3 id=\"podcast-title\" class=\"cljcastr\">\n\ndefn\n\n</h3>\n\n
  ;;       \n
  ;;       <h2 id=\"episode-title\" class=\"cljcastr\">\n\n876b4c306172 Cora Sutton\n\n</h2>\n\n
  ;;       \n
  ;;       <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">\n\n
  ;;       July 9, 2025\n\n</span>\n\n\n
  ;;       </div>"

  ;; Remove empty text nodes
  (el->html "#episode-info"
            {:remove-children #(empty? (str/replace (get-text %) #"[\n\s]+" ""))})
  ;; => "  <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n
  ;;       <h3 id=\"podcast-title\" class=\"cljcastr\">\n\ndefn\n\n</h3>\n
  ;;       <h2 id=\"episode-title\" class=\"cljcastr\">\n\n876b4c306172 Cora Sutton\n\n</h2>\n
  ;;       <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">\n\nJuly 9, 2025\n\n</span>\n
  ;;       </div>"

  (let [empty-text? #(and (instance? js/Text %)
                          (empty? (str/replace (get-text %) #"[\n\s]+" "")))]
    (el->html "html"
              {:remove-children (fn [el]
                                  (or (empty-text? el)
                                      ;; Bye-bye Proton Pass!
                                      (str/starts-with? (or (get-name el) "")
                                                        "protonpass-")
                                      ;; Be gone, Translator!
                                      (some #(str/starts-with? % "translate-")
                                            (get-classes el))
                                      (and (= "audio" (get-name el))
                                           (not (some #{"cljcastr"} (get-classes el))))))}))
  ;; => "  <html xmlns=\"http://www.w3.org/1999/xhtml\">\n
  ;;       <head>\n
  ;;       <title>\n\ncljcastr episode player\n\n</title>\n
  ;;       <link rel=\"stylesheet\" href=\"main.css\"></link>\n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.js\" type=\"application/javascript\"></script>\n
  ;;       <script>\n\n
  ;;       var SCITTLE_NREPL_WEBSOCKET_PORT = 1340;\n\n
  ;;       </script>\n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.nrepl.js\" type=\"application/javascript\"></script>\n
  ;;       <script type=\"application/x-scittle\" src=\"player.cljs\"></script>\n
  ;;       </head>\n
  ;;       <body>\n
  ;;       <div id=\"episode-container\" class=\"cljcastr\" style=\"width: 100%; display: flex; flex-direction: column; align-items: center;\">\n
  ;;       ...
  ;;       </div>\n
  ;;       </div>\n
  ;;       </body>\n
  ;;       </html>"

  (has-class? "#button-backward" "cljcastr")
  ;; => true

  (descendant? "#button-backward" "#episode-container")
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (descendant? "#button-backward" "#episode-info")
  ;; => nil

  (el->html "html"
            {:filter-children (fn [el]
                                (cond
                                  (instance? js/Text el)
                                  (-> (get-text el)
                                      (str/replace #"[\n\s]+" "")
                                      not-empty)
                                  
                                  (instance? js/Element el)
                                  (or (contains? #{"html" "head" "body"}
                                                 (get-name el))
                                      (descendant? el "head")
                                      (and (descendant? el "body")
                                           (has-class? el "cljcastr")))))})
  ;; => "  <html xmlns=\"http://www.w3.org/1999/xhtml\">\n
  ;;       <head>\n
  ;;       <title>cljcastr episode player</title>\n
  ;;       <link rel=\"stylesheet\" href=\"main.css\"></link>\n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.js\" type=\"application/javascript\"></script>\n
  ;;       <script>var SCITTLE_NREPL_WEBSOCKET_PORT = 1340;</script>\n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.nrepl.js\" type=\"application/javascript\"></script>\n
  ;;       <script type=\"application/x-scittle\" src=\"player.cljs\"></script>\n
  ;;       </head>\n
  ;;       <body>\n
  ;;       <div id=\"episode-container\" class=\"cljcastr\" style=\"width: 100%; display: flex; flex-direction: column; align-items: center;\">\n
  ;;       <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n
  ;;       ...
  ;;       </div>\n
  ;;       </div>\n
  ;;     </body>\n
  ;;     </html>\n
  ;;     "

  (->> (get-children "#podcast-title")
       first
       get-text)
  ;; => "\ndefn\n"

  (->> (get-children "#podcast-title")
       first
       get-text
       str/trim)
  ;; => "defn"

  (el->html "#podcast-title")
  ;; => "  <h3 id=\"podcast-title\" class=\"cljcastr\">defn</h3>"

  (el->html "#episode-info")
  ;; => "  <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">  <h3 id=\"podcast-title\" class=\"cljcastr\">defn</h3>  <h2 id=\"episode-title\" class=\"cljcastr\">876b4c306172 Cora Sutton</h2>  <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">July 9, 2025</span></div>"

  (->> (range 3)
       (map #(str (indent % "  ") "indent " %)))
  ;; => ("indent 0" "  indent 1" "    indent 2")

  (el->html "#episode-info")
  ;; => "<div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n
  ;;       <h3 id=\"podcast-title\" class=\"cljcastr\">defn</h3>\n
  ;;       <h2 id=\"episode-title\" class=\"cljcastr\">876b4c306172 Cora Sutton</h2>\n
  ;;       <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">July 9, 2025</span>\n
  ;;     </div>\n"

  (el->html "html"
            {:filter-children (fn [el]
                                (cond
                                  (instance? js/Text el)
                                  (-> (get-text el)
                                      (str/replace #"[\n\s]+" "")
                                      not-empty)
                                  
                                  (instance? js/Element el)
                                  (or (contains? #{"html" "head" "body"}
                                                 (get-name el))
                                      (descendant? el "head")
                                      (and (descendant? el "body")
                                           (has-class? el "cljcastr")))))})
  ;; => "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n
  ;;       <head>\n
  ;;         <title>cljcastr episode player</title>\n
  ;;         ...
  ;;       </head>\n
  ;;       <body>\n
  ;;         <audio class=\"audio-for-speech\" src=\"\"></audio>\n
  ;;         <div id=\"episode-container\" class=\"cljcastr\" style=\"width: 100%; display: flex; flex-direction: column; align-items: center;\">\n
  ;;           <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n
  ;;             <h3 id=\"podcast-title\" class=\"cljcastr\">defn</h3>\n
  ;;             <h2 id=\"episode-title\" class=\"cljcastr\">876b4c306172 Cora Sutton</h2>\n
  ;;             <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">July 9, 2025</span>\n
  ;;           </div>\n
  ;;           ...
  ;;         </div>\n
  ;;       </body>\n
  ;;     </html>\n
  ;;     "

  (el->html "html"
            {:filter-children (fn [el]
                                (cond
                                  (instance? js/Text el)
                                  (-> (get-text el)
                                      (str/replace #"[\n\s]+" "")
                                      not-empty)
                                  
                                  (instance? js/Element el)
                                  (or (contains? #{"html" "head" "body"}
                                                 (get-name el))
                                      (descendant? el "head")
                                      (and (descendant? el "body")
                                           (has-class? el "cljcastr")))))})
  ;; => "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n
  ;;     <head>\n
  ;;       <title>cljcastr episode player</title>\n
  ;;       <link rel=\"stylesheet\" href=\"main.css\"></link>\n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.js\" type=\"application/javascript\"></script>\n
  ;;       <script>var SCITTLE_NREPL_WEBSOCKET_PORT = 1340;</script>\n
  ;;       <script src=\"https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.nrepl.js\" type=\"application/javascript\"></script>\n
  ;;       <script type=\"application/x-scittle\" src=\"player.cljs\"></script>\n
  ;;     </head>\n
  ;;     <body>\n
  ;;       <audio class=\"audio-for-speech\" src=\"\"></audio>\n
  ;;       <div id=\"episode-container\" class=\"cljcastr\" style=\"width: 100%; display: flex; flex-direction: column; align-items: center;\">\n
  ;;         <div id=\"episode-info\" class=\"cljcastr\" style=\"max-width: 420px; text-align: center;\">\n
  ;;           <h3 id=\"podcast-title\" class=\"cljcastr\">defn</h3>\n
  ;;           <h2 id=\"episode-title\" class=\"cljcastr\">876b4c306172 Cora Sutton</h2>\n
  ;;           <span id=\"episode-date\" class=\"cljcastr\" style=\"color: grey;\">July 9, 2025</span>\n
  ;;         </div>\n
  ;;         ...
  ;;       </div>\n
  ;;     </body>\n
  ;;     </html>\n
  ;;     "

  (do
    (log (dom->html))
    nil)
  ;; => nil

  (let [filename "boring.html"
        html "<html><body><h1>Yawn...</body></html>"
        blob-url (-> (js/Blob. [html] (clj->js {:type "text/html"}))
                     js/URL.createObjectURL)]
    (-> (create-el {:tag "a"
                    :attrs {:download filename
                            :href blob-url}})
        .click))
  ;; => nil

  (save-html!)
  ;; => nil

  (doseq [[el-id {:keys [click-handler]}] playback-controls]
    (add-event-listener! (str "#button-" (name el-id)) "click"
                         click-handler))
  ;; => nil

  (let [audio (get-el "audio.cljcastr")]
    (add-event-listener! "#progress-slider" "input"
                         #(->> % .-target .-value (set! (.-currentTime audio))))
    (add-event-listener! audio "timeupdate"
                         #(update-playback-position-indicators!
                           (.-currentTime audio) (.-duration audio)
                           (->> [:elapsed-time
                                 :remaining-time
                                 :progress-slider
                                 :progress-bar]
                                (map (fn [el-id]
                                       [el-id (str "#" (name el-id))]))
                                (into {}))))
    audio)
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]
  
  (->> (get-styles "#progress-bar")
       (map (fn [[k v]]
              (str (name k) ": " v)))
       (str/join "; "))
  ;; => "color: rgb(247, 147, 30); top: 10px; overflow: hidden;
  ;;     background-color: lightgray; width: 100%; z-index: 0;
  ;;     appearance: none; position: absolute; border: 0px;
  ;;     border-radius: 4px; height: 8px; left: 0px; margin: 0px;"

  (let [el (get-el "#progress-bar")
        styles (get-styles el)]
    (str "#" (get-id el) " {\n  "
         (->> styles
              (map (fn [[k v]]
                     (str (name k) ": " v)))
              (str/join ";\n  "))
         "\n}"))
  ;; => "#progress-bar {\n
  ;;       color: rgb(247, 147, 30);\n
  ;;       top: 10px;\n
  ;;       overflow: hidden;\n
  ;;       background-color: lightgray;\n
  ;;       width: 100%;\n
  ;;       z-index: 0;\n
  ;;       appearance: none;\n
  ;;       position: absolute;\n
  ;;       border: 0px;\n
  ;;       border-radius: 4px;\n
  ;;       height: 8px;\n
  ;;       left: 0px;\n
  ;;       margin: 0px;\n
  ;;     }"

  (let [el (get-el "audio.cljcastr")
        styles (get-styles el)]
    (str "#" (get-id el) " {\n  "
         (->> styles
              (map (fn [[k v]]
                     (str (name k) ": " v)))
              (str/join ";\n  "))
         "\n}"))
  ;; => "# {\n  display: none;\n}"

  (let [el (get-el "audio.cljcastr")
        styles (get-styles el)
        id (get-id el)
        classes (get-classes el)
        sel (if (not-empty id)
              (str "#" (get-id el))
              (str (get-name el) (if (seq classes)
                                   (str "." (first classes)))))]
    (str sel " {\n  "
         (->> styles
              (map (fn [[k v]]
                     (str (name k) ": " v)))
              (str/join ";\n  "))
         "\n}"))
  ;; => "audio.cljcastr {\n  display: none;\n}"

  (let [el (get-el "audio.cljcastr")
        styles (get-styles el)
        id (get-id el)
        classes (get-classes el)
        sel (if (not-empty id)
              (str "#" (get-id el))
              (str (get-name el) (if (seq classes)
                                   (str "." (first classes)))))]
    (str sel " {\n  "
         (->> styles
              (map (fn [[k v]]
                     (str (name k) ": " v)))
              (str/join ";\n  "))
         "\n}"))
  ;; => "audio.cljcastr {\n  display: none;\n}"

  (->> (el->css "#progress-bar")
       log)
  ;; => "#progress-bar {\n
  ;;       color: rgb(247, 147, 30);\n
  ;;       top: 10px;\n
  ;;       overflow: hidden;\n
  ;;       background-color: lightgray;\n
  ;;       width: 100%;\n
  ;;       z-index: 0;\n
  ;;       appearance: none;\n
  ;;       position: absolute;\n
  ;;       border: 0px;\n
  ;;       border-radius: 4px;\n
  ;;       height: 8px;\n
  ;;       left: 0px;\n
  ;;       margin: 0px;\n
  ;;     }"

  (->> (els->css (get-els ".cljcastr"))
       log)
  ;; => "#episode-container {\n
  ;;       width: 100%;\n
  ;;       display: flex;\n
  ;;       flex-direction: column;\n
  ;;       align-items: center;\n
  ;;     }\n
  ;;     \n
  ;;     #episode-info {\n
  ;;       max-width: 420px;\n
  ;;       text-align: center;\n
  ;;     }\n
  ;;     \n
  ;;     ...

  (def style-groups
    (->> (get-els ".cljcastr")
         (map el->clj)
         (remove (comp empty? :styles))
         (group-by :styles)))
  ;; => #'cljcastr.player/style-groups

  (count style-groups)
  ;; => 13

  (->> style-groups
       (map (fn [[_styles els]]
              (map #(select-keys % [:tag :id :classes]) els))))
  ;; => (({:tag "div", :id "progress-container", :classes ("cljcastr")})
  ;;     ({:tag "div", :id "progress", :classes ("cljcastr")})
  ;;     ({:tag "progress", :id "progress-bar", :classes ("cljcastr")})
  ;;     ({:tag "img", :id "episode-art", :classes ("cljcastr")})
  ;;     ({:tag "input", :id "progress-slider", :classes ("cljcastr")})
  ;;     ({:tag "span", :id "episode-date", :classes ("cljcastr")})
  ;;     ({:tag "div", :id "episode-info", :classes ("cljcastr")})
  ;;     ({:tag "img", :id "button-backward-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-play-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-pause-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-forward-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-speed-icon", :classes ("cljcastr" "playback-icon")})
  ;;     ({:tag "audio", :classes ("cljcastr")})
  ;;     ({:tag "span", :id "elapsed-time", :classes ("cljcastr")})
  ;;     ({:tag "button", :id "button-backward", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-play", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-pause", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-forward", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-speed", :classes ("cljcastr" "playback-button")})
  ;;     ({:tag "span", :id "remaining-time", :classes ("cljcastr")})
  ;;     ({:tag "div", :id "episode-container", :classes ("cljcastr")}))

  (->> (get-els ".cljcastr")
       group-styles
       (filter (fn [[_ els]] (> (count els) 1)))
       (map (fn [[_styles els]]
              (map #(select-keys % [:tag :id :classes]) els))))
  ;; => (({:tag "img", :id "button-backward-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-play-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-pause-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-forward-icon", :classes ("cljcastr" "playback-icon")}
  ;;      {:tag "img", :id "button-speed-icon", :classes ("cljcastr" "playback-icon")})
  ;;     ({:tag "button", :id "button-backward", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-play", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-pause", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-forward", :classes ("cljcastr" "playback-button")}
  ;;      {:tag "button", :id "button-speed", :classes ("cljcastr" "playback-button")}))

  (->> (get-els ".cljcastr")
       group-styles
       (filter (fn [[_ els]] (> (count els) 1)))
       (map (fn [[_styles els]]
              (map (fn [el]
                     (-> (select-keys el [:tag :id :classes])
                         (update :classes #(remove (fn [cls] (= "cljcastr" cls)) %))))
                   els))))
  ;; => (({:tag "img", :id "button-backward-icon", :classes ("playback-icon")}
  ;;      {:tag "img", :id "button-play-icon", :classes ("playback-icon")}
  ;;      {:tag "img", :id "button-pause-icon", :classes ("playback-icon")}
  ;;      {:tag "img", :id "button-forward-icon", :classes ("playback-icon")}
  ;;      {:tag "img", :id "button-speed-icon", :classes ("playback-icon")})
  ;;     ({:tag "button", :id "button-backward", :classes ("playback-button")}
  ;;      {:tag "button", :id "button-play", :classes ("playback-button")}
  ;;      {:tag "button", :id "button-pause", :classes ("playback-button")}
  ;;      {:tag "button", :id "button-forward", :classes ("playback-button")}
  ;;      {:tag "button", :id "button-speed", :classes ("playback-button")}))

  (->> (get-els ".cljcastr")
       group-styles
       (filter (fn [[_ els]] (> (count els) 1)))
       (map (fn [[_styles els]]
              (->> els
                   (map (fn [el]
                          (update el :classes #(remove (fn [cls] (= "cljcastr" cls)) %))))
                   els->css)))
       (str/join "\n\n")
       log)
  ;; => "#button-backward-icon {\n  width: 35px;\n  ...}"

  (->> (get-els ".cljcastr")
       group-styles
       (filter (fn [[_ els]] (> (count els) 1)))
       (map (fn [[_styles [el & _]]]
              (let [sel (str "." (->> (:classes el)
                                      (remove (partial = "cljcastr"))
                                      first))]
                (el->css el sel))))
       (str/join "\n\n")
       log)
  ;; => ".playback-icon {\n  width: 35px;\n  ...}"

  (->> (get-els ".cljcastr")
       group-styles
       (map (fn [[_styles [el & rest-els]]]
              (if (seq rest-els)
                (let [sel (str "." (->> (:classes el)
                                        (remove (partial = "cljcastr"))
                                        first))]
                  (el->css el sel))
                (el->css el))))
       (str/join "\n\n")
       log)
  ;; => "#progress-container {\n  position: relative;\n  height: 28px;\n  ...}"

  (save-css!)
  ;; => nil

  (->> (create-el {:tag "link",
                   :attrs {:rel "stylesheet", :href "main.css"}})
       (add-child! "head"))
  ;; => #object[HTMLHeadElement [object HTMLHeadElement]]

  (->> (get-els "body > .cljcastr")
       (map #(el->html % {:exclude-styles? true}))
       (str/join "\n")
       log)
  ;; => "<div id=\"episode-container\" class=\"cljcastr\">\n
  ;;       <div id=\"episode-info\" class=\"cljcastr\">\n
  ;;         <h3 id=\"podcast-title\" class=\"cljcastr\">defn</h3>\n
  ;;         <h2 id=\"episode-title\" class=\"cljcastr\">876b4c306172 Cora Sutton</h2>\n
  ;;         <span id=\"episode-date\" class=\"cljcastr\">July 9, 2025</span>\n
  ;;       </div>\n
  ;;       ...
  ;;     </div>\n
  ;;     "

  (save-html! {:exclude-styles? true})
  ;; => nil

  )
