(ns adamrenklint.prng
  (:require [clojure.string :as str]
            [reagent.core :as r]
            [reagent.dom :as rd]))

(def empty-counts (into {} (map (fn [i]
                                  [i 0])
                                (range 10))))

(defn ->buckets [values]
  (reduce (fn [buckets val]
            (let [digit (-> val (* 10) js/Math.floor)]
              (update buckets digit (fnil conj []) val)))
          {}
          values))

(defn with-commas [s]
  (-> s str (str/replace #"\B(?=(\d{3})+(?!\d))" ",")))

(defn counts-graph [{:keys [values total]}]
  (let [buckets (->buckets values)
        counts (into empty-counts (mapv (fn [[digit values]]
                                          [digit (count values)])
                                        buckets))
        high (apply max (vals counts))]
    [:div.counts-graph
     {:style {:margin-bottom 10}}
     (for [[digit count] counts]
       [:div.line
        {:style {:width "100%"
                 :height 34
                 :position "relative"
                 :display "flex"
                 :align-items "center"}}
        [:div
         {:style {:margin-left 5}}
         [:strong digit]
         (str " (" (.toFixed (* (/ count total) 100) 1) "%)")]
        [:div
         {:style {:width (str (* (/ count high) 100) "%")
                  :position "absolute"
                  :background "var(--lesslight)"
                  :z-index -1
                  :top 0
                  :bottom 0
                  :left 0}}]])]))

(defn slider [{:keys [min max value on-change]}]
  [:input
   {:type "range"
    :style {:padding 0}
    :min min
    :max max
    :value value
    :on-change on-change}])

(defn gen-measure-vanilla-rand [size rand-fn]
  (let [start (js/performance.now)
        values (doall (take size (repeatedly rand-fn)))
        end (js/performance.now)]
    [values (- end start)]))

(defn vanilla-rand-section [{:keys [rand-fn]}]
  (let [[values duration] (gen-measure-vanilla-rand (js/Math.pow 10 3) rand-fn)
        !state (r/atom {:pow 3 :values values :duration duration})]
    (fn [{:keys [rand-fn]}]
      (let [{:keys [values pow duration]} @!state
            total (js/Math.pow 10 pow)
            regenerate #(let [[values duration] (gen-measure-vanilla-rand (js/Math.pow 10 (or % pow)) rand-fn)]
                         (swap! !state assoc :values values :duration duration))]
        [:<>
         [counts-graph {:values values
                        :total total}]
         [:div.seq
          [slider {:min 1
                   :max 6
                   :value pow
                   :on-change #(let [pow (js/parseInt (.. % -target -value))]
                                 (regenerate pow)
                                 (swap! !state assoc :pow pow))}]
          [:div.center (with-commas total) " samples (" (.toFixed duration 1) "ms)"]
          [:button {:on-click #(regenerate)}
           "Regenerate"]]]))))

(defn gen-measure-custom-rand [size resolution rand-fn seed]
  (let [start (js/performance.now)
        values (mapv (fn [i]
                       (rand-fn seed (* i resolution)))
                     (range size))
        end (js/performance.now)]
    [values (- end start)]))

(def resolutions [0.00001 0.0001 0.001 0.01 0.1 1 10 100])
(defn scale->resolution [scale]
  (nth resolutions scale))

(defn custom-rand-section [{:keys [rand-fn]}]
  (let [resolution 4
        [values duration] (gen-measure-custom-rand (js/Math.pow 10 3) resolution rand-fn 12345)
        !state (r/atom {:seed 12345 :pow 3 :values values :duration duration :resolution resolution})]
    (fn [{:keys [rand-fn]}]
      (let [{:keys [values seed pow duration resolution]} @!state
            total (js/Math.pow 10 pow)
            regenerate (fn [{:keys [resolution pow seed]}]
                         (let [seed (or seed (:seed @!state))
                               pow (or pow (:pow @!state))
                               resolution (or resolution (:resolution @!state))
                               [values duration] (gen-measure-custom-rand (js/Math.pow 10 pow)
                                                                          (scale->resolution resolution)
                                                                          rand-fn seed)]
                           (swap! !state assoc
                                  :values values :duration duration
                                  :pow pow :seed seed :resolution resolution)))]
        [:<>
         [counts-graph {:values values
                        :total total}]
         [:div.seq
          [slider {:min 0
                   :max (dec (count resolutions))
                   :value resolution
                   :on-change #(let [value (js/parseInt (.. % -target -value))]
                                 (regenerate {:resolution value}))}]
          [:div.center (str "Resolution " (scale->resolution resolution))]
          [:div.center "Seed " seed]]
         [:div.seq
          [slider {:min 1
                   :max 6
                   :value pow
                   :on-change #(let [value (js/parseInt (.. % -target -value))]
                                 (regenerate {:pow value}))}]
          [:div.center (with-commas total) " samples (" (.toFixed duration 1) "ms)"]
          [:button {:on-click #(regenerate {:seed (rand-int 100000)})}
           "Regenerate"]]]))))

(defn insert-el [id]
  (let [ref (.getElementById js/document id)
        el (.createElement js/document "div")]
    (.insertBefore (.-parentNode ref) el (.-nextSibling ref))
    el))

(defn custom-rand [seed t]
  (-> (* (inc t) 777)
      (+ seed)
      (* 17.1737)
      (mod 10)
      (/ 10)))

(rd/render [vanilla-rand-section {:rand-fn rand}] (insert-el "vanilla-rand"))
(rd/render [custom-rand-section {:rand-fn custom-rand}] (insert-el "custom-rand"))
