;; 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.dashboard.fonts
  (:require-macros [app.main.style :as stl])
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.media :as cm]
   [app.common.schema :as sm]
   [app.common.types.font :as ctf]
   [app.common.uuid :as uuid]
   [app.config :as cf]
   [app.main.data.fonts :as df]
   [app.main.data.modal :as modal]
   [app.main.data.notifications :as ntf]
   [app.main.repo :as rp]
   [app.main.store :as st]
   [app.main.ui.components.context-menu-a11y :refer [context-menu*]]
   [app.main.ui.components.file-uploader :refer [file-uploader]]
   [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
   [app.main.ui.icons :as deprecated-icon]
   [app.main.ui.notifications.context-notification :refer [context-notification]]
   [app.util.dom :as dom]
   [app.util.http :as http]
   [app.util.i18n :as i18n :refer [tr]]
   [app.util.keyboard :as kbd]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]
   [okulary.core :as l]
   [rumext.v2 :as mf]))

(def ^:private accept-font-types
  (str (str/join "," cm/font-types)
       ;; A workaround to solve a problem with chrome input selector
       ",.ttf,application/font-woff,.woff,.woff2,.otf"))

(defn- use-page-title
  [team section]
  (mf/with-effect [team]
    (when team
      (let [tname (if (:is-default team)
                    (tr "dashboard.your-penpot")
                    (:name team))]
        (case section
          :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
          :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname)))))))

(defn- bad-font-family-tmp?
  [font]
  (and (contains? font :font-family-tmp)
       (str/blank? (:font-family-tmp font))))

(defn- handle-font-upload
  [{:keys [id] :as item}]
  (->> (df/upload-font-variant item)
       ;; This delay fixes the UI problem. When the font is uploaded quickly,
       ;; the user only sees a flicker on the button. With a small delay we
       ;; have a clear transition from uploading state to uploaded
       (rx/delay-at-least 2000)
       (rx/map (fn [font]
                 [id font]))))

(mf/defc header*
  {::mf/memo true
   ::mf/private true}
  [{:keys [section team]}]
  (use-page-title team section)
  [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
   [:div#dashboard-fonts-title {:class (stl/css :dashboard-title)}
    [:h1 (tr "labels.fonts")]]])

(mf/defc font-variant-display-name*
  {::mf/private true}
  [{:keys [variant]}]
  [:span (cm/font-display-variant (:variant-name variant)
                                  (:font-weight variant)
                                  (:font-style variant))])

(mf/defc uploaded-fonts*
  {::mf/private true}
  [{:keys [team installed-fonts]}]
  (let [fonts*     (mf/use-state {})
        fonts      (deref fonts*)
        font-vals  (mf/with-memo [fonts]
                     (->> fonts
                          (into [] (map val))
                          (not-empty)))

        team-id    (:id team)

        input-ref  (mf/use-ref)

        uploading* (mf/use-state #{})
        uploading  (deref uploading*)

        disable-upload-all?
        (some bad-font-family-tmp? fonts)

        problematic-fonts?
        (some :height-warning? (vals fonts))

        on-click
        (mf/use-fn #(dom/click (mf/ref-val input-ref)))

        on-selected
        (mf/use-fn
         (mf/deps team-id installed-fonts)
         (fn [blobs]
           (->> (df/process-upload blobs team-id)
                (rx/subs! (fn [result]
                            (swap! fonts* df/merge-and-group-fonts installed-fonts result))
                          (fn [error]
                            (js/console.error "error" error))))))

        on-upload
        (mf/use-fn
         (mf/deps fonts)
         (fn [event]
           (let [id   (-> (dom/get-current-target event)
                          (dom/get-data "id")
                          (uuid/parse))
                 item (get fonts id)]
             (swap! uploading* conj id)
             (->> (handle-font-upload item)
                  (rx/subs! (fn [[previous-id font]]
                              (swap! fonts* dissoc previous-id)
                              (swap! uploading* disj previous-id)
                              (st/emit! (df/add-font font)))
                            (fn [cause]
                              (st/emit! (ntf/error (tr "errors.bad-font" (first (:names item)))))
                              (js/console.error "Unexpected error on uploading font" cause)))))))

        on-blur-name
        (mf/use-fn
         (mf/deps installed-fonts)
         (fn [event]
           (let [target (dom/get-current-target event)
                 id     (-> target
                            (dom/get-data "id")
                            (uuid/parse))
                 name   (dom/get-value target)]
             (when (and (not (str/blank? name))
                        (sm/validate ctf/schema:font-family name))
               (swap! fonts* df/rename-and-regroup id name installed-fonts)))))

        on-change-name
        (mf/use-fn
         (fn [event]
           (let [target (dom/get-current-target event)
                 id     (-> target
                            (dom/get-data "id")
                            (uuid/parse))
                 name   (dom/get-value target)]
             (swap! fonts* update id assoc :font-family-tmp name))))

        on-delete
        (mf/use-fn
         (mf/deps team)
         (fn [event]
           (let [id (-> (dom/get-current-target event)
                        (dom/get-data "id")
                        (uuid/parse))]
             (swap! fonts* dissoc id))))

        on-upload-all
        (mf/use-fn
         (mf/deps font-vals)
         (fn [_]
           (->> (rx/from font-vals)
                (rx/tap #(swap! uploading* conj (:id %)))
                (rx/mapcat (fn [{:keys [id] :as item}]
                             (->> (handle-font-upload item)
                                  (rx/catch (fn [cause]
                                              (st/emit! (ntf/error (tr "errors.bad-font" (first (:names item)))))
                                              (js/console.error "Unexpected error on uploading font" cause)
                                              (rx/of [id nil]))))))

                (rx/subs! (fn [[previous-id font]]
                            (swap! fonts* dissoc previous-id)
                            (swap! uploading* disj previous-id)
                            (when font
                              (st/emit! (df/add-font font))))))))

        on-dismis-all
        (mf/use-fn
         (mf/deps fonts)
         (fn [_]
           (run! #(swap! fonts* dissoc (:id %)) (vals fonts))))]

    [:div {:class (stl/css :dashboard-fonts-upload)}
     [:div {:class (stl/css :dashboard-fonts-hero)}
      [:div {:class (stl/css :desc)}
       [:h2 (tr "labels.upload-custom-fonts")]
       [:> i18n/tr-html* {:content (tr "dashboard.fonts.hero-text1")}]

       [:button {:class (stl/css :btn-primary)
                 :on-click on-click
                 :tab-index "0"}
        [:span (tr "labels.add-custom-font")]
        [:& file-uploader {:input-id "font-upload"
                           :accept accept-font-types
                           :multi true
                           :ref input-ref
                           :on-selected on-selected}]]

       (when-let [url cf/terms-of-service-uri]
         [:& context-notification {:content (tr "dashboard.fonts.hero-text2" url)
                                   :level :default
                                   :is-html true}])

       (when problematic-fonts?
         [:& context-notification {:content (tr "dashboard.fonts.warning-text")
                                   :level :warning
                                   :is-html true}])]]

     [:*
      (when (seq fonts)
        [:div {:class (stl/css :font-item :table-row)}
         [:span (tr "dashboard.fonts.fonts-added" (i18n/c (count fonts)))]
         [:div {:class (stl/css :table-field :options)}
          [:button {:class (stl/css-case
                            :btn-primary true
                            :disabled disable-upload-all?)
                    :on-click on-upload-all
                    :data-testid "upload-all"
                    :disabled disable-upload-all?}
           [:span (tr "dashboard.fonts.upload-all")]]
          [:button {:class (stl/css :btn-secondary)
                    :on-click on-dismis-all
                    :data-testid "dismiss-all"}
           [:span (tr "dashboard.fonts.dismiss-all")]]]])

      (for [{:keys [id] :as item} (sort-by :font-family font-vals)]
        (let [uploading?      (contains? uploading id)
              disable-upload? (or uploading? (bad-font-family-tmp? item))]
          [:div {:class (stl/css :font-item :table-row)
                 :key (dm/str id)}
           [:div {:class (stl/css :table-field :family)}
            [:input {:type "text"
                     :data-id (dm/str id)
                     :on-blur on-blur-name
                     :on-change on-change-name
                     :default-value (:font-family item)}]]
           [:div {:class (stl/css :table-field :variants)}
            [:span {:class (stl/css :label)}
             [:> font-variant-display-name* {:variant item}]]]

           [:div {:class (stl/css :table-field :filenames)}
            (for [item (:names item)]
              [:span {:key (dm/str "name-" item)} item])]

           [:div {:class (stl/css :table-field :options)}
            (when (:height-warning? item)
              [:span {:class (stl/css :icon :failure)}
               deprecated-icon/msg-neutral])

            [:button {:on-click on-upload
                      :data-id (dm/str id)
                      :class (stl/css-case
                              :btn-primary true
                              :upload-button true
                              :disabled disable-upload?)
                      :disabled disable-upload?}
             (if ^boolean uploading?
               (tr "labels.uploading")
               (tr "labels.upload"))]
            [:span {:class (stl/css :icon :close)
                    :data-id (dm/str id)
                    :on-click on-delete}
             deprecated-icon/close]]]))]]))

(mf/defc installed-font-context-menu*
  {::mf/private true}
  [{:keys [is-open on-close on-edit on-download on-delete]}]
  (let [options (mf/with-memo [on-edit on-download on-delete]
                  [{:name    (tr "labels.edit")
                    :id      "font-edit"
                    :handler on-edit}
                   {:name    (tr "labels.download-simple")
                    :id      "font-download"
                    :handler on-download}
                   {:name    (tr "labels.delete")
                    :id      "font-delete"
                    :handler on-delete}])]
    [:> context-menu*
     {:on-close on-close
      :show is-open
      :fixed false
      :min-width true
      :top -15
      :left -115
      :options options}]))

(mf/defc installed-font*
  {::mf/private true
   ::mf/wrap [mf/memo]}
  [{:keys [font-id variants can-edit]}]
  (let [font        (first variants)

        menu-open*  (mf/use-state false)
        menu-open?  (deref menu-open*)
        edition*    (mf/use-state false)
        edition?    (deref edition*)

        state*      (mf/use-state (:font-family font))
        font-family (deref state*)

        variants
        (mf/with-memo [variants]
          (sort-by (fn [item]
                     [(:font-weight item)
                      (if (= "normal" (:font-style item)) 1 2)])
                   variants))

        on-change
        (mf/use-fn
         (fn [event]
           (reset! state* (dom/get-target-val event))))

        on-edit
        (mf/use-fn #(reset! edition* true))

        on-menu-open
        (mf/use-fn #(reset! menu-open* true))

        on-menu-close
        (mf/use-fn #(reset! menu-open* false))

        on-save
        (mf/use-fn
         (mf/deps font-family)
         (fn [_]
           (reset! edition* false)
           (when-not (str/blank? font-family)
             (if (sm/validate ctf/schema:font-family font-family)
               (st/emit! (df/update-font {:id font-id :name font-family}))
               (st/emit! (ntf/error (tr "errors.font-family-invalid-chars")))))))

        on-key-down
        (mf/use-fn
         (mf/deps on-save)
         (fn [event]
           (when (kbd/enter? event)
             (on-save event))))

        on-cancel
        (mf/use-fn
         (fn [_]
           (reset! edition* false)
           (reset! state* (:font-family font))))

        on-delete-font
        (mf/use-fn
         (mf/deps font-id)
         (fn []
           (let [options   {:type :confirm
                            :title (tr "modals.delete-font.title")
                            :message (tr "modals.delete-font.message")
                            :accept-label (tr "labels.delete")
                            :on-accept (fn [_props]
                                         (st/emit! (df/delete-font font-id)))}]
             (st/emit! (modal/show options)))))

        on-download
        (mf/use-fn
         (mf/deps variants)
         (fn [_event]
           (let [variant    (first variants)
                 variant-id (:id variant)
                 multiple?  (> (count variants) 1)
                 cmd        (if multiple? :download-font-family :download-font)
                 params     (if multiple? {:font-id font-id} {:id variant-id})]
             (->> (rp/cmd! cmd params)
                  (rx/mapcat (fn [{:keys [name uri]}]
                               (->> (http/send! {:uri uri :method :get :response-type :blob})
                                    (rx/map :body)
                                    (rx/map (fn [blob] (d/vec2 name blob))))))
                  (rx/subs! (fn [[filename blob]]
                              (dom/trigger-download filename blob))
                            (fn [error]
                              (js/console.error "error downloading font" error)
                              (st/emit! (ntf/error (tr "errors.generic")))))))))

        on-delete-variant
        (mf/use-fn
         (fn [event]
           (let [id      (-> (dom/get-current-target event)
                             (dom/get-data "id")
                             (uuid/parse))
                 options {:type :confirm
                          :title (tr "modals.delete-font-variant.title")
                          :message (tr "modals.delete-font-variant.message")
                          :accept-label (tr "labels.delete")
                          :on-accept (fn [_props]
                                       (st/emit! (df/delete-font-variant id)))}]
             (st/emit! (modal/show options)))))]

    [:div {:class (stl/css :font-item :table-row)}
     [:div {:class (stl/css-case :table-field true
                                 :family true
                                 :is-edition edition?)}
      (if ^boolean edition?
        [:input {:type "text"
                 :auto-focus true
                 :default-value font-family
                 :on-key-down on-key-down
                 :on-change on-change}]
        [:span (:font-family font)])]

     [:div {:class (stl/css :table-field :variants)}
      (for [{:keys [id] :as item} variants]
        [:div {:class (stl/css-case :variant true
                                    :inhert-variant (not can-edit))
               :key (dm/str id)}
         [:span {:class (stl/css :label)}
          [:> font-variant-display-name* {:variant item}]]
         (when can-edit
           [:span
            {:class (stl/css :icon :close)
             :data-id (dm/str id)
             :on-click on-delete-variant}
            deprecated-icon/add])])]

     (if ^boolean edition?
       [:div {:class (stl/css :table-field :options)}
        [:button
         {:disabled (str/blank? font-family)
          :on-click on-save
          :class (stl/css-case :btn-primary true
                               :btn-disabled (str/blank? font-family))}
         (tr "labels.save")]
        [:button {:class (stl/css :icon :close)
                  :on-click on-cancel}
         deprecated-icon/close]]

       (when can-edit
         [:div {:class (stl/css :table-field :options)}
          [:span {:class (stl/css :icon)
                  :on-click on-menu-open}
           deprecated-icon/menu]

          [:> installed-font-context-menu*
           {:on-close on-menu-close
            :is-open menu-open?
            :on-delete on-delete-font
            :on-download on-download
            :on-edit on-edit}]]))]))

(mf/defc installed-fonts*
  [{:keys [fonts can-edit]}]
  (let [sterm (mf/use-state "")

        matches?
        #(str/includes? (str/lower (:font-family %)) @sterm)

        on-change
        (mf/use-fn
         (fn [event]
           (let [val (dom/get-target-val event)]
             (reset! sterm (str/lower val)))))]

    [:div {:class (stl/css :dashboard-installed-fonts)}
     (cond
       (seq fonts)
       [:*
        [:h3 (tr "labels.installed-fonts")]
        [:div {:class (stl/css :installed-fonts-header)}
         [:div {:class (stl/css :table-field :family)} (tr "labels.font-family")]
         [:div {:class (stl/css :table-field :variants)} (tr "labels.font-variants")]
         [:div {:class (stl/css :table-field :search-input)}
          [:input {:placeholder (tr "labels.search-font")
                   :default-value ""
                   :on-change on-change}]]]
        (for [[font-id variants] (->> (vals fonts)
                                      (filter matches?)
                                      (group-by :font-id))]
          [:> installed-font* {:key (dm/str font-id "-installed")
                               :font-id font-id
                               :can-edit can-edit
                               :variants variants}])]

       (nil? fonts)
       [:div {:class (stl/css :fonts-placeholder)}
        [:div {:class (stl/css :icon)} deprecated-icon/loader]
        [:div {:class (stl/css :label)} (tr "dashboard.loading-fonts")]]

       :else
       (if ^boolean can-edit
         [:div {:class (stl/css :fonts-placeholder)}
          [:div {:class (stl/css :icon)} deprecated-icon/text]
          [:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]]

         [:> empty-placeholder*
          {:title (tr "dashboard.fonts.empty-placeholder-viewer")
           :subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
           :type 2}]))]))

(def ^:private ref:fonts
  (l/derived :fonts st/state))

(mf/defc fonts-page*
  [{:keys [team]}]
  (let [fonts       (mf/deref ref:fonts)
        permissions (:permissions team)
        can-edit    (:can-edit permissions)]
    [:*
     [:> header* {:team team :section :fonts}]
     [:section {:class (stl/css :dashboard-container :dashboard-fonts)}
      (when ^boolean can-edit
        [:> uploaded-fonts* {:team team :installed-fonts fonts}])
      [:> installed-fonts*
       {:team team :fonts fonts :can-edit can-edit}]]]))

(mf/defc font-providers-page*
  [{:keys [team]}]
  [:*
   [:> header* {:team team :section :providers}]
   [:section {:class (stl/css :dashboard-container)}
    [:span "font providers"]]])
