;; 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.plugins.parser
  (:require
   [app.common.data :as d]
   [app.common.geom.point :as gpt]
   [app.common.json :as json]
   [app.common.types.path :as path]
   [app.common.uuid :as uuid]
   [app.util.object :as obj]
   [cuerdas.core :as str]))

(defn parse-id
  [id]
  (when id (uuid/parse id)))

(defn parse-keyword
  [kw]
  (when kw (keyword kw)))

(defn parse-hex
  [color]
  (if (string? color) (-> color str/lower) color))

(defn parse-point
  "Parses a point-like JS object into a `gpt/point` record.

  The schema for shape interactions (`schema:open-overlay-interaction`,
  `::gpt/point`) requires a Point record — returning a plain map caused
  plugin `addInteraction` calls with an `open-overlay` action and a
  `manualPositionLocation` to be silently rejected. See issue #8409."
  [^js point]
  (when point
    (gpt/point (obj/get point "x")
               (obj/get point "y"))))

(defn parse-shape-type
  [type]
  (case type
    "board"     :frame
    "boolean"   :bool
    "rectangle" :rect
    "ellipse"   :circle
    (parse-keyword type)))

;; {
;;    name?: string;
;;    nameLike?: string;
;;    type?:
;;      | 'board'
;;      | 'group'
;;      | 'boolean'
;;      | 'rectangle'
;;      | 'path'
;;      | 'text'
;;      | 'ellipse'
;;      | 'svg-raw'
;;      | 'image';
;;  }
(defn parse-criteria
  [^js criteria]
  (when (some? criteria)
    (d/without-nils
     {:name (obj/get criteria "name")
      :name-like (obj/get criteria "nameLike")
      :type (-> (obj/get criteria "type") parse-shape-type)})))

;;export type ImageData = {
;;  name?: string;
;;  width: number;
;;  height: number;
;;  mtype?: string;
;;  id: string;
;;  keepAspectRatio?: boolean;
;;}
(defn parse-image-data
  [^js image-data]
  (when (some? image-data)
    (d/without-nils
     {:id (-> (obj/get image-data "id") parse-id)
      :name (obj/get image-data "name")
      :width (obj/get image-data "width")
      :height (obj/get image-data "height")
      :mtype (obj/get image-data "mtype")
      :keep-aspect-ratio (obj/get image-data "keepAspectRatio")})))

;; export type Gradient = {
;;   type: 'linear' | 'radial';
;;   startX: number;
;;   startY: number;
;;   endX: number;
;;   endY: number;
;;   width: number;
;;   stops: Array<{ color: string; opacity?: number; offset: number }>;
;; }
(defn parse-gradient-stop
  [^js stop]
  (when (some? stop)
    (d/without-nils
     {:color (-> (obj/get stop "color") parse-hex)
      :opacity (obj/get stop "opacity")
      :offset (obj/get stop "offset")})))

(defn parse-gradient
  [^js gradient]
  (when (some? gradient)
    (d/without-nils
     {:type (-> (obj/get gradient "type") parse-keyword)
      :start-x (obj/get gradient "startX")
      :start-y (obj/get gradient "startY")
      :end-x (obj/get gradient "endX")
      :end-y (obj/get gradient "endY")
      :width (obj/get gradient "width")
      :stops (->> (obj/get gradient "stops")
                  (mapv parse-gradient-stop))})))

;; export interface Color {
;;   id?: string;
;;   fileId?: string;
;;   refId?: string; // deprecated
;;   refFile?: string; // deprecated
;;   name?: string;
;;   path?: string;
;;   color?: string;
;;   opacity?: number;
;;   gradient?: Gradient;
;;   image?: ImageData;
;; }
(defn parse-color-data
  [^js color]
  (when (some? color)
    (let [id (or (obj/get color "id") (obj/get color "refId"))
          file-id (or (obj/get color "fileId") (obj/get color "refFile"))]
      (d/without-nils
       {:id (parse-id id)
        :file-id (parse-id file-id)
        :color (-> (obj/get color "color") parse-hex)
        :opacity (obj/get color "opacity")
        :gradient (-> (obj/get color "gradient") parse-gradient)
        :image (-> (obj/get color "image") parse-image-data)}))))

(defn parse-color
  [^js color]
  (when (some? color)
    (d/without-nils
     (-> (parse-color-data color)
         (assoc :name (obj/get color "name")
                :path (obj/get color "path"))))))

;; export interface Shadow {
;;   id?: string;
;;   style?: 'drop-shadow' | 'inner-shadow';
;;   offsetX?: number;
;;   offsetY?: number;
;;   blur?: number;
;;   spread?: number;
;;   hidden?: boolean;
;;   color?: Color;
;; }
(defn parse-shadow
  [^js shadow]
  (when (some? shadow)
    (d/without-nils
     {:id (-> (obj/get shadow "id") parse-id)
      :style (-> (obj/get shadow "style") parse-keyword)
      :offset-x (obj/get shadow "offsetX")
      :offset-y (obj/get shadow "offsetY")
      :blur (obj/get shadow "blur")
      :spread (obj/get shadow "spread")
      :hidden (obj/get shadow "hidden")
      :color (-> (obj/get shadow "color") parse-color)})))

(defn parse-shadows
  [^js shadows]
  (when (some? shadows)
    (into [] (map parse-shadow) shadows)))

;;export interface Fill {
;;  fillColor?: string;
;;  fillOpacity?: number;
;;  fillColorGradient?: Gradient;
;;  fillColorRefFile?: string;
;;  fillColorRefId?: string;
;;  fillImage?: ImageData;
;;}
(defn parse-fill
  [^js fill]
  (when (some? fill)
    (d/without-nils
     {:fill-color (-> (obj/get fill "fillColor") parse-hex)
      :fill-opacity (obj/get fill "fillOpacity")
      :fill-color-gradient (-> (obj/get fill "fillColorGradient") parse-gradient)
      :fill-color-ref-file (-> (obj/get fill "fillColorRefFile") parse-id)
      :fill-color-ref-id (-> (obj/get fill "fillColorRefId") parse-id)
      :fill-image (-> (obj/get fill "fillImage") parse-image-data)})))

(defn parse-fills
  [^js fills]
  (when (some? fills)
    (into [] (map parse-fill) fills)))

;; export interface Stroke {
;;   strokeColor?: string;
;;   strokeColorRefFile?: string;
;;   strokeColorRefId?: string;
;;   strokeOpacity?: number;
;;   strokeStyle?: 'solid' | 'dotted' | 'dashed' | 'mixed' | 'none' | 'svg';
;;   strokeWidth?: number;
;;   strokeAlignment?: 'center' | 'inner' | 'outer';
;;   strokeCapStart?: StrokeCap;
;;   strokeCapEnd?: StrokeCap;
;;   strokeColorGradient?: Gradient;
;;   strokeImage?: ImageData;
;; }
(defn parse-stroke
  [^js stroke]
  (when (some? stroke)
    (d/without-nils
     {:stroke-color (-> (obj/get stroke "strokeColor") parse-hex)
      :stroke-color-ref-file (-> (obj/get stroke "strokeColorRefFile") parse-id)
      :stroke-color-ref-id (-> (obj/get stroke "strokeColorRefId") parse-id)
      :stroke-opacity (obj/get stroke "strokeOpacity")
      :stroke-style (-> (obj/get stroke "strokeStyle") parse-keyword)
      :stroke-width (obj/get stroke "strokeWidth")
      :stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword)
      :stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword)
      :stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword)
      :stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)
      :stroke-image (-> (obj/get stroke "strokeImage") parse-image-data)})))

(defn parse-strokes
  [^js strokes]
  (when (some? strokes)
    (into [] (map parse-stroke) strokes)))

;; export interface Blur {
;;   id?: string;
;;   value?: number;
;;   hidden?: boolean;
;; }
(defn parse-blur
  [^js blur]
  (when (some? blur)
    (d/without-nils
     {:id (-> (obj/get blur "id") parse-id)
      :value (obj/get blur "value")
      :hidden (obj/get blur "hidden")})))


;; export interface Export {
;;   type: 'png' | 'jpeg' | 'webp' | 'svg' | 'pdf';
;;   scale: number;
;;   suffix: string;
;; }
(defn parse-export
  [^js export]
  (when (some? export)
    (d/without-nils
     {:type (-> (obj/get export "type") parse-keyword)
      :scale (obj/get export "scale" 1)
      :suffix (obj/get export "suffix" "")
      :skip-children (obj/get export "skipChildren" false)})))

(defn parse-exports
  [^js exports]
  (when (some? exports)
    (into [] (map parse-export) exports)))

;; export interface GuideColumnParams {
;;   color: { color: string; opacity: number };
;;   type?: 'stretch' | 'left' | 'center' | 'right';
;;   size?: number;
;;   margin?: number;
;;   itemLength?: number;
;;   gutter?: number;
;; }
(defn parse-frame-guide-column-params
  [^js params]
  (when params
    (d/without-nils
     {:color (-> (obj/get params "color") parse-color)
      :type (-> (obj/get params "type") parse-keyword)
      :size (obj/get params "size")
      :margin (obj/get params "margin")
      :item-length (obj/get params "itemLength")
      :gutter (obj/get params "gutter")})))

;; export interface GuideColumn {
;;   type: 'column';
;;   display: boolean;
;;   params: GuideColumnParams;
;; }
(defn parse-frame-guide-column
  [^js guide]
  (when guide
    (d/without-nils
     {:type (-> (obj/get guide "type") parse-keyword)
      :display (obj/get guide "display")
      :params (-> (obj/get guide "params") parse-frame-guide-column-params)})))

;; export interface GuideRow {
;;   type: 'row';
;;   display: boolean;
;;   params: GuideColumnParams;
;; }

(defn parse-frame-guide-row
  [^js guide]
  (when guide
    (d/without-nils
     {:type (-> (obj/get guide "type") parse-keyword)
      :display (obj/get guide "display")
      :params (-> (obj/get guide "params") parse-frame-guide-column-params)})))

;;export interface GuideSquareParams {
;;  color: { color: string; opacity: number };
;;  size?: number;
;;}
(defn parse-frame-guide-square-params
  [^js params]
  (when (some? params)
    (d/without-nils
     {:color (-> (obj/get params "color") parse-color)
      :size (obj/get params "size")})))

;; export interface GuideSquare {
;;   type: 'square';
;;   display: boolean;
;;   params: GuideSquareParams;
;; }
(defn parse-frame-guide-square
  [^js guide]
  (when guide
    (d/without-nils
     {:type (-> (obj/get guide "type") parse-keyword)
      :display (obj/get guide "display")
      :params (-> (obj/get guide "params") parse-frame-guide-square-params)})))

(defn parse-frame-guide
  [^js guide]
  (when (some? guide)
    (case (obj/get guide "type")
      "column"
      (parse-frame-guide-column guide)

      "row"
      (parse-frame-guide-row guide)

      "square"
      (parse-frame-guide-square guide))))

(defn parse-frame-guides
  [^js guides]
  (when (some? guides)
    (into [] (map parse-frame-guide) guides)))

;; export interface Dissolve {
;;   type: 'dissolve';
;;   duration: number;
;;   easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Slide {
;;   type: 'slide';
;;   way: 'in' | 'out';
;;   direction?:
;;     | 'right'
;;     | 'left'
;;     | 'up'
;;     | 'down';
;;   duration: number;
;;   offsetEffect?: boolean;
;;   easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export interface Push {
;;   type: 'push';
;;   direction?:
;;     | 'right'
;;     | 'left'
;;     | 'up'
;;     | 'down';
;;
;;   duration: number;
;;   easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
;; }
;;
;; export type Animation = Dissolve | Slide | Push;

(defn parse-animation
  [^js animation]
  (when animation
    (let [animation-type (-> (obj/get animation "type") parse-keyword)]
      (d/without-nils
       (case animation-type
         :dissolve
         {:animation-type animation-type
          :duration (obj/get animation "duration")
          :easing (-> (obj/get animation "easing") parse-keyword)}

         :slide
         {:animation-type animation-type
          :way (-> (obj/get animation "way") parse-keyword)
          :direction (-> (obj/get animation "direction") parse-keyword)
          :duration (obj/get animation "duration")
          :easing (-> (obj/get animation "easing") parse-keyword)
          :offset-effect (boolean (obj/get animation "offsetEffect"))}

         :push
         {:animation-type animation-type
          :direction (-> (obj/get animation "direction") parse-keyword)
          :duration (obj/get animation "duration")
          :easing (-> (obj/get animation "easing") parse-keyword)}

         nil)))))

;;export type Action =
;;  | NavigateTo
;;  | OpenOverlay
;;  | ToggleOverlay
;;  | CloseOverlay
;;  | PreviousScreen
;;  | OpenUrl;
;;
;;export interface NavigateTo {
;;  type: 'navigate-to';
;;  destination: Board;
;;  preserveScrollPosition?: boolean;
;;  animation: Animation;
;;}
;;
;;export interface OverlayAction {
;;  destination: Board;
;;  relativeTo?: Shape;
;;  position?:
;;    | 'manual'
;;    | 'center'
;;    | 'top-left'
;;    | 'top-right'
;;    | 'top-center'
;;    | 'bottom-left'
;;    | 'bottom-right'
;;    | 'bottom-center';
;;  manualPositionLocation?: Point;
;;  closeWhenClickOutside?: boolean;
;;  addBackgroundOverlay?: boolean;
;;  animation: Animation;
;;}
;;
;;export interface OpenOverlay extends OverlayAction {
;;  type: 'open-overlay';
;;}
;;
;;export interface ToggleOverlay extends OverlayAction {
;;  type: 'toggle-overlay';
;;}
;;
;;export interface CloseOverlay {
;;  type: 'close-overlay';
;;  destination?: Board;
;;  animation: Animation;
;;}
;;
;;export interface PreviousScreen {
;;  type: 'previous-screen';
;;}
;;
;;export interface OpenUrl {
;;  type: 'open-url';
;;  url: string;
;;}
(defn parse-action
  [action]
  (when action
    (let [action-type (-> (obj/get action "type") parse-keyword)]
      (d/without-nils
       (case action-type
         :navigate-to
         {:action-type :navigate
          :destination (-> (obj/get action "destination") (obj/get "$id"))
          :preserve-scroll (obj/get action "preserveScrollPosition")
          :animation (-> (obj/get action "animation") parse-animation)}

         (:open-overlay
          :toggle-overlay)
         {:action-type action-type
          :destination (-> (obj/get action "destination") (obj/get "$id"))
          :relative-to (-> (obj/get action "relativeTo") (obj/get "$id"))
          :overlay-pos-type (or (-> (obj/get action "position") parse-keyword) :center)
          :overlay-position (-> (obj/get action "manualPositionLocation") parse-point)
          :close-click-outside (obj/get action "closeWhenClickOutside")
          :background-overlay (obj/get action "addBackgroundOverlay")
          :animation (-> (obj/get action "animation") parse-animation)}

         :close-overlay
         {:action-type action-type
          :destination (-> (obj/get action "destination") (obj/get "$id"))
          :animation (-> (obj/get action "animation") parse-animation)}

         :previous-screen
         {:action-type :prev-screen}

         :open-url
         {:action-type action-type
          :url (obj/get action "url")}

         nil)))))

(defn parse-interaction
  [trigger ^js action delay]
  (when (and (string? trigger) (some? action))
    (let [trigger (parse-keyword trigger)
          action  (parse-action action)]
      (d/without-nils
       (d/patch-object {:event-type trigger :delay delay}  action)))))

(defn orientation->axis
  [axis]
  (case axis
    "horizontal" :y
    "vertical"   :x))

(defn parse-commands
  [commands]
  (-> (json/->clj commands)
      (path/decode-segments)))
