(ns frontend.handler.query.builder
  "DSL query builder handler"
  (:require [clojure.walk :as walk]
            [logseq.common.util.page-ref :as page-ref]
            [lambdaisland.glogi :as log]
            [frontend.db.query-dsl :as query-dsl]))

;; TODO: make it extensible for Datalog/SPARQL etc.

(def operators [:and :or :not])
(def operators-set (set operators))

(def page-filters ["all page tags"
                   "namespace"
                   "tags"
                   "property"
                   "sample"])
(def block-filters ["page reference"
                    "property"
                    "task"
                    "priority"
                    "page"
                    "full text search"
                    "between"
                    "sample"])

(defn- vec-dissoc-item
  [vec idx]
  (into (subvec vec 0 idx) (subvec vec (inc idx))))

(defn- vec-assoc-item
  [vec idx item]
  (into (conj (subvec vec 0 idx) item)
        (subvec vec idx)))

(defn- vec-replace-item
  [v idx item]
  (into (if (and (coll? item)
                 (not (operators-set (first item))))
          (vec (concat (subvec v 0 idx) item))
          (conj (subvec v 0 idx) item))
        (subvec v (inc idx))))

(defn add-element
  [q loc x]
  {:pre [(vector? loc) (some? x)]}
  (cond
    (and (seq loc) (= 1 (count loc)))
    (vec-assoc-item q (first loc) x)

    (seq loc)
    (update-in q (vec (butlast loc))
               (fn [v]
                 (vec-assoc-item v (last loc) x)))

    (seq q)
    (conj q x)

    :else
    [x]))

(defn append-element
  [q loc x]
  {:pre [(vector? loc) (some? x)]}
  (let [idx (count (get-in q (vec (butlast loc))))
        loc' (vec-replace-item loc (dec (count loc)) idx)]
    (add-element q loc' x)))

(defn remove-element
  [q loc]
  (if (seq loc)
    (let [idx (last loc)
          ks (vec (butlast loc))
          f #(vec-dissoc-item % idx)]
      (if (seq ks)
        (let [result (update-in q ks f)]
          (if (seq (get-in result ks))
            result
            ;; remove the wrapped empty vector
            (remove-element result ks)))
        (f q)))
    ;; default to AND operator
    [:and]))

(defn replace-element
  [q loc x]
  {:pre [(vector? loc) (seq loc) (some? x)]}
  (if (= 1 (count loc))
    (vec-replace-item q (first loc) x)
    (update-in q (vec (butlast loc))
               (fn [v]
                 (vec-replace-item v (last loc) x)))))

(defn- fallback-to-default [result default-value failed-data]
  (if (empty? result)
    (do
      (log/error :query-builder/wrap-unwrap-operator-failed failed-data)
      default-value)
    result))

(defn wrap-operator
  [q loc operator]
  {:pre [(seq q) (operators-set operator)]}
  (let [result (if (or (= loc [0]) (empty? loc))
                 [operator q]
                 (when-let [x (get-in q loc)]
                   (let [x' [operator x]]
                     (replace-element q loc x'))))]
    (fallback-to-default result q {:op "wrap-operator"
                                   :q q
                                   :loc loc
                                   :operator operator})))

(defn unwrap-operator
  [q loc]
  {:pre [(seq q) (seq loc)]}
  (let [result (if (and (= loc [0]) (operators-set (first q)))
                 (second q)
                 (when-let [x (get-in q loc)]
                   (when (and (operators-set (first x))
                              (seq (rest x)))
                     (let [x' (rest x)]
                       (replace-element q loc x')))))]
    (fallback-to-default result q {:op "unwrap-operator"
                                   :q q
                                   :loc loc})))

(defn ->page-ref
  [x]
  (if (string? x)
    (symbol (page-ref/->page-ref x))
    (->page-ref (second x))))

(defn- ->dsl*
  [f]
  (cond
    (and (vector? f) (= :priority (keyword (first f))))
    (vec (cons (symbol :priority) (map symbol (rest f))))

    (and (vector? f) (= :task (keyword (first f))))
    (vec (cons (symbol :task) (map symbol (rest f))))

    (and (vector? f) (= :page-ref (keyword (first f))))
    (->page-ref (second f))

    (and (vector? f) (= :page-tags (keyword (first f))))
    [(symbol :page-tags) (->page-ref (second f))]

    (and (vector? f) (= :between (keyword (first f))))
    (into [(symbol :between)] (map ->page-ref (rest f)))

    ;; property key value
    (and (vector? f) (= 3 (count f)) (contains? #{:page-property :property} (keyword (first f))))
    (let [l (if (page-ref/page-ref? (str (last f)))
              (symbol (last f))
              (last f))]
      (into [(symbol (first f))] [(second f) l]))

    (and (vector? f) (contains? #{:page :tags :namespace} (keyword (first f))))
    (into [(symbol (first f))] (map ->page-ref (rest f)))

    :else f))

(defn ->dsl
  [col]
  (->
   (walk/prewalk
    (fn [f]
      (let [f' (->dsl* f)]
        (cond
          (and (vector? f') (keyword (first f')))
          (cons (symbol (first f')) (rest f'))

          :else f')))
    col)
   (query-dsl/simplify-query)))

(defn from-dsl
  [dsl-form]
  (walk/prewalk
   (fn [f]
     (cond
       (and (vector? f) (vector? (first f)))
       [:page-ref (page-ref/get-page-name (str f))]

       (and (string? f) (page-ref/get-page-name f))
       [:page-ref (page-ref/get-page-name f)]

       (and (list? f)
            (symbol? (first f))
            (operators-set (keyword (first f)))) ; operator
       (into [(keyword (first f))] (rest f))

       (list? f)
       (vec f)

       :else f))
   dsl-form))
