;; 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.workspace.viewport-wasm
  (:require-macros [app.main.style :as stl])
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.files.helpers :as cfh]
   [app.common.geom.shapes :as gsh]
   [app.common.types.color :as clr]
   [app.common.types.component :as ctk]
   [app.common.types.path :as path]
   [app.common.types.shape :as cts]
   [app.common.types.shape.layout :as ctl]
   [app.main.data.modal :as modal]
   [app.main.data.workspace :as dw]
   [app.main.data.workspace.transforms :as dwt]
   [app.main.data.workspace.variants :as dwv]
   [app.main.features :as features]
   [app.main.refs :as refs]
   [app.main.store :as st]
   [app.main.ui.context :as ctx]
   [app.main.ui.flex-controls :as mfc]
   [app.main.ui.hooks :as ui-hooks]
   [app.main.ui.measurements :as msr]
   [app.main.ui.workspace.shapes.path.editor :refer [path-editor*]]
   [app.main.ui.workspace.shapes.text.editor :as editor-v1]
   [app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline*]]
   [app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
   [app.main.ui.workspace.shapes.text.v3-editor :as editor-v3]
   [app.main.ui.workspace.top-toolbar :refer [top-toolbar*]]
   [app.main.ui.workspace.viewport.actions :as actions]
   [app.main.ui.workspace.viewport.comments :as comments]
   [app.main.ui.workspace.viewport.debug :as wvd]
   [app.main.ui.workspace.viewport.drawarea :as drawarea]
   [app.main.ui.workspace.viewport.frame-grid :as frame-grid]
   [app.main.ui.workspace.viewport.gradients :as gradients]
   [app.main.ui.workspace.viewport.grid-layout-editor :as grid-layout]
   [app.main.ui.workspace.viewport.guides :as guides]
   [app.main.ui.workspace.viewport.hooks :as hooks]
   [app.main.ui.workspace.viewport.interactions :as interactions]
   [app.main.ui.workspace.viewport.outline :as outline]
   [app.main.ui.workspace.viewport.pixel-overlay :as pixel-overlay]
   [app.main.ui.workspace.viewport.presence :as presence]
   [app.main.ui.workspace.viewport.rulers :as rulers]
   [app.main.ui.workspace.viewport.scroll-bars :as scroll-bars]
   [app.main.ui.workspace.viewport.selection :as selection]
   [app.main.ui.workspace.viewport.snap-distances :as snap-distances]
   [app.main.ui.workspace.viewport.snap-points :as snap-points]
   [app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar*
                                                   path-edition-bar*
                                                   view-only-bar*]]
   [app.main.ui.workspace.viewport.utils :as utils]
   [app.main.ui.workspace.viewport.viewport-ref :as vp-ref :refer [create-viewport-ref]]
   [app.main.ui.workspace.viewport.widgets :as widgets]
   [app.render-wasm.api :as wasm.api]
   [app.render-wasm.rulers-state :as rs]
   [app.util.debug :as dbg]
   [app.util.text-editor :as ted]
   [app.util.theme :as theme]
   [app.util.timers :as ts]
   [app.util.webapi :as webapi]
   [beicon.v2.core :as rx]
   [promesa.core :as p]
   [rumext.v2 :as mf]))

;; --- Viewport

(defn- apply-modifiers-to-objects
  [objects modifiers]
  (->> modifiers
       (reduce
        (fn [objs [id t]]
          (if (contains? objs id)
            (update objs id gsh/apply-transform t)
            objs))
        objects)))

(defn apply-modifiers-to-selected
  [selected objects modifiers]
  (apply-modifiers-to-objects objects (select-keys modifiers selected)))

(defn- apply-wasm-modifiers-to-ids
  "Like `apply-modifiers-to-objects`, but only updates ids in `id-set`. During WASM
  drag, `wasm-modifiers` can list every propagated descendant (large variants); SVG
  outlines only need geometry for `selected` / hover / highlight — not the whole page."
  [objects wasm-modifiers id-set]
  (if (or (empty? wasm-modifiers) (empty? id-set))
    objects
    (reduce
     (fn [objs id]
       (if-let [t (get wasm-modifiers id)]
         (if (contains? objs id)
           (update objs id gsh/apply-transform t)
           objs)
         objs))
     objects
     id-set)))


(defn- outline-wasm-source-ids
  "Superset of shape ids that `shape-outlines` may look up (all outline usages here)."
  [base-objects selected highlighted edition hover-ids hover frame-hover]
  (let [outlined-frame-id (->> hover-ids
                               (filter #(cfh/frame-shape? (get base-objects %)))
                               (remove selected)
                               (last))
        ids (-> #{}
                (into (or selected #{}))
                (into (or highlighted #{}))
                (into (or hover-ids #{})))]
    (cond-> ids
      (uuid? (:id hover)) (conj (:id hover))
      (uuid? frame-hover) (conj frame-hover)
      (uuid? outlined-frame-id) (conj outlined-frame-id)
      (uuid? edition) (disj edition))))

(mf/defc transition-overlay*
  "Frozen-frame overlay shown while switching pages or recovering from WebGL
   context loss. `image` is either an `ImageBitmap` snapshot of the canvas or
   a data-url string placeholder (initial load)."
  [{:keys [image clip-path]}]
  (let [canvas-ref (mf/use-ref nil)
        bitmap?    (not (string? image))]
    (mf/with-effect [image]
      (when bitmap?
        (when-let [canvas (mf/ref-val canvas-ref)]
          ;; A closed ImageBitmap reports zero size; skip instead of throwing.
          (when (pos? (.-width ^js image))
            (set! (.-width ^js canvas) (.-width ^js image))
            (set! (.-height ^js canvas) (.-height ^js image))
            (let [ctx (.getContext ^js canvas "2d")]
              (.drawImage ^js ctx image 0 0))))))
    (if bitmap?
      ;; Full-bleed so the snapshot overlays the canvas 1:1.
      [:canvas {:data-testid "canvas-wasm-transition"
                :ref canvas-ref
                :style {:position "absolute"
                        :inset 0
                        :width "100%"
                        :height "100%"
                        :object-fit "cover"
                        :pointer-events "none"
                        :clip-path clip-path}}]
      [:img {:data-testid "canvas-wasm-transition"
             :src image
             :draggable false
             :style {:position "absolute"
                     :inset 0
                     :width "100%"
                     :height "100%"
                     :object-fit "cover"
                     :pointer-events "none"
                     :clip-path clip-path}}])))

(mf/defc viewport*
  [{:keys [selected wglobal layout file page palete-size]}]
  (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
        ;; that the new parameter is sent

        {:keys [edit-path
                panning
                selrect
                transform
                highlighted
                vbox
                vport
                zoom
                edition]}
        (mf/deref refs/workspace-local)

        {:keys [options-mode
                tooltip
                show-distances?
                preview-id
                picking-color?]}
        wglobal

        permissions       (mf/use-ctx ctx/permissions)
        read-only?        (mf/use-ctx ctx/workspace-read-only?)

        drawing           (mf/deref refs/workspace-drawing)
        focus             (mf/deref refs/workspace-focus-selected)
        wasm-modifiers    (mf/deref refs/workspace-wasm-modifiers)
        workspace-editor-state (mf/deref refs/workspace-editor-state)

        file-id           (get file :id)
        vern              (get file :vern)
        objects           (get page :objects)
        page-id           (get page :id)
        background        (get page :background clr/canvas)
        guides            (get page :guides)

        base-objects      (ui-hooks/with-focus-objects objects focus)

        objects-modified
        (mf/with-memo
          [base-objects wasm-modifiers]
          (apply-modifiers-to-selected selected base-objects wasm-modifiers))

        selected-shapes   (->> selected
                               (into [] (keep (d/getf objects-modified)))
                               (not-empty))
        ;; STATE
        alt?                 (mf/use-state false)
        shift?               (mf/use-state false)
        mod?                 (mf/use-state false)
        space?               (mf/use-state false)
        z?                   (mf/use-state false)
        cursor               (mf/use-state (utils/get-cursor :pointer-inner))
        hover-ids            (mf/use-state nil)
        hover                (mf/use-state nil)
        measure-hover        (mf/use-state nil)
        hover-disabled?      (mf/use-state false)
        hover-top-frame-id   (mf/use-state nil)
        frame-hover          (mf/use-state nil)
        active-frames        (mf/use-state #{})
        canvas-init?         (mf/use-state false)
        initialized?         (mf/use-state false)
        dragging-guide-id*   (mf/use-state nil)
        guide-hover-axis*    (mf/use-state nil)

        on-guide-drag
        (mf/use-fn
         #(reset! dragging-guide-id* %))

        on-guide-hover
        (mf/use-fn
         #(reset! guide-hover-axis* %))

        ;; REFS
        [viewport-ref
         on-viewport-ref] (create-viewport-ref)

        canvas-ref        (mf/use-ref nil)
        text-editor-ref   (mf/use-ref nil)
        last-vern-ref     (mf/use-ref nil)

        ;; WASM grid overlay was visible last run (`hover-grid?` true). Used so `clear-grid`
        ;; (expensive: `_hide_grid` + full `request-render`) runs only when hiding the overlay.
        prev-hover-grid-shown?-ref (mf/use-ref false)

        ;; STATE REFS
        disable-paste-ref (mf/use-ref false)
        in-viewport-ref   (mf/use-ref false)

        ;; STREAMS
        move-stream       (mf/use-memo #(rx/subject))

        guide-frame       (mf/use-memo
                           (mf/deps @hover-ids base-objects)
                           (fn []
                             (let [parent-id
                                   (->> @hover-ids
                                        (d/seek (partial cfh/root-frame? base-objects)))]
                               (when (some? parent-id)
                                 (get base-objects parent-id)))))

        outline-wasm-ids
        (mf/with-memo
          [base-objects selected highlighted edition @hover-ids @hover @frame-hover]
          (outline-wasm-source-ids base-objects selected highlighted edition @hover-ids @hover @frame-hover))

        objects-for-outlines
        (mf/with-memo
          [base-objects wasm-modifiers outline-wasm-ids]
          (if (seq wasm-modifiers)
            (apply-wasm-modifiers-to-ids base-objects wasm-modifiers outline-wasm-ids)
            base-objects))

        zoom              (d/check-num zoom 1)

        drawing-tool      (:tool drawing)
        drawing-obj       (:object drawing)

        selected-frames   (into #{} (map :frame-id) selected-shapes)

        ;; Only when we have all the selected shapes in one frame
        selected-frame    (when (= (count selected-frames) 1) (get base-objects (first selected-frames)))

        edit-path-state   (get edit-path edition)
        edit-path-mode    (get edit-path-state :edit-mode)

        path-editing?     (some? edit-path-state)
        path-drawing?     (or (= edit-path-mode :draw)
                              (and (= :path (get drawing-obj :type))
                                   (not= :curve drawing-tool)))

        editing-shape     (when edition
                            (get base-objects edition))

        editing-shape     (mf/with-memo [editing-shape path-editing? base-objects]
                            (if path-editing?
                              (path/convert-to-path editing-shape base-objects)
                              editing-shape))

        create-comment?   (= :comments drawing-tool)

        text-editing?     (cfh/text-shape? editing-shape)
        grid-editing?     (and edition (ctl/grid-layout? base-objects edition))

        mode-inspect?     (= options-mode :inspect)

        ;; True when we are opening a new file or switching to a new page
        page-transition?  (mf/deref wasm.api/page-transition?)
        context-loss-overlay? (mf/deref wasm.api/context-loss-overlay?)
        transition-reveal-rulers? (mf/deref wasm.api/transition-reveal-rulers?)

        on-click          (actions/on-click hover selected edition path-drawing? drawing-tool space? selrect z?)
        on-context-menu   (actions/on-context-menu hover hover-ids read-only?)
        on-double-click   (actions/on-double-click hover hover-ids selected hover-top-frame-id path-drawing? base-objects edition drawing-tool z? read-only?)

        comp-inst-ref     (mf/use-ref false)
        on-drag-enter     (actions/on-drag-enter comp-inst-ref)
        on-drag-over      (actions/on-drag-over move-stream)
        on-drag-end       (actions/on-drag-over comp-inst-ref)
        on-drop           (actions/on-drop file comp-inst-ref)
        on-pointer-down   (actions/on-pointer-down @hover selected edition drawing-tool text-editing? path-editing? grid-editing?
                                                   path-drawing? create-comment? space? panning z? read-only?)

        on-pointer-up     (actions/on-pointer-up disable-paste-ref)

        on-pointer-enter  (actions/on-pointer-enter in-viewport-ref)
        on-pointer-leave  (actions/on-pointer-leave in-viewport-ref)
        on-pointer-move   (actions/on-pointer-move move-stream)
        on-move-selected  (actions/on-move-selected hover hover-ids selected space? z? read-only?)
        on-menu-selected  (actions/on-menu-selected hover hover-ids selected read-only?)

        on-frame-enter    (actions/on-frame-enter frame-hover)
        on-frame-leave    (actions/on-frame-leave frame-hover)
        on-frame-select   (actions/on-frame-select selected read-only?)

        disable-events?          (contains? layout :comments)
        comments-mode?           (= drawing-tool :comments)
        show-comments?           (or comments-mode?
                                     (contains? layout :display-comments))
        show-cursor-tooltip?     tooltip
        show-draw-area?          drawing-obj
        show-gradient-handlers?  (= (count selected) 1)
        show-grids?              (and (contains? layout :display-guides) (not page-transition?))

        show-frame-outline?      (and (= transform :move) (not panning) (not page-transition?))
        show-outlines?           (and (nil? transform)
                                      (not panning)
                                      (not edition)
                                      (not drawing-obj)
                                      (not (#{:comments :path :curve} drawing-tool))
                                      (not page-transition?))

        show-pixel-grid?         (and (contains? layout :show-pixel-grid)
                                      (>= zoom 8)
                                      (not page-transition?))
        show-text-editor?        (and editing-shape (= :text (:type editing-shape)) (not page-transition?))

        has-grid?                (and (some? @hover-top-frame-id)
                                      (ctl/grid-layout? objects @hover-top-frame-id))

        hover-grid?              (and has-grid? (not page-transition?))

        show-grid-editor?        (and editing-shape (ctl/grid-layout? editing-shape) (not page-transition?))
        show-presence?           (and page-id (not page-transition?))
        show-prototypes?         (and (= options-mode :prototype) (not page-transition?))
        show-selection-handlers? (and (seq selected) (not show-text-editor?) (not page-transition?))
        show-snap-distance?      (and (contains? layout :dynamic-alignment)
                                      (= transform :move)
                                      (seq selected)
                                      (not page-transition?))
        show-snap-points?        (and (or (contains? layout :dynamic-alignment)
                                          (contains? layout :snap-guides))
                                      (or drawing-obj transform)
                                      (not page-transition?))
        show-selrect?            (and selrect (empty? drawing) (not text-editing?) (not page-transition?))
        show-measures?           (and (not transform)
                                      (not path-editing?)
                                      (or show-distances? mode-inspect? read-only?)
                                      (not page-transition?))
        show-artboard-names?     (and (contains? layout :display-artboard-names) (not page-transition?))
        hide-ui?                 (contains? layout :hide-ui)

        rulers-ui                (rs/display-state
                                  {:layout layout
                                   :selected-shapes selected-shapes
                                   :base-objects base-objects})
        show-rulers?             (:show-rulers? rulers-ui)
        frame-visible?           (:frame-visible? rulers-ui)
        offset-x                 (:offset-x rulers-ui)
        offset-y                 (:offset-y rulers-ui)
        ruler-selection          (:ruler-selection rulers-ui)

        disabled-guides?         (or drawing-tool transform path-drawing? path-editing?
                                     (contains? layout :lock-guides))

        wasm-guides
        (mf/with-memo [guides focus show-rulers? show-grids? @dragging-guide-id*]
          (guides/wasm-visible-guides
           {:guides guides
            :visible? (and show-rulers? show-grids?)
            :focused focus
            :dragging-id @dragging-guide-id*}))

        single-select?           (= (count selected-shapes) 1)

        first-shape (first selected-shapes)

        show-add-variant?        (and single-select?
                                      (or (ctk/is-variant-container? first-shape)
                                          (ctk/is-variant? first-shape)))

        show-scrollbar?          (not page-transition?)

        add-variant
        (mf/use-fn
         (mf/deps first-shape)
         #(st/emit!
           (dwv/add-new-variant (:id first-shape))))

        show-padding?
        (and (nil? transform)
             single-select?
             (= (:type first-shape) :frame)
             (= (:layout first-shape) :flex)
             (zero? (:rotation first-shape)))

        show-margin?
        (and (nil? transform)
             single-select?
             (= (:layout selected-frame) :flex)
             (zero? (:rotation first-shape)))

        rule-area-size (/ rulers/ruler-area-size zoom)
        preview-blend (-> refs/workspace-preview-blend
                          (mf/deref))
        shapes-loading? (mf/deref wasm.api/shapes-loading?)
        transition-image (mf/deref wasm.api/transition-image*)]

    ;; NOTE: We need this page-id dependency to react to it and reset the
    ;;       canvas, even though we are not using `page-id` inside the hook.
    ;;       We think moving this out to a handler will make the render code
    ;;       harder to follow through.
    (mf/with-effect [page-id]
      (when-let [canvas (mf/ref-val canvas-ref)]
        (let [timeout-id-ref   (volatile! nil)
              unmounted?       (volatile! false)
              modal-shown?     (volatile! false)

              show-unavailable
              (fn []
                (when-not (or @unmounted? @modal-shown?)
                  (vreset! modal-shown? true)
                  (reset! canvas-init? false)
                  (st/emit! (modal/show {:type :webgl-unavailable}))))

              try-init
              (fn try-init [retries]
                (when-not @unmounted?
                  (let [init? (try
                                (wasm.api/init-canvas-context canvas)
                                (catch :default e
                                  (js/console.error "Error initializing canvas context:" e)
                                  false))]
                    (cond
                      init?
                      (reset! canvas-init? true)

                      (pos? retries)
                      (vreset! timeout-id-ref
                               (js/setTimeout #(try-init (dec retries)) 200))

                      :else
                      (show-unavailable)))))]
          (reset! canvas-init? false)
          (->> @wasm.api/module
               (p/fmap (fn [ready?]
                         (when ready?
                           (try-init 3)))))
          (fn []
            (vreset! unmounted? true)
            (when-let [timeout-id @timeout-id-ref]
              (js/clearTimeout timeout-id))
            (wasm.api/clear-canvas)))))

    (mf/with-effect [show-text-editor? workspace-editor-state edition @canvas-init? @initialized?]
      (let [active-editor-state (get workspace-editor-state edition)]
        (when (and show-text-editor? active-editor-state @canvas-init? @initialized?)
          (let [content (-> active-editor-state
                            (ted/get-editor-current-content)
                            (ted/export-content))]
            (wasm.api/use-shape edition)
            (wasm.api/set-shape-text-content edition content)
            (let [dimension (wasm.api/get-text-dimensions)]
              (st/emit! (dwt/resize-text-editor edition dimension))
              (wasm.api/request-render "content"))))))

    (mf/with-effect [@canvas-init?]
      (when @canvas-init?
        (let [canvas (mf/ref-val canvas-ref)
              cancel (webapi/on-dpr-change
                      (fn [new-dpr]
                        (wasm.api/resize-canvas! canvas new-dpr)
                        (wasm.api/render-ui-only)
                        (ts/raf (fn [_] (wasm.api/render-sync)))))]
          cancel)))

    (mf/with-effect [vport]
      (when (and @canvas-init? @initialized?)
        (wasm.api/resize-canvas! (mf/ref-val canvas-ref))
        (wasm.api/set-view-box zoom vbox)))

    (mf/with-effect [@canvas-init? preview-blend]
      (when (and @canvas-init? preview-blend)
        (wasm.api/request-render "with-effect")))

    (mf/with-effect [@canvas-init? vern zoom vbox background]
      (when @canvas-init?
        (if (not @initialized?)
          (do
            (mf/set-ref-val! last-vern-ref vern)
            ;; Initial file open uses the same transition workflow as page switches,
            ;; but with a solid background-color blurred placeholder.
            (wasm.api/start-initial-load-transition! background)
            ;; Keep the blurred previous-page preview (page switch) or
            ;; blank canvas (first load) visible while shapes load.
            ;; The loading overlay is suppressed because on-shapes-ready
            ;; is set.
            (wasm.api/initialize-viewport base-objects zoom vbox
                                          :background background
                                          :on-shapes-ready
                                          (fn []
                                            ;; The target page's shapes are now loaded; arm
                                            ;; the transition so the next full frame (the one
                                            ;; that actually shows this page) removes the blur.
                                            (wasm.api/arm-page-transition-end!)
                                            (st/emit! (dw/update-page-position-data))))
            (reset! initialized? true))

          (when (and (some? vern) (not= vern (mf/ref-val last-vern-ref)))
            (wasm.api/initialize-viewport base-objects zoom vbox :background background)
            (mf/set-ref-val! last-vern-ref vern)))))

    (mf/with-effect [focus]
      (when (and @canvas-init? @initialized?)
        (ts/asap #(if (empty? focus)
                    (wasm.api/clear-focus-mode)
                    (wasm.api/set-focus-mode focus)))))

    (mf/with-effect [background]
      (when (and @canvas-init? @initialized?)
        (wasm.api/set-canvas-background background)))

    ;; Grid overlay: `clear-grid` must run only when the overlay was shown and is now off
    ;; (e.g. leave grid frame, or `page-transition?`). Do not call it on every
    ;; `hover-top-frame-id` change while not hovering a grid frame.
    (mf/with-effect [@canvas-init? hover-grid?]
      (when @canvas-init?
        (when (and (not hover-grid?) (mf/ref-val prev-hover-grid-shown?-ref))
          (wasm.api/clear-grid))
        (mf/set-ref-val! prev-hover-grid-shown?-ref hover-grid?)))

    (mf/with-effect [@canvas-init? has-grid? hover-grid?
                     (if (and has-grid? hover-grid?) @hover-top-frame-id ::no-grid-hover-id)]
      (when (and @canvas-init? hover-grid?)
        (wasm.api/show-grid @hover-top-frame-id)))

    ;; Rulers-wasm: push visibility / offsets / selection band into the
    ;; render-wasm overlay (always active, no feature flag).
    (mf/with-effect [@canvas-init?]
      (when @canvas-init?
        (wasm.api/push-ruler-theme-colors!)
        (theme/add-color-scheme-listener!
         (fn []
           (wasm.api/push-ruler-theme-colors!)
           (wasm.api/request-render "rulers-colors-theme")))))

    ;; Text-editor-wasm: push the theme colors (selection background, caret)
    ;; into the WASM text editor so the selection follows the design tokens per
    ;; theme (purple on light, teal on dark) instead of a hardcoded default.
    (mf/with-effect [@canvas-init?]
      (when @canvas-init?
        (wasm.api/text-editor-apply-theme)
        (theme/add-color-scheme-listener!
         (fn []
           (wasm.api/text-editor-apply-theme)
           (wasm.api/request-render "text-editor-colors-theme")))))

    ;; Ruler overlay updates below only change the UI surface, not the shapes,
    ;; and they fire on a stable viewbox (toggles / selection changes, not pan).
    ;; They re-present via `render-from-backbuffer!` — reusing the crisp last
    ;; frame + fresh UI — instead of a full `request-render` (which would kick
    ;; off a progressive tile-by-tile re-render that flashes) or a cached-atlas
    ;; blit (whose scale-capped atlas flashes crisp->blurry on zoomed-in views,
    ;; e.g. when the text editor opens at high zoom).
    (mf/with-effect [@canvas-init? frame-visible?]
      (when @canvas-init?
        (wasm.api/set-rulers-frame-visible! frame-visible?)
        (wasm.api/render-from-backbuffer!)))

    (mf/with-effect [@canvas-init? show-rulers?]
      (when @canvas-init?
        (wasm.api/set-rulers-visible! show-rulers?)
        (wasm.api/render-from-backbuffer!)))

    (mf/with-effect [@canvas-init? show-rulers? offset-x offset-y]
      (when (and @canvas-init? show-rulers?)
        (wasm.api/set-rulers-offsets! offset-x offset-y)))

    (mf/with-effect [@canvas-init? show-rulers?
                     (some-> ruler-selection :x) (some-> ruler-selection :y)
                     (some-> ruler-selection :width) (some-> ruler-selection :height)]
      (when (and @canvas-init? show-rulers?)
        (wasm.api/set-rulers-selection! ruler-selection)
        (wasm.api/render-from-backbuffer!)))

    ;; Paint background + rulers instantly, before shapes finish loading. Runs
    ;; after the ruler push effects so the WASM ruler state is already set.
    (mf/with-effect [@canvas-init? page-id]
      (when @canvas-init?
        (wasm.api/render-ui-only)))

    ;; Ruler guides: push the WASM-visible guide set to the render engine.
    ;; `wasm-guides` is also passed to the SVG overlay for index-based hit
    ;; testing — it must stay in sync with what we serialize here.
    (mf/with-effect [@canvas-init? wasm-guides objects]
      (when @canvas-init?
        (wasm.api/set-guides wasm-guides objects)))

    (hooks/setup-dom-events zoom disable-paste-ref in-viewport-ref read-only? drawing-tool path-drawing?)
    (hooks/setup-viewport-size vport viewport-ref)
    (hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?)
    (hooks/setup-keyboard alt? mod? space? z? shift?)
    (hooks/setup-hover-shapes page-id move-stream base-objects selected mod? hover measure-hover
                              hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only? transform)
    (hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?)
    (hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)

    [:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"}

     (cond
       (some? preview-id)
       nil

       (and read-only? (:can-edit permissions))
       [:> view-only-bar* {}]

       :else
       [:*
        (when-not hide-ui?
          [:> top-toolbar* {:layout layout}])

        (when (and ^boolean path-editing?
                   ^boolean single-select?)
          [:> path-edition-bar* {:shape editing-shape
                                 :edit-path-state edit-path-state
                                 :layout layout}])

        (when (and ^boolean grid-editing?
                   ^boolean single-select?)
          [:> grid-edition-bar* {:shape editing-shape}])])

     [:div {:class (stl/css :viewport-overlays)}
      (when show-comments?
        [:> comments/comments-layer* {:vbox vbox
                                      :page-id page-id
                                      :file-id file-id
                                      :vport vport
                                      :zoom zoom
                                      :show-rulers show-rulers?}])

      (when picking-color?
        [:> pixel-overlay/pixel-overlay-wasm* {:viewport-ref viewport-ref
                                               :canvas-ref canvas-ref}])]

     [:canvas {:id "render"
               :data-testid "canvas-wasm-shapes"
               :ref canvas-ref
               :class (stl/css :render-shapes)
               :key (dm/str "render" page-id)
               :style {:background-color background
                       :pointer-events "none"}}]

     ;; Show the transition image when switching pages or recovering from WebGL context loss.
     (when (and (or page-transition? context-loss-overlay?)
                (some? transition-image))
       [:> transition-overlay*
        {:image transition-image
         ;; Initial load: clip to the live canvas frame (rounded
         ;; corner + ruler strips when present) so it shows
         ;; through. No frame in hide-UI mode -> no clip.
         :clip-path (when (and transition-reveal-rulers? frame-visible?)
                      (let [strip (if show-rulers? rulers/ruler-area-size 0)]
                        (dm/str "inset(" strip "px 0 0 " strip "px round "
                                rulers/canvas-border-radius "px)")))}])


     [:svg.viewport-controls
      {:xmlns "http://www.w3.org/2000/svg"
       :xmlnsXlink "http://www.w3.org/1999/xlink"
       :preserveAspectRatio "xMidYMid meet"
       :key (str "viewport" page-id)
       :id "viewport-controls"
       :view-box (utils/format-viewbox vbox)
       :ref on-viewport-ref
       :class (dm/str @cursor " "
                      (stl/css-case
                       :global/drawing drawing-tool
                       :global/cursor-resize-ew-0 (= @guide-hover-axis* :x)
                       :global/cursor-resize-ns-0 (= @guide-hover-axis* :y)
                       :viewport-controls true))
       :style {:touch-action "none"
               :pointer-events (if page-transition? "none" "auto")}
       :fill "none"
       :on-click         on-click
       :on-context-menu  on-context-menu
       :on-double-click  on-double-click
       :on-drag-enter    on-drag-enter
       :on-drag-over     on-drag-over
       :on-drag-end      on-drag-end
       :on-drop          on-drop
       :on-pointer-down  on-pointer-down
       :on-pointer-enter on-pointer-enter
       :on-pointer-leave on-pointer-leave
       :on-pointer-move  on-pointer-move
       :on-pointer-up    on-pointer-up}

      [:defs
       ;; This clip is so the handlers are not over the rulers
       [:clipPath {:id "clip-handlers"}
        [:rect {:x (+ (:x vbox) rule-area-size)
                :y (+ (:y vbox) rule-area-size)
                :width (max 0 (- (:width vbox) rule-area-size))
                :height (max 0 (- (:height vbox) rule-area-size))}]]]

      [:g {:style {:pointer-events (if (or disable-events? shapes-loading?) "none" "auto")}}
       ;; Text editor handling:
       ;; - When text-editor-wasm/v1 is active, contenteditable is rendered in viewport-overlays (HTML DOM)
       (when show-text-editor?
         (cond
           (features/active-feature? @st/state "text-editor-wasm/v1")
           [:> editor-v3/text-editor* {:shape editing-shape
                                       :canvas-ref canvas-ref
                                       :ref text-editor-ref}]

           (features/active-feature? @st/state "text-editor/v2")
           [:& editor-v2/text-editor {:shape editing-shape
                                      :canvas-ref canvas-ref
                                      :ref text-editor-ref}]

           :else [:> editor-v1/text-editor-svg* {:shape editing-shape
                                                 :ref text-editor-ref}]))

       (when show-frame-outline?
         (let [outlined-frame-id (->> @hover-ids
                                      (filter #(cfh/frame-shape? (get base-objects %)))
                                      (remove selected)
                                      (last))
               outlined-frame (get objects outlined-frame-id)]
           [:*
            [:> outline/shape-outlines*
             {:objects objects-for-outlines
              :hover #{outlined-frame-id}
              :zoom zoom}]

            (when (ctl/any-layout? outlined-frame)
              [:g.ghost-outline.blurrable
               [:> outline/shape-outlines*
                {:objects objects-for-outlines
                 :selected selected
                 :zoom zoom}]])]))

       (when show-outlines?
         [:> outline/shape-outlines*
          {:objects objects-for-outlines
           :selected selected
           :hover #{(:id @hover) @frame-hover}
           :highlighted highlighted
           :edition edition
           :zoom zoom}])

       (when (and show-selection-handlers?
                  selected-shapes)
         [:> selection/area*
          {:shapes selected-shapes
           :zoom zoom
           :edition edition
           :disabled (or drawing-tool edition @space? @mod?)
           :on-move-selected on-move-selected
           :on-context-menu on-menu-selected}])

       (when show-text-editor?
         [:> text-edition-outline*
          {:shape (get base-objects edition)
           :zoom zoom}])

       (when (and (seq selected-shapes)
                  (not transform)
                  (not text-editing?)
                  (not edition)
                  (not read-only?)
                  (not mode-inspect?)
                  (not page-transition?))
         [:> msr/selection-size-badge*
          {:shapes selected-shapes
           :zoom zoom}])

       (when show-measures?
         [:> msr/measurement*
          {:bounds vbox
           :selected-shapes selected-shapes
           :frame selected-frame
           :hover-shape @measure-hover
           :zoom zoom}])

       (when show-padding?
         [:> mfc/padding-control*
          {:frame first-shape
           :hover @frame-hover
           :zoom zoom
           :is-alt @alt?
           :is-shift @shift?
           :on-move-selected on-move-selected
           :on-context-menu on-menu-selected}])

       (when show-padding?
         [:> mfc/gap-control*
          {:frame first-shape
           :hover @frame-hover
           :zoom zoom
           :is-alt @alt?
           :is-shift @shift?
           :on-move-selected on-move-selected
           :on-context-menu on-menu-selected}])

       (when show-margin?
         [:> mfc/margin-control*
          {:shape first-shape
           :parent selected-frame
           :hover @frame-hover
           :zoom zoom
           :is-alt @alt?
           :is-shift @shift?}])

       (when-not shapes-loading?
         [:> widgets/frame-titles*
          {:objects objects-modified
           :selected selected
           :zoom zoom
           :is-show-artboard-names show-artboard-names?
           :on-frame-enter on-frame-enter
           :on-frame-leave on-frame-leave
           :on-frame-select on-frame-select
           :focus focus}])

       (when show-prototypes?
         [:> widgets/frame-flows*
          {:flows (:flows page)
           :objects objects-modified
           :selected selected
           :zoom zoom
           :on-frame-enter on-frame-enter
           :on-frame-leave on-frame-leave
           :on-frame-select on-frame-select}])

       (when (and ^boolean show-draw-area?
                  ^boolean (cts/shape? drawing-obj))
         [:> drawarea/draw-area*
          {:shape drawing-obj
           :zoom zoom
           :tool drawing-tool}])

       (when show-grids?
         [:> frame-grid/frame-grid*
          {:zoom zoom
           :selected selected
           :transform transform
           :focus focus
           :vbox vbox
           :clip-rulers show-rulers?}])

       (when show-pixel-grid?
         [:> widgets/pixel-grid* {:vbox vbox
                                  :zoom zoom
                                  :clip-rulers show-rulers?}])

       (when show-snap-points?
         [:> snap-points/snap-points*
          {:layout layout
           :transform transform
           :drawing drawing-obj
           :zoom zoom
           :page-id page-id
           :selected selected
           :objects objects-modified
           :focus focus}])

       (when show-snap-distance?
         [:> snap-distances/snap-distances*
          {:layout layout
           :zoom zoom
           :transform transform
           :selected selected
           :selected-shapes selected-shapes
           :page-id page-id}])

       (when show-cursor-tooltip?
         [:> widgets/cursor-tooltip* {:zoom zoom
                                      :tooltip tooltip}])

       (when show-selrect?
         [:> widgets/selection-rect* {:data selrect
                                      :zoom zoom}])

       (when show-presence?
         [:& presence/active-cursors
          {:page-id page-id}])

       ;; NOTE: ruler guides are being migrated to the WASM render engine.
       ;; The SVG-overlay rendering is temporarily disabled while we implement
       ;; the new path.
       (when (and show-rulers? show-grids?)
         [:> guides/viewport-guides*
          {:zoom zoom
           :vbox vbox
           :guides guides
           :wasm-guides wasm-guides
           :wasm-guides? true
           :hover-frame guide-frame
           :disabled-guides disabled-guides?
           :modifiers wasm-modifiers
           :on-guide-drag on-guide-drag
           :on-guide-hover on-guide-hover}])

       ;; DEBUG LAYOUT DROP-ZONES
       (when (dbg/enabled? :layout-drop-zones)
         [:> wvd/debug-drop-zones* {:selected-shapes selected-shapes
                                    :objects base-objects
                                    :hover-top-frame-id @hover-top-frame-id
                                    :zoom zoom}])

       (when (dbg/enabled? :layout-content-bounds)
         [:> wvd/debug-content-bounds* {:selected-shapes selected-shapes
                                        :objects base-objects
                                        :hover-top-frame-id @hover-top-frame-id
                                        :zoom zoom}])

       (when (dbg/enabled? :layout-lines)
         [:> wvd/debug-layout-lines* {:selected-shapes selected-shapes
                                      :objects base-objects
                                      :hover-top-frame-id @hover-top-frame-id
                                      :zoom zoom}])

       (when (dbg/enabled? :parent-bounds)
         [:> wvd/debug-parent-bounds* {:selected-shapes selected-shapes
                                       :objects base-objects
                                       :hover-top-frame-id @hover-top-frame-id
                                       :zoom zoom}])

       (when (dbg/enabled? :grid-layout)
         [:> wvd/debug-grid-layout* {:selected-shapes selected-shapes
                                     :objects base-objects
                                     :hover-top-frame-id @hover-top-frame-id
                                     :zoom zoom}])

       (when (dbg/enabled? :text-outline)
         [:> wvd/debug-text-wasm-position-data*
          {:selected-shapes selected-shapes
           :objects base-objects
           :zoom zoom}])

       (when show-selection-handlers?
         [:g.selection-handlers {:clipPath "url(#clip-handlers)"}
          (when-not text-editing?
            (if (and editing-shape path-editing?)
              [:> path-editor* {:shape editing-shape
                                :state edit-path-state
                                :zoom zoom}]
              (when selected-shapes
                [:> selection/handlers*
                 {:selected selected
                  :shapes selected-shapes
                  :zoom zoom
                  :disabled (or drawing-tool @space?)}])))

          (when show-prototypes?
            [:> interactions/interactions*
             {:selected selected
              :page-id page-id
              :zoom zoom
              :objects objects-modified
              :current-transform transform
              :is-hover-disabled hover-disabled?}])])

       (when show-gradient-handlers?
         [:> gradients/gradient-handlers*
          {:id (first selected)
           :zoom zoom}])

       (when show-add-variant?
         [:> widgets/button-add* {:shape first-shape
                                  :zoom zoom
                                  :on-click add-variant}])

       [:g.grid-layout-editor {:clipPath "url(#clip-handlers)"}
        (when show-grid-editor?
          [:& grid-layout/editor
           {:zoom zoom
            :objects objects-modified
            :shape (or (get objects-modified edition)
                       (get objects-modified @hover-top-frame-id))
            :view-only (not show-grid-editor?)}])]

       (when show-scrollbar?
         [:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"}
          [:> scroll-bars/viewport-scrollbars*
           {:objects base-objects
            :zoom zoom
            :vbox vbox
            :bottom-padding (when palete-size (+ palete-size 8))}]])]]]))
