 (defn set-pad-color
   "Set the color of one of the 64 touch pads to the closest
   approximation available for a desired color."
-  [x y color]
+  [controller x y color]
   {:pre [(<= 0 x 7) (<= 0 y 7)]}
   (let [note (+ 36 x (* y 8))]  ;; Calculate note from grid coordinates
-    (midi/midi-note-on @user-port-out note (velocity-for-color color))))
+    (midi/midi-note-on (:user-port-out controller) note (velocity-for-color color))))
 
 (def monochrome-button-states
   "The control values and modes for a labeled button which does not

  "Render a frame of the welcome animation, or if it is done, start
  the main interface update thread, and terminate the task running the
  animation."
  [counter task]
  (try
    (cond
      (< @counter 8)
      (doseq [y (range 0 (inc @counter))]
        (let [color (com.evocomputing.colors/create-color
                     :h 0 :s 0 :l (max 10 (- 50 (/ (* 50 (- @counter y)) 4))))]
          (set-pad-color 3 y color)
          (set-pad-color 4 y color)))

      (< @counter 12)
      (doseq [x (range 0 (- @counter 7))
              y (range 0 8)]
        (let [color (com.evocomputing.colors/create-color
                     :h 340 :s 100 :l (if (= x (- @counter 8)) 75 50))]
          (set-pad-color (- 3 x) y color)
          (set-pad-color (+ 4 x) y color)))

      (< @counter 15)
      (doseq [y (range 0 8)]
        (let [color (com.evocomputing.colors/create-color
                     :h (* 13 (- @counter 11)) :s 100 :l 50)]
          (set-pad-color (- @counter 7) y color)
          (set-pad-color (- 14 @counter) y color)))

      (= @counter 15)
      (show-labels :bright :amber)
      
      (= @counter 16)
      (doseq [x (range 0 8)]
        (set-track-select-state x :bright :amber))
      
      (= @counter 17)
      (doseq [x (range 0 8)]
        (set-scene-launch-color x (com.evocomputing.colors/create-color :h 45 :s 100 :l 50))
        (set-track-select-state x :bright :red))

      (< @counter 26)
      (doseq [x (range 0 8)]
        (let [color (com.evocomputing.colors/create-color
                     :h (+ 60 (* 40 (- @counter 18))) :s 100 :l 50)]
          (set-pad-color x (- 25 @counter) color)))
      
      (= @counter 26)
      (do
        (show-labels :dim :amber)
        (doseq [x (range 0 8)]
          (set-track-select-state x :off)))

      (= @counter 27)
      (doseq [x (range 0 8)]
          (set-scene-launch-color x off-color))

      (< @counter 37)
      (doseq [x (range 0 8)]
        (set-pad-color x (- 35 @counter) off-color))
      
      :else
      (do
        (clear-interface)
        ;; 
        (at-at/kill @task)))
    (catch Throwable t
      (warn t "Animation frame failed")))

  (swap! counter inc))


(defn welcome-animation
  "Provide a fun animation to make it clear the Push is online."
  []
  (set-display-line 1 (concat (repeat 24 \space) (seq "Welcome toAfterglow")))
  (set-display-line 3 (concat (repeat 27 \space)
                              (seq (str "version" (env  :afterglow-version)))))
  (let [counter (atom 0)
        task (atom nil)]
    (reset! task (at-at/every 30 #(welcome-frame counter task)
                              controllers/pool))))

(defn clear-interface
  "Clears the text display and all illuminated buttons and pads."
  []
  (doseq [line (range 1 5)]
    (clear-display-line line))
  (doseq [x (range 8)]
    (set-track-select-state x :off)
    (set-scene-launch-color x off-color)
    (doseq [y (range 8)]
      (set-pad-color x y off-color)))
  (doseq [[_ button] control-buttons]
    (set-button-state button :off)))


(defn connect-to-push
  "Try to establish the connection to the Ableton Push, if that has
  not already been made, initialize the display, and start the UI
  updater thread. Since SysEx messages are required for updating the
  display, assumes that if you are on a Mac, you have installed
  [mmj](http://www.humatic.de/htools/mmj.htm) to provide a
  working implementation."
  {:doc/format :markdown}
  [& {:keys [prefix] :or {prefix (when (re-find #"Mac" (System/getProperty "os.name"))
                                   "Ableton Push - ")}}]
  (swap! user-port-out #(or % (midi/midi-out (str prefix "User Port"))))
  (swap! live-port-out #(or % (midi/midi-out (str prefix "Live Port"))))
  (clear-interface)
  (welcome-animation))

   
   })

(defn button-state
  "Calculate the numeric value that corresponds to a particular
  named state for the specified button, and (if supported and
  supplied), a named color."
  ([button state]
   (button-state button state :amber))
  ([button state color-key]
   (let [base-value ((keyword state) monochrome-button-states)
         color-shift (or (when (and (= (:kind button) :color)
                                    (not= state :off))
                           ((keyword color-key) color-button-colors))
                         0)]
     (+ base-value color-shift))))

(defn set-button-state
  "Set one of the labeled buttons to a particular state, and, if
  supported, color. If the state is already a number, it is used
  as-is, otherwise it is calculated using button-state."
  ([controller button state]
   (set-button-state controller button state :amber))
  ([controller button state color-key]
   (let [state (if (number? state)
                 state
                 (button-state button state color-key))]
     (midi/midi-control (:user-port-out controller) (:control button) state))))

(defn track-select-state
  "Calculate the numeric value that corresponds to a particular
  named state for the specified top-row pad, and (if supplied),
  named color."
  ([x state]
   (track-select-state x state :amber))
  ([x state color-key]
   {:pre [(<= 0 x 7)]}
   (let [base-value ((keyword state) monochrome-button-states)
         color-shift (or (when-not (= state :off)
                           ((keyword color-key) color-button-colors))
                         0)]
     (+ base-value color-shift))))

(defn set-track-select-state
  "Set one of the top-row pads to a particular state and color.
  If state is already a number, it is used as-is, otherwise it is
  calculated using track-select-state."
  ([controller x state]
   (set-track-select-state controller x state :amber))
  ([controller x state color-key]
   {:pre [(<= 0 x 7)]}
   (let [state (if (number? state)
                 state
                 (track-select-state x state color-key))]
     (midi/midi-control (:user-port-out controller) (+ x 20) state))))

(defn set-scene-launch-color
  "Set the color of one of the 8 scene-launch touch pads (right above
  the 8x8 pad of larger, velocity sensitive, pads) to the closest
  approximation available for a desired color."
  [controller x color]
  {:pre [(<= 0 x 7)]}
  (let [control (+ 102 x)]  ;; Calculate controller number
    (midi/midi-control (:user-port-out controller) control (velocity-for-color color))))

(defn set-display-line
  "Sets a line of the text display."
  [controller line bytes]
  {:pre [(<= 0 line 3)]}
  (let [message (concat [240 71 127 21 (+ line 24) 0 69 0]
                        (take 68 (concat (map int bytes) (repeat 32)))
                        [247])]
    (midi/midi-sysex (:live-port-out controller) message)))
 
(defn clear-display-line
  "Clears a line of the text display."
  [controller line]
  {:pre [(<= 0 line 3)]}
  (midi/midi-sysex (:live-port-out controller) [240 71 127 21 (+ line 28) 0 0 247]))

(defn- show-labels
  "Illuminates all buttons with text labels, for development assistance."
  ([controller]
   (show-labels controller :bright :amber))
  ([controller state]
   (show-labels controller state :amber))
  ([controller state color]
   (doseq [[_ button] control-buttons]
     (set-button-state controller button state color))))

(defn update-text
  "Sees if any text has changed since the last time the display
  was updated, and if so, sends the necessary MIDI SysEx values
  to update it on the Push."
  [controller]
  (doseq [row (range 4)]
    (when-not (.equals (get (:next-display controller) row)
                       (get (:last-display controller) row))
      (set-display-line controller row (get (:next-display controller) row))
      (System/arraycopy (get (:next-display controller) row) 0
                        (get (:last-display controller) row) 0 68))))

(defn update-text-buttons
  "Sees if any labeled buttons have changed state since the last time
  the interface was updated, and if so, sends the necessary MIDI
  control values to update them on the Push."
  [controller]
  ;; First turn off any which were on before but no longer are
  (doseq [[button old-state] @(:last-text-buttons controller)]
    (when-not (button @(:next-text-buttons controller))
      (when-not (#{0 :off} old-state)
        (set-button-state controller button :off))))

  ;; Then, set any currently requested states
  (doseq [[button state] @(:next-text-buttons controller)]
    (set-button-state controller button state))

  ;; And record the new state for next time
  (reset! (:last-text-buttons controller) @(:next-text-buttons controller)))

(defn write-display-cell
  "Update a single text cell (of which there are four per row) in the
  display to be rendered on the next update."
  [controller row cell text]
  {:pre [(<= 0 row 3) (<= 0 cell 3)]}
  (let [bytes (take 17 (concat (map int text) (repeat 32)))]
    (doseq [[i val] (map-indexed vector bytes)]
      (aset (get (:next-display controller) row) (+ (* cell 17) i) (util/ubyte val)))))

(defn update-interface
  "Determine the desired current state of the interface, and send any
  changes needed to get it to that state."
  [controller]
  (try
    ;; Assume we are starting out with a blank interface.
    (for [row (range 4)]
      (Arrays/fill (get (:next-display controller) row) (byte 32)))
    (reset! (:next-text-buttons controller) {})

    ;; TODO: Loop over the most recent four active cues, rendering information
    ;;       about them.

    (let [metronome-button (:metronome control-buttons)]
      (if @(:metronome-mode controller)
        (let [metronome (:metronome (:show controller))]
          (swap! (:next-text-buttons controller)
                 assoc metronome-button (button-state metronome-button :bright))
          (write-display-cell controller 3 0 (metro-marker metronome)))
        (swap! (:next-text-buttons controller)
               assoc metronome-button (button-state metronome-button :dim))))
    
    (update-text controller)
    (update-text-buttons controller)
    (catch Throwable t
      (warn t "Problem updating Ableton Push Interface"))))

(declare clear-interface)

