(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 ^:async fetch-text
  "Fetches the specified URL and returns a promise to deliver the response body
   as text."
  [url]
  (let [resp (await (js/fetch (js/Request. url)))]
    (->> (await (.text resp))
         (log "Fetched text from url:" url))))

(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 remove-child!
  "Removes the specified element from the DOM, returning it. A parent node may
   also be specified, for example to disambiguate the selector for a child
   element."
  ([el]
   (let [child (get-el el)]
     (remove-child! (.-parentNode child) child)))
  ([parent child]
   (-> (get-el parent)
       (.removeChild (get-el child)))))

(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 clone-el
  "Returns a copy of the specified element. A map of overrides may also be
   specified, with the same keys as `create-el`."
  ([el]
   (clone-el el {}))
  ([el overrides]
   (-> (el->clj el)
       (merge overrides)
       create-el)))

(defn css->styles
  "Returns a map of styles corresponding to the specified CSS style
   declaration."
  [css]
  (let [styles-str (-> css
                       (str/replace "\n" " ")
                       (str/replace #"^.+[{]([^}]+)[}].*$" "$1"))]
    (-> (create-el {:tag "div", :attrs {:style styles-str}})
        get-styles)))

(defn get-rulesets
  "Returns a sequence of CSS rulesets from either the specified CSS string if
   present or the contents of the `style#css-rulesets` element otherwise. Each
   ruleset is a pair of condition (i.e. the list of selectors associated with a
   given CSS declaration block) and map corresponding to the CSS styles in the
   declaration block.

   See https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Syntax/Introduction#css_rulesets
   for explanations of CSS terminology like ruleset, condition, and declaration
   block.

   Example:

     (get-rulesets \"div { color: red; } a { color: blue; }\")
     ;; => [[\"div\" {:color \"red\"}] [\"a\" {:color \"blue\"}]]"
  ([]
   (when-let [el (get-el "style#css-rulesets")]
     (-> (get-text el) get-rulesets)))
  ([css]
   (when-not (empty? css)
     (->> css
          (re-seq #"(?s)^\s*([^{]+)\s*[{]([^}]+)[}]")
          (map (fn [[_match sel declarations]]
                 [(str/trim sel) (css->styles declarations)]))))))

(defn ^:async fetch-rulesets
  "Returns a promise to deliver rulesets from all stylesheet links in the DOM or
   the CSS files at the specified sequence of URLs. `urls` may also be a single
   URL as a string."
  ([]
   (->> (get-els "link[rel=\"stylesheet\"]")
        (map #(get-attr % :href))
        fetch-rulesets))
  ([urls]
   (let [urls (if (sequential? urls) urls [urls])]
     (->> (await (js/Promise.all (mapv fetch-text urls)))
          (mapcat get-rulesets)
          (log "Fetched rulesets:")))))

(defn get-ruleset
  "Returns the CSS ruleset from the active stylesheet (i.e. the contents of the
   `style#css-rulesets` element) corresponding to the specified selector."
  [sel]
  (->> (get-rulesets)
       (some (fn [[rs-sel _declarations :as ruleset]]
               (and (= sel rs-sel) ruleset)))))

(defn rulesets->css
  "Given a sequence of rulesets, returns the corresponding CSS."
  [rulesets]
  (->> rulesets
       (map (partial apply format-css))
       (str/join "\n\n")))

(defn set-stylesheet!
  "Sets the document's stylesheet to the specified CSS and returns it."
  [css]
  (when-not (get-el "style#css-rulesets")
    (->> (create-el {:tag "style", :id "css-rulesets"})
         (add-child! "head")))
  (set-text! "style#css-rulesets" css)
  css)

(defn set-rulesets!
  "Given a sequence of rulesets, transforms them into a stylesheet which becomes
   the text of the `style#css-rulesets` element. Returns the stylesheet."
  [rulesets]
  (-> (rulesets->css rulesets)
      set-stylesheet!))

(defn dedup-rulesets
  "Deduplicates rulesets by taking the last ruleset for each selector."
  [rulesets]
  (->> rulesets
       (group-by first)
       (map (fn [[_sel rulesets']]
              (last rulesets')))))

(defn add-rulesets!
  "Adds the specified rulesets to the document's stylesheet, returning the new
   stylesheet."
  [rulesets]
  (-> (get-rulesets)
      (concat rulesets)
      dedup-rulesets
      set-rulesets!))

(defn add-ruleset!
  "Adds the specified ruleset to the document's stylesheet, returning the new
   stylesheet."
  [ruleset]
  (add-rulesets! [ruleset]))

(defn update-ruleset!
  "Updates the declarations in the ruleset with the same selector by merging the
   existing declarations with the declarations of the specified ruleset. Returns
   the new stylesheet."
  [[sel declarations :as ruleset]]
  (let [[_sel cur-declarations] (get-ruleset sel)]
    (add-ruleset! [sel (merge cur-declarations declarations)])))

(defn get-listener-record
  ([state-atom el event-type f]
   (get-listener-record state-atom el event-type f nil))
  ([state-atom el event-type f id]
   (let [el (get-el el)
         event-type (keyword event-type)]
     (some #(and (if id
                   (= id (:id %))
                   (= f (:f %)))
                 %)
           (get-in @state-atom [:listeners el event-type])))))

(defn remove-event-listener!
  "Removes the event listener with the specified id and event type."
  ([state-atom el event-type f]
   (remove-event-listener! state-atom el event-type f nil))
  ([state-atom el event-type f id]
   (let [el (get-el el)
         event-type (keyword event-type)]
     (when-let [record
                (get-listener-record state el :click f id)]
       (.removeEventListener el (name event-type) (:wrapped-f record))
       (swap! state-atom update-in [:listeners el event-type]
              (fn [fns] (->> fns
                             (remove #(or (= record %)
                                          (and id (= id (:id %)))))
                             (into []))))))))

(defn add-event-listener!
  "Adds an event listener for the specified event type to an element. If a state
   atom is specified, the listener will be recorded in the state atom. Returns
   the element."
  ([el event-type f]
   (add-event-listener! nil el event-type f))
  ([state-atom el event-type f]
   (add-event-listener! state-atom el event-type f nil))
  ([state-atom el event-type f id]
   (let [el (get-el el)
         event-type (keyword event-type)
         path [:listeners el event-type]
         listeners (when state-atom
                     (get-in @state-atom path))]
     (if (some #(= f (:f %)) listeners)
       (log "Not adding duplicate" (name event-type) "listener to" el)
       (let [wrapped-f
             (fn [ev]
               (log (str "Handling " (name event-type) " on")
                    el ev)
               (if f
                 (f ev)
                 (log "No event handler function defined")))]
         (remove-event-listener! state-atom el event-type f id)
         (.addEventListener el (name event-type) wrapped-f)
         (when state-atom
           (swap! state-atom update-in path
                  conj {:id id
                        :wrapped-f wrapped-f
                        :f f
                        :el-id (get-id el)}))
         (log "Added listener for" (name event-type) "events to" el)))
     el)))

(defn clear-event-listeners!
  "Removes all event listeners from the specified element, or if an event type
   is specified, all listeners for events of that type. Returns the element."
  ([state-atom el]
   (let [el (get-el el)]
     (clear-event-listeners! state-atom el
                             (-> (get-in @state-atom [:listeners el])
                                 keys))))
  ([state-atom el event-types]
   (let [el (get-el el)]
     (doseq [event-type (if (sequential? event-types) event-types [event-types])
             f (get-in @state-atom [:listeners el (keyword event-type)])
             :let [event-type (keyword event-type)]]
       (log "Removing listener for" (name event-type) "events from" el)
       (.removeEventListener el (name event-type) (:wrapped-f f))
       (swap! state-atom update-in [:listeners el event-type]
              (fn [fns] (->> fns
                             (remove #(= f %))
                             (into [])))))
     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?
                     :remove-children (fn [el]
                                        (and (instance? js/Element el)
                                             (= "css-rulesets" (get-id el))))}))))

(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 ^:async dom->css []
  (let [external-rulesets (await (fetch-rulesets))
        style-rulesets (->> (get-els "style")
                            (mapcat (comp get-rulesets get-text)))
        inline-rulesets (->> (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")
                             get-rulesets)]
    (->> (concat external-rulesets style-rulesets inline-rulesets)
         dedup-rulesets
         rulesets->css)))

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

(def seek-dur-sec 15)

(def playback-buttons
  {: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)}})

(defn init-ui! []
  (doseq [[el-id {:keys [click-handler]}] playback-buttons]
    (add-event-listener! state (str "#button-" (name el-id)) :click
                         click-handler el-id))
  (let [audio (get-el "audio.cljcastr")]
    (add-event-listener! state "#progress-slider" "input"
                         #(->> % .-target .-value (set! (.-currentTime audio)))
                         :seek)
    (add-event-listener! state "#speed-select" "change"
                         #(->> % .-target .-value (set! (.-playbackRate audio)))
                         :speed)
    (add-event-listener! state 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 {})))
                         :time-update)
    audio)
  (log "UI initialised"))

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

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

(comment

  (el->html "#button-speed")
  ;; => "<button aria-label=\"Playback speed\" id=\"button-speed\" class=\"cljcastr playback-button\">\n
  ;;       <img src=\"https://www.svgrepo.com/show/151785/speed-meter.svg\" draggable=\"false\" id=\"button-speed-icon\" class=\"cljcastr playback-icon\"></img>\n
  ;;     </button>\n"

  (def speed-control
    (-> (create-el
         {:tag "div", :id "speed-control", :classes ["cljcastr"]
          :children
          [(create-el
            {:tag "label", :id "speed-menu", :classes ["cljcastr"]
             :attrs {:title "Playback speed"}
             :children
             [(get-el "#button-speed-icon")
              (create-el
               {:tag "select", :id "speed-select", :classes ["cljcastr"]
                :attrs {:aria-label "Playback speed"}
                :children
                (->> [0.75 1 1.25 1.5 2]
                     (map (fn [speed]
                            (create-el {:tag "option"
                                        :classes ["cljcastr"]
                                        :text (str speed "x")
                                        :attrs (merge {:value (js/parseFloat speed)}
                                                      (when (= 1 speed)
                                                        {:selected true}))}))))})]})]})
        log))
  ;; => #'cljcastr.player/speed-control

  (-> (clone-el "#button-speed-icon")
      log)
  ;; => #object[HTMLImageElement [object HTMLImageElement]]

  (= (get-el "#button-speed-icon")
     (clone-el "#button-speed-icon"))
  ;; => false

  (def speed-control
    (-> (create-el
         {:tag "div", :id "speed-control", :classes ["cljcastr"]
          :children
          [(create-el
            {:tag "label", :id "speed-menu", :classes ["cljcastr"]
             :attrs {:title "Playback speed"}
             :children
             [(clone-el "#button-speed-icon")
              (create-el
               {:tag "select", :id "speed-select", :classes ["cljcastr"]
                :attrs {:aria-label "Playback speed"}
                :children
                (->> [0.75 1 1.25 1.5 2]
                     (map (fn [speed]
                            (create-el {:tag "option"
                                        :classes ["cljcastr"]
                                        :text (str speed "x")
                                        :attrs (merge {:value (js/parseFloat speed)}
                                                      (when (= 1 speed)
                                                        {:selected true}))}))))})]})]})
        log))
  ;; => #'cljcastr.player/speed-control

  (add-child! "#playback-controls" speed-control)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (remove-child! "#button-speed")
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  (get-el "#button-speed")
  ;; => nil

  (init-ui!)
  ;; => "#error {:message \"Cannot read properties of null (reading 'addEventListener')\", ...}"

  (init-ui!)
  ;; => "UI initialised"

  (get-value "#speed-select")
  ;; => "1.25"

  (add-event-listener! "#speed-select" "change"
                       (fn [ev]
                         (let [select-el (.-target ev)]
                           (log "New speed selected:" (get-value select-el)))))
  ;; => #object[HTMLSelectElement [object HTMLSelectElement]]

  (init-ui!)
  ;; => "UI initialised"

  (def event-testing-el (create-el {:tag "div", :id "event-testing"}))
  ;; => #'cljcastr.player/event-testing-el

  (defn named-f [ev]
    (log "I am Jack's named function"))
  ;; => #'cljcastr.player/named-f

  (.addEventListener event-testing-el "click" named-f)
  ;; => nil

  (.click event-testing-el)
  ;; => nil

  ;; Add named function again
  (.addEventListener event-testing-el "click" named-f)

  ;; Named function only called once
  (.click event-testing-el)
  ;; => nil

  (let [anon-f (fn [ev]
                 (log "I am anonymous"))]
    (.addEventListener event-testing-el "click" anon-f)

    ;; Add the same anonymous function again
    (.addEventListener event-testing-el "click" anon-f)

    ;; Each function only called once
    (.click event-testing-el))
  ;; => nil

  ;; Add another anonymous function
  (.addEventListener event-testing-el "click"
                     (fn [ev]
                       (log "I am super anonymous")))
  ;; => nil

  ;; Add the "same" anonymous function again
  (.addEventListener event-testing-el "click"
                     (fn [ev]
                       (log "I am super anonymous")))
  ;; => nil

  ;; Super anonymous function called twice
  (.click event-testing-el)
  ;; => nil

  (let [anon-f (fn [ev]
                 (log "I am anonymous"))]
    {:is-anon-f-the-same-as-itself?
     (= anon-f anon-f)
     :is-one-super-anon-f-the-same-as-another?
     (= (fn [ev] (log "I am super anonymous"))
        (fn [ev] (log "I am super anonymous")))})
  ;; => {:is-anon-f-the-same-as-itself? true,
  ;;     :is-one-super-anon-f-the-same-as-another? false}

  (.removeEventListener event-testing-el "click" named-f)
  ;; => nil

  (.removeEventListener event-testing-el "click"
                        (fn [ev]
                          (log "I am super anonymous")))
  ;; => nil

  (.click event-testing-el)
  ;; => nil

  (def event-testing-el (create-el {:tag "div", :id "event-testing"}))
  ;; => #'cljcastr.player/event-testing-el

  (add-event-listener! state event-testing-el :click named-f)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (add-event-listener! state event-testing-el :click named-f)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (.click event-testing-el)
  ;; => nil

  (:listeners @state)
  ;; => {#object[HTMLDivElement [object HTMLDivElement]]
  ;;     {:click [{:wrapped-f #object[Function],
  ;;               :f #object[Function],
  ;;               :el-id "event-testing"}]}}

  (let [anon-f (fn [ev]
                 (log "I am anonymous"))]
    (add-event-listener! state event-testing-el "click" anon-f)

    ;; Add the same anonymous function again
    (add-event-listener! state event-testing-el "click" anon-f)

    ;; Each function only called once
    (.click event-testing-el))
  ;; => nil

  (add-event-listener! state event-testing-el "click"
                       (fn [ev]
                         (log "I am super anonymous")))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (add-event-listener! state event-testing-el "click"
                       (fn [ev]
                         (log "I am super anonymous")))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (.click event-testing-el)
  ;; => nil

  (clear-event-listeners! state event-testing-el)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (.click event-testing-el)
  ;; => nil

  (defn some-listener [ev]
    (log "Just logging the event"))
  ;; => #'cljcastr.player/some-listener

  (add-event-listener! state event-testing-el :click some-listener)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (.click event-testing-el)
  ;; => nil

  (defn some-listener [ev]
    (let [el (.-target ev)
          event-type (.-type ev)]
      (log "Handling event of type" event-type "on element" el)))
  ;; => #'cljcastr.player/some-listener

  (add-event-listener! state event-testing-el :click some-listener)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (.click event-testing-el)
  ;; => nil

  @state
  ;; => {:initialised? true,
  ;;     :listeners
  ;;     {#object[HTMLDivElement [object HTMLDivElement]]
  ;;      {:click
  ;;       ({:wrapped-f #object[Function], :f #object[Function], :el-id "event-testing"}
  ;;        {:wrapped-f #object[Function], :f #object[Function], :el-id "event-testing"})}}}

  (remove-event-listener! state event-testing-el :click some-listener)
  ;; => {:initialised? true,
  ;;     :listeners
  ;;     {#object[HTMLDivElement [object HTMLDivElement]]
  ;;      {:click
  ;;       [{:wrapped-f #object[Function], :f #object[Function], :el-id "event-testing"}]}}}

  (.click event-testing-el)
  ;; => nil

  (add-event-listener! state event-testing-el :click some-listener)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (get-listener-record state event-testing-el :click some-listener)
  ;; => {:wrapped-f #object[Function], :f #object[Function], :el-id "event-testing"}

  (remove-event-listener! state event-testing-el :click some-listener)
  ;; => {:initialised? true,
  ;;     :listeners
  ;;     {#object[HTMLDivElement [object HTMLDivElement]]
  ;;      {:click
  ;;       [{:wrapped-f #object[Function], :f #object[Function], :el-id "event-testing"}]}}}

  (clear-event-listeners! state event-testing-el)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (add-event-listener! state event-testing-el :click
                       some-listener :some-listener)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (defn some-listener [ev]
    (let [el (.-target ev)
          event-type (.-type ev)]
      (log "Handling event" ev "on element" el)))
  ;; => #'cljcastr.player/some-listener

  (add-event-listener! state event-testing-el :click
                       some-listener :some-listener)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (.click event-testing-el)
  ;; => nil

  (let [css "
.speedmenu {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  color: #fc7e0f;
  border: 1px solid #fc7e0f;
  border-radius: 999px;
  padding: 5px 12px 5px 11px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.1s ease-out;
}"]
    (css->styles css))
  ;; => {:align-items "center",
  ;;     :color "#fc7e0f",
  ;;     :transition "background-color 0.1s ease-out",
  ;;     :cursor "pointer",
  ;;     :position "relative",
  ;;     :padding "5px 12px 5px 11px",
  ;;     :gap "0.4em",
  ;;     :-webkit-tap-highlight-color "transparent",
  ;;     :display "inline-flex",
  ;;     :border "1px solid #fc7e0f",
  ;;     :border-radius "999px"}

  (let [css "
.speedmenu {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  color: #fc7e0f;
  border: 1px solid #fc7e0f;
  border-radius: 999px;
  padding: 5px 12px 5px 11px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.1s ease-out;
}"]
    (->> css
         css->styles
         (set-styles! "#speed-menu")))
  ;; => #object[HTMLLabelElement [object HTMLLabelElement]]

  (let [css "
.speedmenu select {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  font: inherit;
  font-size: 0.9em;
  font-variant-numeric: tabular-nums;
  color: inherit;
  background: transparent;
  border: 0;
  margin: 0;
  padding: 0 1.4em 0 0;
  cursor: pointer;
  outline: none;
}"]
    (->> css
         css->styles
         (set-styles! "#speed-select")))
  ;; => #object[HTMLSelectElement [object HTMLSelectElement]]

  (let [css "
.speedmenu::after {
  content: \"\";
  position: absolute;
  right: 13px;
  top: 50%;
  width: 0.4em;
  height: 0.4em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-65%) rotate(45deg);
  pointer-events: none;
}"]
    (->> css
         css->styles
         (set-styles! "#speed-select::after")))
  ;; => "#error {:message \"Cannot set properties of null (setting 'style')\", ...}"

  (->> (create-el {:tag "style", :id "css-rulesets", :text "div { color: red; }"})
       (add-child! "head"))
  ;; => #object[HTMLHeadElement [object HTMLHeadElement]]

  (set-text! "#css-rulesets" "div { color: black; }")
  ;; => #object[HTMLStyleElement [object HTMLStyleElement]]

  (get-rulesets)
  ;; => (["div" {:color "black"}])

  (let [css "
.speedmenu {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  color: #fc7e0f;
  border: 1px solid #fc7e0f;
  border-radius: 999px;
  padding: 5px 12px 5px 11px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.1s ease-out;
}

.speedmenu::after {
  content: \"\";
  position: absolute;
  right: 13px;
  top: 50%;
  width: 0.4em;
  height: 0.4em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-65%) rotate(45deg);
  pointer-events: none;
}
"]
    (get-rulesets css))
  ;; => ([".speedmenu"
  ;;      {:align-items "center", :color "#fc7e0f", ...}]
  ;;     [".speedmenu::after"
  ;;      {:border-right "2px solid currentColor", :transform "translateY(-65%) rotate(45deg)", ...}])

  (let [rulesets [["#speed-menu::after"
                   {:border-right "2px solid currentColor"
                    :transform "translateY(-65%) rotate(45deg)"
                    :top "50%"
                    :content "\"\""
                    :width "0.4em"
                    :right "13px"
                    :position "absolute"
                    :pointer-events "none"
                    :height "0.4em"
                    :border-bottom "2px solid currentColor"}]]]
    (set-rulesets! rulesets))
  ;; => "#speed-menu::after {\n  border-right: 2px solid currentColor;\n  ...}"

  (-> (get-rulesets)
      (concat [["#episode-info *" {:color "red"}]])
      set-rulesets!)
  ;; => "#speed-menu::after {\n  border-right: 2px solid currentColor;\n ...}\n\n
  ;;     #episode-info * {\n  color: red\n}"

  (add-ruleset! ["#episode-info *" {:color "red"}])
  ;; => "#speed-menu::after {\n  border-right: 2px solid currentColor; ... }\n\n
  ;;     #episode-info * {\n  color: red\n}\n\n
  ;;     #episode-info * {\n  color: red\n}"

  (add-ruleset! ["#episode-info *" {:color "red"}])
  ;; => "#speed-menu::after {\n  border-right: 2px solid currentColor; ... }\n\n
  ;;     #episode-info * {\n  color: red\n}"

  (get-ruleset "#episode-info *")
  ;; => ["#episode-info *" {:color "red"}]

  (update-ruleset! ["#speed-menu::after"
                    {:border-right "5px solid currentColor"
                     :border-bottom "5px solid currentColor"}])
  ;; => "#speed-menu::after {\n  border-right: 5px solid currentColor; ...}\n\n
  ;;     #episode-info * {\n  color: red\n}"

  (let [rulesets [["#speed-menu::after"
                   {:border-right "2px solid currentColor"
                    :transform "translateY(-65%) rotate(45deg)"
                    :top "50%"
                    :content "\"\""
                    :width "0.4em"
                    :right "13px"
                    :position "absolute"
                    :pointer-events "none"
                    :height "0.4em"
                    :border-bottom "2px solid currentColor"}]]]
    (set-rulesets! rulesets))
  ;; => "#speed-menu::after {\n  border-right: 2px solid currentColor;\n  ...}"

  (save-css! "test.css")
  ;; => nil

  ;; Turn the contents of all style elements into CSS rulesets
  (->> (get-els "style")
       (mapcat (comp get-rulesets get-text)))
  ;; => (["#speed-menu::after" {:border-right "2px solid currentColor", ...}])

  ;; Turn inline styles into CSS rulesets: grab the body of the current dom->css
  ;; function and slap a call to get-rulesets on the end
  (->> (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")
       get-rulesets)

  ;; Combine the rulesets and turn them into CSS
  (->> (concat (->> (get-els "style")
                    (mapcat (comp get-rulesets get-text)))
               (->> (dom->css)
                    get-rulesets))
       dedup-rulesets
       rulesets->css)
  ;; => "#speed-menu::after {\n  border-right: 2px solid currentColor;\n  ... }\n\n
  ;;     #speed-menu {\n  align-items: center;\n  color: rgb(252, 126, 15);\n  ... }\n\n
  ;;     #speed-select {\n  color: inherit;\n  background: transparent;\n  ... }"

  (dom->css)
  ;; => "#speed-menu::after {\n  border-right: 2px solid currentColor;\n  ... }\n\n
  ;;     #speed-menu {\n  align-items: center;\n  color: rgb(252, 126, 15);\n  ... }\n\n
  ;;     #speed-select {\n  color: inherit;\n  background: transparent;\n  ... }"

  (->> (get-els "link[rel=\"stylesheet\"]")
       (map #(get-attr % :href)))
  ;; => ("main.css")

  (let [url "main.css"]
    (js/fetch (js/Request. url)))
  ;; => #<Promise #object[Response [object Response]]>

  (let [url "main.css"]
    (-> (js/fetch (js/Request. url))
        (.then #(log "fetch returned:" %))))
  ;; => #<Promise #object[Response [object Response]]>

  (let [url "main.css"]
    (-> (js/fetch (js/Request. url))
        (.then (fn [resp]
                 (log "fetch returned:" resp)
                 (.text resp)))))
  ;; => #<Promise "#progress-container {\n  position: relative;\n  ...">

  (let [url "main.css"]
    (-> (js/fetch (js/Request. url))
        (.then (fn [resp]
                 (log "fetch returned:" resp)
                 (-> (.text resp)
                     get-rulesets)))))
  ;; => #error {:message "[object Promise] is not ISeqable", ... }

  (let [url "main.css"]
    (-> (js/fetch (js/Request. url))
        (.then (fn [resp]
                 (log "fetch returned:" resp)
                 (.text resp)))
        (.then (fn [text]
                 (log "got text:" text)
                 (->> (get-rulesets text)
                      (log "rulesets:"))))))
  ;; => #<Promise (["#progress-container" {:position "relative", ...}] ...)>

  (let [url "main.css"
        fetch-rulesets (^:async fn []
                        (let [text (await (fetch-text url))]
                          (log "rulesets:" (get-rulesets text))))]
    (fetch-rulesets url))
  ;; => #<Promise (["#progress-container" {:position "relative", ...}] ...)>

  (let [url "main.css"]
    (-> (fetch-text url)
        get-rulesets))
  ;; => "#error {:message \"[object Promise] is not ISeqable\", }"

  (fetch-rulesets "main.css")
  ;; => #<Promise (["#progress-container" {:position "relative", ...}] ...)>

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

  (fetch-rulesets)
  ;; => "#error {:message \"[object Promise] is not ISeqable\", }"

  (fetch-rulesets)
  ;; => #<Promise (["#progress-container" {:position "relative", ...}] ...)>

  (save-css!)
  ;; => #<Promise nil>

  ;; Remove the silly test stylesheet
  (remove-child! "link[href=\"moar.css\"]")
  ;; => #object[HTMLLinkElement [object HTMLLinkElement]]

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

  )
