(ns com.oly.static-sites.clojure-demos.core
  (:require
   ["@codemirror/language" :refer [LanguageSupport StreamLanguage]]
   ["@codemirror/legacy-modes/mode/yaml" :refer [yaml]]
   ["@codemirror/state" :as cm-state :refer [EditorState Transaction]]
   ["@codemirror/theme-one-dark" :refer [oneDark]]
   ["@codemirror/view" :as cm-view :refer [EditorView ViewUpdate]]
   ["@nextjournal/lang-clojure" :refer [clojure]]
   ["react-dom/client" :refer [createRoot]]
   ["tarts" :as tarts]
   [ajax.core :refer [GET raw-response-format]]
   [cl-eorg.html :as h :refer [body headers org->replacements]]
   [cl-eorg.parser :as o :refer [parse]]
   [cl-eorg.themes.tachyon :refer [tachyon-theme]]
   [com.oly.static-sites.clojure-demos.components
    :refer [header-lvl1 header-lvl2 header-lvl3 notification]]
   [com.oly.static-sites.clojure-demos.helpers :refer [slugify]]
   [com.oly.static-sites.clojure-demos.state :refer [site-state]]
   [com.oly.static-sites.clojure-demos.tailwind-theme :refer [tailwind-theme]]
;;   [clojure.data.xml :as xml]
   [clojure.string :as str]
   [honey.sql :as sql]
   [promesa.core :as promesa]
   [honey.sql.helpers :as sqlh]
   [reagent.core :as reagent]
   [reitit.coercion.spec :as rss]
   [reitit.frontend :as rf]
   [reitit.frontend.easy :as rfe]
   [reitit.frontend.history :refer [ignore-anchor-click?]]
   [sci.core :as sci]
   [sci.configs.funcool.promesa :as sci-promesa]
   [sci.configs.reagent.reagent :as sci-reagent]
   [sci.configs.tonsky.datascript :as sci-datascript]
   [com.oly.static-sites.clojure-demos.routes :refer [site-data routes]]
   [com.oly.static-sites.clojure-demos.sitemap :as sm #_#_:refer [spit-pwa-sitemap]]
   [com.oly.static-sites.ui-tachyon.core :as ui]
   [tick.core :as t]
   (com.oly.static-sites.clojure-demos.sci-init :refer [sci-ctx yaml-mode]))
  #_(:require-macros [com.oly.static-sites.clojure-demos.sitemap :as sm #_#_:refer [spit-pwa-sitemap]]))


(defn fetch-selected-text
  "Get the users selected text"
  [updated-view transactions]
  (reduce (fn [text t]
            (if (= "select.pointer" (str (.annotation t (.-userEvent Transaction))))
              (conj text
                    (.sliceDoc
                     ^EditorState (.-state updated-view)
                     (.-from (.-main (.-selection t)))
                     (.-to (.-main (.-selection t)))))
              text))
          []
          transactions))

(defn err-boundary
  "https://github.com/reagent-project/reagent/blob/master/doc/ReactFeatures.md#error-boundaries"
  [children]
  (let [err-state (reagent/atom nil)]
    (reagent/create-class
     {:display-name "Error Boundary"
      :get-derived-state-from-error (fn [e]
                                      (reset! err-state e #_[err info])
                                      #js {})
      :reagent-render (fn [children]
                        (if @err-state
                          [:div (str @err-state)] children))})))

(defn code-editor
  [{:keys [exports class results]} content]
  (let [language class
        editor (atom nil)
        evaled-result (reagent/atom nil)
        update-timeout (reagent/atom nil)
        update-fn (.of (.-updateListener EditorView)
                       (fn [^ViewUpdate view-update]
                         (when (and (not= results "none")
                                    (some #(= language %) ["clojure" "clojurescript"]))
                           (js/clearTimeout @update-timeout)
                           (reset! update-timeout
                                   (js/setTimeout
                                    (fn []
                                      (let [selected-text (fetch-selected-text view-update (.-transactions view-update))
                                            eval-code (if (seq selected-text)
                                                        (first selected-text)
                                                        (.-doc (.-state view-update)))]
                                        (reset! evaled-result (sci/eval-string* sci-ctx
                                                                                (.toString eval-code)))
                                        (prn "updated delayed"))) 1000)))))
        start-state
        (.create EditorState
                 (clj->js {:doc content
                           :mode "text/x-clojure"
                           ;:mode "text/yaml"
                           :extensions [(clojure) yaml-mode update-fn oneDark #_cm-keymap/of #_cm-commands/default-keymap]}))
        view (atom nil)]
    (reagent/create-class
     {:component-did-mount
      (fn [_]
        (reset! view (EditorView.
                      (clj->js  (merge {} #_(get languages language)
                                       {:state start-state
                                        ;:mode "yaml"
                                        :mode "clojure"
                                        :updateListener prn
                                        :parent @editor})))))
      :component-will-unmount (fn [_] (.destroy @view))
      :reagent-render (fn []
                        (if (= exports "none")
                          nil
                          [:div.mb-8
                           [:div.ba.ma-0.f5.b--black-05.pa2.overflow-auto.editor {:ref #(reset! editor %)}]
                           (when @evaled-result
                             [err-boundary
                              [:pre.border-2.pa-4.pl-6.text-xl
                               {:style {:white-space "pre-line"}}
                               ;; See org mode :results key
                               (case results
                                 "verbatim" (str @evaled-result)
                                 "value" (str @evaled-result)
                                 "output" @evaled-result
                                 "silent" ""
                                 @evaled-result)]])]))})))


(defn link-handler
  "Should use youtube.com/embed for embeds"
  [v _]
  (cond
    (some? (re-matches #"^(https://youtu.be/|https://www.youtube.com/).*" (str (:href v))))
    [:iframe {:src (:href v)
              :title "YouTube video player"
              :width "560"
              :height "315"
              :frameborder "0"
              :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"}]
    :else [:a {:href (:href v)} (str v)]))


(def theme-tachyon
  (merge tachyon-theme
         {:SRC code-editor
          :HEADER1 header-lvl1 #_(fn [v _]  [:h1 {:id (slugify v)} [:a {:name (slugify v)} v]])
          :HEADER2 header-lvl2 #_(fn [v _] [:h2 {:id (slugify v)} [:a {:name (slugify v)} v]])
          :HEADER3 header-lvl3 #_(fn [v _] [:h2 {:id (slugify v)} [:a {:name (slugify v)} v]])
          :LINE :p.f5.f5-ns.lh-copy.mt0
          :BULLETS :ul.mt0
          :LINK :a #_link-handler}))

(def theme
  (merge tailwind-theme
         {:SRC code-editor
          :HEADER1 header-lvl1 #_(fn [v _]  [:h1.text-2xl.fw6.lh-title.mt-0.mb-2 {:id (slugify v)} [:a {:name (slugify v)} v]])
          :HEADER2 header-lvl2 #_(fn [v _] [:h2.text-xl.fw6.lh-title.mt-0.mb-2 {:id (slugify v)} [:a {:name (slugify v)} v]])
          :HEADER3 header-lvl3 #_(fn [v _] [:h2.text-base.lh-title.mt-0.mb-2 {:id (slugify v)} [:a {:name (slugify v)} v]])
          :LINE :p.f5.f5-ns.lh-copy.mt0
          :BULLETS :ul.mt0
          :LINK link-handler}))

(def theme-toc-tachyon
    (merge tachyon-theme
           {:HEADER1 (fn [v _] [:li [:a {:href (str "#" (slugify v))}
                                     [:span.f4.fw6.f3-ns.lh-title.mt0.mb2 v]]])
            :HEADER2 (fn [v _] [:li.ml2 [:a {:href (str "#" (slugify v))}
                                         [:span.f4.fw6.f4-ns.lh-title.mt0.mb2 v]]])
            :HEADER3 (fn [v _] [:li.ml4 [:a {:href (str "#" (slugify v))}
                                         [:span.f5.fw6.f5-ns.lh-title.mt0.mb2 v]]])
            :HEADER4 (fn [v _] [:li.ml6 [:a {:href (str "#" (slugify v))}
                                         [:span.f6.fw6.f5-ns.lh-title.mt0.mb2 v]]])
            :HEADER5 (fn [v _] [:li.ml8 [:a {:href (str "#" (slugify v))}
                                         [:span.f6.fw6.f5-ns.lh-title.mt0.mb2 v]]])
            :HEADER6 (fn [v _] [:li.ml10 [:a {:href (str "#" (slugify v))}
                                          [:span.f6.fw6.f5-ns.lh-title.mt0.mb2 v]]])}))

(def theme-toc
  (merge tailwind-theme
         {:HEADER1 (fn [v _] [:li.list-decimal [:a.text-blue-800 {:class "hover:text-blue-500" :href (str "#" (slugify v))}
                                                [:span.text-xl.fw6.lh-title.mt0.mb2 v]]])
          :HEADER2 (fn [v _] [:li.list-decimal.ml-4 [:a.text-blue-800 {:href (str "#" (slugify v))}
                                                     [:span.text-lg.fw6.lh-title.mt0.mb2 v]]])
          :HEADER3 (fn [v _] [:li.list-decimal.ml-8 [:a.text-blue-800 {:class "hover:text-blue-500" :href (str "#" (slugify v))}
                                                     [:span.text-base.fw6.lh-title.mt0.mb2 v]]])
          :HEADER4 (fn [v _] [:li.list-decimal.ml-12 [:a.text-blue-800 {:href (str "#" (slugify v))}
                                                      [:span.text-sm.fw6.lh-title.mt0.mb2 v]]])
          :HEADER5 (fn [v _] [:li.list-decimal.ml-26 [:a.text-blue-800 {:href (str "#" (slugify v))}
                                                      [:span.text-xs.fw6.f5-ns.lh-title.mt0.mb2 v]]])
          :HEADER6 (fn [v _] [:li.list-decimal.ml-30 [:a.hover:decoration-blue-400 {:href (str "#" (slugify v))}
                                                      [:span.text-xs.fw6.f5-ns.lh-title.mt0.mb2 v]]])}))


;; form one component to render an article
(defn article [{:keys [title description tagline]}]
  [:article.prose {:data-name "article-full-bleed-background"}
   [:div.cf {:style {:background "url(http://placekitten.com/g/600/300)"
                     :no-repeat "center center fixed" :background-size "cover"}}
    [:div.fl.pa3.pa4-ns.bg-white.black-70.measure-narrow.f3.times
     [:header.b--black-70.pv4 {:class (when tagline "bb")}
      [:h3.f2.fw7.ttu.tracked.lh-title.mt0.mb3.avenir title]
      (when tagline [:h4.f3.fw4.i.lh-title.mt0 tagline])]
     [:section.pt5.pb4 [:p.times.lh-copy.measure.f5.mt0 description]]]]])

;; form one component to render article tiles
(defn articles [{:keys [title body articles]}]
  [:section.mw7.center.avenir
   [:h2.text-4xl.mt-4.mb-4 title]
   (map (fn [{:keys [title author link description img-src img-alt] :as article}]
          [:article.bt.bb.b--black-10.border-b-2 {:key title}
           [:a {:href link}
            [:div.flex.flex-column.flex-row-ns.p-4
             [:div.flex-none.w-32
              (when img-src
                [:img.w-32 #_h-24.block {:src img-src :alt img-alt}])]
             [:div.ml-4.flex-grow #_{:class "w-2/3"}
              [:h1.text-2xl title]
              [:p description]
              [:p author]]]]])
        articles)])

;; form one component to render a nav link
(defn navbar-link [opts]
  (ui/navbar-link opts))

;; form one component to render a navbar
(defn navbar [links]
  (ui/navbar links))

(defn footer []
  (ui/footer))

(def toc (partial contains? (into #{} (map keyword [:HEADER1 :HEADER2 :HEADER3 :HEADER4 :HEADER5 :HEADER6]))))
(def org-code (partial contains? (into #{} (map keyword [:SRC]))))

(defn build-file-tar [code-blocks]
  (tarts (clj->js (mapv (fn [block] {:name (str (:tangle (second block)))
                                     :content (str (last block))})
                        code-blocks))))

(defn build-file-tar-hm [code-blocks]
  (tarts (clj->js code-blocks)))

(defn build-tarts-map [blocks]
  (->> blocks
       (group-by (fn [block] (:tangle (second block))))
       (reduce (fn [m [k v]]
                 (conj m {:name (str k)
                          :content (str (str/join "\n\n" (mapv last v)))})) [])))

(defn org->split2
  "Split out meta and body"
  [dsl]
  {:header (filter (fn filter-headers [tag] (headers (first tag))) dsl)
   :toc (filter (fn filter-toc [tag] (toc (first tag))) dsl)
   :code (filter (fn filter-code [tag] (org-code (first tag))) dsl)
   :body (filter (fn filter-body [tag] (body (first tag))) dsl)})

(defn home-page []
  [:<>
   [articles
    {:title "Clojure Demos"
     :body (-> site-data :homepage :intro)
     :articles
     [{:title "Clojure Basics"
       :description "Getting started with clojure syntax datatype's sequences conditions"
       :link (rfe/href :page {:page "clojure-basics"})
       :img-src "https://clojure.org/images/clojure-logo-120b.png"}
      {:title "Reagent Demo"
       :description "React application using reagent"
       :link (rfe/href :page {:page "reagent-demo"})
       :img-src "https://raw.githubusercontent.com/reagent-project/reagent/master/logo/logo-text.png"}]}]])



(defn grouped-lists-page [data]
  [articles
   {:title (-> data :title)
    :body (-> data :intro)
    :articles
    (mapv (fn fmt-map [demo]
            {:title (:title demo)
             :description (:description demo)
             :link (rfe/href :page {:page (:page demo)})
             :img-src (:icon-image demo)})
          (-> data :demos))}])

(defn lists-page [data]
  [articles
   {:title (-> data :title)
    :body (-> data :intro)
    :articles
    (mapv (fn fmt-map [demo]
            {:title (:title demo)
             :description (:description demo)
             :link (rfe/href :page {:page (:page demo)})
             :img-src (:icon-image demo)})
          (-> data :demos))}])

(defn list-view [route]
  (let [group (keyword (name (:name (:data route))))]
    (if-let [page-data (-> site-data :pages group)]
      (lists-page page-data)
      [:div "Page not found"])))

(defn grouped-page [route]
  (let [group (keyword (name (:name (:data route))))]
    (if (vector? (-> site-data :pages group ))
      (into [:div]
            (mapv #(lists-page %) (-> site-data :pages group)))
      (lists-page (-> site-data :pages group )))))

(defn default-page [route]
  (let [demo-key (keyword (-> route :parameters :path :page))
        content (reagent/atom {})]
    (GET (str "/" (-> site-data :demos demo-key :file))
      {:response-format (raw-response-format)
       :handler (fn [response]
                  (->> response
                       parse
                       org->split2
                       (reset! content)))})
    (fn [route]
      (if @content
        [:main
         [:h1.mt-8.mb-8.text-4xl (:content (last (first (:header @content))))]
         [:div
          (into [:div] (org->replacements theme (:body @content)))]]
        [:<>]))))

(defn default-page-header [route]
  (let [demo-key (keyword (-> route :parameters :path :page name))
        org-file (-> site-data :demos demo-key :file)
        content (reagent/atom {})]
    (when org-file
      (GET (str "/" org-file)
           {:response-format (raw-response-format)
            :handler (fn [response]
                       (->> response
                            parse
                            org->split2
                            (reset! content)))}))
    (fn [route]
      (if @content
        [:main
         [:h1.mt-8.mb-8.text-4xl (:content (last (first (:header @content))))]
         [:div.mw7.center.avenir
          (into [:ol.list-inside.m-6.font-semibold] (org->replacements theme-toc (:toc @content)))]
         [:p "The code in these examples is evaluated when modified, you can highlight a partial expression to evalute the selection, You can also download the code as a tar if you like and use it in your favourite editor."]
         [:a.bg-sky-500.text-white.m-2.p-2.pl-4.pr-4.inline-block
          {:download (str (slugify (:content (last (first (:header @content))))) ".tar")
           :title (:content (last (first (:header @content))))
           :href (.createObjectURL
                  js/URL
                  (js/Blob. #js [(build-file-tar-hm
                                  (build-tarts-map
                                   (:code @content)))]
                            {:type "application/tar"}))}
          "Download Code"]
         [:div
          (into [:div] (org->replacements theme (:body @content)))]]
        [:<> "Sorry page not found"]))))

;; form one render about page component
(defn about-page []
  [default-page {:parameters {:path {:page "about"}}}])


(defn render-view
  "We lookup the view to render from a key in the route, this keeps our routes as pure data"
  [view]
  (case view
    :homepage home-page
    :grouped-page grouped-page
    :grouped-lists grouped-lists-page
    :list list-view
    :default-page-header default-page-header
    :about-page about-page
    lists-page))

;; top level component contains nav and adds in the select page into a containing element
;; we are adding in a style sheet but this will often be done in index.html
(defn current-page []
  (let [route (reagent/cursor site-state [:current-route])]
    (swap! site-state dissoc :notification)
    (fn []
      [:div
       [notification]
       [navbar (concat
                [{:href (rfe/href :frontpage) :title "title here" :text "home" :key "homepage"}]
                (mapv (fn build-nav [[_ page]]
                        {:href (rfe/href (:key page))
                         :text (:title page)})
                      (-> site-data :pages))
                [{:title "About page" :href (rfe/href :about) :text "About" :key "about"}
                 #_{:href (rfe/href ::i-do-not-exist) :text "missing"}])]
       [:main.m-auto {:class "w-3/5 md:w-4/5"}
        (when-let [view (-> @route :data :view render-view)]
          [:div
           [view @route]])]
       [footer]])))

;; This simply calls reagent render and puts the result in a div with the id of app
;; you can create your own index.html or figwheel provides one with the app id which will replace the default data
;; ^:after-load is meta data its not needed but informs figwheel to run this code after a page load
(defn mount-root-page []
  ;; this select the main node from the html file and injects your page content
  (.render
   (createRoot (.getElementById js/document "app"))
   (reagent/as-element [err-boundary [current-page]])))

(defn ^:after-load render-site []
  ;; this select the main node from the html file and injects your page content
  (mount-root-page))

(defn ^:dev/after-load startup! []
  (rfe/start!
   (rf/router routes {:data {:coercion rss/coercion}})
   (fn [m] (swap! site-state assoc :current-route m))
   ;; set to false to enable HistoryAPI
   {:use-fragment false
    :ignore-anchor-click?
    (fn [router e el uri]
      ;; Add additional check on top of the default checks\
      (and
       (ignore-anchor-click? router e el uri)
       (not (let [href (or (.-href el) "")
                  result (str/includes? href "#")]
              #_(when result
                ;(.preventDefault e)
                  #_(js/console.log "will prevent by href" href))
              result))))})
  (render-site))

;; Generate the sitemap at start, routes are stored as plain data in cljc files
;; this means cljs and clj can read and manipulate them
(sm/spit-pwa-sitemap
 "bases/clojure-demos/resources/public/sitemap.xml"
 "https://clojure-demos.digitaloctave.com")

;; we defonce the startup so that hot reloading does not reinitialize the state of the site
(def launch (do (startup!) true))


(comment
  @site-state
  (org->replacements tachyon-theme [[:SRC {:LANGUAGE "shell"} "hi"]])

  (GET "/test.org" {:handler (fn [response] (swap! site-state assoc :content response))}))
