;; 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.modifiers
  "Events related with shapes transformations"
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.files.helpers :as cfh]
   [app.common.geom.matrix :as gmt]
   [app.common.geom.modifiers :as gm]
   [app.common.geom.point :as gpt]
   [app.common.geom.rect :as grc]
   [app.common.geom.shapes :as gsh]
   [app.common.math :as mth]
   [app.common.types.component :as ctk]
   [app.common.types.container :as ctn]
   [app.common.types.modifiers :as ctm]
   [app.common.types.path :as path]
   [app.common.types.shape-tree :as ctst]
   [app.common.types.shape.attrs :refer [editable-attrs]]
   [app.common.types.shape.layout :as ctl]
   [app.common.uuid :as uuid]
   [app.main.data.helpers :as dsh]
   [app.main.data.workspace.comments :as-alias dwcm]
   [app.main.data.workspace.guides :as-alias dwg]
   [app.main.data.workspace.shapes :as dwsh]
   [app.main.data.workspace.undo :as dwu]
   [app.main.features :as features]
   [app.main.streams :as ms]
   [app.render-wasm.api :as wasm.api]
   [app.render-wasm.gesture :as wasm-gesture]
   [app.render-wasm.shape :as wasm.shape]
   [beicon.v2.core :as rx]
   [potok.v2.core :as ptk]))

(def ^:private xf:without-uuid-zero
  (remove #(= % uuid/zero)))

;; Lets set-wasm-modifiers call clean-modifiers only on the
;; non-translation→translation transition instead of every frame.
(def ^:private wasm-structure-modifiers-active? (volatile! false))

;; Tracks whether the WASM renderer is currently in "interactive
;; transform" mode (a drag / resize / rotate gesture in progress).
;; Paired with `set-modifiers-start` / `set-modifiers-end` so the
;; native side only toggles once per gesture, regardless of how many
;; `set-wasm-modifiers` calls fire in between.
;; State lives in `app.render-wasm.gesture` so `reload-renderer!` can reset it after
;; `_clean_up` without an api ↔ modifiers circular dependency.

(defn- ensure-interactive-transform-start!
  []
  (when (wasm-gesture/try-begin-interactive-transform!)
    (wasm.api/set-modifiers-start)))

(defn- ensure-interactive-transform-end!
  []
  (when (wasm-gesture/try-end-interactive-transform!)
    (wasm.api/set-modifiers-end)))

(def ^:private transform-attrs
  #{:selrect
    :points
    :x
    :y
    :r1
    :r2
    :r3
    :r4
    :shadow
    :blur
    :background-blur
    :strokes
    :width
    :height
    :content
    :transform
    :transform-inverse
    :rotation
    :flip-x
    :flip-y
    :grow-type
    :position-data
    :layout-gap
    :layout-padding
    :layout-item-h-sizing
    :layout-item-max-h
    :layout-item-max-w
    :layout-item-min-h
    :layout-item-min-w
    :layout-item-v-sizing
    :layout-padding-type
    :layout-item-margin
    :layout-item-margin-type
    :layout-grid-cells
    :layout-grid-columns
    :layout-grid-rows})

;; -- temporary modifiers -------------------------------------------

;; During an interactive transformation of shapes (e.g. when resizing or rotating
;; a group with the mouse), there are a lot of objects that need to be modified
;; (in this case, the group and all its children).
;;
;; To avoid updating the shapes theirselves, and forcing redraw of all components
;; that depend on the "objects" global state, we set a "modifiers" structure, with
;; the changes that need to be applied, and store it in :workspace-modifiers global
;; variable. The viewport reads this and merges it into the objects list it uses to
;; paint the viewport content, redrawing only the objects that have new modifiers.
;;
;; When the interaction is finished (e.g. user releases mouse button), the
;; apply-modifiers event is done, that consolidates all modifiers into the base
;; geometric attributes of the shapes.

(defn- check-delta
  "If the shape is a component instance, check whether the transformation is a
  free change that must not mark the shape as touched.

  For the instance ROOT, position is free placement, but its rotation and flips
  are inherited content: transforming the copy as a whole overrides them, so a
  change there marks the root as touched.

  For DESCENDANTS, position, size, rotation and flips are compared RELATIVE TO
  THE ROOT: a transformation of the whole instance preserves them all and does
  not mark the descendants as touched; editing an individual shape does."
  [shape root transformed-shape transformed-root]
  (let [is-root?     (and (some? root)
                          (= (dm/get-prop shape :id) (dm/get-prop root :id)))

        center       (fn [shape]
                       (grc/rect->center (:selrect shape)))

        rel-pos      (fn [shape root]
                       (when root
                         ;; vector from the root center to the shape center,
                         ;; expressed in the root's local (untransformed) axes
                         (-> (gpt/subtract (center shape) (center root))
                             (gpt/transform (:transform-inverse root (gmt/matrix))))))

        orientation  (fn [shape root]
                       ;; the shape's rotation/flip orientation taken from its
                       ;; transform matrix, so it is reliable regardless of how the
                       ;; transform was applied (the WASM apply-transform path does
                       ;; not refresh the :rotation attribute). For the ROOT the
                       ;; absolute orientation; for a DESCENDANT the orientation
                       ;; relative to the root — invariant when the whole instance
                       ;; is rotated or flipped as a unit.
                       (let [t (:transform shape (gmt/matrix))]
                         (if is-root?
                           t
                           (gmt/multiply (:transform-inverse root (gmt/matrix)) t))))

        pos-before   (rel-pos shape root)
        pos-after    (rel-pos transformed-shape transformed-root)

        distance
        (if (and pos-before pos-after)
          (gpt/distance-vector pos-before pos-after)
          (gpt/point 0 0))

        orientation-unchanged?
        (gmt/close? (orientation shape root)
                    (orientation transformed-shape transformed-root))

        selrect (:selrect shape)
        transformed-selrect (:selrect transformed-shape)]

    ;; There are cases in that the coordinates change slightly (e.g. when rounding
    ;; to pixel, or when recalculating text positions in different zoom levels).
    ;; To take this into account, we ignore movements smaller than 1 pixel.
    ;;
    ;; When the change is a resize, also has a transformation that may have the
    ;; shape position unchanged. But in this case we do not want to ignore it.
    (and (and (< (:x distance) 1) (< (:y distance) 1))
         (mth/close? (:width selrect) (:width transformed-selrect))
         (mth/close? (:height selrect) (:height transformed-selrect))
         orientation-unchanged?)))

(defn calculate-ignore-tree
  "Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
  [modif-tree objects]

  (letfn [(get-ignore-tree
            ([ignore-tree shape]
             (let [shape-id (dm/get-prop shape :id)
                   transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers]))

                   root
                   (if (:component-root shape)
                     shape
                     (ctn/get-component-shape objects shape {:allow-main? true}))

                   transformed-root
                   (if (:component-root shape)
                     transformed-shape
                     (gsh/transform-shape root (dm/get-in modif-tree [(:id root) :modifiers])))]

               (get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))

            ([ignore-tree shape root transformed-root]
             (let [shape-id (dm/get-prop shape :id)
                   transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers]))]
               (get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))

            ([ignore-tree shape transformed-shape root transformed-root]
             (let [shape-id (dm/get-prop shape :id)

                   ignore-tree
                   (cond-> ignore-tree
                     (and (some? root) (ctk/in-component-copy? shape))
                     (assoc
                      shape-id
                      (check-delta shape root transformed-shape transformed-root)))

                   set-child
                   (fn [ignore-tree child]
                     (get-ignore-tree ignore-tree child root transformed-root))]

               (->> (:shapes shape)
                    (map (d/getf objects))
                    (reduce set-child ignore-tree)))))]

    ;; we check twice because we want only to search parents of components but once the
    ;; tree is traversed we only want to process the objects in components
    (->> (keys modif-tree)
         (map #(get objects %))
         (reduce get-ignore-tree nil))))

(defn calculate-ignore-tree-wasm
  "Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
  [transforms objects]

  (letfn [(get-ignore-tree
            ([ignore-tree shape]
             (let [shape-id (dm/get-prop shape :id)
                   transformed-shape (gsh/apply-transform shape (get transforms shape-id))

                   root
                   (if (:component-root shape)
                     shape
                     (ctn/get-component-shape objects shape {:allow-main? true}))

                   transformed-root
                   (if (:component-root shape)
                     transformed-shape
                     (gsh/apply-transform root (get transforms (:id root))))]

               (get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))

            ([ignore-tree shape root transformed-root]
             (let [shape-id (dm/get-prop shape :id)
                   transformed-shape (gsh/apply-transform shape (get transforms shape-id))]
               (get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))

            ([ignore-tree shape transformed-shape root transformed-root]
             (let [shape-id (dm/get-prop shape :id)

                   ignore-tree
                   (cond-> ignore-tree
                     (and (some? root) (ctk/in-component-copy? shape))
                     (assoc
                      shape-id
                      (check-delta shape root transformed-shape transformed-root)))

                   set-child
                   (fn [ignore-tree child]
                     (get-ignore-tree ignore-tree child root transformed-root))]

               (->> (:shapes shape)
                    (map (d/getf objects))
                    (reduce set-child ignore-tree)))))]

    ;; we check twice because we want only to search parents of components but once the
    ;; tree is traversed we only want to process the objects in components
    (->> (keys transforms)
         (map #(get objects %))
         (reduce get-ignore-tree nil))))

(defn assoc-position-data
  [shape position-data old-shape]
  (let [deltav (gpt/to-vec (gpt/point (ctm/safe-size-rect old-shape))
                           (gpt/point (ctm/safe-size-rect shape)))
        position-data
        (-> position-data
            (gsh/move-position-data deltav))]
    (cond-> shape
      (d/not-empty? position-data)
      (assoc :position-data position-data))))

(defn- set-wasm-props!
  [objects prev-wasm-props wasm-props]
  (let [;; Set old value for previous properties
        clean-props
        (->> prev-wasm-props
             (map (fn [[id {:keys [property] :as change}]]
                    (let [shape (get objects id)]
                      [id (assoc change :value (get shape property))]))))

        wasm-props
        (concat clean-props wasm-props)

        ;; Stores a map shape -> set of properties changed
        ;; this is the standard format used by process-shape-changes
        shape-changes
        (-> (group-by first wasm-props)
            (update-vals #(into #{} (map (comp :property second)) %)))

        ;; Create a new objects only with the temporary modifications
        objects-changed
        (->> wasm-props
             (group-by first)
             (reduce
              (fn [objects [id properties]]
                (let [shape
                      (->> properties
                           (reduce
                            (fn [shape [_ operation]]
                              (ctm/apply-modifier shape operation))
                            (get objects id)))]
                  (assoc objects id shape)))
              objects))]
    (wasm.shape/process-shape-changes! objects-changed shape-changes)))

(defn clear-local-transform []
  (ptk/reify ::clear-local-transform
    ptk/EffectEvent
    (effect [_ state _]
      (when (features/active-feature? state "render-wasm/v1")
        ;; End interactive transform mode BEFORE cleaning modifiers so
        ;; the final full-quality render triggered by subsequent shape
        ;; updates is not still classified as "interactive" (which would
        ;; skip shadows / blur).
        (ensure-interactive-transform-end!)
        (wasm.api/clean-modifiers)
        (vreset! wasm-structure-modifiers-active? false)
        (set-wasm-props! (dsh/lookup-page-objects state) (:wasm-props state) [])))

    ptk/UpdateEvent
    (update [_ state]
      (-> state
          (dissoc :workspace-modifiers :wasm-props :prev-wasm-props)
          (dissoc :app.main.data.workspace.transforms/current-move-selected)))))

(defn create-modif-tree
  [ids modifiers]
  (dm/assert!
   "expected valid coll of uuids"
   (every? uuid? ids))
  (into {} (map #(vector % {:modifiers modifiers})) ids))

(defn build-modif-tree
  [ids objects get-modifier]
  (dm/assert!
   "expected valid coll of uuids"
   (every? uuid? ids))
  (into {} (map #(vector % {:modifiers (get-modifier (get objects %))})) ids))

(defn modifier-remove-from-parent
  [modif-tree objects shapes]
  (->> shapes
       (reduce
        (fn [modif-tree child-id]
          (let [parent-id (get-in objects [child-id :parent-id])]
            (update-in modif-tree [parent-id :modifiers] ctm/remove-children [child-id])))
        modif-tree)))

(defn add-grid-children-modifiers
  [modifiers frame-id shapes objects [row column :as cell]]
  (let [frame (get objects frame-id)
        ids (set shapes)

        ;; Temporary remove the children when moving them
        frame (-> frame
                  (update :shapes #(d/removev ids %))
                  (ctl/assign-cells objects))

        ids (->> ids
                 (remove #(ctl/position-absolute? objects %))
                 (ctst/sort-z-index objects)
                 reverse)

        frame (-> frame
                  (update :shapes d/concat-vec ids)
                  (cond-> (some? cell)
                    (ctl/push-into-cell ids row column))
                  (ctl/assign-cells objects))]
    (-> modifiers
        (ctm/change-property :layout-grid-rows (:layout-grid-rows frame))
        (ctm/change-property :layout-grid-columns (:layout-grid-columns frame))
        (ctm/change-property :layout-grid-cells (:layout-grid-cells frame)))))

(defn build-change-frame-modifiers
  [modif-tree objects selected target-frame-id drop-index cell-data]

  (let [origin-frame-ids (->> selected (group-by #(get-in objects [% :frame-id])))
        child-set (set (get-in objects [target-frame-id :shapes]))

        target-frame        (get objects target-frame-id)
        target-flex-layout? (ctl/flex-layout? target-frame)
        target-grid-layout? (ctl/grid-layout? target-frame)

        children-ids (concat (:shapes target-frame) selected)

        set-parent-ids
        (fn [modif-tree shapes target-frame-id]
          (reduce
           (fn [modif-tree id]
             (update-in
              modif-tree
              [id :modifiers]
              #(-> %
                   (ctm/change-property :frame-id target-frame-id)
                   (ctm/change-property :parent-id target-frame-id))))
           modif-tree
           shapes))

        update-frame-modifiers
        (fn [modif-tree [original-frame shapes]]
          (let [shapes (->> shapes (d/removev #(= target-frame-id %)))
                shapes (cond->> shapes
                         (and (or target-grid-layout? target-flex-layout?)
                              (= original-frame target-frame-id))
                         ;; When movining inside a layout frame remove the shapes that are not immediate children
                         (filterv #(contains? child-set %)))
                children-ids (->> (dm/get-in objects [original-frame :shapes])
                                  (remove (set selected)))
                h-sizing? (and (ctl/flex-layout? objects original-frame)
                               (ctl/change-h-sizing? original-frame objects children-ids))
                v-sizing? (and (ctl/flex-layout? objects original-frame)
                               (ctl/change-v-sizing? original-frame objects children-ids))]
            (cond-> modif-tree
              (not= original-frame target-frame-id)
              (-> (modifier-remove-from-parent objects shapes)
                  (update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index)
                  (set-parent-ids shapes target-frame-id)
                  (cond-> h-sizing?
                    (update-in [original-frame :modifiers] ctm/change-property :layout-item-h-sizing :fix))
                  (cond-> v-sizing?
                    (update-in [original-frame :modifiers] ctm/change-property :layout-item-v-sizing :fix)))

              (and target-flex-layout? (= original-frame target-frame-id))
              (update-in [target-frame-id :modifiers] ctm/add-children shapes drop-index)

              ;; Add the object to the cell
              target-grid-layout?
              (update-in [target-frame-id :modifiers] add-grid-children-modifiers target-frame-id shapes objects cell-data))))]

    (as-> modif-tree $
      (reduce update-frame-modifiers $ origin-frame-ids)
      (cond-> $
        ;; Set fix position to target frame (horizontal)
        (and (ctl/flex-layout? objects target-frame-id)
             (ctl/change-h-sizing? target-frame-id objects children-ids))
        (update-in [target-frame-id :modifiers] ctm/change-property :layout-item-h-sizing :fix)

        ;; Set fix position to target frame (vertical)
        (and (ctl/flex-layout? objects target-frame-id)
             (ctl/change-v-sizing? target-frame-id objects children-ids))
        (update-in [target-frame-id :modifiers] ctm/change-property :layout-item-v-sizing :fix)))))

(defn modif->js
  [modif-tree objects]
  (clj->js (into {}
                 (map (fn [[k v]]
                        [(get-in objects [k :name]) v]))
                 modif-tree)))

(defn apply-text-modifier
  [shape {:keys [width height]}]
  (cond-> shape
    (some? width)
    (gsh/transform-shape (ctm/change-dimensions-modifiers shape :width width {:ignore-lock? true}))

    (some? height)
    (gsh/transform-shape (ctm/change-dimensions-modifiers shape :height height {:ignore-lock? true}))))

(defn apply-text-modifiers
  [objects text-modifiers]
  (loop [modifiers (seq text-modifiers)
         result objects]
    (if (empty? modifiers)
      result
      (let [[id text-modifier] (first modifiers)]
        (recur (rest modifiers)
               (d/update-when result id apply-text-modifier text-modifier))))))

#_(defn apply-path-modifiers
    [objects path-modifiers]
    (letfn [(apply-path-modifier
              [shape {:keys [content-modifiers]}]
              (let [shape (update shape :content upc/apply-content-modifiers content-modifiers)
                    [points selrect] (helpers/content->points+selrect shape (:content shape))]
                (assoc shape :selrect selrect :points points)))]
      (loop [modifiers (seq path-modifiers)
             result objects]
        (if (empty? modifiers)
          result
          (let [[id path-modifier] (first modifiers)]
            (recur (rest modifiers)
                   (update objects id apply-path-modifier path-modifier)))))))

(defn- calculate-modifiers
  ([state modif-tree]
   (calculate-modifiers state false false modif-tree nil))

  ([state ignore-constraints ignore-snap-pixel modif-tree page-id]
   (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree page-id nil))

  ([state ignore-constraints ignore-snap-pixel modif-tree page-id params]
   (let [page-id
         (or page-id (:current-page-id state))

         objects
         (dsh/lookup-page-objects state page-id)

         snap-pixel?
         (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))]

     (as-> objects $
       (apply-text-modifiers $ (get state :workspace-text-modifier))
       ;;(apply-path-modifiers $ (get-in state [:workspace-local :edit-path]))
       (gm/set-objects-modifiers modif-tree $ (merge
                                               params
                                               {:ignore-constraints ignore-constraints
                                                :snap-pixel? snap-pixel?}))))))

(defn- calculate-update-modifiers
  [old-modif-tree state ignore-constraints ignore-snap-pixel modif-tree]
  (let [objects
        (dsh/lookup-page-objects state)

        snap-pixel?
        (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))

        objects
        (-> objects
            (apply-text-modifiers (get state :workspace-text-modifier)))]

    (gm/set-objects-modifiers
     old-modif-tree
     modif-tree
     objects
     {:ignore-constraints ignore-constraints
      :snap-pixel? snap-pixel?})))

(defn update-modifiers
  ([modif-tree]
   (update-modifiers modif-tree false))

  ([modif-tree ignore-constraints]
   (update-modifiers modif-tree ignore-constraints false))

  ([modif-tree ignore-constraints ignore-snap-pixel]
   (ptk/reify ::update-modifiers
     ptk/UpdateEvent
     (update [_ state]
       (update state :workspace-modifiers calculate-update-modifiers state ignore-constraints ignore-snap-pixel modif-tree)))))


(defn set-modifiers
  ([modif-tree]
   (set-modifiers modif-tree false))

  ([modif-tree ignore-constraints]
   (set-modifiers modif-tree ignore-constraints false))

  ([modif-tree ignore-constraints ignore-snap-pixel]
   (set-modifiers modif-tree ignore-constraints ignore-snap-pixel nil))

  ([modif-tree ignore-constraints ignore-snap-pixel params]
   (ptk/reify ::set-modifiers
     ptk/UpdateEvent
     (update [_ state]
       (let [page-id   (:current-page-id state)
             modifiers (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree page-id params)]
         (assoc state :workspace-modifiers modifiers))))))

(defn- parse-structure-modifiers
  [modif-tree]
  (into
   []
   (mapcat
    (fn [[parent-id data]]
      (when (ctm/has-structure? (:modifiers data))
        (->> (concat
              (get-in data [:modifiers :structure-parent])
              (get-in data [:modifiers :structure-child]))
             (mapcat
              (fn [modifier]
                (case (:type modifier)
                  :remove-children
                  (->> (:value modifier)
                       (map (fn [child-id]
                              {:type :remove-children
                               :parent parent-id
                               :id child-id
                               :index 0
                               :value 0})))

                  :add-children
                  (->> (:value modifier)
                       (map (fn [child-id]
                              {:type :add-children
                               :parent parent-id
                               :id child-id
                               :index (:index modifier)
                               :value 0})))

                  :scale-content
                  [{:type :scale-content
                    :parent parent-id
                    :id parent-id
                    :index 0
                    :value (:value modifier)}]
                  nil)))))))
   modif-tree))


(def ^:private xf:parse-geometry-modifier
  (let [default-transform (gmt/matrix)]
    (keep (fn [[id data]]
            (cond
              (= id uuid/zero)
              nil

              (ctm/has-geometry? (:modifiers data))
              (let [parent (:geometry-parent (:modifiers data))
                    kind (if (d/not-empty? parent) :parent :child)]
                (d/vec2 id {:transform (ctm/modifiers->transform (:modifiers data)) :kind kind}))

              ;; Unit matrix is used for reflowing
              :else
              (d/vec2 id {:transform default-transform :kind :parent}))))))

(defn- parse-geometry-modifiers
  [modif-tree]
  (into [] xf:parse-geometry-modifier modif-tree))

(defn- extract-property-changes
  [modif-tree]
  (->> modif-tree
       (mapcat (fn [[id {:keys [modifiers]}]]
                 (->> (:structure-parent modifiers)
                      (map #(vector id %)))))
       (filter (fn [[_ {:keys [type]}]]
                 (= type :change-property)))))

(defn set-temporary-selrect
  [selrect]
  (ptk/reify ::set-temporary-selrect
    ptk/EffectEvent
    (effect [_ _ _]
      (rx/push! ms/workspace-selrect selrect))))

(defn set-temporary-modifiers
  [modifiers]
  (ptk/reify ::set-temporary-modifiers
    ptk/EffectEvent
    (effect [_ _ _]
      (rx/push! ms/wasm-modifiers (into {} modifiers)))))

(def ^:private xf:map-key (map key))

(defn- translate-selrect
  "Shift `selrect`'s center by (tx, ty). Width/height/transform are
   invariant under pure translation, so only `:center` moves."
  [selrect tx ty]
  (update selrect :center
          (fn [c] (gpt/point (+ (:x c) tx) (+ (:y c) ty)))))

(defn- cached-translation-selrect
  "Translation-only fast path for the live selection rect. Avoids a WASM
   `get-selection-rect` call per drag frame by caching the gesture-start
   base: on the first emission, ask WASM and back out the current delta
   to recover the base; on every later emission, shift the cached base
   by the new (tx, ty)."
  [ids ^js first-matrix cache]
  (let [tx (.-e first-matrix)
        ty (.-f first-matrix)]
    (if-let [base @cache]
      (translate-selrect base tx ty)
      (when-let [computed (wasm.api/get-selection-rect ids)]
        (vreset! cache (translate-selrect computed (- tx) (- ty)))
        computed))))

#_:clj-kondo/ignore
(defn set-wasm-modifiers
  [modif-tree & {:keys [ignore-constraints ignore-snap-pixel
                        subtree-ids-by-id selection-rect-cache]
                 :or {ignore-constraints false ignore-snap-pixel false}
                 :as params}]
  (ptk/reify ::set-wasm-modifiers
    ptk/UpdateEvent
    (update [_ state]
      (let [property-changes (extract-property-changes modif-tree)]
        (if (d/not-empty? property-changes)
          (-> state
              (assoc :prev-wasm-props (:wasm-props state))
              (assoc :wasm-props property-changes))
          state)))

    ptk/WatchEvent
    (watch [_ state _]
      ;; Entering an interactive transform (drag/resize/rotate). Flip
      ;; the renderer into fast + atlas-backdrop mode so the live
      ;; preview is cheap, tiles never appear sequentially and the main
      ;; thread is not blocked. The pair is closed in
      ;; `clear-local-transform`.
      (ensure-interactive-transform-start!)
      (let [snap-pixel?  (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
            translation? (every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]

        (if translation?
          ;; Pure translation: no structure changes needed. If structure
          ;; modifiers were active from a previous non-translation frame
          ;; (e.g. shape hovered over a frame then dragged back out),
          ;; clear them now so the shape is not clipped by the old frame.
          (when @wasm-structure-modifiers-active?
            (wasm.api/clean-modifiers)
            (vreset! wasm-structure-modifiers-active? false))
          (let [objects (dsh/lookup-page-objects state)]
            (set-wasm-props! objects (:prev-wasm-props state) (:wasm-props state))
            (wasm.api/clean-modifiers)
            (wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree))
            (vreset! wasm-structure-modifiers-active? true)))
        (let [geometry-entries (parse-geometry-modifiers modif-tree)
              root-modifiers   (into [] (map (fn [[id data]] [id (:transform data)])) geometry-entries)
              wasm-ready?      (wasm.api/initialized?)
              ;; While the GL context is down (lost / mid-reload), keep the
              ;; root transforms so SVG selection/preview can still move.
              ;; `propagate-modifiers` returns [] when not ready, do not
              ;; treat that as "no modifiers".
              modifiers
              (cond
                (or (not wasm-ready?)
                    (and translation? (not snap-pixel?)))
                root-modifiers

                :else
                (let [propagated (wasm.api/propagate-modifiers geometry-entries snap-pixel?)]
                  (if (seq propagated) propagated root-modifiers)))]
          (when wasm-ready?
            (wasm.api/set-modifiers modifiers))
          (let [ids     (into [] xf:map-key geometry-entries)
                selrect (when wasm-ready?
                          (if (and translation? (not snap-pixel?) selection-rect-cache (seq modifiers))
                            (cached-translation-selrect ids (second (first modifiers)) selection-rect-cache)
                            (wasm.api/get-selection-rect ids)))]
            (rx/of (set-temporary-selrect selrect)
                   (set-temporary-modifiers modifiers))))))))

(defn propagate-structure-modifiers
  [modif-tree objects]
  (letfn [(propagate-children
            [modif-tree parent-id modifiers]
            (let [new-modifiers (ctm/select-child-structre-modifiers modifiers)]
              (->> (get-in objects [parent-id :shapes])
                   (reduce
                    #(update-in %1 [%2 :modifiers] ctm/add-modifiers new-modifiers)
                    modif-tree))))]
    (loop [pending    (into [] (keys modif-tree))
           modif-tree modif-tree]
      (if-let [next (first pending)]
        (let [pending (rest pending)
              modifiers (get-in modif-tree [next :modifiers])

              [pending modif-tree]
              (if (ctm/has-structure-child? modifiers)
                [(into pending (get-in objects [next :shapes]))
                 (propagate-children modif-tree next modifiers)]
                [pending modif-tree])]
          (recur pending modif-tree))
        modif-tree))))

#_:clj-kondo/ignore
(defn apply-wasm-modifiers
  [modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis undo-transation?
                        subtree-ids-by-id]
                 :or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-transation? true}
                 :as params}]
  (ptk/reify ::apply-wasm-modifiers
    ptk/WatchEvent
    (watch [_ state _]
      (let [translation?
            (every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]
        (wasm.api/clean-modifiers)
        (when-not translation?
          (wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree)))

        ;; Apply property changes (e.g. grow-type) to WASM shapes before
        ;; propagating geometry, so propagate_modifiers sees the updated state.
        (doseq [[id {:keys [property value]}] (extract-property-changes modif-tree)]
          (when (= property :grow-type)
            (wasm.api/use-shape id)
            (wasm.api/set-shape-grow-type value)))

        (let [objects (dsh/lookup-page-objects state)

              geometry-entries
              (parse-geometry-modifiers modif-tree)

              snap-pixel?
              (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))

              transforms
              (cond
                (and translation? (not snap-pixel?))
                ;; Mirror WASM `propagate_modifiers` in CLJS: splat the
                ;; translation matrix onto every descendant. Without
                ;; this step the commit would only touch the dragged
                ;; primaries and descendants would snap back to their
                ;; pre-drag positions on drop.
                ;;
                ;; Skipped when `snap-pixel?` is on: WASM applies
                ;; per-shape pixel correction (different scale/translate
                ;; per descendant) which we can't replicate cheaply on
                ;; the CLJS side.
                (reduce
                 (fn [acc [id data]]
                   (let [t (:transform data)
                         subtree-ids
                         (or (get subtree-ids-by-id id)
                             (cfh/get-children-ids-with-self objects id))]
                     (reduce (fn [a sid] (assoc a sid t)) acc subtree-ids)))
                 {}
                 geometry-entries)

                ;; Context lost / mid-reload: do not call into WASM. Use
                ;; root transforms (and splat translation onto descendants
                ;; when we can) so the commit still lands in file data.
                (not (wasm.api/initialized?))
                (if translation?
                  (reduce
                   (fn [acc [id data]]
                     (let [t (:transform data)
                           subtree-ids
                           (or (get subtree-ids-by-id id)
                               (cfh/get-children-ids-with-self objects id))]
                       (reduce (fn [a sid] (assoc a sid t)) acc subtree-ids)))
                   {}
                   geometry-entries)
                  (into {}
                        (map (fn [[id data]] [id (:transform data)]))
                        geometry-entries))

                :else
                (into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)))

              ignore-tree
              (calculate-ignore-tree-wasm transforms objects)

              options
              (-> params
                  (assoc :reg-objects? true)
                  (assoc :ignore-tree ignore-tree)
                  (assoc :translation? translation?)
                  ;; Attributes that can change in the transform. This
                  ;; way we don't have to check all the attributes
                  (assoc :attrs transform-attrs))

              modif-tree
              (propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))

              ids
              (into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))

              update-shape
              (fn [shape]
                (let [shape-id  (dm/get-prop shape :id)
                      transform (get transforms shape-id)
                      modifiers (dm/get-in modif-tree [shape-id :modifiers])]
                  (-> shape
                      (gsh/apply-transform transform)
                      (ctm/apply-structure-modifiers modifiers))))

              bool-ids
              (into #{}
                    (comp
                     (mapcat (partial cfh/get-parents-with-self objects))
                     (filter cfh/bool-shape?)
                     (map :id))
                    ids)

              undo-id (js/Symbol)]
          (rx/concat
           (if undo-transation?
             (rx/of (dwu/start-undo-transaction undo-id))
             (rx/empty))
           (rx/of
            (clear-local-transform)
            (ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms})
            (ptk/event ::dwcm/move-frame-comment-threads transforms)
            (dwsh/update-shapes ids update-shape options)

            ;; The update to the bool path needs to be in a different operation because it
            ;; needs to have the updated children info.
            ;; `update-layout? false`: recalculating a bool path can never change
            ;; `:hidden`, and the layout check would recompute the whole boolean
            ;; path in WASM once per bool shape just to find that out.
            (dwsh/update-shapes bool-ids path/update-bool-shape (assoc options
                                                                       :with-objects? true
                                                                       :update-layout? false)))

           (if undo-transation?
             (rx/of (dwu/commit-undo-transaction undo-id))
             (rx/empty))))))))

(def ^:private
  xf-rotation-shape
  (comp
   (remove #(get % :blocked false))
   (filter #(:rotation (get editable-attrs (:type %))))
   (map :id)))

;; Rotation use different algorithm to calculate children
;; modifiers (and do not use child constraints).
(defn set-wasm-rotation-modifiers
  ([angle shapes]
   (set-wasm-rotation-modifiers angle shapes (-> shapes gsh/shapes->rect grc/rect->center)))

  ([angle shapes center]
   (ptk/reify ::set-wasm-rotation-modifiers
     ptk/EffectEvent
     (effect [_ state _]
       (ensure-interactive-transform-start!)
       (let [objects (dsh/lookup-page-objects state)
             ids     (sequence xf-rotation-shape shapes)

             get-modifier
             (fn [shape]
               (ctm/rotation-modifiers shape center angle))

             modif-tree
             (build-modif-tree ids objects get-modifier)

             modifiers
             (mapv (fn [[id {:keys [modifiers]}]]
                     (d/vec2 id (ctm/modifiers->transform modifiers)))
                   modif-tree)]

         (wasm.api/set-modifiers modifiers))))))

(defn set-rotation-modifiers
  ([angle shapes]
   (set-rotation-modifiers angle shapes (-> shapes gsh/shapes->rect grc/rect->center)))

  ([angle shapes center]
   (ptk/reify ::set-rotation-modifiers
     ptk/UpdateEvent
     (update [_ state]
       (let [objects (dsh/lookup-page-objects state)
             ids     (sequence xf-rotation-shape shapes)

             get-modifier
             (fn [shape]
               (ctm/rotation-modifiers shape center angle))

             modif-tree
             (-> (build-modif-tree ids objects get-modifier)
                 (gm/set-objects-modifiers objects))]

         (assoc state :workspace-modifiers modif-tree))))))

;; This function is similar to set-rotation-modifiers but:
;; - It consideres the center for everyshape instead of the center of the total selrect
;; - The angle param is the desired final value, not a delta
(defn set-delta-rotation-modifiers
  [angle shapes {:keys [center delta? page-id] :or {center nil delta? false}}]
  (ptk/reify ::set-delta-rotation-modifiers
    ptk/UpdateEvent
    (update [_ state]
      (let [page-id (or page-id (:current-page-id state))
            objects (dsh/lookup-page-objects state page-id)
            ids
            (->> shapes
                 (remove #(get % :blocked false))
                 (filter #(contains? (get editable-attrs (:type %)) :rotation))
                 (map :id))

            get-modifier
            (fn [shape]
              (let [delta  (if delta? angle (- angle (:rotation shape)))
                    center (or center (gsh/shape->center shape))]
                (ctm/rotation-modifiers shape center delta)))

            modif-tree
            (-> (build-modif-tree ids objects get-modifier)
                (gm/set-objects-modifiers objects))]

        (assoc state :workspace-modifiers modif-tree)))))

(defn apply-modifiers*
  "A lower-level version of apply-modifiers, that expects receive ready
  to use objects, object-modifiers and text-modifiers."
  [objects object-modifiers text-modifiers options]
  (ptk/reify ::apply-modifiers*
    ptk/WatchEvent
    (watch [_ _ _]
      (let [ids
            (into [] xf:without-uuid-zero (keys object-modifiers))

            ids-with-children
            (into ids
                  (mapcat (partial cfh/get-children-ids objects))
                  ids)

            ignore-tree
            (calculate-ignore-tree object-modifiers objects)

            options
            (-> options
                (assoc :reg-objects? true)
                (assoc :ignore-tree ignore-tree)
                ;; Attributes that can change in the transform. This
                ;; way we don't have to check all the attributes
                (assoc :attrs transform-attrs))

            update-shape
            (fn [shape]
              (let [shape-id    (dm/get-prop shape :id)
                    modifiers   (dm/get-in object-modifiers [shape-id :modifiers])
                    text-shape? (cfh/text-shape? shape)
                    pos-data    (when ^boolean text-shape?
                                  (dm/get-in text-modifiers [shape-id :position-data]))]

                (-> shape
                    (gsh/transform-shape modifiers)
                    (cond-> (d/not-empty? pos-data)
                      (assoc-position-data pos-data shape)))))]

        (rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
               (ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
               (dwsh/update-shapes ids update-shape options))))))

(defn apply-modifiers
  ([]
   (apply-modifiers nil))
  ([{:keys [modifiers undo-transation? ignore-constraints
            ignore-snap-pixel page-id]
     :or {undo-transation? true  ignore-constraints false
          ignore-snap-pixel false}
     :as options}]
   (ptk/reify ::apply-modifiers
     ptk/WatchEvent
     (watch [_ state _]
       (let [text-modifiers (get state :workspace-text-modifier)
             page-id        (or page-id (:current-page-id state))
             objects        (dsh/lookup-page-objects state page-id)

             object-modifiers
             (if (some? modifiers)
               (calculate-modifiers state ignore-constraints ignore-snap-pixel modifiers page-id)
               (get state :workspace-modifiers))

             undo-id
             (js/Symbol)]

         (rx/concat
          (if undo-transation?
            (rx/of (dwu/start-undo-transaction undo-id))
            (rx/empty))
          (rx/of (apply-modifiers* objects object-modifiers text-modifiers options)
                 (fn [state]
                   (let [ids (into [] xf:without-uuid-zero (keys object-modifiers))]
                     (update state :workspace-text-modifier #(apply dissoc % ids)))))
          (if (nil? modifiers)
            (rx/of (clear-local-transform))
            (rx/empty))
          (if undo-transation?
            (rx/of (dwu/commit-undo-transaction undo-id))
            (rx/empty))))))))

;; Pure function to determine next grow-type for text layers
(defn next-grow-type
  [current-grow-type scalev]
  (cond
    (= current-grow-type :fixed)
    :fixed

    (and (not (mth/close? (:y scalev) 1.0))
         (or (= current-grow-type :auto-width)
             (= current-grow-type :auto-height)))
    :fixed

    (and (not (mth/close? (:x scalev) 1.0))
         (= current-grow-type :auto-width))
    :auto-height

    :else
    current-grow-type))
