;; 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

(ns app.worker.import.parser
  (:require
   [app.common.colors :as cc]
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.geom.matrix :as gmt]
   [app.common.geom.point :as gpt]
   [app.common.svg.path :as svg.path]
   [app.common.types.shape.interactions :as ctsi]
   [app.common.uuid :as uuid]
   [app.util.json :as json]
   [cuerdas.core :as str]))

(def url-regex
  #"url\(#([^\)]*)\)")

(def uuid-regex
  #"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}")

(def uuid-regex-prefix
  #"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}-")

(defn valid?
  [root]
  (contains? (:attrs root) :xmlns:penpot))

(defn branch?
  [node]
  (and (contains? node :content)
       (some? (:content node))))

(defn close?
  [node]
  (and (vector? node)
       (= ::close (first node))))

(defn find-node
  [node tag]
  (when (some? node)
    (->> node :content (d/seek #(= (:tag %) tag)))))

(defn find-node-by-id
  [id coll]
  (->> coll (d/seek #(= id (-> % :attrs :id)))))

(defn find-all-nodes
  [node tag]
  (when (some? node)
    (let [predicate?
          (if (set? tag)
            ;; We can pass a tag set or a single tag
            #(contains? tag (:tag %))
            #(= (:tag %) tag))]
      (->> node :content (filterv predicate?)))))

(defn get-data
  ([node]
   (or (find-node node :penpot:shape)
       (find-node node :penpot:page)))

  ([node tag]
   (-> (get-data node)
       (find-node tag))))

(defn get-type
  [node]
  (if (close? node)
    (second node)
    (let [data (get-data node)]
      (-> (get-in data [:attrs :penpot:type])
          (keyword)))))

(defn shape?
  [node]
  (or (close? node)
      (some? (get-data node))))

(defn get-id
  [node]
  (let [attr-id (get-in node [:attrs :id])
        id (when (string? attr-id) (re-find uuid-regex attr-id))]
    (when (some? id)
      (uuid/uuid id))))

(defn str->bool
  [val]
  (when (some? val) (= val "true")))

(defn get-meta
  ([m att]
   (get-meta m att identity))
  ([m att val-fn]
   (let [ns-att (->> att d/name (str "penpot:") keyword)
         val (or (get-in m [:attrs ns-att])
                 (get-in (get-data m) [:attrs ns-att]))]
     (when val (val-fn val)))))

(defn find-node-by-metadata-value
  [meta value coll]
  (->> coll (d/seek #(= value (get-meta % meta)))))

(defn get-children
  [node]
  (cond-> (:content node)
    ;; We add a "fake" node to know when we are leaving the shape children
    (shape? node)
    (conj [::close (get-type node)])))

(defn node-seq
  [content]
  (->> content (tree-seq branch? get-children)))

(defn parse-style
  "Transform style list into a map"
  [style-str]
  (if (string? style-str)
    (->> (str/split style-str ";")
         (map str/trim)
         (map #(str/split % ":"))
         (group-by first)
         (map (fn [[key val]]
                (vector (keyword key) (second (first val)))))
         (into {}))
    style-str))

(defn add-attrs
  [m attrs]
  (reduce-kv
   (fn [m k v]
     (if (#{:style :data-style} k)
       (merge m (parse-style v))
       (assoc m k v)))
   m
   attrs))

(defn without-penpot-prefix
  [m]
  (let [no-penpot-prefix?
        (fn [[k _]]
          (not (str/starts-with? (d/name k) "penpot:")))]
    (into {} (filter no-penpot-prefix?) m)))

(defn remove-penpot-prefix
  [m]
  (into {}
        (map (fn [[k v]]
               (if (str/starts-with? (d/name k) "penpot:")
                 [(-> k d/name (str/replace "penpot:" "") keyword) v]
                 [k v])))
        m))

(defn camelize [[k v]]
  [(-> k d/name str/camel keyword) v])

(defn camelize-keys
  [m]
  (assert (map? m) (str m))

  (into {} (map camelize) m))

(defn fix-style-attr
  [m]
  (let [fix-style
        (fn [[k v]]
          (if (= k :style)
            [k (-> v parse-style camelize-keys)]
            [k v]))]

    (d/deep-mapm (comp camelize fix-style) m)))

(defn string->uuid
  "Looks in a map for keys or values that have uuid shape and converts them
  into uuid objects"
  [m]
  (letfn [(convert [value]
            (cond
              (and (string? value) (re-matches uuid-regex value))
              (uuid/uuid value)

              (and (keyword? value) (re-matches uuid-regex (d/name value)))
              (uuid/uuid (d/name value))

              (vector? value)
              (mapv convert value)

              :else
              value))]
    (->> m
         (d/deep-mapm
          (fn [pair] (->> pair (mapv convert)))))))

(def search-data-node? #{:rect :path :circle})

(defn get-svg-data
  [type node]
  (let [node-attrs (add-attrs {} (:attrs node))]
    (cond
      (search-data-node? type)
      (let [data-tags #{:ellipse :rect :path :text :foreignObject}]
        (->> node
             (node-seq)
             (filter #(contains? data-tags (:tag %)))
             (map #(:attrs %))
             (reduce add-attrs node-attrs)))

      (= type :image)
      (let [data-tags #{:rect :image}]
        (->> node
             (node-seq)
             (filter #(contains? data-tags (:tag %)))
             (map #(:attrs %))
             (reduce add-attrs node-attrs)))

      (= type :text)
      (->> node
           (node-seq)
           (filter #(contains? #{:g :foreignObject} (:tag %)))
           (map #(:attrs %))
           (reduce add-attrs node-attrs))

      (= type :frame)
      (let [;; Old .penpot files doesn't have "g" nodes. They have a clipPath reference as a node attribute
            to-url #(dm/str "url(#" % ")")
            frame-clip-rect-node  (->> (find-all-nodes node :defs)
                                       (mapcat #(find-all-nodes % :clipPath))
                                       (filter #(= (to-url (:id (:attrs %))) (:clip-path node-attrs)))
                                       (mapcat #(find-all-nodes % #{:rect :path}))
                                       (first))

            ;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have
            g-nodes    (find-all-nodes node :g)
            defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes))
            gg-nodes   (flatten (map #(find-all-nodes % :g) g-nodes))


            rect-nodes (flatten [[(find-all-nodes node :rect)]
                                 (map #(find-all-nodes % #{:rect :path}) defs-nodes)
                                 (map #(find-all-nodes % #{:rect :path}) g-nodes)
                                 (map #(find-all-nodes % #{:rect :path}) gg-nodes)])
            svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)]
        (merge
         (add-attrs {} (:attrs frame-clip-rect-node))
         (add-attrs {} (:attrs svg-node))
         node-attrs))

      (= type :svg-raw)
      (let [svg-content (get-data node :penpot:svg-content)
            tag (-> svg-content :attrs :penpot:tag keyword)

            svg-node (if (= :svg tag)
                       (->> node :content last :content last)
                       (->> node :content last))
            svg-node (d/update-in-when svg-node [:attrs :style] parse-style)]
        (merge (add-attrs {} (:attrs svg-node)) node-attrs))

      (= type :bool)
      (->> node
           (:content)
           (filter #(= :path (:tag %)))
           (map #(:attrs %))
           (reduce add-attrs node-attrs))

      :else
      node-attrs)))

(def has-position? #{:frame :rect :image :text})

(defn parse-position
  [props node svg-data]
  (let [x (get-meta node :x d/parse-double)
        y (get-meta node :y d/parse-double)
        width (get-meta node :width d/parse-double)
        height (get-meta node :height d/parse-double)

        values (->> (select-keys svg-data [:x :y :width :height])
                    (d/mapm (fn [_ val] (d/parse-double val))))

        values
        (cond-> values
          (some? x) (assoc :x x)
          (some? y) (assoc :y y)
          (some? width) (assoc :width width)
          (some? height) (assoc :height height))]
    (d/merge props values)))

(defn parse-circle
  [props svg-data]
  (let [values (->> (select-keys svg-data [:cx :cy :rx :ry])
                    (d/mapm (fn [_ val] (d/parse-double val))))]
    (-> props
        (assoc :x (- (:cx values) (:rx values))
               :y (- (:cy values) (:ry values))
               :width (* (:rx values) 2)
               :height (* (:ry values) 2)))))

(defn parse-path
  [props center svg-data]
  (let [content (svg.path/parse (:d svg-data))]
    (-> props
        (assoc :content content)
        (assoc :center center))))

(defn parse-stops
  [gradient-node]
  (->> gradient-node
       (node-seq)
       (filter #(= :stop (:tag %)))
       (mapv (fn [{{:keys [offset stop-color stop-opacity]} :attrs}]
               {:color stop-color
                :opacity (d/parse-double stop-opacity)
                :offset (d/parse-double offset)}))))

(defn parse-gradient
  [node ref-url]
  (let [[_ url] (re-find url-regex ref-url)
        gradient-node (->> node (node-seq) (find-node-by-id url))
        stops (parse-stops gradient-node)]

    (when (contains? (:attrs gradient-node) :penpot:gradient)
      (cond-> {:stops stops}
        (= :linearGradient (:tag gradient-node))
        (assoc :type :linear
               :start-x (-> gradient-node :attrs :x1 d/parse-double)
               :start-y (-> gradient-node :attrs :y1 d/parse-double)
               :end-x   (-> gradient-node :attrs :x2 d/parse-double)
               :end-y   (-> gradient-node :attrs :y2 d/parse-double)
               :width   1)

        (= :radialGradient (:tag gradient-node))
        (assoc :type :radial
               :start-x (get-meta gradient-node :start-x d/parse-double)
               :start-y (get-meta gradient-node :start-y d/parse-double)
               :end-x   (get-meta gradient-node :end-x   d/parse-double)
               :end-y   (get-meta gradient-node :end-y   d/parse-double)
               :width   (get-meta gradient-node :width   d/parse-double))))))

(defn add-svg-position [props node]
  (let [svg-content (get-data node :penpot:svg-content)]
    (cond-> props
      (contains? (:attrs svg-content) :penpot:x)
      (assoc :x (-> svg-content :attrs :penpot:x d/parse-double))

      (contains? (:attrs svg-content) :penpot:y)
      (assoc :y (-> svg-content :attrs :penpot:y d/parse-double))

      (contains? (:attrs svg-content) :penpot:width)
      (assoc :width (-> svg-content :attrs :penpot:width d/parse-double))

      (contains? (:attrs svg-content) :penpot:height)
      (assoc :height (-> svg-content :attrs :penpot:height d/parse-double)))))

(defn add-common-data
  [props node]

  (let [name              (get-meta node :name)
        blocked           (get-meta node :blocked str->bool)
        hidden            (get-meta node :hidden str->bool)
        transform         (get-meta node :transform gmt/str->matrix)
        transform-inverse (get-meta node :transform-inverse gmt/str->matrix)
        flip-x            (get-meta node :flip-x str->bool)
        flip-y            (get-meta node :flip-y str->bool)
        proportion        (get-meta node :proportion d/parse-double)
        proportion-lock   (get-meta node :proportion-lock str->bool)
        rotation          (get-meta node :rotation d/parse-double)
        constraints-h     (get-meta node :constraints-h keyword)
        constraints-v     (get-meta node :constraints-v keyword)
        fixed-scroll      (get-meta node :fixed-scroll str->bool)]

    (-> props
        (assoc :name name)
        (assoc :blocked blocked)
        (assoc :hidden hidden)
        (assoc :flip-x flip-x)
        (assoc :flip-y flip-y)
        (assoc :proportion proportion)
        (assoc :proportion-lock proportion-lock)
        (assoc :rotation rotation)

        (cond-> (some? transform)
          (assoc :transform transform))

        (cond-> (some? transform-inverse)
          (assoc :transform-inverse transform-inverse))

        (cond-> (some? constraints-h)
          (assoc :constraints-h constraints-h))

        (cond-> (some? constraints-v)
          (assoc :constraints-v constraints-v))

        (cond-> (some? fixed-scroll)
          (assoc :fixed-scroll fixed-scroll)))))

(defn add-position
  [props type node svg-data]
  (let [center-x (get-meta node :center-x d/parse-double)
        center-y (get-meta node :center-y d/parse-double)
        center (gpt/point center-x center-y)]
    (cond-> props
      (has-position? type)
      (parse-position node svg-data)

      (= type :svg-raw)
      (add-svg-position node)

      (= type :circle)
      (parse-circle svg-data)

      (= type :path)
      (parse-path center svg-data))))

(defn add-library-refs
  [props node]

  (let [stroke-color-ref-id   (get-meta node :stroke-color-ref-id uuid/uuid)
        stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid)
        component-id          (get-meta node :component-id uuid/uuid)
        component-file        (get-meta node :component-file uuid/uuid)
        shape-ref             (get-meta node :shape-ref uuid/uuid)
        component-root?       (get-meta node :component-root str->bool)
        main-instance?        (get-meta node :main-instance str->bool)]

    (cond-> props
      (some? stroke-color-ref-id)
      (assoc :stroke-color-ref-id stroke-color-ref-id
             :stroke-color-ref-file stroke-color-ref-file)

      (some? component-id)
      (assoc :component-id component-id
             :component-file component-file)

      component-root?
      (assoc :component-root component-root?)

      main-instance?
      (assoc :main-instance main-instance?)

      (some? shape-ref)
      (assoc :shape-ref shape-ref))))

(defn add-fill
  [props node svg-data]

  (let [fill (:fill svg-data)
        fill-color-ref-id        (get-meta node :fill-color-ref-id uuid/uuid)
        fill-color-ref-file      (get-meta node :fill-color-ref-file uuid/uuid)
        meta-fill-color          (get-meta node :fill-color)
        meta-fill-opacity        (get-meta node :fill-opacity)
        meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url#fill-color-gradient")
                                   (parse-gradient node meta-fill-color)
                                   (get-meta node :fill-color-gradient))
        gradient                 (when (str/starts-with? fill "url")
                                   (parse-gradient node fill))]

    (cond-> props
      :always
      (assoc :fill-color nil
             :fill-opacity nil)

      (some? meta-fill-color)
      (assoc :fill-color meta-fill-color
             :fill-opacity (d/parse-double meta-fill-opacity))

      (some? meta-fill-color-gradient)
      (assoc :fill-color-gradient meta-fill-color-gradient
             :fill-color nil
             :fill-opacity nil)

      (some? gradient)
      (assoc :fill-color-gradient gradient
             :fill-color nil
             :fill-opacity nil)

      (cc/valid-hex-color? fill)
      (assoc :fill-color fill
             :fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double))

      (some? fill-color-ref-id)
      (assoc :fill-color-ref-id fill-color-ref-id
             :fill-color-ref-file fill-color-ref-file))))

(defn add-stroke
  [props node svg-data]

  (let [stroke-style     (get-meta node :stroke-style keyword)
        stroke-alignment (get-meta node :stroke-alignment keyword)
        stroke           (:stroke svg-data)
        gradient         (when (str/starts-with? stroke "url(#stroke-color-gradient")
                           (parse-gradient node stroke))

        stroke-cap-start (get-meta node :stroke-cap-start keyword)
        stroke-cap-end   (get-meta node :stroke-cap-end keyword)]

    (cond-> props
      :always
      (assoc :stroke-alignment stroke-alignment
             :stroke-style     stroke-style
             :stroke-color     (-> svg-data :stroke)
             :stroke-opacity   (-> svg-data :stroke-opacity d/parse-double)
             :stroke-width     (-> svg-data :stroke-width d/parse-double))

      (some? gradient)
      (assoc :stroke-color-gradient  gradient
             :stroke-color nil
             :stroke-opacity nil)

      (= stroke-alignment :inner)
      (update :stroke-width / 2)

      (some? stroke-cap-start)
      (assoc :stroke-cap-start stroke-cap-start)

      (some? stroke-cap-end)
      (assoc :stroke-cap-end stroke-cap-end))))

(defn add-radius-data
  [props node svg-data]
  (let [r1 (get-meta node :r1 d/parse-double)
        r2 (get-meta node :r2 d/parse-double)
        r3 (get-meta node :r3 d/parse-double)
        r4 (get-meta node :r4 d/parse-double)

        rx (-> (get svg-data :rx 0) d/parse-double)
        ry (-> (get svg-data :ry 0) d/parse-double)]

    (cond-> props
      (some? r1)
      (assoc :r1 r1 :r2 r2 :r3 r3 :r4 r4
             :rx nil :ry nil)

      (and (nil? r1) (some? rx))
      (assoc :rx rx :ry ry))))

(defn add-image-data
  [props type node]
  (let [metadata {:id     (get-meta node :media-id)
                  :width  (get-meta node :media-width)
                  :height (get-meta node :media-height)
                  :mtype  (get-meta node :media-mtype)
                  :keep-aspect-ratio  (get-meta node :media-keep-aspect-ratio str->bool)}]
    (cond-> props
      (= type :image)
      (assoc :metadata metadata)

      (not= type :image)
      (assoc :fill-image metadata))))

(defn add-text-data
  [props node]
  (-> props
      (assoc :grow-type     (get-meta node :grow-type keyword))
      (assoc :content       (get-meta node :content (comp string->uuid json/decode)))
      (assoc :position-data (get-meta node :position-data (comp string->uuid json/decode)))))

(defn add-group-data
  [props node]
  (let [mask? (get-meta node :masked-group str->bool)]
    (cond-> props
      mask?
      (assoc :masked-group true))))

(defn add-bool-data
  [props node]
  (-> props
      (assoc :bool-type (get-meta node :bool-type keyword))))

(defn parse-shadow [node]
  {:id       (uuid/next)
   :style    (get-meta node :shadow-type keyword)
   :hidden   (get-meta node :hidden str->bool)
   :color    {:color (get-meta node :color)
              :opacity (get-meta node :opacity d/parse-double)}
   :offset-x (get-meta node :offset-x d/parse-double)
   :offset-y (get-meta node :offset-y d/parse-double)
   :blur     (get-meta node :blur d/parse-double)
   :spread   (get-meta node :spread d/parse-double)})

(defn parse-blur [node]
  {:id       (uuid/next)
   :type     (get-meta node :blur-type keyword)
   :hidden   (get-meta node :hidden str->bool)
   :value    (get-meta node :value d/parse-double)})

(defn parse-export [node]
  {:type   (get-meta node :type keyword)
   :suffix (get-meta node :suffix)
   :scale  (get-meta node :scale d/parse-double)})

(defn parse-grid-node [node]
  (let [attrs (-> node :attrs remove-penpot-prefix)
        color {:color (:color attrs)
               :opacity (-> attrs :opacity d/parse-double)}

        params (-> (dissoc attrs :color :opacity :display :type)
                   (d/update-when :size d/parse-double)
                   (d/update-when :item-length d/parse-double)
                   (d/update-when :gutter d/parse-double)
                   (d/update-when :margin d/parse-double)
                   (assoc :color color))]
    {:type    (-> attrs :type keyword)
     :display (-> attrs :display str->bool)
     :params  params}))

(defn parse-grids [node]
  (let [grids-node (get-data node :penpot:grids)]
    (->> grids-node :content (mapv parse-grid-node))))

(defn parse-flow-node [node]
  (let [attrs (-> node :attrs remove-penpot-prefix)]
    {:id             (uuid/next)
     :name           (-> attrs :name)
     :starting-frame (-> attrs :starting-frame uuid)}))

(defn parse-flows [node]
  (let [flows-node (get-data node :penpot:flows)]
    (->> flows-node :content (mapv parse-flow-node))))

(defn parse-guide-node [node]
  (let [attrs (-> node :attrs remove-penpot-prefix)
        id (uuid/next)]
    [id
     {:id       id
      :frame-id (when (:frame-id attrs) (-> attrs :frame-id uuid))
      :axis     (-> attrs :axis keyword)
      :position (-> attrs :position d/parse-double)}]))

(defn parse-guides [node]
  (let [guides-node (get-data node :penpot:guides)]
    (->> guides-node :content (map parse-guide-node) (into {}))))

(defn extract-from-data
  ([node tag]
   (extract-from-data node tag identity))

  ([node tag parse-fn]
   (let [shape-data (get-data node)]
     (->> shape-data
          (node-seq)
          (filter #(= (:tag %) tag))
          (mapv parse-fn)))))

(defn add-shadows
  [props node]
  (let [shadows (extract-from-data node :penpot:shadow parse-shadow)]
    (cond-> props
      (d/not-empty? shadows)
      (assoc :shadow shadows))))

(defn add-blur
  [props node]
  (let [blur (->> (extract-from-data node :penpot:blur parse-blur) (first))]
    (cond-> props
      (some? blur)
      (assoc :blur blur))))

(defn add-exports
  [props node]
  (let [exports (extract-from-data node :penpot:export parse-export)]
    (cond-> props
      (d/not-empty? exports)
      (assoc :exports exports))))

(defn add-layer-options
  [props svg-data]
  (let [blend-mode (get svg-data :mix-blend-mode)
        opacity (-> (get svg-data :opacity) d/parse-double)]

    (cond-> props
      (some? blend-mode)
      (assoc :blend-mode (keyword blend-mode))

      (some? opacity)
      (assoc :opacity opacity))))

(defn remove-prefix [s]
  (cond-> s
    (string? s)
    (str/replace uuid-regex-prefix "")))

(defn get-svg-attrs
  [svg-import svg-data svg-attrs]
  (let [process-attr
        (fn [acc prop]
          (cond
            (and (= prop "style")
                 (contains? (:attrs svg-import) :penpot:svg-style))
            (let [style (get-in svg-import [:attrs :penpot:svg-style])]
              (assoc acc :style (parse-style style)))

            (and (= prop "filter")
                 (contains? (:attrs svg-import) :penpot:svg-filter))
            (let [style (get-in svg-import [:attrs :penpot:svg-filter])]
              (assoc acc :filter (parse-style style)))

            :else
            (let [key (keyword prop)]
              (if-let [v (or (get svg-data key)
                             (get-in svg-data [:attrs key]))]
                (assoc acc key (remove-prefix v))
                acc))))]
    (->> (str/split svg-attrs ",")
         (reduce process-attr {}))))

(defn get-svg-defs
  [node]

  (let [svg-import (get-data node :penpot:svg-import)]
    (->> svg-import
         :content
         (filter #(= (:tag %) :penpot:svg-def))
         (map #(vector (-> % :attrs :def-id)
                       (-> % :content first)))
         (into {}))))

(defn add-svg-attrs
  [props node svg-data]

  (let [svg-import (get-data node :penpot:svg-import)]
    (if (some? svg-import)
      (let [svg-attrs (get-in svg-import [:attrs :penpot:svg-attrs])
            svg-defs (get-in svg-import [:attrs :penpot:svg-defs])
            svg-transform (get-in svg-import [:attrs :penpot:svg-transform])
            viewbox-x (get-in svg-import [:attrs :penpot:svg-viewbox-x])
            viewbox-y (get-in svg-import [:attrs :penpot:svg-viewbox-y])
            viewbox-width (get-in svg-import [:attrs :penpot:svg-viewbox-width])
            viewbox-height (get-in svg-import [:attrs :penpot:svg-viewbox-height])]

        (cond-> props
          :true
          (assoc :svg-attrs (get-svg-attrs svg-import svg-data svg-attrs))

          (some? viewbox-x)
          (assoc :svg-viewbox {:x      (d/parse-double viewbox-x)
                               :y      (d/parse-double viewbox-y)
                               :width  (d/parse-double viewbox-width)
                               :height (d/parse-double viewbox-height)})

          (some? svg-transform)
          (assoc :svg-transform (gmt/str->matrix svg-transform))


          (some? svg-defs)
          (assoc :svg-defs (get-svg-defs node))))

      props)))

(defn parse-fills
  [node svg-data]
  (let [fills-node (get-data node :penpot:fills)
        images (:images node)
        fills (->> (find-all-nodes fills-node :penpot:fill)
                   (mapv (fn [fill-node]
                           (let [fill-image-id (get-meta fill-node :fill-image-id)]
                             {:fill-color  (when (not (str/starts-with? (get-meta fill-node :fill-color) "url"))
                                             (get-meta fill-node :fill-color))
                              :fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url(#fill-color-gradient")
                                                     (parse-gradient node (get-meta fill-node :fill-color)))
                              :fill-image (when fill-image-id
                                            (get images fill-image-id))
                              :fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid)
                              :fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid)
                              :fill-opacity (get-meta fill-node :fill-opacity d/parse-double)})))
                   (mapv d/without-nils)
                   (filterv #(not= (:fill-color %) "none")))]

    (if (seq fills)
      fills
      (->> [(-> (add-fill {} node svg-data)
                (d/without-nils))]
           (filterv #(and (not-empty %) (not= (:fill-color %) "none")))))))

(defn parse-strokes
  [node svg-data]
  (let [strokes-node (get-data node :penpot:strokes)
        images (:images node)
        strokes (->> (find-all-nodes strokes-node :penpot:stroke)
                     (mapv (fn [stroke-node]
                             (let [stroke-image-id (get-meta stroke-node :stroke-image-id)]
                               {:stroke-color  (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url"))
                                                 (get-meta stroke-node :stroke-color))
                                :stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url(#stroke-color-gradient")
                                                         (parse-gradient node (get-meta stroke-node :stroke-color)))
                                :stroke-image (when stroke-image-id
                                                (get images stroke-image-id))
                                :stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid)
                                :stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid)
                                :stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double)
                                :stroke-style (get-meta stroke-node :stroke-style keyword)
                                :stroke-width (get-meta stroke-node :stroke-width d/parse-double)
                                :stroke-alignment (get-meta stroke-node :stroke-alignment keyword)
                                :stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword)
                                :stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)})))
                     (mapv d/without-nils)
                     (filterv #(not= (:stroke-color %) "none")))]

    (if (seq strokes)
      strokes
      (->> [(-> (add-stroke {} node svg-data)
                (d/without-nils))]
           (filterv #(and (not-empty %) (not= (:stroke-color %) "none") (not= (:stroke-style %) :none)))))))

(defn add-svg-content
  [props node]
  (let [svg-content (get-data node :penpot:svg-content)
        attrs (-> (:attrs svg-content) (without-penpot-prefix)
                  (d/update-when :style parse-style))
        tag (-> svg-content :attrs :penpot:tag keyword)

        node-content
        (cond
          (= tag :svg)
          (->> node :content last :content last :content fix-style-attr)

          (some? (:content svg-content))
          (->> (:content svg-content)
               (filter #(= :penpot:svg-child (:tag %)))
               (mapv :content)
               (first)))]
    (-> props
        (assoc
         :content
         {:attrs   attrs
          :tag     tag
          :content node-content}))))

(defn add-frame-data [props node]
  (let [grids (parse-grids node)
        show-content (get-meta node :show-content str->bool)
        hide-in-viewer (get-meta node :hide-in-viewer str->bool)
        use-for-thumbnail (get-meta node :use-for-thumbnail str->bool)]
    (-> props
        (assoc :show-content show-content)
        (assoc :hide-in-viewer hide-in-viewer)
        (cond-> use-for-thumbnail
          (assoc :use-for-thumbnail use-for-thumbnail))
        (cond-> (d/not-empty? grids)
          (assoc :grids grids)))))

(defn get-stroke-images-data
  [node]
  (let [strokes
        (-> node
            (find-node :penpot:shape)
            (find-node :penpot:strokes))]
    (->> (find-all-nodes strokes :penpot:stroke)
         (mapv (fn [stroke-node]
                 (let [id (get-in stroke-node [:attrs :penpot:stroke-image-id])
                       image-node (->> node (node-seq) (find-node-by-id id))]
                   {:id id
                    :href (get-in image-node [:attrs :href])})))
         (filterv #(some? (:id %))))))

(defn has-stroke-images?
  [node]
  (let [stroke-images (get-stroke-images-data node)]
    (> (count stroke-images) 0)))

(defn has-image?
  [node]
  (let [type (get-type node)
        pattern-image
        (-> node
            (find-node :defs)
            (find-node :pattern)
            (find-node :g)
            (find-node :g)
            (find-node :image))]

    (or (= type :image)
        (some? pattern-image))))

(defn get-image-name
  [node]
  (get-meta node :name))

(defn get-image-data
  [node]
  (let [pattern-data
        (-> node
            (find-node :defs)
            (find-node :pattern)
            (find-node :g)
            (find-node :g)
            (find-node :image)
            :attrs)
        image-data (get-svg-data :image node)
        svg-data (or pattern-data image-data)]
    (or (:href svg-data) (:xlink:href svg-data))))

(defn get-fill-images-data
  [node]
  (let [fills
        (-> node
            (find-node :penpot:shape)
            (find-node :penpot:fills))]
    (->> (find-all-nodes fills :penpot:fill)
         (mapv (fn [fill-node]
                 (let [id (get-in fill-node [:attrs :penpot:fill-image-id])
                       image-node (->> node (node-seq) (find-node-by-id id))]
                   {:id id
                    :href (get-in image-node [:attrs :href])
                    :keep-aspect-ratio (not= (get-in image-node [:attrs :preserveAspectRatio]) "none")})))
         (filterv #(some? (:id %))))))

(defn has-fill-images?
  [node]
  (let [fill-images (get-fill-images-data node)]
    (> (count fill-images) 0)))

(defn get-image-fill
  [node]
  (let [linear-gradient-node (-> node
                                 (find-node :defs)
                                 (find-node :linearGradient))
        radial-gradient-node (-> node
                                 (find-node :defs)
                                 (find-node :radialGradient))
        gradient-node (or linear-gradient-node radial-gradient-node)
        stops (parse-stops gradient-node)
        gradient (cond-> {:stops stops}
                   (some? linear-gradient-node)
                   (assoc :type :linear
                          :start-x (-> linear-gradient-node :attrs :x1 d/parse-double)
                          :start-y (-> linear-gradient-node :attrs :y1 d/parse-double)
                          :end-x   (-> linear-gradient-node :attrs :x2 d/parse-double)
                          :end-y   (-> linear-gradient-node :attrs :y2 d/parse-double)
                          :width   1)

                   (some? radial-gradient-node)
                   (assoc :type :linear
                          :start-x (get-meta radial-gradient-node :start-x d/parse-double)
                          :start-y (get-meta radial-gradient-node :start-y d/parse-double)
                          :end-x   (get-meta radial-gradient-node :end-x   d/parse-double)
                          :end-y   (get-meta radial-gradient-node :end-y   d/parse-double)
                          :width   (get-meta radial-gradient-node :width   d/parse-double)))]

    (if (some? (or linear-gradient-node radial-gradient-node))
      {:fill-color-gradient gradient}
      (-> node
          (find-node :defs)
          (find-node :pattern)
          (find-node :g)
          (find-node :rect)
          :attrs
          :style
          parse-style))))

(defn parse-grid-tracks
  [node label]
  (let [node (-> (get-data node :penpot:layout)
                 (find-node label))]
    (->> node
         :content
         (mapv
          (fn [track-node]
            (let [{:keys [type value]} (-> track-node :attrs remove-penpot-prefix)]
              {:type (keyword type)
               :value (d/parse-double value)}))))))

(defn parse-grid-cells
  [node]
  (let [node (-> (get-data node :penpot:layout)
                 (find-node :penpot:grid-cells))]
    (->> node
         :content
         (mapv
          (fn [cell-node]
            (let [{:keys [id
                          area-name
                          row
                          row-span
                          column
                          column-span
                          position
                          align-self
                          justify-self
                          shapes]} (-> cell-node :attrs remove-penpot-prefix)
                  id (uuid/uuid id)]
              [id (d/without-nils
                   {:id id
                    :area-name area-name
                    :row (d/parse-integer  row)
                    :row-span (d/parse-integer row-span)
                    :column (d/parse-integer column)
                    :column-span (d/parse-integer column-span)
                    :position (keyword position)
                    :align-self (keyword align-self)
                    :justify-self (keyword justify-self)
                    :shapes (if (and (some? shapes) (d/not-empty? shapes))
                              (->> (str/split shapes " ")
                                   (mapv uuid/uuid))
                              [])})])))
         (into {}))))

(defn add-layout-container-data [props node]
  (if-let [data (get-data node :penpot:layout)]
    (let [layout-grid-rows (parse-grid-tracks node :penpot:grid-rows)
          layout-grid-columns (parse-grid-tracks node :penpot:grid-columns)
          layout-grid-cells (parse-grid-cells node)]
      (-> props
          (merge
           (d/without-nils
            {:layout (get-meta data :layout keyword)
             :layout-flex-dir (get-meta data :layout-flex-dir keyword)
             :layout-grid-dir (get-meta data :layout-grid-dir keyword)
             :layout-wrap-type (get-meta data :layout-wrap-type keyword)

             :layout-gap-type (get-meta data :layout-gap-type keyword)
             :layout-gap
             (d/without-nils
              {:row-gap (get-meta data :layout-gap-row d/parse-double)
               :column-gap (get-meta data :layout-gap-column d/parse-double)})

             :layout-padding-type (get-meta data :layout-padding-type keyword)
             :layout-padding
             (d/without-nils
              {:p1 (get-meta data :layout-padding-p1 d/parse-double)
               :p2 (get-meta data :layout-padding-p2 d/parse-double)
               :p3 (get-meta data :layout-padding-p3 d/parse-double)
               :p4 (get-meta data :layout-padding-p4 d/parse-double)})

             :layout-justify-items (get-meta data :layout-justify-items keyword)
             :layout-justify-content (get-meta data :layout-justify-content keyword)
             :layout-align-items (get-meta data :layout-align-items keyword)
             :layout-align-content (get-meta data :layout-align-content keyword)}))

          (cond-> (d/not-empty? layout-grid-rows)
            (assoc :layout-grid-rows layout-grid-rows))
          (cond-> (d/not-empty? layout-grid-columns)
            (assoc :layout-grid-columns layout-grid-columns))
          (cond-> (d/not-empty? layout-grid-cells)
            (assoc :layout-grid-cells layout-grid-cells))))
    props))

(defn add-layout-item-data [props node]
  (if-let [data (get-data node :penpot:layout-item)]
    (merge props
           (d/without-nils
            {:layout-item-margin
             (d/without-nils
              {:m1 (get-meta data :layout-item-margin-m1 d/parse-double)
               :m2 (get-meta data :layout-item-margin-m2 d/parse-double)
               :m3 (get-meta data :layout-item-margin-m3 d/parse-double)
               :m4 (get-meta data :layout-item-margin-m4 d/parse-double)})

             :layout-item-margin-type (get-meta data :layout-item-margin-type keyword)
             :layout-item-h-sizing (get-meta data :layout-item-h-sizing keyword)
             :layout-item-v-sizing (get-meta data :layout-item-v-sizing keyword)
             :layout-item-max-h (get-meta data :layout-item-max-h d/parse-double)
             :layout-item-min-h (get-meta data :layout-item-min-h d/parse-double)
             :layout-item-max-w (get-meta data :layout-item-max-w d/parse-double)
             :layout-item-min-w (get-meta data :layout-item-min-w d/parse-double)
             :layout-item-align-self (get-meta data :layout-item-align-self keyword)
             :layout-item-align-absolute (get-meta data :layout-item-align-absolute str->bool)
             :layout-item-align-index (get-meta data :layout-item-align-index d/parse-double)}))
    props))

(defn parse-data
  [type node]

  (when-not (close? node)
    (let [svg-data (get-svg-data type node)]
      (-> {}
          (add-common-data node)
          (add-position type node svg-data)

          (add-layer-options svg-data)
          (add-shadows node)
          (add-blur node)
          (add-exports node)
          (add-svg-attrs node svg-data)
          (add-library-refs node)

          (assoc :fills (parse-fills node svg-data))
          (assoc :strokes (parse-strokes node svg-data))

          (assoc :hide-fill-on-export      (get-meta node :hide-fill-on-export str->bool))

          (cond-> (= :svg-raw type)
            (add-svg-content node))

          (cond-> (= :frame type)
            (-> (add-frame-data node)
                (add-layout-container-data node)))

          (add-layout-item-data node)

          (cond-> (= :group type)
            (add-group-data node))

          (cond-> (or (= :frame type) (= :rect type))
            (add-radius-data node svg-data))

          (cond-> (some? (get-in node [:attrs :penpot:media-id]))
            (->
             (add-radius-data node svg-data)
             (add-image-data type node)))

          (cond-> (= :text type)
            (add-text-data node))

          (cond-> (= :bool type)
            (add-bool-data node))))))

(defn parse-page-data
  [node]
  (let [style      (parse-style (get-in node [:attrs :style]))
        background (:background style)
        grids      (->> (parse-grids node)
                        (group-by :type)
                        (d/mapm (fn [_ v] (-> v first :params))))
        flows      (parse-flows node)
        guides     (parse-guides node)]
    (cond-> {}
      (some? background)
      (assoc-in [:options :background] background)

      (d/not-empty? grids)
      (assoc-in [:options :saved-grids] grids)

      (d/not-empty? flows)
      (assoc-in [:options :flows] flows)

      (d/not-empty? guides)
      (assoc-in [:options :guides] guides))))

(defn parse-interactions
  [node]
  (let [interactions-node (get-data node :penpot:interactions)]
    (->> (find-all-nodes interactions-node :penpot:interaction)
         (mapv (fn [node]
                 (let [interaction {:event-type  (get-meta node :event-type keyword)
                                    :action-type (get-meta node :action-type keyword)}]
                   (cond-> interaction
                     (ctsi/has-delay interaction)
                     (assoc :delay (get-meta node :delay d/parse-double))

                     (ctsi/has-destination interaction)
                     (assoc :destination     (get-meta node :destination uuid/uuid)
                            :preserve-scroll (get-meta node :preserve-scroll str->bool))

                     (ctsi/has-url interaction)
                     (assoc :url (get-meta node :url str))

                     (ctsi/has-overlay-opts interaction)
                     (assoc :overlay-pos-type    (get-meta node :overlay-pos-type keyword)
                            :overlay-position    (gpt/point
                                                  (get-meta node :overlay-position-x d/parse-double)
                                                  (get-meta node :overlay-position-y d/parse-double))
                            :close-click-outside (get-meta node :close-click-outside str->bool)
                            :background-overlay  (get-meta node :background-overlay str->bool)))))))))

