(ns auto-ap.ssr.components.inputs
  (:require
   [auto-ap.ssr.components.tags :as tags]
   [auto-ap.ssr.hiccup-helper :as hh]
   [auto-ap.ssr.hx :as hx :refer [js-fn]]
   [auto-ap.ssr.svg :as svg]
   [auto-ap.time :as atime]
   [clojure.string :as str]
   [hiccup2.core :as hiccup]))


(def default-input-classes
  ["bg-gray-50" "border" "text-sm" "rounded-lg" "" "block"
   "p-2.5" "border-gray-300" "text-gray-900" "focus:ring-blue-500" "focus:border-blue-500"
   "dark:bg-gray-700" "dark:border-gray-600" "dark:placeholder-gray-400" "dark:text-white"
   "dark:focus:ring-blue-500" "dark:focus:border-blue-500"
   "group-[.has-error]:bg-red-50" "group-[.has-error]:border-red-500" "group-[.has-error]:text-red-900"
   "group-[.has-error]:placeholder-red-700" "group-[.has-error]:focus:ring-red-500"
   "group-[.has-error]:dark:bg-gray-700" "group-[.has-error]:focus:border-red-500"
   "group-[.has-error]:dark:text-red-500" "group-[.has-error]:dark:placeholder-red-500"

   "group-[.has-error]:dark:border-red-500"])

(def default-checkbox-classes
  "w-4 h-4 bg-gray-100 indeterminate:bg-gray-300 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600")

(defn select- [params & children]
  (into
   [:select (-> params
                (dissoc :allow-blank? :value :options)
                (update
                 :class (fnil hh/add-class "") default-input-classes))
    (cond->>
     (map (fn [[k v]]
            [:option {:value k :selected (= k (:value params))} v])
          (:options params))
      (:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))]
   children))

(defn checkbox- [params & rest]
  (if (seq rest)
    [:label {:class "text-sm text-gray-800 dark:text-gray-300  "}
     [:input (merge (dissoc params :indeterminate?)
                    {:type "checkbox" :class (hh/add-class default-checkbox-classes (:class params ""))}
                    (when (:indeterminate? params)
                      {:x-init "$el.indeterminate = true"}))]
     [:span.ml-2
      rest]]
    [:input (merge (dissoc params :indeterminate params)
                   {:type "checkbox" :class (hh/add-class default-checkbox-classes (:class params ""))}
                   (when (:indeterminate? params)
                     {:x-init "$el.indeterminate = true"}))]))

(defn typeahead- [params]
  [:div.relative {:x-data (hx/json {:baseUrl (str (:url params))
                                    :value {:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}
                                    :tippy nil
                                    :search ""
                                    :active -1
                                    :elements (if ((:value-fn params identity) (:value params))
                                                [{:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}]
                                                [])})
                  :x-modelable "value.value"
                  :x-model (:x-model params)}
   (if (:disabled params)
     [:span {:x-text "value.label"}]
     [:a {:class                        (-> (hh/add-class (or (:class params) "") default-input-classes)
                                            (hh/add-class "cursor-pointer"))
          "x-tooltip.on.click"              "{content: ()=>$refs.dropdown.innerHTML, placement: 'bottom', onMount(i) {htmx.process(i.popper); }, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}, theme: 'dropdown', allowHTML: true, interactive:true}"
          "@keydown.down.prevent.stop"  "tippy.show();"
          "@keydown.backspace"          "tippy.hide(); value = {value: '', label: '' }"
          :tabindex                     0
          :x-init                      (str  "$nextTick(() => tippy = $el.__x_tippy); " (:x-init params))
          :x-ref                        "input"}
      [:input (-> params
                  (dissoc :class)
                  (dissoc :value-fn)
                  (dissoc :content-fn)

                  (dissoc :placeholder)
                  (dissoc :x-model)

                  (assoc
                   "x-ref" "hidden"
                   :type    "hidden"
                   ":value" "value.value"
                   :x-init (hiccup/raw (str "$watch('value', v => $dispatch('change')); "))))]
      [:div.flex.w-full.justify-items-stretch
       [:span.flex-grow.text-left {"x-text" "value.label"}]
       [:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
        svg/drop-down]
       [:div {:x-show "value.warning"}
        (tags/badge- {:class "peer"
                      :x-tooltip "value.warning"} "!")]]])

   [:template {:x-ref "dropdown"}
    [:ul.dropdown-contents {:class                     "bg-gray-100 dark:bg-gray-600  ring-1"
                            "@keydown.escape"   "tippy.hide(); value = {value: '', label: '' };  "
                            :x-destroy "if ($refs.input) {$refs.input.focus();}"}
     [:input {:type                    "text"
              :autofocus true
              :class                   (-> (:class params)
                                           (or "")
                                           (hh/add-class default-input-classes)
                                           (hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full"))
              "x-model"                "search"
              "placeholder"            (:placeholder params)
              "@keydown.down.prevent"  "active ++; active = active >= elements.length - 1 ? elements.length - 1 : active"
              "@keydown.up.prevent"    "active --; active = active < 0 ? 0 : active"
              "@keydown.enter.prevent.stop" "tippy.hide(); value = elements.length > 0 ? $data.elements[active] : {'value': '', label: ''}; $refs.input.focus()"
              "x-init"                 "$el.focus(); $watch('search', s => { if($el.value.length > 2) {fetch(addQueryParam(baseUrl, 'q', s)).then(data=>data.json()).then(data => {elements = data; active=-1; tippy.popperInstance.update()}) }})"}]
     [:div.dropdown-options {:class "rounded-b-lg overflow-hidden"}
      [:template {:x-for "(element, index) in elements"}
       [:li [:a {:class           "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 [&.active]:dark:bg-primary-700  text-gray-800  dark:text-gray-100"
                 :href            "#"
                 ":class"         "active == index ? 'active' : ''"

                 "@mouseover"     "active = index"
                 "@mouseout"      "active = -1"
                 "@click.prevent" "value = element; tippy.hide();  $refs.input.focus()"
                 "x-html"         "element.label"}]]]
      [:template {:x-if "elements.length == 0"}
       [:li {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800  dark:text-gray-100 text-xs "}
        "No results found"]]]]]])

(defn multi-typeahead-dropdown- [params]
  [:template {:x-ref "dropdown"}
   [:ul.dropdown-contents {:class                     "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1 p-4"
                           "@keydown.escape.prevent"   "tippy.hide();"
                           :x-destroy "if ($refs.input) {$refs.input.focus();}"}
    [:div {:class (-> "relative"
                      #_(hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full"))}
     [:div {:class "absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"}
      [:svg {:class "w-4 h-4 text-gray-500 dark:text-gray-400", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 20 20"}
       [:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"}]]]
     [:input {:type                    "text"
              :class (-> (:class params)
                         (or "")
                         (hh/add-class "block w-full p-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500")
                         (hh/add-class default-input-classes))
              "x-model"                "search"
              "placeholder"            (:placeholder params)
              "@keydown.down.prevent"  "active ++; active = active >= elements.length - 1 ? elements.length - 1 : active"
              "@keydown.up.prevent"    "active --; active = active < 0 ? 0 : active"
              "@keydown.enter.prevent.stop" "if ($data.elements[active]) { if (value.has($data.elements[active].value)) { value.delete($data.elements[active].value) } else {value.add($data.elements[active].value); lookup[$data.elements[active].value] = $data.elements[active].label} } "
              "x-init"                 " $el.focus(); $watch('search', s => { if($el.value.length > 2) {fetch(addQueryParam(baseUrl, 'q',  s)).then(data=>data.json()).then(data => reset_elements(data)) }})"}]]
    [:div.dropdown-options {:class "overflow-hidden  divide-y divide-gray-200 "}
     [:template {:x-for "(element, index) in elements"}
      [:li  {":style" "index  == 0  && 'border: 0 !important;'"}
       [:label {:class           "p-3 group rounded flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-300 [&.active]:dark:bg-primary-700 [&.implied]:text-gray-500 text-gray-800  dark:text-gray-100 cursor-pointer"


                :href            "#"
                ":class" (hx/json {"active"  (hx/js-fn "active==index")
                                   "implied" (hx/js-fn "all_selected && index != 0")})
                "@mouseover"     "active = index"
                "@mouseout"      "active = -1"
                "@click.prevent" "toggle(element)"}
        (checkbox- {":checked" "value.has(element.value) || all_selected"
                    :class "group-[&.implied]:bg-green-200"})
        #_[:input {:type "checkbox"}]
        [:span {"x-html"         "element.label"}]]]]
     [:template {:x-if "elements.length == 0"}
      [:li {:class "px-4 pt-4 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800  dark:text-gray-100 text-xs " "style" "border: 0 !important"}
       "No results found"]]]]])

(defn multi-typeahead-selected-pill- [params]
  [:div.flex-grow.flex
   [:template {:x-if "value.size > 0"}
    [:a.bg-blue-100.rounded-full.px-3
     [:span.text-left
      [:span {"x-text" "value.has('all') ? 'All' : value.size"}]
      " selected"]]]
   [:template {:x-if "value.size == 0"}
    [:span.text-left.text-gray-400 "None selected"]]
   [:div {:class "w-4 h-4 ml-2 inline text-gray-500 self-center rounded-full bg-gray-100 text-gray-500"
          "@click.prevent.stop" "value = new Set([]); all_selected=false;"
          :x-show "value.size > 0"}
    svg/x]])


(defn multi-typeahead- [params]
  [:div.relative {:x-data (hx/json {:baseUrl (str (:url params))
                                    :reset_elements (js-fn "function(e) {
                                            this.elements = [{value: 'all', label:'All'}].concat(e);
                                            this.active = -1
                                          }")
                                    :toggle (js-fn "function(e) { 
                                                         if (e.value  == 'all') {
                                                          if (this.value.size > 0) {
                                                            this.value = new Set([]);
                                                            this.all_selected = false;
                                                          } else {
                                                            this.value = new Set(['all']);
                                                            this.all_selected = true;
                                                          }
                                                         }
                                                         else { 
                                                          if (this.all_selected) {
                                                            this.value.delete('all')
                                                            this.all_selected = false;

                                                          }
                                                          if (this.value.has(e.value)) { 
                                                              this.value.delete(e.value) 
                                                            } else { 
                                                              this.value.add(e.value); this.lookup[e.value] = e.label 
                                                            } 
                                                          }
                                                          }")
                                    :all_selected (boolean (= (:value params) :all)),
                                    :value (cond
                                             (= :all (:value params))
                                             ["all"]

                                             (sequential? (:value params))
                                             (map (fn [v] ((:value-fn params identity) v))
                                                  (:value params))

                                             :else
                                             [])
                                    :tippy nil
                                    :lookup (into {}
                                                  (when (sequential? (:value params))
                                                    (map (fn [v] [((:value-fn params identity) v)
                                                                  ((:content-fn params identity) v)])
                                                         (:value params))))
                                    :x-init (str "$watch('value', v => $dispatch('change')); ")
                                    :search ""
                                    :active -1
                                    :elements (cond->  [{:value "all" :label "All"}]
                                                (sequential? (:value params))
                                                (into (map (fn [v]
                                                             {:value ((:value-fn params identity) v)
                                                              :label ((:content-fn params identity) v)})
                                                           (:value params))))
                                    :x-ref "r"})
                  ;; :x-modelable "value.value" TODO
                  ;; :x-model (:x-model params) TODO
                  :x-init "value=new Set(value || []); "}
   (if (:disabled params)
     [:span {:x-text "value.label"}]
     [:a {:class                        (-> (hh/add-class (or (:class params) "") default-input-classes)
                                            (hh/add-class "cursor-pointer"))
          "x-tooltip.on.click.prevent"              "{content: ()=>$refs.dropdown.innerHTML, placement: 'bottom', onMount(i) {htmx.process(i.popper); }, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}, theme: 'dropdown', allowHTML: true, interactive:true}"
          "@keydown.down.prevent.stop"  "tippy.show();"
          "@keydown.backspace"          "tippy.hide(); value=new Set( []);"
          :tabindex                     0
          :x-init                      (str  "$nextTick(() => tippy = $el.__x_tippy); " (:x-init params))
          :x-ref                        "input"}
      [:template {:x-for "v in Array.from(value.values())"}
       [:input (-> params
                   (dissoc :class :value-fn :content-fn :placeholder :x-model)
                   (assoc
                    :type    "hidden"
                    "x-bind:value" "v"))]]
      [:template {:x-if "value.size == 0"}
       [:input (-> params
                   (dissoc :class :value-fn :content-fn :placeholder :x-model)
                   (assoc :type    "hidden"
                          :value ""))]]
      [:div.flex.w-full.justify-items-stretch
       (multi-typeahead-selected-pill- params)
       [:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
        svg/drop-down]
       [:div {:x-show "value.warning"
              :x-ref "warning_badge"
              :x-effect "if (value.warning) { $nextTick(()=> warning_badge.update()) }"}
        (tags/badge- {:class "peer"} "!")


        [:div {:x-show "value.warning"
               :x-ref "warning_pop"
               :class "hidden peer-hover:block bg-red-50 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"
               :x-text "value.warning"}]]]
      (multi-typeahead-dropdown- params)])])


(defn use-size [size]
  (if (= :small size)
    (str " " "text-xs p-2")
    (str " " "text-sm p-2.25")))



(defn text-input- [{:keys [size error?] :as params}]
  [:input
   (-> params
       (dissoc :error?)
       (assoc :type "text" :autocomplete "off")
       (update
        :class #(-> ""
                    (hh/add-class default-input-classes)
                    (hh/add-class %)))
       (update :class #(str % (use-size size))))])

(defn text-area- [{:keys [] :as params}]
  [:textarea
   (-> params
       (update :class #(-> ""
                           (hh/add-class default-input-classes)
                           (hh/add-class %))))])

(defn money-input- [{:keys [size] :as params}]
  [:input
   (->  params
        (update :class (fnil hh/add-class "") default-input-classes)
        (update :class hh/add-class "appearance-none text-right")
        (update :class #(str % (use-size size)))
        (assoc :type "number"
               :step "0.01")
        (dissoc :size))])

(defn int-input- [{:keys [size] :as params}]
  [:input
   (->  params
        (update :class (fnil hh/add-class "") default-input-classes)
        (update :class hh/add-class "appearance-none text-right")
        (update :class #(str % (use-size size)))
        (assoc :type "number"
               :step "1")
        (dissoc :size))])

(defn date-input- [{:keys [size] :as params}]
  [:div.shrink {:x-data (hx/json {:value (:value params)
                                  :tippy nil
                                  :dp nil})
                "x-effect" "console.log('changed to' +value)"
                "@change-date.camel" "$dispatch('change')"}
   [:input
    (->  params
         (update :class (fnil hh/add-class "") default-input-classes)
         (assoc :x-model "value")
         (assoc "x-tooltip.on.focus" "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', onMount(i) { htmx.process(i.popper); }, allowHTML: true, interactive:true}")

         (assoc :x-init "$nextTick(() => tippy = $el.__x_tippy); ")
         (assoc :type "text")

         (assoc "autocomplete" "off")
         (assoc "@change" "value = $event.target.value;")

         (assoc                              "@keydown.escape"   "tippy.hide(); ")
         #_(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\") "))
         (update :class #(str % (use-size size) " w-full"))
         (dissoc :size))]
   [:template {:x-ref "tooltip"}

    [:div.shrink
     [:div
      (->  params
           (update :class (fnil hh/add-class "") default-input-classes)
           (assoc :type "text")
           (assoc :value (:value params))
                ;; the data-date field has to be bound before the datepicker can be initialized
           (assoc :x-init "$nextTick(() =>  { dp = initCalendar($el); ;}); ")
           (assoc "x-effect" "if(dp) { dp.setDate(value); } ")
           (assoc ":data-date" "value")
           (assoc "@htmx:before-history-save" "destroyDatepicker(dp)")
           (assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)")
           (assoc "x-destroy" "destroyDatepicker(dp)")
           (assoc "@change-date.camel" "value = dp.getDate(\"mm/dd/yyyy\");")

           (update :class #(str % (use-size size) " w-full"))
           (dissoc :size :name :x-model :x-modelable))]]]])

(defn multi-calendar-input- [{:keys [size] :as params}]
  (let [value (str/join ", "
                        (for [v (:value params)
                              :when v]
                          (some-> v (atime/unparse-local atime/normal-date))))]
    [:div.shrink {:x-data (hx/json {:value value
                                    :dp nil})
                  :x-modelable "value"
                  :x-model (:x-model params)}
     [:template {:x-for "v in value"}
      [:input {:type "hidden" :name (:name params) :x-model "v"}]]
     [:div
      (->  params
           (update :class (fnil hh/add-class "") default-input-classes)
           (assoc :type "text")
           (assoc :value value)
           ;; the data-date field has to be bound before the datepicker can be initialized
           (assoc :x-init "$nextTick(() =>  { dp = initMultiDatepicker($el, value); ;}); ")
           (assoc "x-effect" "if(dp) { dp.setDate(Array.from(value), {clear: true}); } ")
           (assoc ":data-date" "Array.prototype.join.call(value, ', ')")
           (assoc "@htmx:before-history-save" "destroyDatepicker(dp)")
           (assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)")
           (assoc "x-destroy" "destroyDatepicker(dp)")
           (assoc "@change-date.camel" "value = dp.getDate(\"mm/dd/yyyy\");")

           (update :class #(str % (use-size size) " w-full"))
           (dissoc :size :name :x-model :x-modelable))]]))

(defn calendar-input- [{:keys [size] :as params}]
  (let [value (:value params)]
    [:div.shrink {:x-data (hx/json {:value value
                                    :dp nil})
                  :x-modelable "value"
                  :x-model (:x-model params)}
     [:input {:type "hidden" :name (:name params) :x-model "value"}]
     [:div
      (->  params
           (update :class (fnil hh/add-class "") default-input-classes)
           (assoc :type "text")
           (assoc :value value)
           ;; the data-date field has to be bound before the datepicker can be initialized
           (assoc :x-init "$nextTick(() =>  { dp = initCalendar($el); ;}); ")
           (assoc "x-effect" "if(dp) { dp.setDate(value); } ")
           (assoc ":data-date" "value")
           (assoc "@htmx:before-history-save" "destroyDatepicker(dp)")
           (assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)")
           (assoc "x-destroy" "destroyDatepicker(dp)")
           (assoc "@change-date.camel" "value = dp.getDate(\"mm/dd/yyyy\");")

           (update :class #(str % (use-size size) " w-full"))
           (dissoc :size :name :x-model :x-modelable))]]))



(defn field-errors- [{:keys [source key]} & rest]
  (let [errors (:errors (cond-> (meta source)
                          key (get key)))]
    [:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4 (str/join ", " errors)]))

(defn field- [params & rest]
  [:div (-> params
            (update :class #(hh/add-class (or % "") "group")))
   (when (:label params)
     [:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)])
   rest
   (when (:error-source params)
     (field-errors- {:source (:error-source params)
                     :key (:error-key params)}))])

(defn inline-field- [params & rest]
  [:div (-> params
            (update :class #(hh/add-class (or % "") "group flex items-baseline gap-2")))
   (when (:label params)
     [:label {:class "mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)])
   rest
   (when (:error-source params)
     (field-errors- {:source (:error-source params)
                     :key (:error-key params)}))])

(defn errors- [{:keys [errors]}]
  [:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4
   (when (sequential? errors)
     (str/join ", " (filter string? errors)))])

(defn form-errors- [{:keys [errors]}]
  [:div#form-errors (when errors
                      [:span.error-content
                       (errors- {:errors errors})])])

(defn validated-field- [params & rest]
  (field- (cond-> params
            true (dissoc :errors)
            (sequential? (:errors params)) (update :class #(hh/add-class (or % "") "has-error")))
          rest
          (errors- {:errors (:errors params)})))

(defn validated-inline-field- [params & rest]
  (inline-field- (cond-> params
                   true (dissoc :errors)
                   (sequential? (:errors params)) (update :class #(hh/add-class (or % "") "has-error")))
                 rest
                 (errors- {:errors (:errors params)})))

(defn hidden- [{:keys [name value] :as params}]
  [:input (merge {:type "hidden" :value value :name name} params)])



(defn toggle- [params & children]
  [:label {:class "inline-flex items-center cursor-pointer"}
   [:input (merge {:type "checkbox", :class "sr-only peer"} params)]
   [:div {:class "relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"}]
   [:span {:class "ms-3 text-sm font-medium text-gray-900 dark:text-gray-300"} children]])
