(ns frontend.handler.page
  "Provides util handler fns for pages"
  (:require [clojure.string :as string]
            [datascript.core :as d]
            [datascript.impl.entity :as de]
            [frontend.commands :as commands]
            [frontend.config :as config]
            [frontend.context.i18n :refer [t]]
            [frontend.date :as date]
            [frontend.db :as db]
            [frontend.db.async :as db-async]
            [frontend.db.conn :as conn]
            [frontend.handler.common.page :as page-common-handler]
            [frontend.handler.db-based.page :as db-page-handler]
            [frontend.handler.db-based.property :as db-property-handler]
            [frontend.handler.editor :as editor-handler]
            [frontend.handler.notification :as notification]
            [frontend.handler.plugin :as plugin-handler]
            [frontend.handler.property :as property-handler]
            [frontend.handler.ui :as ui-handler]
            [frontend.modules.outliner.op :as outliner-op]
            [frontend.modules.outliner.ui :as ui-outliner-tx]
            [frontend.state :as state]
            [frontend.util :as util]
            [frontend.util.cursor :as cursor]
            [frontend.util.page :as page-util]
            [frontend.util.ref :as ref]
            [frontend.util.url :as url-util]
            [goog.functions :refer [debounce]]
            [goog.object :as gobj]
            [logseq.common.config :as common-config]
            [logseq.common.util :as common-util]
            [logseq.common.util.page-ref :as page-ref]
            [logseq.db :as ldb]
            [logseq.graph-parser.text :as text]
            [promesa.core :as p]))

(def <create! page-common-handler/<create!)
(def <delete! page-common-handler/<delete!)

(defn <unfavorite-page!
  [page-name]
  (p/do!
   (when-let [page-block-uuid (:block/uuid (db/get-page page-name))]
     (page-common-handler/<db-unfavorite-page! page-block-uuid))
   (state/update-favorites-updated!)))

(defn <favorite-page!
  [page-name]
  (p/do!
   (when-let [page-block-uuid (:block/uuid (db/get-page page-name))]
     (page-common-handler/<db-favorite-page! page-block-uuid))
   (state/update-favorites-updated!)))

(defn favorited?
  [page-name]
  (boolean
   (when-let [page-block-uuid (:block/uuid (db/get-page page-name))]
     (page-common-handler/db-favorited? page-block-uuid))))

(defn get-favorites
  "return page-block entities"
  []
  (when-let [db (conn/get-db)]
    (when-let [page (ldb/get-page db common-config/favorites-page-name)]
      (let [blocks (ldb/sort-by-order (:block/_parent page))]
        (keep (fn [block]
                (when-let [block-db-id (:db/id (:block/link block))]
                  (d/entity db block-db-id))) blocks)))))

(defn toggle-favorite! []
  ;; NOTE: in journals or settings, current-page is nil
  (when-let [page-name (state/get-current-page)]
    (if (favorited? page-name)
      (<unfavorite-page! page-name)
      (<favorite-page! page-name))))

(defn rename!
  [page-uuid new-name & {:as _opts}]
  (let [page-uuid (if (util/uuid-string? page-uuid)
                    (uuid page-uuid)
                    (throw (ex-info "Invalid page uuid"
                                    {:page-uuid page-uuid})))]
    (p/do!
     (ui-outliner-tx/transact!
      {:outliner-op :rename-page}
      (outliner-op/rename-page! page-uuid new-name))
     true)))

(defn <reorder-favorites!
  [favorites]
  (let [conn (conn/get-db false)]
    (when-let [favorites-page (db/get-page common-config/favorites-page-name)]
      (let [favorite-page-block-db-id-coll
            (keep (fn [page-uuid]
                    (:db/id (db/get-page page-uuid)))
                  favorites)
            current-blocks (ldb/sort-by-order (ldb/get-page-blocks @conn (:db/id favorites-page)))]
        (p/do!
         (ui-outliner-tx/transact!
          {:outliner-op :reorder-favorites}
          (doseq [[page-block-db-id block] (zipmap favorite-page-block-db-id-coll current-blocks)]
            (when (not= page-block-db-id (:db/id (:block/link block)))
              (outliner-op/save-block! (assoc block :block/link page-block-db-id)))))
         (state/update-favorites-updated!))))))

(defn update-public-attribute!
  [page value]
  (db-property-handler/set-block-property! [:block/uuid (:block/uuid page)] :logseq.property/publishing-public? value))

(defn get-page-ref-text
  [page]
  (ref/->page-ref page))

(defn init-commands!
  []
  (commands/init-commands! get-page-ref-text))

(def rebuild-slash-commands-list!
  (debounce init-commands! 1500))

(defn db-based-save-filter!
  [page filter-page-id {:keys [include? add?]}]
  (let [property-id (if include?
                      :logseq.property.linked-references/includes
                      :logseq.property.linked-references/excludes)]
    (if add?
      (property-handler/set-block-property! (:db/id page) property-id filter-page-id)
      (db-property-handler/delete-property-value! (:db/id page) property-id filter-page-id))))

;; Editor
(defn page-not-exists-handler
  [input id q current-pos]
  (state/clear-editor-action!)
  (if (state/org-mode-file-link? (state/get-current-repo))
    (let [page-ref-text (get-page-ref-text q)
          value (gobj/get input "value")
          old-page-ref (ref/->page-ref q)
          new-value (string/replace value
                                    old-page-ref
                                    page-ref-text)]
      (state/set-edit-content! id new-value)
      (let [new-pos (+ current-pos
                       (- (count page-ref-text)
                          (count old-page-ref))
                       2)]
        (cursor/move-cursor-to input new-pos)))
    (let [current-selected (util/get-selected-text)]
      (cursor/move-cursor-forward input (+ 2 (count current-selected))))))

(defn- tag-on-chosen-handler
  [input id pos format current-pos edit-content q]
  (fn [chosen-result ^js e]
    (util/stop e)
    (state/clear-editor-action!)
    (p/let [_ (when (:convert-page-to-tag? chosen-result)
                (let [entity (db/entity (:db/id chosen-result))]
                  (when (and (ldb/page? entity) (not (ldb/class? entity)))
                    (db-page-handler/convert-page-to-tag! entity))))
            chosen-result (if (:block/uuid chosen-result)
                            (db/entity [:block/uuid (:block/uuid chosen-result)])
                            chosen-result)
            target (first (:block/_alias chosen-result))
            chosen-result (if (and target (not (ldb/class? chosen-result)) (ldb/class? target)) target chosen-result)
            chosen (:block/title chosen-result)
            class? (or (string/includes? chosen (str (t :new-tag) " "))
                       (ldb/class? chosen-result))
            inline-tag? (and class? (= (.-identifier e) "auto-complete/meta-complete")
                             (not= chosen "Page"))
            chosen (-> chosen
                       (string/replace-first (str (t :new-tag) " ") "")
                       (string/replace-first (str (t :new-page) " ") ""))
            wrapped? (= page-ref/left-brackets (common-util/safe-subs edit-content (- pos 2) pos))
            chosen-last-part (if (text/namespace-page? chosen)
                               (text/get-namespace-last-part chosen)
                               chosen)
            wrapped-tag (if (and (util/safe-re-find #"\s+" chosen-last-part) (not wrapped?))
                          (ref/->page-ref chosen-last-part)
                          chosen-last-part)
            q (if (editor-handler/get-selected-text) "" q)
            last-pattern (if wrapped?
                           q
                           (if (= \# (first q))
                             (subs q 1)
                             q))
            last-pattern (str "#" (when wrapped? page-ref/left-brackets) last-pattern)
            tag-in-page-auto-complete? (= page-ref/right-brackets (common-util/safe-subs edit-content current-pos (+ current-pos 2)))]
      (p/do!
       (editor-handler/insert-command! id
                                       (if (and class? (not inline-tag?)) "" (str "#" wrapped-tag))
                                       format
                                       {:last-pattern last-pattern
                                        :end-pattern (when wrapped? page-ref/right-brackets)
                                        :command :page-ref})
       (when-not tag-in-page-auto-complete?
         (db-page-handler/tag-on-chosen-handler chosen chosen-result class? edit-content current-pos last-pattern))
       (when input (.focus input))))))

(defn- page-on-chosen-handler
  [id format q]
  (fn [chosen-result e]
    (util/stop e)
    (state/clear-editor-action!)
    (p/let [repo (state/get-current-repo)
            _ (when-let [id (:block/uuid chosen-result)]
                (db-async/<get-block repo id {:children? false}))
            chosen-result (if (:block/uuid chosen-result)
                            (db/entity [:block/uuid (:block/uuid chosen-result)])
                            chosen-result)
            _ (when-not chosen-result
                (throw (ex-info "No chosen item"
                                {:chosen chosen-result})))
            chosen (:block/title chosen-result)
            chosen' (string/replace-first chosen (str (t :new-page) " ") "")
            [chosen' chosen-result] (or (when (and (:nlp-date? chosen-result) (not (de/entity? chosen-result)))
                                          (when-let [result (date/nld-parse chosen')]
                                            (let [d (doto (goog.date.DateTime.) (.setTime (.getTime result)))
                                                  gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))
                                                  page (date/js-date->journal-title gd)]
                                              [page (db/get-page page)])))
                                        [chosen' chosen-result])
            datoms (state/<invoke-db-worker :thread-api/datoms repo :avet :block/name (util/page-name-sanity-lc chosen'))
            multiple-pages-same-name? (> (count datoms) 1)
            ref-text (if (and (de/entity? chosen-result)
                              (or multiple-pages-same-name? (not (ldb/page? chosen-result))))
                       (ref/->page-ref (:block/uuid chosen-result))
                       (get-page-ref-text chosen'))
            result (when-not (de/entity? chosen-result)
                     (<create! chosen'
                               {:redirect? false
                                :split-namespace? true}))
            ref-text' (if result
                        (let [title (:block/title result)]
                          (ref/->page-ref title))
                        ref-text)]
      (p/do!
       (editor-handler/insert-command! id
                                       ref-text'
                                       format
                                       {:last-pattern (str page-ref/left-brackets (if (editor-handler/get-selected-text) "" q))
                                        :end-pattern page-ref/right-brackets
                                        :postfix-fn   (fn [s] (util/replace-first page-ref/right-brackets s ""))
                                        :command :page-ref})
       (p/let [chosen-result (or result chosen-result)]
         (when (de/entity? chosen-result)
           (state/conj-block-ref! chosen-result)))))))

(defn on-chosen-handler
  [input id pos format]
  (let [current-pos (cursor/pos input)
        edit-content (state/get-edit-content)
        action (state/get-editor-action)
        hashtag? (= action :page-search-hashtag)
        q (or
           (editor-handler/get-selected-text)
           (when hashtag?
             (common-util/safe-subs edit-content pos current-pos))
           (when (> (count edit-content) current-pos)
             (common-util/safe-subs edit-content pos current-pos)))]
    (if hashtag?
      (tag-on-chosen-handler input id pos format current-pos edit-content q)
      (page-on-chosen-handler id format q))))

(defn create-today-journal!
  []
  (when-let [repo (state/get-current-repo)]
    (when (and (state/enable-journals? repo)
               ;; FIXME: There are a lot of long-running actions we don't want interrupted by this fn.
               ;; We should implement an app-wide check rather than list them all here
               (not (:graph/loading? @state/state))
               (not (:graph/importing @state/state))
               (not config/publishing?))
      (when-let [title (date/today)]
        (state/set-today! title)
        (let [today-page (util/page-name-sanity-lc title)
              create-f (fn []
                         (p/let [result (<create! title {:redirect? false
                                                         :split-namespace? false
                                                         :today-journal? true})]
                           (ui-handler/re-render-root!)
                           (plugin-handler/hook-plugin-app :today-journal-created {:title today-page})
                           result))]
          (when-not (db/get-page today-page)
            (create-f)))))))

(defn open-today-in-sidebar
  []
  (when-let [page (db/get-page (date/today))]
    (state/sidebar-add-block!
     (state/get-current-repo)
     (:db/id page)
     :page)))

(defn copy-page-url
  ([]
   (let [id (page-util/get-current-page-uuid)]
     (copy-page-url id)))
  ([page-uuid]
   (if page-uuid
     (util/copy-to-clipboard!
      (url-util/get-logseq-graph-page-url nil (state/get-current-repo) (str page-uuid)))
     (notification/show! "No page found to copy" :warning))))
