;; 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.media
  (:require
   ["@penpot/svgo$default" :as svgo]
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.exceptions :as ex]
   [app.common.files.changes-builder :as pcb]
   [app.common.files.shapes-builder :as sb]
   [app.common.math :as mth]
   [app.common.media :as media]
   [app.common.schema :as sm]
   [app.common.types.container :as ctn]
   [app.common.types.fills :as types.fills]
   [app.common.types.shape :as cts]
   [app.common.uuid :as uuid]
   [app.config :as cf]
   [app.main.data.changes :as dch]
   [app.main.data.helpers :as dsh]
   [app.main.data.media :as dmm]
   [app.main.data.notifications :as ntf]
   [app.main.data.uploads :as uploads]
   [app.main.data.workspace.shapes :as dwsh]
   [app.main.data.workspace.svg-upload :as svg]
   [app.main.repo :as rp]
   [app.main.store :as st]
   [app.util.http :as http]
   [app.util.i18n :refer [tr]]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]
   [potok.v2.core :as ptk]
   [promesa.core :as p]
   [tubax.core :as tubax]))

(def accept-image-types
  (str/join "," media/image-types))

(defn- optimize
  [input]
  (svgo/optimize input svgo/defaultOptions))

(defn valid-svg-string?
  "True when `text` parses as SVG XML. Lets callers reject malformed markup
  up front instead of failing asynchronously inside the import pipeline."
  [text]
  (and (string? text)
       (not (str/blank? text))
       (try
         (tubax/xml->clj text)
         true
         (catch :default _ false))))

(defn svg->clj
  [[name text]]
  (try
    (let [text (if (contains? cf/flags :frontend-svgo)
                 (optimize text)
                 text)
          data (-> (tubax/xml->clj text)
                   (assoc :name name))]
      (rx/of data))
    (catch :default cause
      (js/console.error cause)
      (rx/throw (ex/error :type :svg-parser
                          :hint (ex-message cause))))))

;; TODO: rename to bitmap-image-uploaded
(defn image-uploaded
  [image {:keys [x y]}]
  (ptk/reify ::image-uploaded
    ptk/WatchEvent
    (watch [_ _ _]
      (let [{:keys [name width height id mtype]} image

            fills (types.fills/create
                   {:fill-opacity 1
                    :fill-image {:width width
                                 :height height
                                 :mtype mtype
                                 :id id
                                 :keep-aspect-ratio true}})

            shape {:name name
                   :width width
                   :height height
                   :x (mth/round (- x (/ width 2)))
                   :y (mth/round (- y (/ height 2)))
                   :fills fills}]

        (rx/of (dwsh/create-and-add-shape :rect x y shape))))))

(defn svg-uploaded
  [svg-data file-id position]
  (ptk/reify ::svg-uploaded
    ptk/WatchEvent
    (watch [_ _ _]
      ;; Once the SVG is uploaded, we need to extract all the bitmap
      ;; images and upload them separately, then proceed to create
      ;; all shapes.
      (->> (svg/upload-images svg-data file-id)
           (rx/map #(svg/add-svg-shapes (assoc svg-data :image-data %) position))))))


(defn upload-media-url
  [name file-id url]
  (rp/cmd!
   :create-file-media-object-from-url
   {:name name
    :file-id file-id
    :url url
    :is-local true}))

;; Size of each upload chunk in bytes — read from config directly,
;; same source used by the uploads namespace.
(def ^:private chunk-size cf/upload-chunk-size)

(defn- upload-blob-chunked
  "Uploads `blob` to `file-id` as a chunked media object using the
  three-step session API.  Returns an observable that emits the
  assembled file-media-object map."
  [{:keys [file-id name is-local blob]}]
  (let [mtype (.-type blob)]
    (->> (uploads/upload-blob-chunked blob)
         (rx/mapcat
          (fn [{:keys [session-id]}]
            (rp/cmd! :assemble-file-media-object
                     {:session-id session-id
                      :file-id    file-id
                      :is-local   is-local
                      :name       name
                      :mtype      mtype}))))))

(defn process-uris
  [{:keys [file-id local? name uris mtype on-image on-svg]}]
  (letfn [(svg-url? [url]
            (or (and mtype (= mtype "image/svg+xml"))
                (str/ends-with? url ".svg")))

          (upload [uri]
            (->> (http/send! {:method :get :uri uri :mode :no-cors :response-type :blob})
                 (rx/map :body)
                 (rx/map (fn [content]
                           {:file-id file-id
                            :name (or name (svg/extract-name uri))
                            :is-local local?
                            :content content}))
                 (rx/mapcat #(rp/cmd! :upload-file-media-object %))))

          (fetch-svg [name uri]
            (->> (http/send! {:method :get :uri uri :mode :no-cors})
                 (rx/map #(vector
                           (or name (svg/extract-name uri))
                           (:body %)))))]

    (rx/merge
     (->> (rx/from uris)
          (rx/filter (comp not svg-url?))
          (rx/mapcat upload)
          (rx/tap on-image))

     (->> (rx/from uris)
          (rx/filter svg-url?)
          (rx/merge-map (partial fetch-svg name))
          (rx/merge-map svg->clj)
          (rx/tap on-svg)))))

(defn process-blobs
  [{:keys [file-id local? name blobs force-media on-image on-svg]}]
  (letfn [(svg-blob? [blob]
            (and (not force-media)
                 (= (.-type blob) "image/svg+xml")))

          (upload-blob [blob]
            (let [params {:file-id  file-id
                          :name     (or name (if (dmm/file? blob) (media/strip-image-extension (.-name blob)) "blob"))
                          :is-local local?
                          :blob     blob}]
              (if (>= (.-size blob) chunk-size)
                (upload-blob-chunked params)
                (rp/cmd! :upload-file-media-object
                         {:file-id  file-id
                          :name     (:name params)
                          :is-local local?
                          :content  blob}))))

          (extract-content [blob]
            (let [name (or name (.-name blob))]
              (-> (.text ^js blob)
                  (p/then #(vector name %)))))]

    (rx/merge
     (->> (rx/from blobs)
          (rx/map dmm/validate-file)
          (rx/filter (comp not svg-blob?))
          (rx/mapcat upload-blob)
          (rx/tap on-image))

     (->> (rx/from blobs)
          (rx/map dmm/validate-file)
          (rx/filter svg-blob?)
          (rx/merge-map extract-content)
          (rx/merge-map svg->clj)
          (rx/tap on-svg)))))

(defn handle-media-error
  [cause]
  (ex/print-throwable cause)
  (let [error (ex-data cause)]
    (cond
      (= (:code error) :invalid-svg-file)
      (rx/of (ntf/error (tr "errors.media-type-not-allowed")))

      (= (:code error) :media-type-not-allowed)
      (rx/of (ntf/error (tr "errors.media-type-not-allowed")))

      (= (:code error) :unable-to-access-to-url)
      (rx/of (ntf/error (tr "errors.media-type-not-allowed")))

      (= (:code error) :invalid-image)
      (rx/of (ntf/error (tr "errors.media-type-not-allowed")))

      (= (:code error) :media-max-file-size-reached)
      (rx/of (ntf/error (tr "errors.media-too-large")))

      (= (:code error) :media-type-mismatch)
      (rx/of (ntf/error (tr "errors.media-type-mismatch")))

      (= (:code error) :unable-to-optimize)
      (rx/of (ntf/error (:hint error)))

      :else
      (rx/of (ntf/error (tr "errors.cannot-upload"))))))


(def ^:private
  schema:process-media-objects
  [:map {:title "process-media-objects"}
   [:file-id ::sm/uuid]
   [:local? :boolean]
   [:name {:optional true} :string]
   [:data {:optional true} :any] ; FIXME
   [:uris {:optional true} [:sequential :string]]
   [:mtype {:optional true} :string]])

(defn- process-media-objects
  [{:keys [uris] :as params}]
  (dm/assert!
   (and (sm/check schema:process-media-objects params)
        (or (contains? params :blobs)
            (contains? params :uris))))

  (ptk/reify ::process-media-objects
    ptk/WatchEvent
    (watch [_ _ _]
      (rx/concat
       (rx/of (ntf/show {:content (tr "media.loading")
                         :type :toast
                         :level :info
                         :timeout nil
                         :tag :media-loading}))
       (->> (if (seq uris)
              ;; Media objects is a list of URL's pointing to the path
              (process-uris params)
              ;; Media objects are blob of data to be upload
              (process-blobs params))

            ;; Every stream has its own sideeffect. We need to ignore the result
            (rx/ignore)
            (rx/catch handle-media-error)
            (rx/finalize #(st/emit! (ntf/hide :tag :media-loading))))))))

(defn upload-media-workspace
  [{:keys [position file-id] :as params}]
  (let [params (assoc params
                      :local? true
                      :on-image #(st/emit! (image-uploaded % position))
                      :on-svg   #(st/emit! (svg-uploaded % file-id position)))]
    (process-media-objects params)))

(defn upload-fill-image
  [file on-success]
  (assert (dmm/blob? file) "expected a valid blob for `file` param")

  (ptk/reify ::upload-fill-image
    ptk/WatchEvent
    (watch [_ state _]
      (let [on-upload-success
            (fn [image]
              (on-success image)
              (dmm/notify-finished-loading))

            file-id (:current-file-id state)

            prepare
            (fn [content]
              {:file-id file-id
               :name (if (dmm/file? content) (.-name content) (tr "media.image"))
               :is-local false
               :content content})]

        (dmm/notify-start-loading)
        (->> (rx/of file)
             (rx/map dmm/validate-file)
             (rx/map prepare)
             (rx/mapcat #(rp/cmd! :upload-file-media-object %))
             (rx/tap on-upload-success)
             (rx/catch handle-media-error))))))

(defn create-shapes-svg
  "Convert svg elements into penpot shapes."
  [file-id objects pos svg-data]
  (let [upload-images
        (fn [svg-data]
          (->> (svg/upload-images svg-data file-id)
               (rx/map #(assoc svg-data :image-data %))))

        process-svg
        (fn [svg-data]
          (let [[root-svg-shape children]
                (sb/create-svg-shapes svg-data pos objects uuid/zero nil #{} false)

                frame-shape
                (cts/setup-shape
                 {:type :frame
                  :x (:x pos)
                  :y (:y pos)
                  :width (-> root-svg-shape :selrect :width)
                  :height (-> root-svg-shape :selrect :height)
                  :name (:name root-svg-shape)
                  :frame-id uuid/zero
                  :parent-id uuid/zero
                  :fills (types.fills/create)})

                root-svg-shape
                (-> root-svg-shape
                    (assoc :frame-id (:id frame-shape) :parent-id (:id frame-shape)))

                shapes
                (->> children
                     (filter #(= (:parent-id %) (:id root-svg-shape)))
                     (mapv :id))

                root-svg-shape
                (assoc root-svg-shape :shapes shapes)

                children (->> children (mapv #(assoc % :frame-id (:id frame-shape))))
                children (d/concat-vec [root-svg-shape] children)]

            [frame-shape children]))]

    (->> (upload-images svg-data)
         (rx/map process-svg))))

(defn create-shapes-img
  "Convert a media object that contains a bitmap image into shapes,
  one shape of type :rect containing an image fill and one group that contains it."
  [pos {:keys [name width height id mtype] :as media-obj}]
  (let [frame-shape (cts/setup-shape
                     {:type :frame
                      :x (:x pos)
                      :y (:y pos)
                      :width width
                      :height height
                      :name name
                      :frame-id uuid/zero
                      :parent-id uuid/zero})

        img-fills   (types.fills/create
                     {:fill-opacity 1
                      :fill-image {:id id
                                   :width width
                                   :height height
                                   :mtype mtype
                                   :keep-aspect-ratio true}})

        img-shape   (cts/setup-shape
                     {:type :rect
                      :x (:x pos)
                      :y (:y pos)
                      :width width
                      :height height
                      :fills img-fills
                      :name name
                      :frame-id (:id frame-shape)
                      :parent-id (:id frame-shape)})]
    (rx/of [frame-shape [img-shape]])))

(defn- add-shapes-and-component
  [it file-data page name [shape children]]
  (let [[component-shape updated-shapes]
        (ctn/convert-shape-in-component shape children (:id file-data))

        changes (-> (pcb/empty-changes it)
                    (pcb/with-page page)
                    (pcb/with-objects (:objects page))
                    (pcb/with-library-data file-data)
                    (pcb/add-objects (cons shape children))
                    (pcb/add-component (:id component-shape)
                                       ""
                                       name
                                       updated-shapes
                                       (:id shape)
                                       (:id page)))]

    (dch/commit-changes changes)))

(defn- process-img-component
  [media-obj]
  (ptk/reify ::process-img-component
    ptk/WatchEvent
    (watch [it state _]
      (let [file-id (:current-file-id state)
            page-id (:current-page-id state)

            fdata   (dsh/lookup-file-data state file-id)
            page    (dsh/get-page fdata page-id)
            pos     (dsh/get-viewport-center state)]

        (->> (create-shapes-img pos media-obj)
             (rx/map (partial add-shapes-and-component it fdata page (:name media-obj))))))))

(defn- process-svg-component
  [svg-data]
  (ptk/reify ::process-svg-component
    ptk/WatchEvent
    (watch [it state _]

      (let [file-id (:current-file-id state)
            page-id (:current-page-id state)

            fdata   (dsh/lookup-file-data state file-id)
            page    (dsh/get-page fdata page-id)
            pos     (dsh/get-viewport-center state)]

        (->> (create-shapes-svg file-id (:objects page) pos svg-data)
             (rx/map (partial add-shapes-and-component it fdata page (:name svg-data))))))))

(defn upload-media-components
  [params]
  (let [params (assoc params
                      :local? false
                      :on-image #(st/emit! (process-img-component %))
                      :on-svg #(st/emit! (process-svg-component %)))]
    (process-media-objects params)))

(def ^:private
  schema:clone-media-object
  [:map {:title "clone-media-object"}
   [:file-id ::sm/uuid]
   [:object-id ::sm/uuid]])

(defn clone-media-object
  [{:keys [file-id object-id] :as params}]
  (dm/assert!
   (sm/check schema:clone-media-object params))

  (ptk/reify ::clone-media-objects
    ptk/WatchEvent
    (watch [_ _ _]
      (let [{:keys [on-success on-error]
             :or {on-success identity
                  on-error identity}} (meta params)
            params {:is-local true
                    :file-id file-id
                    :id object-id}]

        (rx/concat
         (rx/of (ntf/show {:content (tr "media.loading")
                           :type :toast
                           :level :info
                           :timeout nil
                           :tag :media-loading}))
         (->> (rp/cmd! :clone-file-media-object params)
              (rx/tap on-success)
              (rx/catch on-error)
              (rx/finalize #(st/emit! (ntf/hide :tag :media-loading)))))))))

(defn create-svg-shape
  [id name svg-string position]
  (ptk/reify ::create-svg-shape
    ptk/WatchEvent
    (watch [_ _ _]
      (->> (svg->clj [name svg-string])
           (rx/take 1)
           (rx/map #(svg/add-svg-shapes id % position {:ignore-selection? true
                                                       :change-selection? false}))))))
(defn create-svg-shape-with-images
  [file-id id name svg-string position on-success on-error]
  (ptk/reify ::create-svg-shape-with-images
    ptk/WatchEvent
    (watch [_ _ _]
      (->> (svg->clj [name svg-string])
           (rx/take 1)
           (rx/mapcat
            (fn [svg-data]
              (->> (svg/upload-images svg-data file-id)
                   (rx/map #(assoc svg-data :image-data %)))))
           (rx/map
            (fn [svg-data]
              (svg/add-svg-shapes
               id
               svg-data
               position
               {:ignore-selection? true
                :change-selection? false})))
           (rx/tap on-success)
           (rx/catch on-error)))))
