;; 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.data.workspace.wasm-text
  "Helpers/events to resize wasm text shapes without depending on workspace.texts.

  This exists to avoid circular deps:
  workspace.texts -> workspace.libraries -> workspace.texts"
  (:require
   [app.common.data.macros :as dm]
   [app.common.files.helpers :as cfh]
   [app.common.geom.matrix :as gmt]
   [app.common.geom.point :as gpt]
   [app.common.types.modifiers :as ctm]
   [app.main.data.helpers :as dsh]
   [app.main.data.workspace :as-alias dw]
   [app.main.data.workspace.modifiers :as dwm]
   [app.main.data.workspace.reflow :as wrf]
   [app.main.data.workspace.shapes :as dwsh]
   [app.main.data.workspace.undo :as dwu]
   [app.render-wasm.api :as wasm.api]
   [app.render-wasm.api.fonts :as wasm.fonts]
   [beicon.v2.core :as rx]
   [potok.v2.core :as ptk]))

(def debounce-resize-text-time 40)

(defn get-wasm-text-new-size
  "Computes the new {width, height} for a text shape from WASM text layout.
  For :fixed grow-type, updates WASM content and returns current dimensions (no resize)."
  ([shape]
   (get-wasm-text-new-size shape (:content shape)))

  ([{:keys [id selrect grow-type] :as shape} content]
   ;; Skip when the WASM context is not ready (e.g. switching renderer while a
   ;; text shape is being edited): there is no design state to query, and
   ;; returning nil makes callers skip the WASM resize/modifier path.
   (when (and id (wasm.api/initialized?))
     (wasm.api/use-shape id)
     ;; While the WASM text editor is actively editing it already holds the live
     ;; content and layout. Re-pushing the content here calls `_clear_shape_text`
     ;; + `_update_shape_text_layout`, which resets the editor and drops every
     ;; keystroke after the first, so we just measure the live layout instead.
     (when-not (and (wasm.api/text-editor-has-focus?)
                    (= id (wasm.api/text-editor-get-active-shape-id)))
       (wasm.api/set-shape-text-content id content)
       (wasm.api/set-shape-text-images id content))
     (let [dimension (when (not= :fixed grow-type)
                       (wasm.api/get-text-dimensions))]
       ;; nil dimension = shape not present in WASM state; skip the resize.
       (when (or (= :fixed grow-type) (some? dimension))
         {:width  (if (#{:fixed :auto-height} grow-type)
                    (:width selrect)
                    (:width dimension))
          :height (if (= :fixed grow-type)
                    (:height selrect)
                    (:height dimension))})))))

(defn resize-wasm-text-modifiers
  ([shape]
   (resize-wasm-text-modifiers shape (:content shape)))

  ([{:keys [id points selrect] :as shape} content]
   (when-let [new-size (get-wasm-text-new-size shape content)]
     (let [width-scale  (/ (:width new-size) (:width selrect))
           height-scale (/ (:height new-size) (:height selrect))
           resize-v     (gpt/point width-scale height-scale)
           origin       (first points)]
       {id
        {:modifiers
         (ctm/resize-modifiers
          resize-v
          origin
          (:transform shape (gmt/matrix))
          (:transform-inverse shape (gmt/matrix)))}}))))

(defn resize-wasm-text
  "Resize a single text shape (auto-width/auto-height) by id.
  No-op if the id is not a text shape or is :fixed."
  [id]
  (ptk/reify ::resize-wasm-text
    ptk/WatchEvent
    (watch [_ state _]
      (let [objects (dsh/lookup-page-objects state)
            shape   (get objects id)
            resize-stream
            (if (and (some? shape)
                     (cfh/text-shape? shape)
                     (not= :fixed (:grow-type shape)))
              (rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
              (rx/empty))]
        (wrf/with-pending :text-resize [id] resize-stream)))))

(defn resize-wasm-text-debounce-commit
  ([]
   (resize-wasm-text-debounce-commit nil nil))
  ([undo-group undo-id]
   (ptk/reify ::resize-wasm-text-debounce-commit
     ptk/WatchEvent
     (watch [_ state _]
       (let [ids (get state ::resize-wasm-text-debounce-ids)
             objects (dsh/lookup-page-objects state)

             modifiers
             (reduce
              (fn [modifiers id]
                (let [shape (get objects id)]
                  (cond-> modifiers
                    (and (some? shape)
                         (cfh/text-shape? shape)
                         (not= :fixed (:grow-type shape)))
                    (merge (resize-wasm-text-modifiers shape)))))
              {}
              ids)

             ;; When undo-id is present, extend the current undo transaction instead of
             ;; creating a new one, and commit it after the resize (single undo action).
             extend-tx? (some? undo-id)
             apply-opts (cond-> {}
                          (some? undo-group) (assoc :undo-group undo-group)
                          extend-tx? (assoc :undo-transation? false))]
         (cond
           (not (empty? modifiers))
           (if extend-tx?
             (rx/concat
              (rx/of (dwm/apply-wasm-modifiers modifiers apply-opts))
              (rx/of (dwu/commit-undo-transaction undo-id)))
             (rx/of (dwm/apply-wasm-modifiers modifiers apply-opts)))

           extend-tx?
           ;; No resize needed (e.g. :fixed grow-type) but we must commit the add
           (rx/of (dwu/commit-undo-transaction undo-id))

           :else
           (rx/empty)))))))

;; This event will debounce the resize events so, if there are many, they
;; are processed at the same time and not one-by-one. This will improve
;; performance because it's better to make only one layout calculation instead
;; of (potentialy) hundreds.
(defn resize-wasm-text-debounce-inner
  ([id]
   (resize-wasm-text-debounce-inner id nil))
  ([id {:keys [undo-group undo-id]}]
   (let [cur-event   (js/Symbol)
         reflow-task (wrf/task :text-resize [id])]
     (ptk/reify ::resize-wasm-text-debounce-inner
       ptk/UpdateEvent
       (update [_ state]
         (-> state
             (update ::resize-wasm-text-debounce-ids (fnil conj []) id)
             (update ::resize-wasm-text-reflow-tasks (fnil conj []) reflow-task)
             (cond-> (nil? (::resize-wasm-text-debounce-event state))
               (assoc ::resize-wasm-text-debounce-event cur-event))))

       ptk/WatchEvent
       (watch [_ state stream]
         (wrf/start! reflow-task)
         (if (= (::resize-wasm-text-debounce-event state) cur-event)
           (let [stopper (->> stream (rx/filter (ptk/type? ::dw/finalize-workspace)))]
             (rx/concat
              (rx/merge
               (->> stream
                    (rx/filter (ptk/type? ::resize-wasm-text-debounce-inner))
                    (rx/debounce debounce-resize-text-time)
                    (rx/take 1)
                    (rx/map (fn [evt]
                              (resize-wasm-text-debounce-commit
                               (some-> evt meta :undo-group)
                               (some-> evt meta :undo-id))))
                    (rx/take-until stopper))
               (rx/of (with-meta
                        (resize-wasm-text-debounce-inner id)
                        {:undo-group undo-group :undo-id undo-id})))
              ;; Cleanup, reached both after the commit and when the stopper
              ;; cancels the debounce, so the batch always drains and stays
              ;; pending until the resize is applied. All exact tasks in the
              ;; batch are retained in state and finished by the cleanup.
              (rx/of (fn [state]
                       (wrf/finish-tasks! (::resize-wasm-text-reflow-tasks state))
                       (dissoc state
                               ::resize-wasm-text-debounce-ids
                               ::resize-wasm-text-reflow-tasks
                               ::resize-wasm-text-debounce-event)))))
           (rx/empty)))))))

(defn resize-wasm-text-debounce
  ([id]
   (resize-wasm-text-debounce id nil))
  ([id {:keys [undo-group undo-id] :as opts}]
   (ptk/reify ::resize-wasm-text-debounce
     ptk/WatchEvent
     (watch [_ state _]
       (let [page-id (:current-page-id state)
             objects (dsh/lookup-page-objects state page-id)
             content (dm/get-in objects [id :content])
             fonts   (wasm.fonts/get-content-fonts content)

             fonts-ready?
             (->> fonts
                  (every?
                   (fn [font]
                     (let [font-data (wasm.fonts/make-font-data font)]
                       (wasm.fonts/font-ready? font-data)))))

             resize-wasm-stream
             (if fonts-ready?
               (let [pass-opts (when (or (some? undo-group) (some? undo-id))
                                 (cond-> {}
                                   (some? undo-group) (assoc :undo-group undo-group)
                                   (some? undo-id) (assoc :undo-id undo-id)))]
                 (rx/of (resize-wasm-text-debounce-inner id pass-opts)))

               ;; Fonts not loaded; retry after 20 msecs
               (->> (rx/of (resize-wasm-text-debounce id opts))
                    (rx/delay 20)))]

         ;; Holds the shape pending across the font-retry loop: the retried
         ;; event opens its task before this one drains, so the task set never
         ;; becomes empty in between.
         (wrf/with-pending :text-resize [id] resize-wasm-stream))))))

(defn resize-wasm-text-all
  "Resize all text shapes (auto-width/auto-height) from a collection of ids."
  ([ids]
   (resize-wasm-text-all ids nil))
  ([ids opts]
   (ptk/reify ::resize-wasm-text-all
     ptk/WatchEvent
     (watch [_ state stream]
       (let [resize-stream
             (->> (rx/from ids)
                  (rx/map #(resize-wasm-text-debounce % opts)))

             buffer-finished-stream
             (->> (rx/merge
                   (->> stream
                        (rx/filter (ptk/type? ::dwsh/update-shapes-buffer-commit))
                        (rx/map (constantly :commit)))
                   ;; Let a buffered commit beat the stop signal.
                   (->> stream
                        (rx/filter (ptk/type? ::dwsh/update-shapes-buffer-stop))
                        (rx/observe-on :async)
                        (rx/map (constantly :stop)))
                   (->> stream
                        (rx/filter (ptk/type? ::dw/finalize-workspace))
                        (rx/map (constantly :finalize))))
                  (rx/take 1))]
         (if (::dwsh/update-shapes-buffer state)
           ;; If we're in the middle of a token propagation we wait until is finished to
           ;; recalculate the text sizes. The shapes stay pending for that whole wait,
           ;; since the per-shape debounce only marks them once dispatched.
           (wrf/with-pending
             :text-resize ids
             (->> buffer-finished-stream
                  (rx/mapcat
                   (fn [reason]
                     (if (= reason :finalize)
                       (rx/empty)
                       resize-stream)))))
           resize-stream))))))
