(ns auto-ap.views.utils
  (:require [re-frame.core :as re-frame]
            [reagent.core :as reagent]
            [react-transition-group :as react-transition-group]
            [react-datepicker]
            [clojure.spec.alpha :as s]
            [cljs-time.coerce :as c]
            [cljs-time.core :as time]
            [auto-ap.subs :as subs]
            [cljs-time.format :as format]
            [goog.i18n.NumberFormat.Format]
            [cljs-time.core :as t]
            [clojure.string :as str]
            [goog.crypt.base64 :as base64]
            [cljs.tools.reader.edn :as edn]
            [reagent.core :as r])
  (:import
   (goog.i18n NumberFormat)
   (goog.i18n.NumberFormat Format)))
(def nff
  (NumberFormat. Format/CURRENCY))

(defn nf
  [num]
  (.format nff (str num)))

(defn ->$ [x]
  (nf x))



(defn- nf%
  [num]
  (.format (doto 
               (NumberFormat. Format/PERCENT)
             (.setMaximumFractionDigits 1)
             (.setMinimumFractionDigits 1))
           (str num)))

(defn ->% [x]
  (nf% x))

(defn active-when= [active-page candidate]
  (when (= active-page candidate) " is-active"))

(defn active-when [active-page f & rest]

  (when (apply f (into [active-page] rest)) " is-active"))

(def login-url
  (let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com"
        redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth"))]
    (str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile")))

(defn dispatch-value-change [event]
  (fn [e]
    (.preventDefault e)
    (re-frame/dispatch (conj event (.. e -target -value)))))


(defn delayed-dispatch [e]
  (fn [x]
    (js/setTimeout #(re-frame/dispatch e) 151)
    false))


(defn dispatch-event [event]
  (fn [e]
    (when (.-stopPropagation e)
      (.stopPropagation e)
      (.preventDefault e))
    (re-frame/dispatch-sync event)))

(defn dispatch-event-with-propagation [event]
  (fn [e]
    (re-frame/dispatch-sync event)))

(def pretty-long (format/formatter "MM/dd/yyyy HH:mm:ss"))
(def pretty (format/formatter "MM/dd/yyyy"))
(def standard (format/formatter "yyyy-MM-dd"))

(defn date->str
  ([d] (date->str d pretty))
  ([d format]
   (when d
     (format/unparse format d))))

(defn date-time->str [d]
  (when d
    (format/unparse pretty-long d)))

(defn str->date [d f]
  (when d
    (format/parse f d)))

(defn dispatch-date-change [event]
  (fn [e g]
    (re-frame/dispatch (conj event
                             (if (str/blank? e)
                               e
                               (date->str (time/from-default-time-zone (c/from-date e)) standard))))))

;; TODO inline on-changes causes each field to be rerendered each time. When we fix this
;; let's make sure that we find away not to trigger a re-render for every component any time any form field
;; changes
(defmulti do-bind (fn [a {:keys [type] :as x}]
                    type))

(defn with-keys [children]
  (map-indexed (fn [i c] ^{:key i} c) children))


(def css-transition-group
  (reagent/adapt-react-class react-transition-group/CSSTransition))


(defn appearing [{:keys [visible? enter-class exit-class timeout]} & children ]
  (let [final-state (reagent/atom visible?)]
    (fn [{:keys [visible?]} & children]
      [css-transition-group {:in visible? :class-names {:exit exit-class :enter enter-class} :timeout timeout :onEnter (fn [] (reset! final-state true )) :onExited (fn [] (reset! final-state false))} 
       (if (or @final-state visible?)
         (first children)
         [:span])])))


(defn multi-field [{:keys [change-event data value template on-change allow-change?]} ]
  (let [value-repr (r/atom (mapv
                            (fn [x]
                              (assoc x :key (random-uuid) :new? false))
                            value))]
    (fn [{:keys [change-event data value template on-change allow-change?]} ]
      (let [value @value-repr
            already-has-new-row? (= [:key :new?] (keys (last value)))
            value (if already-has-new-row?
                         value
                         (conj value {:key (random-uuid)
                                      :new? true}))]
        [:div
         (for [[i override] (map vector (range) value)
               :let [is-disabled? (if (= false allow-change?)
                                    (not (boolean (:new? override)))
                                    nil)]]
           ^{:key (:key override)}
           [:div.level
            [:div.level-left
             [:div.level-item
              (if (:new? override)
                
                [:div.icon.is-medium {:class (if (not= i (dec (count value)))
                                               "has-text-info")}
                 [:i.fa.fa-plus]]
                [:div.icon.is-medium])]
             [:<> (for [[idx template] (map vector (range ) template)]
                    ^{:key idx}

                    [:div.level-item
                     (update template 1 assoc
                             :value (let [value (get-in override (get-in template [1 :field])) ;; TODO this is really ugly to support maps or strings
                                          value (if (map? value)
                                                  (dissoc value :key :new?)
                                                  value)]
                                      (if (= value {})
                                        nil
                                        value))
                             :disabled is-disabled?
                             :on-change (fn [e]
                                          (reset! value-repr
                                                  (into []
                                                        (filter (fn [r]
                                                                  (not= [:key :new?] (keys r)))
                                                                (assoc-in  value (into [i] (get-in template [1 :field]))
                                                                           (if (and e (.. e -target))
                                                                             (.. e -target -value )
                                                                             e)  ))))
                                          (on-change (mapv
                                                      (fn [v]
                                                        (dissoc v :new? :key))
                                                      @value-repr))))])
              ]
             [:div.level-item
              [:a.button.level-item
               
               {:disabled is-disabled?
                :on-click (fn []

                            (when-not is-disabled?
                              (reset! value-repr (into []

                                                       (filter (fn [{:keys [key ] :as v}]
                                                                 (not= key (:key override)))
                                                               (filter (fn [r]
                                                                         (not= [:key :new?] (keys r)))
                                                                       value))))

                              (on-change (mapv
                                          (fn [v]
                                            (dissoc v :new? :key))
                                          @value-repr))))}
               [:span.icon [:span.icon-remove]]]]
             ]])]))))

(defmethod do-bind "select" [dom {:keys [field allow-nil? subscription event class value spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (dispatch-value-change (conj event field))

                    :value (or (get-in subscription field) "")
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)
        options (if allow-nil?
                  (with-keys (conj rest [:option {:value nil}]))
                  (with-keys rest))]
    (into [dom (dissoc keys :allow-nil?)]  options)))


(defmethod do-bind "radio" [dom {:keys [field subscription event class value spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (dispatch-value-change (conj event field))
                    :checked (= (get-in subscription field) value)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field ))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "checkbox" [dom {:keys [field subscription event class value spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (dispatch-event (-> event
                                                   (conj field)
                                                   (conj (not (get-in subscription field)))))
                    :checked (boolean (get-in subscription field))
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field ))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "typeahead" [dom {:keys [field text-field event text-event subscription class spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (fn [selected text-description text-value]
                                 (re-frame/dispatch (conj (conj event field) selected))
                                 (when text-field
                                   (re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
                    :value (get-in subscription field)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "multi-field" [dom {:keys [field event subscription class spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (fn [value]
                                 (re-frame/dispatch (conj (conj event field) value)))
                    :value (get-in subscription field)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))


(defmethod do-bind "typeahead-entity" [dom {:keys [field event text-event subscription class spec match->text] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (fn [selected]
                                 (re-frame/dispatch (conj (conj event field) selected))
                                 #_(when text-field
                                   (re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
                    :value (get-in subscription field)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "typeahead-v3" [dom {:keys [field event text-event subscription class spec match->text] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (fn [selected]
                                 (re-frame/dispatch (conj (conj event field) selected))
                                 #_(when text-field
                                     (re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
                    :value (get-in subscription field)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "date" [dom {:keys [field event subscription class spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        selected (get-in subscription field)
        selected (cond (string? selected)
                       (c/to-date (time/to-default-time-zone (time/from-default-time-zone (str->date selected standard))))

                       
                       (instance? goog.date.DateTime selected)
                       (c/to-date (time/to-default-time-zone (time/from-default-time-zone selected)))

                       (instance? goog.date.Date selected)
                       (c/to-date selected)

                       :else
                   selected )

        keys (assoc keys
                    :on-change (dispatch-date-change (conj event field)) 
                    :selected selected
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "expense-accounts" [dom {:keys [field event subscription class spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :value (get-in subscription field)
                    :event (conj event field)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :spec)]
    (into [dom keys] (with-keys rest))))


(defmethod do-bind "button-radio" [dom {:keys [field event subscription class spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :value (get-in subscription field)
                    :on-change (fn [v]
                                 (re-frame/dispatch (-> event (conj field) (conj v))))
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :event :subscription :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "number" [dom {:keys [field precision event subscription class spec] :as keys :or {precision 2}} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (fn [e]
                                 (.preventDefault e)
                                 (re-frame/dispatch (-> event
                                                        (conj field)
                                                        (conj (let [val (.. e -target -value)]
                                                                (cond (and val
                                                                           (re-matches #"[\-]?(\d+)(\.\d{2})?" val))
                                                                      (js/parseFloat val)

                                                                  (str/blank? val )
                                                                  nil

                                                                  :else
                                                                  val))))))
                    :value (get-in subscription field)
                    
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "textarea->table" [dom {:keys [field event subscription class spec] :as keys :or {precision 2}} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (fn [x]
                                 (re-frame/dispatch (-> event
                                                        (conj field)
                                                        (conj x))))
                    :value (get-in subscription field)
                    
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind "money" [dom {:keys [field event subscription class spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (fn [x]
                                            (re-frame/dispatch (-> event
                                                                   (conj field)
                                                                   (conj x))))
                    :value (get-in subscription field)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defmethod do-bind :default [dom {:keys [field event subscription class spec] :as keys} & rest]
  (let [field (if (keyword? field) [field] field)
        event (if (keyword? event) [event] event)
        keys (assoc keys
                    :on-change (dispatch-value-change (conj event field))
                    :value (get-in subscription field)
                    :class (str class
                                (when (and spec (not (s/valid? spec (get-in subscription field))))
                                  " is-danger")))
        keys (dissoc keys :field :subscription :event :spec)]
    (into [dom keys] (with-keys rest))))

(defn bind-field [all]
   (apply do-bind all))



(defn horizontal-field [label & controls]
  [:div.field.is-horizontal
   (when label 
   [:div.field-label
    label
    ])
   (into
    [:div.field-body]
    (with-keys (map (fn [x] [:div.field x]) controls)))])

(def date-picker
  (do
    (reagent/adapt-react-class (.-default react-datepicker))))

(defn date-picker-friendly [params]
  [date-picker (assoc params
                      :class-name "input"
                      :class "input"
                      :format-week-number (fn [] "")
                      :previous-month-button-label ""
                      :next-month-button-label ""
                      :next-month-label ""
                      :type "date")])



(defn local-now []
  (t/to-default-time-zone (t/now)))

(defn local-today []
  (t/at-midnight (t/to-default-time-zone (t/now))))

(def with-user
  (re-frame/->interceptor
   :id :with-user
   :before (fn [context]
             (-> context
                 (assoc-in [:coeffects :user] (get-in context [:coeffects :db :user]))))))

(def with-role
  (re-frame/->interceptor
   :id :with-role
   :before (fn [context]
             (-> context
                 (assoc-in [:coeffects :role] (-> (get-in context [:coeffects :db :user])
                                                  (str/split  #"\.")
                                                  second
                                                  (base64/decodeString )
                                                  (#(.parse js/JSON % ))
                                                  (js->clj  :keywordize-keys true)
                                                  :user/role))))))

(def with-is-admin?
  (re-frame/->interceptor
   :id :with-is-admin?
   :before (fn [context]
             (-> context
                 (assoc-in [:coeffects :is-admin?] (= "admin"
                                                      (-> (get-in context [:coeffects :db :user])
                                                          (str/split  #"\.")
                                                          second
                                                          (base64/decodeString )
                                                          (#(.parse js/JSON % ))
                                                          (js->clj  :keywordize-keys true)
                                                          :user/role)))))))

(defn query-params []
  (reduce-kv 
   (fn [result k v]
     (assoc result (keyword k) (edn/read-string v)))
   {}
   (:query (cemerick.url/url  (.-location js/window)))))

(defn loading [db]
  (-> db 
      (assoc-in [:status] :loading)
      (assoc-in [:error] nil)))

(defn triggers-loading [form]
  (re-frame/enrich
   (fn [db event]
     (loading db))))

(defn action-cell-width [cnt]
  (str (inc (* cnt 51)) "px"))

(defn days-until [d]
  (let [today (t/at-midnight (t/now))
        d (t/at-midnight d)
        in (if (t/after? today d)
                 (- (t/in-days (t/interval (t/minus d (t/days 1)) today)))
                 (t/in-days (t/interval today d )))]
    in))

(defn copy-to-clipboard [text]
  (let [el (js/document.createElement "textarea")]

    (set! (.-value el) text)
    (.appendChild js/document.body el)
    (.select el)
    (js/document.execCommand "copy")
    (.removeChild js/document.body el)))

(defn account->match-text [x]
  (str (:numeric-code x) " - " (:name x)))
