;; 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.rasterizer
  "A main entry point for the rasterizer process that is
  executed on a separated iframe."
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.exceptions :as ex]
   [app.common.logging :as log]
   [app.config :as cf]
   [app.util.dom :as dom]
   [app.util.http :as http]
   [app.util.object :as obj]
   [app.util.webapi :as wapi]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]))

(log/set-level! :info)

(declare send-success!)
(declare send-failure!)

(defonce data-uri-cache (js/Map.))

(defonce parent-origin
  (dm/str cf/public-uri))

(defn- get-document-element
  [^js svg]
  (.-documentElement svg))

(defn- create-image
  [uri]
  (rx/create
   (fn [subs]
     (let [image (js/Image.)]
       (obj/set! image "onload" #(do
                                   (rx/push! subs image)
                                   (rx/end! subs)))
       (obj/set! image "crossOrigin" "anonymous")
       (obj/set! image "onerror" #(rx/error! subs %))
       (obj/set! image "onabort" #(rx/error! subs (ex/error :type :abort
                                                            :code :operation-aborted
                                                            :hint "operation aborted")))
       (obj/set! image "src" uri)
       (fn []
         ;; NOTE: We intentionally do NOT set `image.src = ""` here.
         ;; Doing so discards the decoded pixel data immediately when this
         ;; observable completes, but downstream operators (e.g. drawImage /
         ;; createImageBitmap in `render-image-bitmap`) still need to read
         ;; the pixel data after the image is emitted. Clearing `src` before
         ;; the downstream pipeline finishes causes a NotReadableError
         ;; ("The requested file could not be read..."). The image element
         ;; will be garbage collected naturally when no references remain.
         (obj/set! image "onload" nil)
         (obj/set! image "onerror" nil)
         (obj/set! image "onabort" nil))))))

(defn- svg-get-adjusted-size
  "Returns the adjusted size of an SVG."
  [width height max]
  ;; Guard against zero/NaN dimensions that would cause division by zero
  ;; or produce invalid sizes, leading to createImageBitmap failures.
  (when (and (pos? width) (pos? height))
    (let [ratio (/ width height)]
      (if (< width height)
        [max (* max (/ 1 ratio))]
        [(* max ratio) max]))))

(defn- svg-get-size-from-viewbox
  "Returns the size of an SVG from its viewbox."
  [svg max]
  (let [doc  (get-document-element svg)
        vbox (dom/get-attribute doc "viewBox")]
    (when (string? vbox)
      (let [[_ _ width height] (str/split vbox #"\s+")
            width  (d/parse-integer width 0)
            height (d/parse-integer height 0)]
        (svg-get-adjusted-size width height max)))))

(defn- svg-get-size-from-intrinsic-size
  "Returns the size of an SVG from its intrinsic size."
  [svg max]
  (let [doc    (get-document-element svg)
        width  (dom/get-attribute doc "width")
        height (dom/get-attribute doc "height")
        width  (d/parse-integer width 0)
        height (d/parse-integer height 0)]
    (svg-get-adjusted-size width height max)))

(defn- svg-has-intrinsic-size?
  "Returns true if the SVG has an intrinsic size."
  [svg]
  (let [doc    (get-document-element svg)
        width  (dom/get-attribute doc "width")
        height (dom/get-attribute doc "height")]
    (d/num? width height)))

(defn- svg-get-size
  [svg max]
  (if (svg-has-intrinsic-size? svg)
    (svg-get-size-from-intrinsic-size svg max)
    (svg-get-size-from-viewbox svg max)))

(defn- svg-set-intrinsic-size!
  "Sets the intrinsic size of an SVG to the given max size."
  [^js svg max]
  (let [doc   (get-document-element svg)
        [w h] (or (svg-get-size svg max)
                  ;; Fallback: if we can't determine size from viewBox or
                  ;; intrinsic attributes, use the max size as a square.
                  [max max])]
    (dom/set-attribute! doc "width" (dm/str w))
    (dom/set-attribute! doc "height" (dm/str h)))
  svg)

(defn- fetch-as-data-uri
  "Fetches a URL as a Data URI."
  [uri]
  (if (.has data-uri-cache uri)
    (let [blob (.get data-uri-cache uri)]
      (rx/from (.text blob)))
    (->> (http/send! {:uri uri
                      :response-type :blob
                      :method :get
                      :mode :cors
                      :omit-default-headers true})
         (rx/catch (fn [cause]
                     (log/error :hint "fetching data uri"
                                :cause cause)
                     (rx/of nil)))
         (rx/mapcat (fn [response]
                      (if (nil? response)
                        (rx/of uri)
                        (->> (rx/of (:body response))
                             (rx/mapcat wapi/read-file-as-data-url)
                             (rx/tap (fn [data-uri] (.set data-uri-cache uri (wapi/create-blob data-uri "text/plain")))))))))))

(defn- svg-update-image!
  "Updates an image in an SVG to a Data URI."
  [image]
  (if-let [href (dom/get-attribute image "href")]
    (if (str/starts-with? href "data:")
      (rx/of image)
      (->> (fetch-as-data-uri href)
           (rx/map (fn [url]
                     (dom/set-attribute! image "href" url)
                     image))))
    (rx/empty)))

(defn- svg-resolve-images!
  "Resolves all images in an SVG to Data URIs."
  [svg]
  (->> (rx/from (dom/query-all svg "image"))
       (rx/mapcat svg-update-image!)
       (rx/ignore)))

(defn- svg-add-style!
  "Adds a <style> node to an SVG."
  [svg styles]
  (let [doc   (get-document-element svg)
        style (dom/create-element svg "http://www.w3.org/2000/svg" "style")]
    (dom/append-child! style (dom/create-text svg styles))
    (dom/append-child! doc style)))

(defn- svg-resolve-external-resources
  "Resolves all external resources in an SVG to Data URIs."
  [styles]
  (->> (rx/from (re-seq #"url\((https?://[^)]+)\)" styles))
       (rx/map second)
       (rx/mapcat (fn [url]
                    (->> (fetch-as-data-uri url)
                         (rx/map (fn [uri] [url uri])))))
       (rx/reduce (fn [styles [url uri]]
                    (str/replace styles url uri))
                  styles)))

(defn- svg-resolve-styles!
  "Resolves all fonts in an SVG to Data URIs."
  [svg styles]
  (->> (svg-resolve-external-resources styles)
       (rx/tap (partial svg-add-style! svg))
       (rx/ignore)))

(defn- svg-resolve-inline-styles!
  "Resolves all inline styles in an SVG to Data URIs."
  [svg]
  (->> (rx/from (dom/query-all svg "[style]"))
       (rx/mapcat (fn [node]
                    (let [styles (dom/get-attribute node "style")]
                      (->> (svg-resolve-external-resources styles)
                           (rx/tap (fn [styles] (dom/set-attribute! node "style" styles)))))))
       (rx/ignore)))

(defn- svg-resolve-style-elements!
  "Resolves all style elements in an SVG to Data URIs."
  [svg]
  (->> (rx/from (dom/query-all svg "style"))
       (rx/mapcat (fn [node]
                    (let [styles (dom/get-text node)]
                      (->> (svg-resolve-external-resources styles)
                           (rx/tap (fn [styles] (dom/set-text! node styles)))))))
       (rx/ignore)))

(defn- svg-resolve-all!
  "Resolves all images and fonts in an SVG to Data URIs."
  [svg styles]
  (rx/concat
   (svg-resolve-images! svg)
   (svg-resolve-styles! svg styles)
   (svg-resolve-inline-styles! svg)
   (svg-resolve-style-elements! svg)
   (rx/of svg)))

(defn- svg-parse
  "Parses an SVG string into an SVG DOM."
  [data]
  (let [parser (js/DOMParser.)]
    (.parseFromString ^js parser data "image/svg+xml")))

(defn- svg-stringify
  "Converts an SVG to a string."
  [svg]
  (let [doc        (get-document-element svg)
        serializer (js/XMLSerializer.)]
    (.serializeToString ^js serializer doc)))

(defn- svg-prepare
  "Prepares an SVG for rendering (resolves images to Data URIs and adds intrinsic size)."
  [data styles width]
  (let [svg (svg-parse data)]
    (->> (svg-resolve-all! svg styles)
         (rx/map #(svg-set-intrinsic-size! % width))
         (rx/map svg-stringify))))

(defn- bitmap->blob
  "Converts an ImageBitmap to a Blob."
  [bitmap]
  (rx/create
   (fn [subs]
     (let [canvas (dom/create-element "canvas")]
       (set! (.-width ^js canvas)  (.-width ^js bitmap))
       (set! (.-height ^js canvas) (.-height ^js bitmap))
       (let [context (.getContext ^js canvas "bitmaprenderer")]
         (.transferFromImageBitmap ^js context bitmap)
         (.toBlob canvas #(do (rx/push! subs %)
                              (rx/end! subs))))

       (constantly nil)))))

(defn- render-image-bitmap
  "Renders a thumbnail using it's SVG and returns an ImageBitmap of the image."
  [payload]
  (let [data   (unchecked-get payload "data")
        styles (unchecked-get payload "styles")
        width  (d/nilv (unchecked-get payload "width") 300)
        quality (d/nilv (unchecked-get payload "quality") "medium")]
    (->> (svg-prepare data styles width)
         (rx/map #(wapi/create-blob % "image/svg+xml"))
         (rx/map wapi/create-uri)
         (rx/mapcat (fn [uri]
                      (->> (create-image uri)
                           (rx/mapcat #(wapi/create-image-bitmap-with-workaround % #js {:resizeWidth width
                                                                                        :resizeQuality quality}))
                           (rx/tap #(wapi/revoke-uri uri))))))))

(defn- render-blob
  "Renders a thumbnail using it's SVG and returns a Blob of the image."
  [payload]
  (->> (render-image-bitmap payload)
       (rx/mapcat bitmap->blob)))

(defn- render
  "Renders a thumbnail and returns a stream."
  [payload]
  (let [result (d/nilv (unchecked-get payload "result") "blob")]
    (case result
      "image-bitmap" (render-image-bitmap payload)
      (render-blob payload))))

(defn- on-message
  "Handles messages from the main thread."
  [event]
  (let [evdata (unchecked-get event "data")
        evorigin (unchecked-get event "origin")]
    (when (str/starts-with? parent-origin evorigin)
      (let [id      (unchecked-get evdata "id")
            payload (unchecked-get evdata "payload")
            scope   (unchecked-get evdata "scope")]
        (when (and (some? payload)
                   (= scope "penpot/rasterizer"))
          (->> (render payload)
               (rx/subs! (partial send-success! id)
                         (partial send-failure! id))))))))

(defn- listen
  "Initializes the listener for messages from the main thread."
  []
  (.addEventListener js/window "message" on-message))

(defn- send-answer!
  "Sends an answer message."
  [id type payload]
  (let [message #js {:id id
                     :type type
                     :scope "penpot/rasterizer"
                     :payload payload}]
    (when-not (identical? js/window js/parent)
      (if (instance? js/ImageBitmap payload)
        (.postMessage js/parent message parent-origin #js [payload])
        (.postMessage js/parent message parent-origin)))))

(defn- send-success!
  "Sends a success message."
  [id payload]
  (send-answer! id "success" payload))

(defn- send-failure!
  "Sends a failure message."
  [id cause]
  (send-answer! id "failure" (ex-message cause)))

(defn- send-ready!
  "Sends a ready message."
  []
  (send-answer! nil "ready" nil))

(defn ^:export init
  []
  (listen)
  (send-ready!)
  (log/info :hint "initialized"
            :public-uri (dm/str cf/public-uri)
            :parent-uri (dm/str parent-origin)))
