;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC

(ns app.util.forms
  (:refer-clojure :exclude [uuid])
  (:require
   [app.common.spec :as us]
   [app.util.i18n :refer [tr]]
   [cljs.spec.alpha :as s]
   [cuerdas.core :as str]
   [rumext.v2 :as mf]))

;; --- Handlers Helpers

(defn- interpret-problem
  [acc {:keys [path pred via] :as problem}]
  (cond
    (and (empty? path)
         (list? pred)
         (= (first (last pred)) 'cljs.core/contains?))
    (let [field (last (last pred))
          path  (conj path field)
          root  (first via)]
      (assoc-in acc path {:code :missing :type :builtin :root root :field field}))

    (and (seq path) (seq via))
    (let [field (first path)
          code  (last via)
          root  (first via)]
      (assoc-in acc path {:code code :type :builtin :root root :field field}))

    :else acc))

(declare create-form-mutator)

(defn use-form
  [& {:keys [initial] :as opts}]
  (let [state      (mf/useState 0)
        render     (aget state 1)

        get-state  (mf/use-callback
                    (mf/deps initial)
                    (fn []
                      {:data (if (fn? initial) (initial) initial)
                       :errors {}
                       :touched {}}))

        state-ref  (mf/use-ref (get-state))
        form       (mf/use-memo (mf/deps initial) #(create-form-mutator state-ref render get-state opts))]

    (mf/use-effect
     (mf/deps initial)
     (fn []
       (if (fn? initial)
         (swap! form update :data merge (initial))
         (swap! form update :data merge initial))))

    form))

(defn- wrap-update-fn
  [f {:keys [spec validators]}]
  (fn [& args]
    (let [state    (apply f args)
          cleaned  (s/conform spec (:data state))
          problems (when (= ::s/invalid cleaned)
                     (::s/problems (s/explain-data spec (:data state))))

          errors   (reduce interpret-problem {} problems)
          errors   (reduce (fn [errors vf]
                             (merge errors (vf errors (:data state))))
                           errors
                           validators)
          errors   (merge errors (:errors state))]

      (assoc state
             :errors errors
             :clean-data (when (not= cleaned ::s/invalid) cleaned)
             :valid (and (empty? errors)
                         (not= cleaned ::s/invalid))))))

(defn- create-form-mutator
  [state-ref render get-state opts]
  (reify
    IDeref
    (-deref [_]
      (mf/ref-val state-ref))

    IReset
    (-reset! [_ new-value]
      (if (nil? new-value)
        (mf/set-ref-val! state-ref (get-state))
        (mf/set-ref-val! state-ref new-value))
      (render inc))

    ISwap
    (-swap! [_ f]
      (let [f (wrap-update-fn f opts)]
        (mf/set-ref-val! state-ref (f (mf/ref-val state-ref)))
        (render inc)))


    (-swap! [_ f x]
      (let [f (wrap-update-fn f opts)]
        (mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x))
        (render inc)))

    (-swap! [_ f x y]
      (let [f (wrap-update-fn f opts)]
        (mf/set-ref-val! state-ref (f (mf/ref-val state-ref) x y))
        (render inc)))

    (-swap! [_ f x y more]
      (let [f (wrap-update-fn f opts)]
        (mf/set-ref-val! state-ref (apply f (mf/ref-val state-ref) x y more))
        (render inc)))))

(defn on-input-change
  ([form field value]
   (on-input-change form field value false))
  ([form field value trim?]
   (swap! form (fn [state]
                 (-> state
                     (assoc-in [:data field] (if trim? (str/trim value) value))
                     (update :errors dissoc field))))))

(defn update-input-value!
  [form field value]
  (swap! form (fn [state]
                (-> state
                    (assoc-in [:data field] value)
                    (update :errors dissoc field)))))

(defn on-input-blur
  [form field]
  (fn [_]
    (let [touched (get @form :touched)]
      (when-not (get touched field)
        (swap! form assoc-in [:touched field] true)))))

;; --- Helper Components

(mf/defc field-error
  [{:keys [form field type]
    :as props}]
  (let [{:keys [message] :as error} (get-in form [:errors field])
        touched? (get-in form [:touched field])
        show? (and touched? error message
                   (cond
                     (nil? type) true
                     (keyword? type) (= (:type error) type)
                     (ifn? type) (type (:type error))
                     :else false))]
    (when show?
      [:ul.form-errors
       [:li {:key (:code error)} (tr (:message error))]])))

(defn error-class
  [form field]
  (when (and (get-in form [:errors field])
             (get-in form [:touched field]))
    "invalid"))

;; --- Form Specs and Conformers

(s/def ::email ::us/email)
(s/def ::not-empty-string ::us/not-empty-string)
(s/def ::color ::us/rgb-color-str)
