;; 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 Sucursal en España SL

(ns app.main.ui.components.numeric-input
  (:require
   [app.common.data :as d]
   [app.common.math :as mth]
   [app.common.schema :as sm]
   [app.main.ui.formats :as fmt]
   [app.main.ui.hooks :as h]
   [app.util.dom :as dom]
   [app.util.globals :as globals]
   [app.util.keyboard :as kbd]
   [app.util.simple-math :as smt]
   [cljs.core :as c]
   [cuerdas.core :as str]
   [goog.events :as events]
   [rumext.v2 :as mf]))

(mf/defc numeric-input*
  {::mf/forward-ref true}
  [{:keys [value min max step data-wrap on-change on-blur on-focus title default
           select-on-focus class is-disabled is-nillable is-integer]
    :rest props} external-ref]
  (let [value-str   value
        min-value   min
        max-value   max
        step-value  step
        wrap-value? data-wrap

        min-value   (d/parse-double min-value)
        max-value   (d/parse-double max-value)
        step-value  (d/parse-double step-value 1)
        default     (d/parse-double default (when-not is-nillable 0))

        select-on-focus? (d/nilv select-on-focus true)

        ;; We need a ref pointing to the input dom element, but the user
        ;; of this component may provide one (that is forwarded here).
        ;; So we use the external ref if provided, and the local one if not.
        local-ref   (mf/use-ref)
        ref         (or external-ref local-ref)

        ;; This `value` represents the previous value and is used as
        ;; initil value for the simple math expression evaluation.
        value       (when (not= :multiple value-str) (d/parse-double value-str default))

        ;; We need to store the handle-blur ref so we can call it on unmount
        dirty-ref   (mf/use-ref false)

        ;; Last value input by the user we need to store to save on unmount
        last-value*  (mf/use-var value)

        drag-state*     (mf/use-ref :idle)
        drag-start-x*   (mf/use-ref 0)
        drag-start-val* (mf/use-ref 0)

        parse-value
        (mf/use-fn
         (mf/deps min-value max-value value is-nillable default is-integer)
         (fn []
           (when-let [node (mf/ref-val ref)]
             (let [new-value (-> (dom/get-value node)
                                 (str/strip-suffix ".")
                                 (smt/expr-eval value))]
               (cond
                 (d/num? new-value)
                 (-> new-value
                     (cond-> is-integer mth/round)
                     (d/max (/ sm/min-safe-int 2))
                     (d/min (/ sm/max-safe-int 2))
                     (cond-> (d/num? min-value)
                       (d/max min-value))
                     (cond-> (d/num? max-value)
                       (d/min max-value)))

                 is-nillable
                 default

                 :else value)))))

        update-input
        (mf/use-fn
         (fn [new-value]
           (when-let [node (mf/ref-val ref)]
             (dom/set-value! node (fmt/format-number new-value)))))

        apply-value
        (mf/use-fn
         (mf/deps on-change update-input value)
         (fn [event new-value]
           (mf/set-ref-val! dirty-ref false)
           (when (and (not= new-value value)
                      (fn? on-change))
             ;; FIXME: on-change very slow, makes the handler laggy
             (on-change new-value event))
           (update-input new-value)))

        set-delta
        (mf/use-fn
         (mf/deps wrap-value? min-value max-value parse-value apply-value)
         (fn [event up? down?]
           (let [current-value (parse-value)
                 current-value
                 (cond
                   (and (not current-value) down? max-value)
                   max-value

                   (and (not current-value) up? min-value)
                   min-value

                   (not current-value)
                   (d/nilv default 0)

                   :else
                   current-value)]
             (when current-value
               (let [increment (cond
                                 (kbd/shift? event)
                                 (if up? (* step-value 10) (* step-value -10))

                                 (kbd/alt? event)
                                 (if up? (* step-value 0.1) (* step-value -0.1))

                                 :else
                                 (if up? step-value (- step-value)))

                     new-value (+ current-value increment)
                     new-value (cond
                                 (and wrap-value? (d/num? max-value min-value)
                                      (> new-value max-value) up?)
                                 (-> new-value (- max-value) (+ min-value) (- step-value))

                                 (and wrap-value? (d/num? max-value min-value)
                                      (< new-value min-value) down?)
                                 (-> new-value (- min-value) (+ max-value) (+ step-value))

                                 (and (d/num? min-value) (< new-value min-value))
                                 min-value

                                 (and (d/num? max-value) (> new-value max-value))
                                 max-value

                                 :else new-value)
                     new-value (if is-integer (mth/round new-value) new-value)]

                 (apply-value event new-value))))))

        handle-key-down
        (mf/use-fn
         (mf/deps set-delta apply-value update-input parse-value)
         (fn [event]
           (mf/set-ref-val! dirty-ref true)
           (let [up?    (kbd/up-arrow? event)
                 down?  (kbd/down-arrow? event)
                 enter? (kbd/enter? event)
                 esc?   (kbd/esc? event)
                 node   (mf/ref-val ref)]
             (when (or up? down?)
               (set-delta event up? down?))
             (reset! last-value* (parse-value))
             (when enter?
               (dom/blur! node))
             (when esc?
               (update-input value-str)
               (dom/blur! node)))))

        handle-change
        (mf/use-fn
         (mf/deps parse-value)
         (fn []
           ;; Store the last value inputed
           (reset! last-value* (parse-value))))

        handle-mouse-wheel
        (mf/use-fn
         (mf/deps set-delta)
         (fn [event]
           (when-let [node (mf/ref-val ref)]
             (when (dom/active? node)
               (dom/prevent-default event)
               (dom/stop-propagation event)
               (let [{:keys [y]} (dom/get-delta-position event)]
                 (set-delta event (< y 0) (> y 0)))))))

        handle-blur
        (mf/use-fn
         (mf/deps parse-value apply-value update-input on-blur)
         (fn [event]
           (when (mf/ref-val dirty-ref)
             (let [new-value (or @last-value* default)]
               (if (or is-nillable new-value)
                 (apply-value event new-value)
                 (update-input new-value)))
             (when (fn? on-blur)
               (on-blur event)))))

        handle-unmount (h/use-ref-callback handle-blur)

        on-click
        (mf/use-fn
         (fn [event]
           (let [target (dom/get-target event)
                 node   (mf/ref-val ref)]
             (when (and (some? node) (not (dom/child? node target)))
               (dom/blur! node)))))

        handle-focus
        (mf/use-callback
         (mf/deps on-focus select-on-focus?)
         (fn [event]
           (when-not (= :dragging (mf/ref-val drag-state*))
             (reset! last-value* (parse-value))
             (let [target (dom/get-target event)]
               (when on-focus
                 (mf/set-ref-val! dirty-ref true)
                 (on-focus event))

               (when select-on-focus?
                 (dom/select-text! target)
                 ;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
                 (.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))))

        on-scrub-pointer-down
        (mf/use-fn
         (mf/deps value value-str min-value max-value default)
         (fn [event]
           (let [disabled? (unchecked-get props "disabled")
                 node      (mf/ref-val ref)
                 is-focused (and (some? node) (dom/active? node))]
             (when-not (or disabled? is-focused (= :multiple value-str))
               (let [client-x  (.-clientX event)
                     start-val (or value default 0)]
                 (mf/set-ref-val! drag-state* :maybe-dragging)
                 (mf/set-ref-val! drag-start-x* client-x)
                 (mf/set-ref-val! drag-start-val* start-val)
                 (dom/capture-pointer event))))))

        on-scrub-pointer-move
        (mf/use-fn
         (mf/deps apply-value update-input step-value min-value max-value)
         (fn [event]
           (let [state (mf/ref-val drag-state*)]
             (when (or (= state :maybe-dragging) (= state :dragging))
               (let [client-x (.-clientX event)
                     start-x  (mf/ref-val drag-start-x*)
                     delta-x  (- client-x start-x)]
                 (when (and (= state :maybe-dragging)
                            (>= (js/Math.abs delta-x) 3))
                   (mf/set-ref-val! drag-state* :dragging)
                   (dom/add-class! (dom/get-body) "cursor-drag-scrub"))
                 (when (= (mf/ref-val drag-state*) :dragging)
                   (let [effective-step (cond
                                          (.-shiftKey event) (* step-value 10)
                                          (.-ctrlKey event)  (* step-value 0.1)
                                          :else              step-value)
                         steps   (js/Math.round (/ delta-x 1))
                         new-val (+ (mf/ref-val drag-start-val*)
                                    (* steps effective-step))
                         new-val (cond-> new-val
                                   (d/num? min-value) (mth/max min-value)
                                   (d/num? max-value) (mth/min max-value))]
                     (update-input new-val)
                     (apply-value event new-val))))))))

        on-scrub-pointer-up
        (mf/use-fn
         (mf/deps ref)
         (fn [event]
           (let [state (mf/ref-val drag-state*)]
             (when (= state :maybe-dragging)
               (mf/set-ref-val! drag-state* :idle)
               (dom/release-pointer event)
               (when-let [node (mf/ref-val ref)]
                 (dom/focus! node)))
             (when (= state :dragging)
               (mf/set-ref-val! drag-state* :idle)
               (dom/remove-class! (dom/get-body) "cursor-drag-scrub")
               (dom/release-pointer event)))))

        on-scrub-lost-pointer-capture
        (mf/use-fn
         (fn [_event]
           (mf/set-ref-val! drag-state* :idle)
           (dom/remove-class! (dom/get-body) "cursor-drag-scrub")))

        props (mf/spread-props props
                               {:value mf/undefined
                                :on-change handle-change
                                :class class
                                :type "text"
                                :ref ref
                                :default-value (fmt/format-number value)
                                :title title
                                :disabled is-disabled
                                :on-key-down handle-key-down
                                :on-blur handle-blur
                                :on-focus handle-focus
                                :on-pointer-down on-scrub-pointer-down
                                :on-pointer-move on-scrub-pointer-move
                                :on-pointer-up on-scrub-pointer-up
                                :on-lost-pointer-capture on-scrub-lost-pointer-capture})]

    (mf/with-effect [value]
      (when-let [input-node (mf/ref-val ref)]
        (dom/set-value! input-node (fmt/format-number value))))

    (mf/with-effect [handle-unmount] handle-unmount)

    (mf/with-layout-effect []
      (let [keys [(events/listen globals/window "pointerdown" on-click)
                  (events/listen globals/window "click" on-click)]]
        #(run! events/unlistenByKey keys)))

    (mf/with-layout-effect [handle-mouse-wheel]
      (when-let [node (mf/ref-val ref)]
        (let [key (events/listen node "wheel" handle-mouse-wheel #js {:passive false})]
          #(events/unlistenByKey key))))

    [:> :input props]))