;; 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.shapes.custom-stroke
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.files.helpers :as cfh]
   [app.common.geom.rect :as grc]
   [app.common.geom.shapes :as gsh]
   [app.common.geom.shapes.bounds :as gsb]
   [app.common.geom.shapes.text :as gst]
   [app.common.types.path :as path]
   [app.common.uuid :as uuid]
   [app.config :as cf]
   [app.main.ui.context :as muc]
   [app.main.ui.shapes.attrs :as attrs]
   [app.main.ui.shapes.embed :as embed]
   [app.main.ui.shapes.gradients :as grad]
   [app.util.object :as obj]
   [cuerdas.core :as str]
   [rumext.v2 :as mf]))

;; FIXME: this clearly should be renamed to something different, this
;; namespace has also fill related code

(mf/defc inner-stroke-clip-path
  {::mf/wrap-props false}
  [{:keys [shape render-id index]}]
  (let [shape-id (dm/get-prop shape :id)
        suffix   (if (some? index) (dm/str "-" index) "")
        clip-id  (dm/str "inner-stroke-" render-id "-" shape-id suffix)
        href     (dm/str "#stroke-shape-" render-id "-" shape-id suffix)]
    [:> "clipPath" {:id clip-id}
     [:use {:href href}]]))

(mf/defc outer-stroke-mask
  {::mf/wrap-props false}
  [{:keys [shape stroke render-id index]}]
  (let [shape-id       (dm/get-prop shape :id)
        suffix         (if (some? index) (dm/str "-" index) "")
        mask-id        (dm/str "outer-stroke-" render-id "-" shape-id suffix)
        shape-id       (dm/str "stroke-shape-" render-id "-" shape-id suffix)
        href           (dm/str "#" shape-id)

        stroke-width   (case (:stroke-alignment stroke :center)
                         :center (/ (:stroke-width stroke 0) 2)
                         :outer (:stroke-width stroke 0)
                         0)
        stroke-margin  (gsb/shape-stroke-margin shape stroke-width)

        ;; NOTE: for performance reasons we may can delimit a bit the
        ;; dependencies to really useful shape attrs instead of using
        ;; the shepe as-is.
        selrect       (mf/with-memo [shape]
                        (if (cfh/text-shape? shape)
                          (gst/shape->rect shape)
                          (grc/points->rect (:points shape))))

        x             (- (dm/get-prop selrect :x) stroke-margin)
        y             (- (dm/get-prop selrect :y) stroke-margin)
        w             (+ (dm/get-prop selrect :width) (* 2 stroke-margin))
        h             (+ (dm/get-prop selrect :height) (* 2 stroke-margin))]

    [:mask {:id mask-id
            :x x
            :y y
            :width w
            :height h
            :maskUnits "userSpaceOnUse"}
     [:use
      {:href href
       :style {:fill "none"
               :stroke "white"
               :strokeWidth (* stroke-width 2)}}]

     [:use
      {:href href
       :style {:fill "black"
               :stroke "none"}}]]))

(mf/defc cap-markers
  {::mf/wrap-props false}
  [{:keys [stroke render-id index]}]
  (let [id-prefix (dm/str "marker-" render-id)

        gradient  (:stroke-color-gradient stroke)
        image     (:stroke-image stroke)
        cap-start (:stroke-cap-start stroke)
        cap-end   (:stroke-cap-end stroke)

        color     (cond
                    (some? gradient)
                    (str/ffmt "url(#stroke-color-gradient-%-%)" render-id index)

                    (some? image)
                    (str/ffmt "url(#stroke-fill-%-%)" render-id index)

                    :else
                    (:stroke-color stroke))

        opacity   (when-not (some? gradient)
                    (:stroke-opacity stroke))]

    [:*
     (when (or (= cap-start :line-arrow)
               (= cap-end :line-arrow))
       [:marker {:id (dm/str id-prefix "-line-arrow")
                 :viewBox "0 0 3 6"
                 :refX "2"
                 :refY "3"
                 :markerWidth "8.5"
                 :markerHeight "8.5"
                 :orient "auto-start-reverse"
                 :fill color
                 :fillOpacity opacity}
        [:path {:d "M 0.5 0.5 L 3 3 L 0.5 5.5 L 0 5 L 2 3 L 0 1 z"}]])

     (when (or (= cap-start :triangle-arrow)
               (= cap-end :triangle-arrow))
       [:marker {:id (dm/str id-prefix "-triangle-arrow")
                 :viewBox "0 0 3 6"
                 :refX "2"
                 :refY "3"
                 :markerWidth "8.5"
                 :markerHeight "8.5"
                 :orient "auto-start-reverse"
                 :fill color
                 :fillOpacity opacity}
        [:path {:d "M 0 0 L 3 3 L 0 6 z"}]])

     (when (or (= cap-start :square-marker)
               (= cap-end :square-marker))
       [:marker {:id (dm/str id-prefix "-square-marker")
                 :viewBox "0 0 6 6"
                 :refX "3"
                 :refY "3"
                 :markerWidth "4.2426" ;; diagonal length of a 3x3 square
                 :markerHeight "4.2426"
                 :orient "auto-start-reverse"
                 :fill color
                 :fillOpacity opacity}
        [:rect {:x 0 :y 0 :width 6 :height 6}]])

     (when (or (= cap-start :circle-marker)
               (= cap-end :circle-marker))
       [:marker {:id (dm/str id-prefix "-circle-marker")
                 :viewBox "0 0 6 6"
                 :refX "3"
                 :refY "3"
                 :markerWidth "4"
                 :markerHeight "4"
                 :orient "auto-start-reverse"
                 :fill color
                 :fillOpacity opacity}
        [:circle {:cx "3" :cy "3" :r "3"}]])

     (when (or (= cap-start :diamond-marker)
               (= cap-end :diamond-marker))
       [:marker {:id (dm/str id-prefix "-diamond-marker")
                 :viewBox "0 0 6 6"
                 :refX "3"
                 :refY "3"
                 :markerWidth "6"
                 :markerHeight "6"
                 :orient "auto-start-reverse"
                 :fill color
                 :fillOpacity opacity}
        [:path {:d "M 3 0 L 6 3 L 3 6 L 0 3 z"}]])

     ;; If the user wants line caps but different in each end,
     ;; simulate it with markers.
     (when (and (or (= cap-start :round)
                    (= cap-end :round))
                (not= cap-start cap-end))
       [:marker {:id (dm/str id-prefix "-round")
                 :viewBox "0 0 6 6"
                 :refX "3"
                 :refY "3"
                 :markerWidth "6"
                 :markerHeight "6"
                 :orient "auto-start-reverse"
                 :fill color
                 :fillOpacity opacity}
        [:path {:d "M 3 2.5 A 0.5 0.5 0 0 1 3 3.5 "}]])

     (when (and (or (= cap-start :square)
                    (= cap-end :square))
                (not= cap-start cap-end))
       [:marker {:id (dm/str id-prefix "-square")
                 :viewBox "0 0 6 6"
                 :refX "3"
                 :refY "3"
                 :markerWidth "6"
                 :markerHeight "6"
                 :orient "auto-start-reverse"
                 :fill color
                 :fillOpacity opacity}
        [:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])]))

(mf/defc stroke-defs
  {::mf/wrap-props false}
  [{:keys [shape stroke render-id index]}]
  (let [open-path?    (and ^boolean (cfh/path-shape? shape)
                           ^boolean (path/shape-with-open-path? shape))
        gradient      (:stroke-color-gradient stroke)
        alignment     (:stroke-alignment stroke :center)
        width         (:stroke-width stroke 0)

        props         #js {:id (dm/str "stroke-color-gradient-" render-id "-" index)
                           :gradient gradient
                           :shape shape
                           :force-transform (cfh/path-shape? shape)}
        stroke-image  (:stroke-image stroke)
        uri           (when stroke-image (cf/resolve-file-media stroke-image))
        embed         (embed/use-data-uris [uri])

        stroke-width  (case (:stroke-alignment stroke :center)
                        :center (/ (:stroke-width stroke 0) 2)
                        :outer (:stroke-width stroke 0)
                        0)
        margin        (gsb/shape-stroke-margin stroke stroke-width)

        selrect       (mf/with-memo [shape]
                        (if (cfh/text-shape? shape)
                          (gst/shape->rect shape)
                          (grc/points->rect (:points shape))))

        stroke-margin (+ stroke-width margin)

        w             (+ (dm/get-prop selrect :width) (* 2 stroke-margin))
        h             (+ (dm/get-prop selrect :height) (* 2 stroke-margin))
        image-props   #js {:href (get embed uri uri)
                           :preserveAspectRatio "xMidYMid slice"
                           :width 1
                           :height 1
                           :id (dm/str "stroke-image-" render-id "-" index)}]
    [:*
     (when (some? gradient)
       (case (:type gradient)
         :linear [:> grad/linear-gradient props]
         :radial [:> grad/radial-gradient props]))

     (when (:stroke-image stroke)
       ;; We need to make the pattern size and the image fit so it's not repeated
       [:pattern {:id (dm/str "stroke-fill-" render-id "-" index)
                  :patternContentUnits "objectBoundingBox"
                  :x (- (/ stroke-margin (dm/get-prop selrect :width)))
                  :y (- (/ stroke-margin (dm/get-prop selrect :height)))
                  :width (/ w (dm/get-prop selrect :width))
                  :height (/ h (dm/get-prop selrect :height))
                  :viewBox "0 0 1 1"
                  :preserveAspectRatio "xMidYMid slice"
                  :patternTransform (when (cfh/path-shape? shape) (gsh/transform-str shape))}
        [:> :image image-props]])

     (cond
       (and (not open-path?)
            (= :inner alignment)
            (> width 0))
       [:& inner-stroke-clip-path {:shape shape
                                   :render-id render-id
                                   :index index}]

       (and (not open-path?)
            (= :outer alignment)
            (> width 0))
       [:& outer-stroke-mask {:shape shape
                              :stroke stroke
                              :render-id render-id
                              :index index}]

       (or (some? (:stroke-cap-start stroke))
           (some? (:stroke-cap-end stroke)))
       [:& cap-markers {:stroke stroke
                        :render-id render-id
                        :index index}])]))

;; Outer alignment: display the shape in two layers. One without
;; stroke (only fill), and another one only with stroke at double
;; width (transparent fill) and passed through a mask that shows the
;; whole shape, but hides the original shape without stroke

(mf/defc outer-stroke
  {::mf/wrap-props false}
  [{:keys [children shape stroke index]}]
  (let [shape-id     (dm/get-prop shape :id)
        render-id    (mf/use-ctx muc/render-id)

        props        (obj/get children "props")
        style        (obj/get props "style")

        stroke-width (:stroke-width stroke 0)

        suffix       (if (some? index) (dm/str "-" index) "")
        mask-id      (dm/str "outer-stroke-" render-id "-" shape-id suffix)
        shape-id     (dm/str "stroke-shape-" render-id "-" shape-id suffix)
        href         (dm/str "#" shape-id)]

    [:g.outer-stroke-shape
     [:defs
      [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}]
      (let [type  (obj/get children "type")
            style (-> (obj/clone style)
                      (obj/unset! "fill")
                      (obj/unset! "fillOpacity")
                      (obj/unset! "stroke")
                      (obj/unset! "strokeWidth")
                      (obj/unset! "strokeOpacity")
                      (obj/unset! "strokeStyle")
                      (obj/unset! "strokeDasharray"))
            props (-> (obj/clone props)
                      (obj/set! "id" shape-id)
                      (obj/set! "style" style))]

        [:> type props])]

     [:use {:href href
            :mask (dm/str "url(#" mask-id ")")
            :style (-> (obj/clone style)
                       (obj/set! "strokeWidth" (* stroke-width 2))
                       (obj/set! "fill" "none")
                       (obj/unset! "fillOpacity"))}]

     [:use {:href href
            :style (-> (obj/clone style)
                       (obj/set! "stroke" "none"))}]]))


;; Inner alignment: display the shape with double width stroke, and
;; clip the result with the original shape without stroke.

(mf/defc inner-stroke
  {::mf/wrap-props false}
  [props]
  (let [child        (unchecked-get props "children")
        shape        (unchecked-get props "shape")
        stroke       (unchecked-get props "stroke")
        index        (unchecked-get props "index")

        shape-id     (dm/get-prop shape :id)
        render-id    (mf/use-ctx muc/render-id)

        type         (obj/get child "type")

        props        (-> (obj/get child "props") obj/clone)
        ;; FIXME: check if style need to be cloned
        style        (-> (obj/get props "style") obj/clone)
        transform    (obj/get props "transform")

        stroke-width (:stroke-width stroke 0)

        suffix       (if (some? index) (dm/str "-" index) "")
        clip-id      (dm/str "inner-stroke-" render-id "-" shape-id suffix)
        shape-id     (dm/str "stroke-shape-" render-id "-" shape-id suffix)
        clip-path    (dm/str "url('#" clip-id "')")

        style        (obj/set! style "strokeWidth" (* stroke-width 2))

        props        (-> props
                         (obj/set! "id" (dm/str shape-id))
                         (obj/set! "style" style)
                         (obj/unset! "transform"))]

    [:g.inner-stroke-shape
     {:transform transform}
     [:defs
      [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}]
      [:> type props]]

     [:use {:href (dm/str "#" shape-id)
            :clipPath clip-path}]]))

;; The SVG standard does not implement yet the 'stroke-alignment'
;; attribute, to define the position of the stroke relative to the
;; stroke axis (inner, center, outer). Here we implement a patch to be
;; able to draw the stroke in the three cases. See discussion at:
;; https://stackoverflow.com/questions/7241393/can-you-control-how-an-svgs-stroke-width-is-drawn

(mf/defc shape-custom-stroke
  {::mf/wrap-props false}
  [props]
  (let [child           (unchecked-get props "children")
        shape           (unchecked-get props "shape")
        stroke          (unchecked-get props "stroke")
        index           (unchecked-get props "index")

        render-id       (mf/use-ctx muc/render-id)
        render-id       (d/nilv (unchecked-get props "render-id") render-id)

        stroke-width    (:stroke-width stroke 0)
        stroke-style    (:stroke-style stroke :none)
        stroke-position (:stroke-alignment stroke :center)

        has-stroke?     (and (> stroke-width 0)
                             (not= stroke-style :none))
        closed?         (or (not ^boolean (cfh/path-shape? shape))
                            (not ^boolean (path/shape-with-open-path? shape)))
        inner?          (= :inner stroke-position)
        outer?          (= :outer stroke-position)]

    (cond
      (and has-stroke? inner? closed?)
      [:& inner-stroke {:shape shape :stroke stroke :index index} child]

      (and has-stroke? outer? closed?)
      [:& outer-stroke {:shape shape :stroke stroke :index index} child]

      :else
      [:g.stroke-shape
       [:defs
        [:& stroke-defs {:shape shape :stroke stroke :render-id render-id :index index}]]
       child])))

(defn- build-fill-element
  [shape child position render-id]
  (let [type         (obj/get child "type")
        props        (-> (obj/get child "props")
                         (obj/clone))
        props        (attrs/add-fill-props! props shape position render-id)]
    (mf/html [:> type props])))

(defn- build-stroke-element
  [child value position render-id open-path?]
  (let [props (obj/get child "props")
        type  (obj/get child "type")

        style (-> (obj/get props "style")
                  (obj/clone)
                  (obj/set! "fill" "none")
                  (obj/set! "fillOpacity" "none")
                  (attrs/add-stroke! value render-id position open-path?))

        style (if (:stroke-image value)
                (obj/set! style "stroke" (dm/fmt "url(#stroke-fill-%-%)" render-id position))
                style)

        props (-> (obj/clone props)
                  (obj/unset! "fill")
                  (obj/unset! "fillOpacity")
                  (obj/set! "style" style))]

    (mf/html [:> type props])))

(mf/defc shape-fills
  {::mf/wrap-props false}
  [props]
  (let [child      (unchecked-get props "children")
        shape      (unchecked-get props "shape")

        shape-id   (dm/get-prop shape :id)

        position   (d/nilv (unchecked-get props "position") 0)

        render-id  (mf/use-ctx muc/render-id)
        render-id  (d/nilv (unchecked-get props "render-id") render-id)]

    [:g.fills {:id (dm/fmt "fills-%" shape-id)}
     (build-fill-element shape child position render-id)]))

(mf/defc shape-strokes
  {::mf/wrap-props false}
  [props]
  (let [child         (unchecked-get props "children")
        shape         (unchecked-get props "shape")

        shape-id      (dm/get-prop shape :id)

        render-id     (mf/use-ctx muc/render-id)
        render-id     (d/nilv (unchecked-get props "render-id") render-id)

        strokes       (get shape :strokes)

        ;; Generate a unique id when the strokes change. This way we can solve some
        ;; render issues in Safari https://tree.taiga.io/project/penpot/issue/9040
        prefix        (mf/use-memo (mf/deps strokes) #(dm/str (uuid/next)))

        stroke-id     (dm/str (dm/fmt "strokes-%-%" prefix shape-id))

        shape-blur   (get shape :blur)

        shape-fills   (get shape :fills)
        shape-shadow  (get shape :shadow)
        shape-strokes (not-empty strokes)

        svg-attrs     (attrs/get-svg-props shape render-id)

        style         (-> (obj/get props "style")
                          (obj/clone)
                          (obj/merge! (obj/get svg-attrs "style")))

        props        (mf/spread-props svg-attrs
                                      {:id stroke-id
                                       :className "strokes"
                                       :style style})

        open-path?    (and ^boolean (cfh/path-shape? shape)
                           ^boolean (path/shape-with-open-path? shape))]

    (when-not ^boolean (cfh/frame-shape? shape)
      (when (and (some? shape-blur)
                 (not ^boolean (:hidden shape-blur)))
        (obj/set! props "filter" (dm/fmt "url(#filter-blur-%)" render-id)))

      (when (and (empty? shape-fills)
                 (some? (->> shape-shadow (remove :hidden) not-empty)))
        (obj/set! props "filter" (dm/fmt "url(#filter-%)" render-id))))

    (when (some? shape-strokes)
      [:> :g props
       (for [[index value] (reverse (d/enumerate shape-strokes))
             :when (not (:hidden value))]
         [:& shape-custom-stroke {:shape shape
                                  :stroke value
                                  :index index
                                  :key (dm/str index "-" stroke-id)}
          (build-stroke-element child value index render-id open-path?)])])))

(mf/defc shape-custom-strokes
  {::mf/wrap-props false}
  [props]
  [:*
   [:> shape-fills props]
   [:> shape-strokes props]])
