;; Woods Recipe - OPTIMIZED VERSION
;;
;; Uses fused-pipeline for GPU acceleration when available,
;; falls back to individual primitives on CPU.
;;
;; Key optimizations:
;; 1. Uses streaming_gpu primitives with fast CUDA kernels
;; 2. Uses fused-pipeline to batch effects into single kernel passes
;; 3. GPU persistence - frames stay on GPU throughout pipeline

(stream "woods-recipe-optimized"
  :fps 30
  :width 1920
  :height 1080
  :seed 42

  ;; Load standard primitives (includes proper asset resolution)
  ;; Auto-selects GPU versions when available, falls back to CPU
  (include :name "tpl-standard-primitives")

  ;; === SOURCES (using streaming: which has proper asset resolution) ===
  (def sources [
    (streaming:make-video-source "woods-1" 30)
    (streaming:make-video-source "woods-2" 30)
    (streaming:make-video-source "woods-3" 30)
    (streaming:make-video-source "woods-4" 30)
    (streaming:make-video-source "woods-5" 30)
    (streaming:make-video-source "woods-6" 30)
    (streaming:make-video-source "woods-7" 30)
    (streaming:make-video-source "woods-8" 30)
  ])

  ;; Per-pair config
  (def pair-configs [
    {:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
    {:dir 1  :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
    {:dir 1  :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
    {:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5}
    {:dir -1 :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
    {:dir 1  :rot-a 30 :rot-b -30 :zoom-a 1.3 :zoom-b 0.7}
    {:dir -1 :rot-a -45 :rot-b 45 :zoom-a 0.5 :zoom-b 1.5}
    {:dir 1  :rot-a 45 :rot-b -45 :zoom-a 1.5 :zoom-b 0.5}
  ])

  ;; Audio
  (def music (streaming:make-audio-analyzer "woods-audio"))
  (audio-playback "woods-audio")

  ;; === SCANS ===

  ;; Cycle state
  (scan cycle (streaming:audio-beat music t)
    :init {:active 0 :beat 0 :clen 16}
    :step (if (< (+ beat 1) clen)
            (dict :active active :beat (+ beat 1) :clen clen)
            (dict :active (mod (+ active 1) (len sources)) :beat 0
                  :clen (+ 8 (mod (* (streaming:audio-beat-count music t) 7) 17)))))

  ;; Spin scan
  (scan spin (streaming:audio-beat music t)
    :init {:angle 0 :dir 1 :speed 2}
    :step (let [new-dir (if (< (core:rand) 0.05) (* dir -1) dir)
                new-speed (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) speed)]
            (dict :angle (+ angle (* new-dir new-speed))
                  :dir new-dir
                  :speed new-speed)))

  ;; Ripple scan
  (scan ripple-state (streaming:audio-beat music t)
    :init {:gate 0 :cx 960 :cy 540}
    :step (let [new-gate (if (< (core:rand) 0.15) (+ 3 (core:rand-int 0 5)) (core:max 0 (- gate 1)))
                new-cx (if (> new-gate gate) (+ 200 (core:rand-int 0 1520)) cx)
                new-cy (if (> new-gate gate) (+ 200 (core:rand-int 0 680)) cy)]
            (dict :gate new-gate :cx new-cx :cy new-cy)))

  ;; Pair states
  (scan pairs (streaming:audio-beat music t)
    :init {:states (map (core:range (len sources)) (lambda (_)
      {:inv-a 0 :inv-b 0 :hue-a 0 :hue-b 0 :hue-a-val 0 :hue-b-val 0 :mix 0.5 :mix-rem 5 :angle 0 :rot-beat 0 :rot-clen 25}))}
    :step (dict :states (map states (lambda (p)
      (let [new-inv-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-a) 1)))
            new-inv-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- (get p :inv-b) 1)))
            old-hue-a (get p :hue-a)
            old-hue-b (get p :hue-b)
            new-hue-a (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-a 1)))
            new-hue-b (if (< (core:rand) 0.1) (+ 1 (core:rand-int 1 4)) (core:max 0 (- old-hue-b 1)))
            new-hue-a-val (if (> new-hue-a old-hue-a) (+ 30 (* (core:rand) 300)) (get p :hue-a-val))
            new-hue-b-val (if (> new-hue-b old-hue-b) (+ 30 (* (core:rand) 300)) (get p :hue-b-val))
            mix-rem (get p :mix-rem)
            old-mix (get p :mix)
            new-mix-rem (if (> mix-rem 0) (- mix-rem 1) (+ 1 (core:rand-int 1 10)))
            new-mix (if (> mix-rem 0) old-mix (* (core:rand-int 0 2) 0.5))
            rot-beat (get p :rot-beat)
            rot-clen (get p :rot-clen)
            old-angle (get p :angle)
            new-rot-beat (if (< (+ rot-beat 1) rot-clen) (+ rot-beat 1) 0)
            new-rot-clen (if (< (+ rot-beat 1) rot-clen) rot-clen (+ 20 (core:rand-int 0 10)))
            new-angle (+ old-angle (/ 360 rot-clen))]
        (dict :inv-a new-inv-a :inv-b new-inv-b
              :hue-a new-hue-a :hue-b new-hue-b
              :hue-a-val new-hue-a-val :hue-b-val new-hue-b-val
              :mix new-mix :mix-rem new-mix-rem
              :angle new-angle :rot-beat new-rot-beat :rot-clen new-rot-clen))))))

  ;; === OPTIMIZED PROCESS-PAIR MACRO ===
  ;; Uses fused-pipeline to batch rotate+hue+invert into single kernel
  (defmacro process-pair-fast (idx)
    (let [;; Get sources for this pair (with safe modulo indexing)
          num-sources (len sources)
          src-a (nth sources (mod (* idx 2) num-sources))
          src-b (nth sources (mod (+ (* idx 2) 1) num-sources))
          cfg (nth pair-configs idx)
          pstate (nth (bind pairs :states) idx)

          ;; Read frames (GPU decode, stays on GPU)
          frame-a (streaming:source-read src-a t)
          frame-b (streaming:source-read src-b t)

          ;; Get state values
          dir (get cfg :dir)
          rot-max-a (get cfg :rot-a)
          rot-max-b (get cfg :rot-b)
          zoom-max-a (get cfg :zoom-a)
          zoom-max-b (get cfg :zoom-b)
          pair-angle (get pstate :angle)
          inv-a-on (> (get pstate :inv-a) 0)
          inv-b-on (> (get pstate :inv-b) 0)
          hue-a-on (> (get pstate :hue-a) 0)
          hue-b-on (> (get pstate :hue-b) 0)
          hue-a-val (get pstate :hue-a-val)
          hue-b-val (get pstate :hue-b-val)
          mix-ratio (get pstate :mix)

          ;; Calculate rotation angles
          angle-a (* dir pair-angle rot-max-a 0.01)
          angle-b (* dir pair-angle rot-max-b 0.01)

          ;; Energy-driven zoom (maps audio energy 0-1 to 1-max)
          zoom-a (core:map-range e 0 1 1 zoom-max-a)
          zoom-b (core:map-range e 0 1 1 zoom-max-b)

          ;; Define effect pipelines for each source
          ;; These get compiled to single CUDA kernels!
          effects-a [{:op "zoom" :amount zoom-a}
                     {:op "rotate" :angle angle-a}
                     {:op "hue_shift" :degrees (if hue-a-on hue-a-val 0)}
                     {:op "invert" :amount (if inv-a-on 1 0)}]
          effects-b [{:op "zoom" :amount zoom-b}
                     {:op "rotate" :angle angle-b}
                     {:op "hue_shift" :degrees (if hue-b-on hue-b-val 0)}
                     {:op "invert" :amount (if inv-b-on 1 0)}]

          ;; Apply fused pipelines (single kernel per source!)
          processed-a (streaming:fused-pipeline frame-a effects-a)
          processed-b (streaming:fused-pipeline frame-b effects-b)]

      ;; Blend the two processed frames
      (blending:blend-images processed-a processed-b mix-ratio)))

  ;; === FRAME PIPELINE ===
  (frame
    (let [now t
          e (streaming:audio-energy music now)

          ;; Get cycle state
          active (bind cycle :active)
          beat-pos (bind cycle :beat)
          clen (bind cycle :clen)

          ;; Transition logic
          phase3 (* beat-pos 3)
          fading (and (>= phase3 (* clen 2)) (< phase3 (* clen 3)))
          fade-amt (if fading (/ (- phase3 (* clen 2)) clen) 0)
          next-idx (mod (+ active 1) (len sources))

          ;; Process active pair with fused pipeline
          active-frame (process-pair-fast active)

          ;; Crossfade with zoom during transition
          ;; Old pair: zooms out (1.0 -> 2.0) and fades out
          ;; New pair: starts small (0.1), zooms in (-> 1.0) and fades in
          result (if fading
                   (let [next-frame (process-pair-fast next-idx)
                         ;; Active zooms out as it fades
                         active-zoom (+ 1.0 fade-amt)
                         active-zoomed (streaming:fused-pipeline active-frame
                                         [{:op "zoom" :amount active-zoom}])
                         ;; Next starts small and zooms in
                         next-zoom (+ 0.1 (* fade-amt 0.9))
                         next-zoomed (streaming:fused-pipeline next-frame
                                       [{:op "zoom" :amount next-zoom}])]
                     (blending:blend-images active-zoomed next-zoomed fade-amt))
                   active-frame)

          ;; Final effects pipeline (fused!)
          spin-angle (bind spin :angle)
          rip-gate (bind ripple-state :gate)
          rip-amp (* rip-gate (core:map-range e 0 1 5 50))
          rip-cx (bind ripple-state :cx)
          rip-cy (bind ripple-state :cy)

          ;; Fused final effects
          final-effects [{:op "rotate" :angle spin-angle}
                         {:op "ripple" :amplitude rip-amp :frequency 8 :decay 2
                          :phase (* now 5) :center_x rip-cx :center_y rip-cy}]]

      ;; Apply final fused pipeline
      (streaming:fused-pipeline result final-effects
        :rotate_angle spin-angle
        :ripple_phase (* now 5)
        :ripple_amplitude rip-amp))))
