(ns cljcastr.player
  (:require [cljcastr.dom :as dom]
            [cljcastr.js-utils :as util :refer [log ->boolean ->int]]
            [cljcastr.time :as time]
            [cljcastr.transcript :as transcript]
            [clojure.string :as str]
            [promesa.core :as p]))

(declare play-episode!)
(declare seek-to-ts!)

(defonce state (atom {}))

;; If this isn't a serial podcast with seasons, use this fake season number
(def fake-season-number -1)

(def svg-ns "http://www.w3.org/2000/svg")

(def default-opts {:color-played "#ff9800"
                   :color-buffered "#ffbd52"
                   :color-position "black"
                   :enabled-buttons [:shuffle
                                     :back
                                     :rewind
                                     :play
                                     :pause
                                     :stop
                                     :fast-forward
                                     :next
                                     :repeat
                                     :repeat-one]
                   :title-fmt [:episode/artist " - " :episode/title]
                   :volume-image "/img/volume.png"
                   :audio-selector "#cljcastr-player-audio"
                   :controls-selector "#cljcastr-player-controls"
                   :cover-selector "#cover-image"
                   :main-selector "#wrapper"
                   :timeline-canvas-selector "canvas.cljcastr-player-timeline"
                   :timeline-position-selector "#cljcastr-player-position"
                   :title-selector "#title"
                   :transcribe-link-selector "#transcribe-link"
                   :transcribe-url nil})

(def buttons
  {
   :shuffle
   {:label "Toggle shuffle"
    :paths
    [["drop-shadow"
      "M116 4H12c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8V12c0-4.42-3.58-8-8-8z"]
     ["bg"
      "M110.16 3.96h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.46c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
     ["shine"
      "M40.16 12.86c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
      ".75"]
     ["fg"
      "M43.7 62.21v-25.7a2.258 2.258 0 0 1 3.4-2l43.5 25.7c1.13.72 1.47 2.22.75 3.35c-.19.3-.45.55-.75.75l-43.5 25.6c-1.08.63-2.46.27-3.09-.81c-.21-.36-.32-.77-.31-1.19v-25.7z"]]}

   :back
   {:label "Back episode"
    :paths
    [["drop-shadow"
      "M116.46 3.96h-104c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8v-104c0-4.42-3.58-8-8-8z"]
     ["bg"
      "M110.16 3.96h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.46c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
     ["fg"
      "M108.46 63.96v-25.7c0-1.8-1.7-2.9-3-2l-35 24.4v-22.4c.13-1.13-.69-2.15-1.82-2.28c-.45-.05-.9.05-1.28.28l-36.9 25.1v-23.5a1.9 1.9 0 0 0-1.9-1.9h-10.2a1.9 1.9 0 0 0-1.9 1.9v52.3c0 1.05.85 1.9 1.9 1.9h10.3a1.9 1.9 0 0 0 1.9-1.9v-23.6l36.9 25c.98.58 2.24.26 2.82-.72c.23-.39.33-.84.28-1.28v-22.3l35 24.4c1.4.9 3-.2 3-2l-.1-25.7z"]
     ["shine"
      "M40.16 12.86c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 6.5-3 13.3c0 4.8 0 7.3 2.5 7.3c3.4 0 3.4-5.9 6.2-10.3c5.4-8.7 18.9-8.6 18.9-11.6z"
      ".75"]]}

   :rewind
   {:label "Rewind"
    :paths
    [["drop-shadow"
      "M116.46 4h-104c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8V12c0-4.42-3.58-8-8-8z"]
     ["bg"
      "M110.16 4h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.5c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
     ["shine"
      "M40.16 12.9c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
      ".75"]
     ["fg"
      "M104.46 64V38.3c0-1.8-1.7-2.9-3-2l-37 24.4V38.3c.13-1.13-.69-2.15-1.82-2.28c-.45-.05-.9.05-1.28.28L21.46 62a2.529 2.529 0 0 0 0 4.1l39.8 25.7c.98.58 2.24.26 2.82-.72c.23-.39.33-.84.28-1.28V67.3l37 24.4c1.4.9 3-.2 3-2l.1-25.7z"]]}

   :play
   {:label "Play"
    :paths
    [["drop-shadow"
      "M116.46 3.96h-104c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8v-104c0-4.42-3.58-8-8-8z"]
     ["bg"
      "M110.16 3.96h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.46c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
     ["shine"
      "M40.16 12.86c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
      ".75"]
     [:el "g"
      :paths
      [["fg"
        "M43.7 62.21v-25.7a2.258 2.258 0 0 1 3.4-2l43.5 25.7c1.13.72 1.47 2.22.75 3.35c-.19.3-.45.55-.75.75l-43.5 25.6c-1.08.63-2.46.27-3.09-.81c-.21-.36-.32-.77-.31-1.19v-25.7z"]]]]}

   :pause
   {:label "Pause"
    :paths
    [["drop-shadow"
      "M116.46 3.96h-104c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8v-104c0-4.42-3.58-8-8-8z"]
     ["bg"
      "M110.16 3.96h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.46c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
     ["shine"
      "M40.16 12.86c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
      ".75"]
     ["fg"
      "M54.46 91.96h-12c-1.1 0-2-.9-2-2v-52c0-1.1.9-2 2-2h12c1.1 0 2 .9 2 2v52a2 2 0 0 1-2 2z"]
     ["fg"
      "M86.46 91.96h-12c-1.1 0-2-.9-2-2v-52c0-1.1.9-2 2-2h12c1.1 0 2 .9 2 2v52a2 2 0 0 1-2 2z"]
     ]}

   :stop
   {:label "Stop"
    :paths [["drop-shadow"
             "M116.46 3.96h-104c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8v-104c0-4.42-3.58-8-8-8z"]
            ["bg"
             "M110.16 3.96h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.46c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
            ["shine"
             "M40.16 12.86c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
             ".75"]
            ["fg"
             "M89.66 91.96h-50.4c-1.55 0-2.8-1.25-2.8-2.8v-50.4c0-1.55 1.25-2.8 2.8-2.8h50.4a2.728 2.728 0 0 1 2.8 2.66v50.54a2.728 2.728 0 0 1-2.66 2.8h-.14z"]
            ]}

   :fast-forward
   {:label "Fast forward"
    :paths [
            ["drop-shadow"
             "M116.46 4.14h-104c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8v-104c0-4.42-3.58-8-8-8z"]
            ["bg"
             "M110.16 4.14h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.64c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
            ["shine"
             "M40.16 13.04c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
             ".75"]
            ["fg"
             "M107.46 62.14l-39.9-25.7c-.98-.58-2.24-.26-2.82.72c-.23.39-.33.84-.28 1.28v22.3l-37-24.3c-1.4-.9-3 .2-3 2v51.4c0 1.8 1.7 2.9 3 2l37-24.4v22.3c-.13 1.13.69 2.15 1.82 2.28c.45.05.9-.05 1.28-.28l39.9-25.6c1.1-.76 1.38-2.28.62-3.38c-.17-.25-.38-.46-.62-.62z"]
            ["fg"
             "M107.36 66.14l-39.8 25.7c-.35.24-.78.34-1.2.3a.632.632 0 0 1-.4-.1c-.1 0-.2-.1-.4-.2c-.24-.11-.45-.28-.6-.5c-.12-.1-.2-.24-.2-.4c-.2-.33-.31-.71-.3-1.1v-22.4l-3 2l-34 22.4c-.35.24-.78.34-1.2.3a.632.632 0 0 1-.4-.1c-.1 0-.2-.1-.4-.2c-.7-.46-1.12-1.26-1.1-2.1v-51.3c-.08-1 .54-1.91 1.5-2.2c.1 0 .2-.1.4-.1c.42-.04.85.06 1.2.3l34 22.4l3 2v-22.4c0-.42.11-.83.3-1.2c.1-.1.1-.2.2-.4c.17-.2.38-.36.6-.5c.1-.1.2-.1.4-.2s.3-.1.4-.1c.42-.04.85.06 1.2.3l39.8 25.8c1.1.76 1.38 2.28.62 3.38c-.17.24-.38.45-.62.62z"
             ".2"]
            ]}

   :next
   {:label "Next episode"
    :paths [
            ["drop-shadow"
             "M116.46 4h-104c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8V12c0-4.42-3.58-8-8-8z"]
            ["bg"
             "M110.16 4h-98.2a7.555 7.555 0 0 0-7.5 7.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H110.06c4.14.01 7.49-3.34 7.5-7.48V11.5c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
            ["fg"
             "M107.56 36h-10.2a1.9 1.9 0 0 0-1.9 1.9v23.5l-35.9-25c-.98-.58-2.24-.26-2.82.72c-.23.39-.33.84-.28 1.28v22.3l-35-24.4c-1.4-.9-3 .2-3 2v51.4c0 1.8 1.7 2.9 3 2l35-24.4v22.3c-.13 1.13.69 2.15 1.82 2.28c.45.05.9-.05 1.28-.28l35.9-25v23.5c0 1.05.85 1.9 1.9 1.9h10.3a1.9 1.9 0 0 0 1.9-1.9V37.9c-.05-1.07-.93-1.9-2-1.9z"]
            ["shine"
             "M40.16 12.9c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
             ".75"]
            ]}

   :repeat
   {:label "Toggle repeat"
    :paths [
            ["drop-shadow"
             "M116 4H12c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8V12c0-4.42-3.58-8-8-8z"]
            ["bg"
             "M109.7 4H11.5A7.555 7.555 0 0 0 4 11.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H109.6c4.14.01 7.49-3.34 7.5-7.48V11.5c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
            ["shine"
             "M39.7 12.9c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
             ".75"]
            ["fg"
             "M83.6 67a3.996 3.996 0 0 1-5.64-.44c-.61-.71-.95-1.62-.96-2.56V50c0-1.1-.9-2-2-2H41c-4.8 0-11.5 1.5-12 12L14.2 73.3A36.01 36.01 0 0 1 13 64c0-17.7 12.5-32 28-32h34c1.1 0 2-.9 2-2V16a4 4 0 0 1 6.6-3l24 24a3.994 3.994 0 0 1 .35 5.65c-.11.13-.23.24-.35.35l-24 24z"]
            ["fg"
             "M38.4 61a3.996 3.996 0 0 1 5.64.44c.61.71.95 1.62.96 2.56v14c0 1.1.9 2 2 2h34c4.8 0 11.6-1.5 12-12l14.8-13.3c.81 3.03 1.21 6.16 1.2 9.3c0 17.7-12.5 32-28 32H47c-1.1 0-2 .9-2 2v14a4 4 0 0 1-6.6 3l-24-24a3.994 3.994 0 0 1-.35-5.65c.11-.13.23-.24.35-.35l24-24z"]
            ]}

   :repeat-one
   {:label "Toggle repeat one"
    :paths [
            ["drop-shadow"
             "M116 4H12c-4.42 0-8 3.58-8 8v104c0 4.42 3.58 8 8 8h104c4.42 0 8-3.58 8-8V12c0-4.42-3.58-8-8-8z"]
            ["bg"
             "M109.7 4H11.5A7.555 7.555 0 0 0 4 11.5v97.9c-.01 4.14 3.34 7.49 7.48 7.5H109.6c4.14.01 7.49-3.34 7.5-7.48V11.5c.09-4.05-3.13-7.41-7.18-7.5h-.22z"]
            ["shine"
             "M39.7 12.9c0-2.3-1.6-3-10.8-2.7c-7.7.3-11.5 1.2-13.8 4s-2.9 8.5-3 15.3c0 4.8 0 9.3 2.5 9.3c3.4 0 3.4-7.9 6.2-12.3c5.4-8.7 18.9-10.6 18.9-13.6z"
             ".75"]
            ["fg"
             "M108.8 54.7L94 68c-.4 10.5-7.2 12-12 12H68.8c1.6 5.21 1.6 10.79 0 16H82c15.5 0 28-14.3 28-32c.01-3.14-.4-6.27-1.2-9.3z"]
            ["fg"
             "M108.6 37l-24-24a3.996 3.996 0 0 0-5.64.44c-.61.71-.95 1.62-.96 2.56v14c0 1.1-.9 2-2 2H42c-15.5 0-28 14.3-28 32c-.01 3.14.4 6.27 1.2 9.3l.1-.1c3.03-6.02 8.2-10.7 14.5-13.1l.2-.1c.5-10.5 7.2-12 12-12h34c1.1 0 2 .9 2 2v14a4 4 0 0 0 6.6 3l24-24a3.863 3.863 0 0 0 0-6z"]
            ["fg"
             "M40.7 63.4c-13.03 0-23.6 10.57-23.6 23.6s10.57 23.6 23.6 23.6S64.3 100.03 64.3 87c-.02-13.02-10.58-23.58-23.6-23.6zm5.3 42.2c0 .55-.45 1-1 1h-6.7c-.55 0-1-.45-1-1V80.5c-.03-.55-.5-.97-1.05-.95c-.08 0-.17.02-.25.05l-5.9 1.7a.992.992 0 0 1-1.25-.65c-.03-.08-.04-.16-.05-.25v-4.5c.02-.42.3-.78.7-.9l15.1-5a.992.992 0 0 1 1.25.65c.03.08.04.16.05.25l.1 34.7z"]
            ]}
   })

(defn file-extension [path]
  (->> path
       (re-find #"^.+[.]([^.]+)$")
       last))

(defn parse-opts []
  (merge default-opts
         (->> js/CLJCASTR_PLAYER_OPTS
              js->clj
              (map (fn [[k v]]
                     (let [k (-> k util/snake->kebab keyword)
                           v (case k
                               :enabled-buttons
                               (->> v (map (comp keyword util/snake->kebab)) set)

                               :title-fmt
                               (->> v (map #(if (str/starts-with? % ":")
                                              (keyword (subs % 1))
                                              %)))

                               v)]
                       [k v])))
              (into {}))))

(defn ns-keyword->path [kw]
  (let [ks (-> kw str (str/replace ":" "") (str/split "/"))]
    (mapv keyword ks)))

(defn fmt [fmt-str data]
  (->> fmt-str
       (map (fn [arg]
              (if (keyword? arg)
                (get-in data (ns-keyword->path arg))
                arg)))
       (apply str)))

(defn parse-xml [xml-str]
  (.parseFromString (js/window.DOMParser.) xml-str "text/xml"))

(defn fetch-xml [path]
  (p/->> (js/fetch (js/Request. path))
         (.text)
         parse-xml
         (log :info "Fetched XML:")))

(defn xml-get [el k]
  (when-let [html (-> (dom/get-el el k) dom/get-html)]
    (-> html
        (str/replace #"^<!\[CDATA\[(:?\n)?" "")
        (str/replace #"(:?\n)?\]\]>$" ""))))

(defn xml-get-attr [el k attr]
  (when-let [el (dom/get-el el k)]
    (.getAttribute el attr)))

(defn mk-svg-path
  ([cls d]
   (mk-svg-path cls d nil))
  ([cls d opacity]
   (let [path (js/document.createElementNS svg-ns "path")]
     (doto path
       (.setAttribute "d" d)
       (.setAttribute "class" cls))
     (if opacity
       (doto path
         (.setAttribute "opacity" opacity))
       path)))
  ([_ el-name _ paths]
   (dom/set-children! (js/document.createElementNS svg-ns el-name)
                      (map (partial apply mk-svg-path) paths))))

(defn mk-svg []
  (let [svg (js/document.createElementNS svg-ns "svg")]
    (doto svg
      (.setAttribute "viewBox" "0 0 128 128")
      (.setAttribute "preserveAspectRatio" "xMidYMid meet")
      (.setAttribute "aria-hidden" "true")
      (.setAttribute "role" "img")
      (.setAttribute "class" "control clickable"))))

(defn init-state [{:keys [season single-episode] :as opts}
                  {:keys [episodes] :as podcast}]
  (let [episode-numbers (->> episodes
                             (mapcat (fn [[season episodes']]
                                       (map (fn [[number _episode]] [season number])
                                            episodes')))
                             sort)
        season (-> (or season fake-season-number) ->int)
        state
        {:opts (merge default-opts opts)
         :podcast podcast
         :paused? true
         :shuffling? false
         :repeating? false
         :repeating-all? false
         :active-episode (if single-episode
                           [season (->int single-episode)]
                           (first episode-numbers))
         :prev-episodes (list)
         :next-episodes (rest episode-numbers)}]
    (log :debug "State initialised" state)
    state))

(defn get-episode
  "Returns the episode corresponding to the episode index
   `[season episode-number]` from `podcast`."
  [podcast episode-index]
  (get-in podcast (cons :episodes episode-index)))

(defn get-active-episode []
  (let [{:keys [podcast active-episode]} @state]
    (get-episode podcast active-episode)))

(defn toggle-repeat [{:keys [repeating? repeating-all?] :as state}]
  (cond
    repeating-all?
    (assoc state
           :repeating? true
           :repeating-all? false)

    repeating?
    (assoc state :repeating? false)

    :else
    (assoc state :repeating-all? true)))

(defn toggle-shuffle [{:keys [podcast active-episode shuffling?] :as state}]
  (let [num-episodes (count (:episodes podcast))]
    (if shuffling?
      (assoc state
             :shuffling? false
             :next-episodes (range (inc active-episode) (inc num-episodes))
             :prev-episodes (range 1 active-episode))
      (assoc state
             :shuffling? true
             :next-episodes (->> (range 1 (inc num-episodes))
                                 (remove #(= active-episode %))
                                 shuffle)))))

(defn advance-episode [{:keys [active-episode next-episodes prev-episodes] :as state}]
  (let [next-episode (first next-episodes)]
    (assoc state
           :active-episode (or next-episode active-episode)
           :prev-episodes (cons active-episode prev-episodes)
           :next-episodes (rest next-episodes))))

(defn auto-advance-episode [{:keys [active-episode next-episodes prev-episodes
                                    repeating? repeating-all?]
                             :as state}]
  (let [next-episode (first next-episodes)]
    (cond
      repeating?
      state

      (and repeating-all? (not next-episode))
      (let [prev-episodes (cons active-episode prev-episodes)
            next-episodes (rest (reverse prev-episodes))
            active-episode (first (reverse prev-episodes))]
        (assoc state
               :active-episode active-episode
               :next-episodes next-episodes
               :prev-episodes prev-episodes))

      :else
      (advance-episode state))))

(defn back-episode [{:keys [active-episode next-episodes prev-episodes] :as state}]
  (if-let [prev-episode (first prev-episodes)]
    (assoc state
           :active-episode prev-episode
           :prev-episodes (rest prev-episodes)
           :next-episodes (cons active-episode next-episodes))
    state))

(defn move-to-episode [{:keys [podcast active-episode next-episodes prev-episodes shuffling?] :as state} n]
  (let [num-episodes (count (:episodes podcast))
        next-episodes (cond
                        (some #(= n %) next-episodes)
                        (->> next-episodes (drop-while #(not= n %)) rest)

                        shuffling?
                        next-episodes

                        :else
                        (range (inc n) (inc num-episodes)))]
    (assoc state
           :active-episode n
           :prev-episodes (if shuffling?
                            (cons active-episode prev-episodes)
                            (range 1 n))
           :next-episodes next-episodes)))

(defn ->episode [{:keys [artist] :as podcast} item-el]
  {:artist artist
   :title (xml-get item-el "title")
   :season (-> (xml-get item-el "season")
               (or fake-season-number)
               ->int)
   :number (-> (xml-get item-el "episode")
               (or (xml-get item-el "bonusNumber"))
               ->int)
   :description (xml-get item-el "description")
   :preview? (xml-get item-el "previewEpisode")
   :transcript-url (xml-get item-el "transcriptUrl")
   :src (xml-get-attr item-el "enclosure" "url")})

(defn ->podcast [xml]
  (let [podcast {:artist (xml-get xml "author")
                 :title (xml-get xml "title")
                 :image (xml-get-attr xml "image" "href")}]
    (assoc podcast :episodes
           (->> (dom/get-els xml "item")
                (map (partial ->episode podcast))
                (group-by :season)
                (map (fn [[season episodes]]
                       [season (->> episodes
                                    (map (juxt :number identity))
                                    (into {}))]))
                (into {})))))

(defn load-podcast [feed-url]
  (log :info "Loading feed URL:" feed-url)
  (p/->> (fetch-xml feed-url)
         ->podcast
         (log :debug "Loaded podcast:")))

(defn get-episode-duration []
  (.-duration (:audio-el @state)))

(defn get-playback-position []
  (.-currentTime (:audio-el @state)))

(defn set-playback-position! [ss]
  (set! (.-currentTime (:audio-el @state)) ss))

(defn format-playback
  ([]
   (format-playback nil (get-playback-position)))
  ([num pos]
   (format-playback (or num (:active-episode @state))
                    pos
                    (get-episode-duration)))
  ([num pos dur]
   (str "episode " num
        " [" (time/sec->ts pos) " / " (time/sec->ts dur) "]")))

(defn format-playlist [state]
  (-> state
      (select-keys [:prev-episodes :active-episode :next-episodes])
      pr-str))

(defn get-buffered []
  (let [buffered (.-buffered (:audio-el @state))]
    (->> (range (.-length buffered))
         (map (fn [i]
                [(.start buffered i)
                 (.end buffered i)])))))

(defn get-seekable []
  (let [seekable (.-seekable (:audio-el @state))]
    (->> (range (.-length seekable))
         (map (fn [i]
                [(.start seekable i)
                 (.end seekable i)])))))

(defn draw-rect! [ctx x y w h color]
  (set! (.-fillStyle ctx) color)
  (.fillRect ctx x y w h))

(defn display-timeline!
  [{:keys [color-played color-buffered color-position timeline-position-selector]
    :as opts}]
  (let [canvas (:timeline-canvas-el @state)
        canvas-width (.-width canvas)
        canvas-height (.-height canvas)
        ctx (.getContext canvas "2d")
        buffered (get-buffered)
        pos (get-playback-position)
        duration (get-episode-duration)
        sec-width (/ canvas-width duration)
        cur-pos-x (* pos sec-width)
        cur-pos-y (/ canvas-height 2)
        cur-pos-r (/ canvas-height 2)]
    (dom/set-html! timeline-position-selector
                   (str (time/sec->ts pos true) " / " (time/sec->ts duration true)))
    (draw-rect! ctx 0 0 canvas-width canvas-height "lightgray")
    (doseq [[start end] buffered
            :let [start-x (* start sec-width)
                  end-x (* end sec-width)
                  width (- end-x start-x)]]
      (draw-rect! ctx start-x 0 width canvas-height color-buffered))
    (draw-rect! ctx 0 0 cur-pos-x canvas-height color-played)
    (.beginPath ctx)
    (.arc ctx cur-pos-x cur-pos-y cur-pos-r
          0 (* js/Math.PI 2) false)
    (set! (.-fillStyle ctx) color-position)
    (.fill ctx)))

(defn set-metadata! [podcast episode]
  (set! (.-metadata js/navigator.mediaSession)
        (js/MediaMetadata. (clj->js {:title (:title episode)
                                     :artist (:artist episode)
                                     :podcast (:title podcast)
                                     :artwork [{:src (:image podcast)}]}))))

(defn timestamp-click-handler
  ([ev]
   (timestamp-click-handler ev.target.id ev))
  ([target-id ev]
   (doseq [button (dom/get-els ".play-from-ts-button")]
     (dom/remove-child! button))
   (let [ts (js/parseFloat ev.target.dataset.timestamp)
         hash (str "#" target-id)]
     (log :info "Clicked timestamp in transcript:" ts)
     (js/window.location.replace hash)
     (set-playback-position! ts)
     (seek-to-ts!)
     (.preventDefault ev))))

(defn seek-to-ts! []
  (when-not (empty? js/window.location.hash)
    (let [ts (-> js/window.location.hash
                 (str/replace-first "#timestamp-" "")
                 (str/replace "-" "."))
          el (js/document.querySelector js/window.location.hash)]
      (log :info "Jumping to timestamp from location:" ts)
      (set-playback-position! (js/parseFloat ts))
      (when (and el (:paused? @state))
        (let [parent (dom/get-parent el)
              button (dom/create-el
                      "button"
                      {:text "▶️"
                       :attrs {:title "Play from here"
                               :aria-label "Play from current timestamp"}
                       :class "play-from-ts-button"
                       :styles "margin-right: 5px;"})]
          (dom/add-listener! state button "mouseover"
                             #(set! (.-transform (.-style button)) "scale(1.2)"))
          (dom/add-listener! state button "mouseout"
                             #(set! (.-transform (.-style button)) "scale(1.0)"))
          (dom/add-listener! state button "click"
                             (fn [_ev]
                               (dom/remove-child! button)
                               (play-episode!)))
          (dom/insert-child-before! el button))
        (js/window.scrollTo (clj->js {:top (.-offsetTop el)}))))))

(defn transcript-id
  ([i]
   (str "transcript-p-" i))
  ([i el-type]
   (str (transcript-id i) "-" (name el-type))))

(defn create-transcript-el [i el-type v]
  (let [id (if (= :p el-type)
             (transcript-id i)
             (transcript-id i el-type))
        span (when-not (empty? v)
               (dom/create-el "span"
                              {:id id
                               :class (str "transcript-" (name el-type))
                               :text v}))]
    (case el-type
      :p (dom/create-el "div" {:id id
                               :class "transcript-paragraph"})
      :ts (when span
            (let [sec (str (time/ts->sec v))
                  a-id (str "timestamp-" (str/replace sec "." "-"))
                  a-el (dom/create-el
                        "a" {:attrs {:data-timestamp sec
                                     :href (str "#" a-id)
                                     :id a-id}
                             :class "transcript-ts"
                             ;; Don't display milliseconds in timestamps
                             :text (time/sec->ts sec true)})]
              (dom/add-class! span "timestamp")
              (dom/set-attribute! span "data-timestamp" sec)
              (dom/add-listener! state a-el "click"
                                 (partial timestamp-click-handler a-id))
              (dom/set-child! span a-el))
            span)
      :speaker (when span
                 (dom/set-text! span (str v ":"))
                 span)
      :text span)))

(defn create-transcript-paragraph [i {:keys [ts speaker text]}]
  (let [p-el (create-transcript-el i :p nil)
        ts-el (create-transcript-el i :ts ts)
        speaker-el (create-transcript-el i :speaker speaker)
        text-el (create-transcript-el i :text text)
        text-wrapper-el (dom/create-el "div" {:class "transcript-text-wrapper"})]
    (when speaker-el
      (dom/add-child! text-wrapper-el speaker-el))
    (dom/add-child! text-wrapper-el text-el)
    (when ts-el
      (dom/add-child! p-el ts-el))
    (when text-el
      (dom/add-child! p-el text-wrapper-el))
    p-el))

(defn create-footnote-li [i {:keys [href link-text text permalink]}]
  (dom/create-el :li
                 {:class "footnote"
                  :id (str "footnote-" (inc i))
                  :children
                  [(dom/create-link-new-tab href link-text)
                   (when-not (empty? text)
                     (dom/create-el :span {:text (str " - " text)}))
                   (when-not (empty? permalink)
                     (dom/create-el :span
                                    {:children
                                     [(dom/create-el :span {:text " ["})
                                      (dom/create-link-new-tab permalink "PERMALINK")
                                      (dom/create-el :span {:text "]"})]}))]}))

(defn load-transcript [transcript-url]
  (log :info "Fetching transcript from:" transcript-url)
  (p/let [transcript-type (-> transcript-url file-extension keyword)
          transcript
          (p/->> (js/fetch (js/Request. transcript-url))
                 (.text)
                 (transcript/parse-transcript transcript-type true)
                 (merge {:transcript-type transcript-type}))]
    (swap! state assoc :transcript transcript)
    transcript))

(defn display-transcribe-link! [{:keys [transcribe-link-selector]
                                 :as opts}]
  (log :debug "display-transcribe-link! transcribe-link-selector"
       transcribe-link-selector)
  (let [preview? (-> (get-active-episode) :preview? ->boolean)
        transcribe? (-> (util/get-query-params) :transcribe ->boolean)]
    (log :debug "Preview episode?" preview?)
    (log :debug "transcribe param:" transcribe?)
    (when (or preview? transcribe?)
      (log :info "Displaying edit transcript link")
      (dom/set-styles! transcribe-link-selector "display: block;"))))

(defn display-footnotes! [{:keys [footnotes-selector footnotes-clear-styles]
                           :as opts}
                          {:keys [footnotes] :as transcript}]
  (when (and (seq footnotes) footnotes-selector)
    (log :debug "Displaying" (inc (count footnotes)) "footnotes")
    (when footnotes-clear-styles
      (log :debug "Clearing styles on element" footnotes-clear-styles)
      (dom/clear-styles! footnotes-clear-styles))
    (dom/set-child! footnotes-selector
                    (dom/create-el :ol
                                   {:class "footnotes"
                                    :children (map-indexed create-footnote-li footnotes)}))))

(defn display-transcript! [{:keys [transcript-selector
                                   footnotes-selector footnotes-clear-styles]
                            :as opts}
                           {:keys [transcript-type] :as transcript}]
  (log :debug "Displaying transcript of type" transcript-type
       "in element" transcript-selector (:transcript transcript))
  (display-transcribe-link! opts)
  (let [el (dom/get-el transcript-selector)]
    (if (= :json transcript-type)
      (do
        (dom/set-html! transcript-selector (:transcript transcript))
        (doseq [span (dom/get-els "span.timestamp")
                :let [id (str "timestamp-"
                              (-> span
                                  (dom/get-attribute "data-timestamp")
                                  (str/replace "." "-")))]]
          (set! (.-id span) id)
          (set! (.-title span) "Jump to here")
          (dom/add-listener! state span "click" timestamp-click-handler)))
      (let [paragraphs (->> (:transcript transcript)
                            (map-indexed create-transcript-paragraph))]
        (log :debug "Setting paragraphs as children of" el)
        (dom/set-children! el paragraphs)))
    (display-footnotes! opts transcript)
    (seek-to-ts!)
    el))

(defn activate-episode!
  [{:keys [single-episode audio-selector transcript-selector] :as opts}]
  (let [{:keys [podcast active-episode paused?]} @state
        {:keys [artist title number transcript transcript-url src] :as episode}
        (get-active-episode)]
    (log :debug "Activating episode:" (clj->js episode))
    (when (and transcript-selector transcript-url)
      (when-not transcript
        (p/let [transcript (load-transcript transcript-url)]
          (swap! state update-in (concat [:podcast :episodes] active-episode)
                 assoc :transcript transcript)
          (display-transcript! opts transcript))))
    (let [audio-el (:audio-el @state)]
      (dom/set-attribute! audio-el :title (str artist " - " title))
      ;; TODO: fix multi-episode
      #_(when-not single-episode
          (let [episode-spans (dom/get-children "#episodes")]
            (doseq [span episode-spans]
              (dom/set-styles! span "font-weight: normal;"))
            (-> episode-spans
                (nth episode-index)
                (dom/set-styles! "font-weight: bold;"))))
      (dom/set-attribute! audio-el :src src)
      (when-not paused?
        (.play audio-el)))
    (display-timeline! opts)
    (set-metadata! podcast episode)
    episode))

(defn button-selector [button-name]
  (str "#" (name button-name) "-button"))

(defn toggle-button! [button-name src tgt]
  (if-let [button (dom/get-el (button-selector button-name))]
    (doseq [cls ["drop-shadow" "bg" "shine"]
            p (.querySelectorAll button (str "." cls src))]
      (dom/add-class! p (str cls tgt))
      (dom/remove-class! p (str cls src)))
    (log :debug (str "Couldn't resolve button with selector: "
                     (button-selector button-name)
                     "; button is probably not enabled"))))

(defn turn-off-button! [button-name]
  (toggle-button! button-name "" "-off"))

(defn turn-on-button! [button-name]
  (toggle-button! button-name "-off" ""))

(defn show-button! [button-name]
  (dom/set-styles! (button-selector button-name) "display: inline"))

(defn hide-button! [button-name]
  (dom/set-styles! (button-selector button-name) "display: none"))

(defn toggle-repeat! []
  (let [{:keys [repeating? repeating-all?]} (swap! state toggle-repeat)]
    (cond
      repeating-all?
      (do
        (log :info "Repeating all")
        (show-button! :repeat)
        (hide-button! :repeat-one)
        (turn-on-button! :repeat))

      repeating?
      (do
        (log :info "Repeating one")
        (hide-button! :repeat)
        (show-button! :repeat-one)
        (turn-off-button! :repeat))

      :else
      (do
        (log :info "Repeat off")
        (hide-button! :repeat-one)
        (show-button! :repeat)))))

(defn toggle-shuffle! []
  (let [{:keys [shuffling?]} @state]
    (if shuffling?
      (turn-off-button! :shuffle)
      (turn-on-button! :shuffle))
    (swap! state toggle-shuffle))
  (let [{:keys [shuffling? next-episodes]} @state]
    (log :info (str "Shuffle " (if shuffling? "on" "off")
                    "; playlist:")
         (format-playlist @state))))

(defn advance-episode! []
  (let [{:keys [next-episodes]} @state
        next-episode (first next-episodes)]
    (when next-episode
      (log :info (str "Advancing to " (format-playback next-episode 0)))
      (swap! state advance-episode)
      (activate-episode!)
      (log :debug "Playlist:" (format-playlist @state)))))

(defn auto-advance-episode! []
  (log :info (str "Ended " (format-playback)))
  (let [cur-episode (:active-episode @state)
        {:keys [active-episode]} (swap! state auto-advance-episode)]
    (if (= cur-episode active-episode)
      (log :debug "Playlist:" (format-playlist @state))
      (do
        (activate-episode!)
        (log :info (str "Advanced to episode " active-episode "; playlist:")
             (format-playlist @state))))))

(defn back-episode! []
  (let [{:keys [active-episode prev-episodes]} @state
        prev-episode (first prev-episodes)
        at-start-of-episode? (<= (get-playback-position) 1.0)]
    (if (and at-start-of-episode? prev-episode)
      (do
        (log :info (str "Moving back to " (format-playback prev-episode 0)))
        (swap! state back-episode)
        (activate-episode!)
        (log :debug "Playlist:" (format-playlist @state)))
      (do
        (log :info (str "Moving back to " (format-playback active-episode 0)
                        "; playlist:")
             (format-playlist @state))
        (set-playback-position! 0.0)))))

(defn rewind-episode! []
  (let [cur-pos (get-playback-position)
        new-pos (- cur-pos 15.0)
        seekable (get-seekable)
        [start _] (some (fn [[start end :as seekable]]
                          (and (>= cur-pos start) (<= cur-pos end)
                               seekable))
                        seekable)
        new-pos (max 0 start new-pos)]
    (log :info (str "Rewinding to " (format-playback nil new-pos)))
    (set-playback-position! new-pos)))

(defn fast-forward-episode!
  ([]
   (let [cur-pos (get-playback-position)
         new-pos (+ cur-pos 15.0)]
     (fast-forward-episode! cur-pos new-pos)))
  ([cur-pos new-pos]
   (let [seekable (get-seekable)
         [_ end] (some (fn [[start end :as seekable]]
                         (and (>= cur-pos start) (<= cur-pos end)
                              seekable))
                       seekable)
         end (or end (-> (last seekable) second))
         new-pos (min (get-episode-duration) end new-pos)]
     (log :info (str "Fast forwarding to " (format-playback nil new-pos)))
     (set-playback-position! new-pos))))

(defn play-episode! []
  (log :info (str "Playing from " (format-playback)))
  (.play (:audio-el @state))
  (swap! state assoc :paused? false)
  (hide-button! :play)
  (show-button! :pause)
  (turn-on-button! :stop)
  (when-not js/window.location.hash
    (dom/focus-el (button-selector :pause)))
  (log :debug "Playlist:" (format-playlist @state)))

(defn pause-episode! []
  (log :info (str "Pausing at " (format-playback)))
  (.pause (:audio-el @state))
  (swap! state assoc :paused? true)
  (hide-button! :pause)
  (show-button! :play)
  (dom/focus-el (button-selector :play)))

(defn stop-episode! []
  (log :info (str "Stopping at " (format-playback)))
  (let [audio (:audio-el @state)]
    (.pause audio)
    (set! (.-currentTime audio) 0.0))
  (swap! state assoc :paused? true)
  (show-button! :play)
  (hide-button! :pause)
  (turn-off-button! :stop)
  (dom/focus-el (button-selector :play)))

(defn move-to-episode! [n]
  (log :info (str "Moving to episode " n " from " (format-playback)))
  (if (= (:active-episode @state) n)
    (do
      (stop-episode!)
      (play-episode!))
    (do
      (swap! state move-to-episode n)
      (activate-episode!)))
  (log :debug "Playlist:" (format-playlist @state)))

(defn audio-event-handler
  ([]
   (audio-event-handler nil))
  ([handler]
   (fn [ev]
     (let [pos (get-playback-position)
           buffered (get-buffered)
           seekable (get-seekable)]
       (log :debug (str (.-type ev) " at "
                        (format-playback)
                        "; buffered:" (pr-str buffered)
                        "; seekable:" (pr-str seekable))
            (.-target ev))
       (when handler (handler ev))
       true))))

(defn seek-handler [ev]
  (when (or (= "mouseup" (.-type ev))
            (and (= "mouseleave" (.-type ev))
                 (pos? (.-buttons ev))))
    (let [canvas (:timeline-canvas-el @state)
          canvas-width (.-width canvas)
          canvas-height (.-height canvas)
          duration (get-episode-duration)
          sec-width (/ canvas-width duration)
          x (.-offsetX ev)
          pos (/ x sec-width)]
      (log :debug (str "Requesting seek to " (format-playback nil pos)))
      (fast-forward-episode! pos pos))))

(defn add-click-handler! [button-name f]
  (let [button-id (button-selector button-name)
        button (dom/get-el button-id)]
    (log :debug (str "Installing click handler for " button-name)
         button)
    (dom/add-listener! state button "click"
                       (fn [_ev]
                         (log :debug (str (name button-name) " clicked; "
                                          (format-playback)))
                         (f)))))

(defn mk-player-button [button-name]
  (log :debug "Making player button:" button-name)
  (let [button-el (dom/create-el "button" {:id (str (name button-name) "-button")})
        svg-el (mk-svg)
        span-el (dom/create-el "span" {:class "sr-only"})
        {:keys [label paths]} (buttons button-name)]
    (dom/set-html! span-el label)
    (doseq [path paths]
      (dom/add-child! svg-el (apply mk-svg-path path)))
    (dom/add-child! button-el svg-el)
    (dom/add-child! button-el span-el)
    button-el))

(defn init-buttons! [{:keys [controls-selector enabled-buttons] :as opts}]
  (dom/set-children! (dom/get-el controls-selector)
                     (map mk-player-button enabled-buttons))
  (when (:shuffle enabled-buttons)
    (turn-off-button! :shuffle)
    (add-click-handler! :shuffle toggle-shuffle!))
  (when (:repeat enabled-buttons)
    (turn-off-button! :repeat)
    (add-click-handler! :repeat toggle-repeat!))
  (when (:stop enabled-buttons)
    (turn-off-button! :stop)
    (add-click-handler! :stop stop-episode!))
  (when (:pause enabled-buttons)
    (hide-button! :pause)
    (add-click-handler! :pause pause-episode!))
  (when (:repeat-one enabled-buttons)
    (hide-button! :repeat-one)
    (add-click-handler! :repeat-one toggle-repeat!))
  (when (:play enabled-buttons)
    (dom/focus-el (button-selector :play))
    (add-click-handler! :play play-episode!))
  (when (:back enabled-buttons)
    (add-click-handler! :back back-episode!))
  (when (:rewind enabled-buttons)
    (add-click-handler! :rewind rewind-episode!))
  (when (:fast-forward enabled-buttons)
    (add-click-handler! :fast-forward fast-forward-episode!))
  (when (:next enabled-buttons)
    (add-click-handler! :next advance-episode!)))

(defn init-audio! [{:keys [audio-selector timeline-canvas-selector] :as opts}]
  (let [audio (dom/get-el audio-selector)
        canvas (dom/get-el timeline-canvas-selector)]
    (swap! state assoc
           :audio-el audio
           :timeline-canvas-el canvas)
    (dom/add-listener! state audio "ended"
                       (audio-event-handler auto-advance-episode!))
    (dom/add-listener! state audio "durationchange"
                       (audio-event-handler (partial display-timeline! opts)))
    (dom/add-listener! state audio "timeupdate"
                       (audio-event-handler (partial display-timeline! opts)))
    (dom/add-listener! state canvas "mouseup" seek-handler)
    (dom/add-listener! state canvas "mouseleave" seek-handler)
    (log :info "Audio initialised")))

(defn episode->span [{:keys [number title] :as episode}]
  (let [span (js/document.createElement "span")]
    (set! (.-innerHTML span) (str number ". " title))
    (dom/add-class! span "clickable")
    (dom/add-listener! state span "click" (partial move-to-episode! number))
    span))

(defn display-podcast! [{:keys [main-selector cover-selector
                                title-selector description-selector
                                transcript-selector
                                title-fmt single-episode]
                         :as opts}
                        {:keys [artist title image episodes] :as podcast}]
  (let [cover (dom/get-el cover-selector)
        main (dom/get-el main-selector)
        episode (get-active-episode)
        fmt-data {:podcast podcast, :episode episode}]
    (when cover
      (set! (.-src cover) image))
    (when title-selector
      (dom/set-html! title-selector (fmt title-fmt fmt-data)))
    (if single-episode
      (when description-selector
        (dom/set-html! description-selector (:description episode)))
      (->> episodes
           (map episode->span)
           (dom/set-children! (dom/get-el "#episodes"))))
    (when main
      (dom/set-styles! main "display: flex;"))
    podcast))

(defn load-ui! [{:keys [feed-url] :as opts}]
  (p/let [podcast (load-podcast feed-url)
          {:keys [opts]} (reset! state (init-state opts podcast))]
    (init-audio! opts)
    (display-podcast! opts podcast)
    (init-buttons! opts)
    (activate-episode! opts)
    (swap! state assoc :loaded true)
    (log :info "UI loaded successfully")))

(set! (.-loadUI js/window) load-ui!)

(when-not (:loaded @state)
  (log :info "Query params:" (util/get-query-params))
  (util/set-log-level!)
  (load-ui! (parse-opts)))
