(ns drum-machine.core
  (:require [om.core :as om :include-macros true]
            [sablono.core :as html :refer-macros [html]]))

(enable-console-print!)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UI

(def tau 6.2831853071)

(defn build-drum [radius sides]
  (mapv #(vector (* radius (.cos js/Math (* tau (/ % sides))))
                 (* radius (.sin js/Math (* tau (/ % sides))))
                 false
                 %) ; or: (int (* % (/ tempo sides)))
        (range (inc sides))))

(def drum-radii [145 95 45])

(defonce app-state
  (atom {:drums (mapv build-drum drum-radii [8 8 4])}))

(defn widget [app]
  (om/component
   (html
    [:div {:id "drum-container"}
     [:svg {:id "drum-circles" :viewBox "-155 -160 320 320" :preserveAspectRatio "xMaxYmax"
            :height "100%" :width "100%"}
      (for [[i vertices] (map-indexed vector (:drums app))]
        [:g
         [:polygon {:points (apply str (for [[x y] vertices] (str x "," y " ")))
                    :stroke "#ECE5CE"
                    :stroke-width "2"
                    :fill "#E08E79"
                    :fill-opacity "0.33"}]
         (for [[x y active n] (butlast vertices)]
           (let [handler (fn [e]
                           (.preventDefault e)
                           (om/update! app [:drums i n 2] (not active)))]
             [:circle {:id (str i "-" n)
                       :cx (str x)
                       :cy (str y)
                       :r 8
                       :fill (if active "#E08E79" "#ECE5CE")
                       :onTouchEnd handler
                       :onClick handler }]))])]
     (for [i (range 3)]
       [:input {:id (str "slider" i)
                :type "range" :value (dec (count (nth (:drums app) i))) :min 3 :max 16
                :style {:width "100%"}
                :on-change #(om/update! app [:drums i]
                                        (build-drum (drum-radii i)
                                                    (-> % .-target .-value js/parseInt)))}])])))

(om/root widget app-state {:target js/document.body})

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Play loop

(defonce sounds
  [(js/Howl. (js-obj "urls" (array "sounds/kick.wav")))
   (js/Howl. (js-obj "urls" (array "sounds/snare.wav")))
   (js/Howl. (js-obj "urls" (array "sounds/closed-hat.wav")))])

(defonce interval-id (atom 0))
(defonce current-beat (atom 0))

(defn run-beat []
  (swap! current-beat inc)
  (doseq [[i vertices] (map-indexed vector (:drums @app-state))]
    (doseq [[x y active n] (butlast vertices)]
      (if (= n (mod @current-beat (dec (count vertices))))
        (do
          (.setAttribute (.getElementById js/document (str i "-" n)) "r" "10") 
          (when active (.play (sounds i))))
        (.setAttribute (.getElementById js/document (str i "-" n)) "r" "8") ))))

(defn stop-beat []
  (js/clearInterval @interval-id))

(defn start-beat []
  (stop-beat)
  (reset! interval-id (js/setInterval run-beat 250)))

(start-beat)