(ns advent.actions
  (:require [play-clj.core :refer :all]
            [play-clj.ui :refer :all]
            [play-clj.utils :refer :all]
            [play-clj.g2d :refer :all]
            [clojure.pprint]
            [clojure.string :as s]
            [clojure.zip :as zip]
            [advent.pathfind]
            [advent.actions :as actions]
            [advent.screens.dialogue :as dialogue]
            [clojure.core.async :refer [put! <! <!! >! >!! chan go thread take! alts!!]])
  (:import [com.badlogic.gdx.graphics Pixmap Pixmap$Filter Texture Texture$TextureFilter]
           [com.badlogic.gdx.graphics.g2d TextureRegion]
           [com.badlogic.gdx Screen]))

(defprotocol IAction
  (begin [this screen entities])
  (done? [this screen entities])
  (continue [this screen entities])
  (terminate [this screen entities])
  (can-skip? [this screen entities])
  (get-channel [this]))


(defmacro get-script [entities & forms]
  `(fn [starting-entities#]
     (let [~entities (atom starting-entities#)]
       (thread ~@forms))))


(defn jump-to [screen entities entity [x y]]
  (let [scale-fn (-> entities :room :scale-fn)
        entity (assoc entity :x x
                      :y y
                      :baseline (- 240 y))]
    (if (:scaled entity)
      (assoc entity :scale-x (scale-fn y) :scale-y (scale-fn y))
      entity)))

(defn start-animation [screen entity anim]
  (let [new-anim (if (keyword? anim)
                   (or (get-in entity [(:facing entity) anim])
                       (anim entity))
                   anim)]
    (if (and anim (not= new-anim (:anim entity)))
      (assoc entity
        :anim new-anim
        :anim-start (:total-time screen))
      entity)))

(defn stop [screen entities target-id]
  (update-in entities [:room :entities target-id] #(start-animation screen % :stand)))


(defn dist [x1 y1 x2 y2]
  (let [dx (- x1 x2)
        dy (- y1 y2)]
    (Math/sqrt (+ (* dx dx) (* dy dy)))))

(defmacro run-action [entities & forms]
  `(let [c# (chan)]
     (do
       (put! (get-in (deref ~entities) [:actions :channel])
             (reify IAction
               (get-channel [_] c#)
               ~@forms))
       (reset! ~entities (<!! c#)))))

(defn walk-straight-to [entities target-id [final-x final-y]]
  (let [{start-x :x start-y :y} (get-in @entities [:room :entities target-id])
        final-x (int final-x)
        final-y (int final-y)]
    (run-action entities
                (begin [this screen entities]
                       entities)

                (continue [this screen entities]
                          (let [{from-x :x from-y :y :keys [left right scale-x] :as target-entity} (get-in entities [:room :entities target-id])]
                            (let [delta-x (- final-x from-x)
                                  delta-y (- final-y from-y)
                                  distance (dist from-x from-y final-x final-y)
                                  speed (* (or scale-x 1.0) 1.5)
                                  moved-x (if (= 0.0 distance)
                                            0
                                            (* speed (/ delta-x distance)))
                                  moved-y (if (= 0.0 distance)
                                            0
                                            (* speed (/ delta-y distance)))]
                              (if (< distance speed)
                                (-> entities
                                    (assoc-in [:room :entities target-id :x] final-x)
                                    (assoc-in [:room :entities target-id :y] final-y))
                                (update-in entities [:room :entities target-id]
                                           #(start-animation screen
                                                             (assoc (jump-to screen entities % [(+ moved-x from-x) (+ moved-y from-y)])
                                                               :facing (cond (< delta-x 0) :left
                                                                             (> delta-x 0) :right
                                                                             :else (:facing %)))
                                                             :walk
                                                             ))))))

                (done? [this screen entities]
                       (let [{from-x :x from-y :y :keys [left right anim] :as target-entity} (get-in entities [:room :entities target-id])]
                         (< (dist final-x final-y from-x from-y) 1)))

                (terminate [this screen entities]
                           (stop screen entities target-id))
                (can-skip? [this screen entities]
                           false))))

(defn walk-to [entities target-id [final-x final-y] & [can-skip?]]
  (let [{start-x :x start-y :y} (get-in @entities [:room :entities target-id])
        final-x (int final-x)
        final-y (int final-y)
        path (vec (take-nth 5 (advent.pathfind/visit-all
                               (:collision (:room @entities))
                               [(int start-x) (int start-y)]
                               [final-x final-y])))
        path (if (seq path)
               (conj path [final-x final-y])
               [])
        targets-left (atom path)]
    (if (seq path)
      (run-action entities
                  (begin [this screen entities]
                         entities)

                  (continue [this screen entities]
                            (let [{from-x :x from-y :y :keys [left right scale-x] :as target-entity} (get-in entities [:room :entities target-id])
                                  [[target-x target-y] remainder] @targets-left]
                              (let [delta-x (- target-x from-x)
                                    delta-y (- target-y from-y)
                                    distance (dist from-x from-y target-x target-y)
                                    speed (* (or scale-x 1.0) 1.5)
                                    moved-x (if (= 0.0 distance)
                                              0
                                              (* speed (/ delta-x distance)))
                                    moved-y (if (= 0.0 distance)
                                              0
                                              (* speed (/ delta-y distance)))]
                                (if (< distance speed)
                                  (do (swap! targets-left rest)
                                      (-> entities
                                          (assoc-in [:room :entities target-id :x] target-x)
                                          (assoc-in [:room :entities target-id :y] target-y)))
                                  (update-in entities [:room :entities target-id]
                                             #(start-animation screen
                                                               (assoc (jump-to screen entities % [(+ moved-x from-x) (+ moved-y from-y)])
                                                                 :facing (cond (< delta-x 0) :left
                                                                               (> delta-x 0) :right
                                                                               :else (:facing %)))
                                                               :walk
                                                               ))))))

                  (done? [this screen entities]
                         (let [{from-x :x from-y :y :keys [left right anim] :as target-entity} (get-in entities [:room :entities target-id])]
                           (< (dist final-x final-y from-x from-y) 1)))

                  (terminate [this screen entities]
                             (stop screen entities target-id))
                  (can-skip? [this screen entities]
                            (or can-skip? false)))
      @entities)))

(defn get-text-duration [text]
  (* (count (s/split text #" ")) 0.5))

(defn talk [entities target-id text & {:keys [stop?]}]
  (let [initial-time (atom nil)
        stop? (if (nil? stop?) true stop?)]
    (run-action entities
                (begin [this screen entities]
                       (let [_ (swap! initial-time #(or % (:total-time screen)))
                             target-y (get-in entities [:room :entities target-id :y])
                             scale-fn (get-in entities [:room :scale-fn])
                             scale (scale-fn target-y)
                             height (* scale 36)]
                         (run! dialogue/talking-screen :on-talk :text text 
                               :x (get-in entities [:room :entities target-id :x]) :y (+ (get-in entities [:room :entities target-id :y]) height)
                               :target-id target-id
                               :scale scale)
                         (update-in entities [:room :entities target-id ] #(start-animation screen % :talk))))

                (continue [this screen entities] entities)

                (done? [this screen entities]
                       (> (- (:total-time screen)
                             @initial-time)
                          (get-text-duration text)))

                (terminate [this screen entities]
                           (run! dialogue/talking-screen :stop-talk :target-id target-id)
                           (if stop?
                             (stop screen entities target-id)
                             entities))
                (can-skip? [this screen entities]
                          true))))

(defn something-else [zipper]
  (-> zipper zip/up zip/up))

(defn previous-choices [zipper]
  (-> zipper zip/up))

(defn nth-child [zipper i]
  (loop [so-far 0
         zipper (zip/down zipper)]
    (if (= so-far i)
      zipper
      (recur (inc so-far) (zip/right zipper)))))

(defn make-zipper [tree]
    (zip/zipper map? (comp #(map second %) #(partition 2 %) :choices) (fn [n c] nil) tree))

(defn present-choices [entities choices]
  (loop [zipper (make-zipper choices)]
    (let [selected-index (atom nil)
          node (zip/node zipper)
          dialogue-choices (partition 2 (:choices node))]
      (run-action entities
                  (begin [this screen entities]
                         (run! dialogue/choice-screen :on-present-choices :choices dialogue-choices :callback #(reset! selected-index %))
                         (run! @(resolve 'advent.screens.scene/scene) :on-deactivate)
                         entities)

                  (continue [this screen entities] entities)

                  (done? [this screen entities] (not (nil? @selected-index)))

                  (terminate [this screen entities]
                             (run! @(resolve 'advent.screens.scene/scene) :on-reactivate)
                             entities)
                  (can-skip? [this screen entities]
                            false))

      (let [zipper (nth-child zipper @selected-index)
                 node (zip/node zipper)]
        (when-let [run (:run node)]
          (run (-> dialogue-choices
                   (nth @selected-index)
                   first)))
        (when-let [next-choices (:choices node)]
          (if (fn? next-choices)
            (recur  (next-choices zipper))
            (recur zipper)))))))

(defn update-state [entities f]
  (run-action entities
              (begin [this screen entities]
                     (update-in entities [:state] f ))
              (continue [this screen entities] entities)
              (done? [this screen entities] true)
              (terminate [this screen entities] 
                         entities)
              (can-skip? [this screen entities]
                         false)))

(defn give [entities item]
  (run-action entities
              (begin [this screen entities]
                     (sound! (sound "pickup.mp3") :play)

                     (-> entities
                         (update-in [:state :inventory] #(conj % item))
                         (assoc-in [:cursor :current] item)))

              (continue [this screen entities] entities)

              (done? [this screen entities] true)

              (terminate [this screen entities] 
                         entities)
              (can-skip? [this screen entities]
                            false)))

(defn remove-entity [entities entity]
  (run-action entities
              (begin [this screen entities]
                     (update-in entities [:room :entities] #(dissoc % entity)))

              (continue [this screen entities] entities)

              (done? [this screen entities] true)

              (terminate [this screen entities] entities)

              (can-skip? [this screen entities]
                            false)))

(defn transition-background [entities new-background [x y]]
  (run-action entities
              (begin [this screen entities] 
                     
                     (-> entities
                         (assoc-in [:transition]
                                   (assoc (texture "black.png")
                                     :scale-x 20
                                     :scale-y 20
                                     :baseline 9500
                                     :opacity 0.1))))

              (continue [this screen entities]
                        (update-in entities [:transition :opacity] + 0.075))

              (done? [this screen entities]
                     (>= (get-in entities [:transition :opacity]) 1.0))

              (terminate [this screen entities]
                         entities)
              (can-skip? [this screen entities]
                        false))
  (run-action entities
              (begin [this screen entities] 
                     (let [ego (get-in entities [:room :entities :ego])
                           entities (-> entities 
                                        (assoc-in [:room] (get-in entities [:rooms new-background]))
                                        (assoc-in [:room :entities :ego] ego))]
                       (-> entities
                           (update-in [:room :entities :ego] #(jump-to screen entities % [x y])))))

              (continue [this screen entities]
                        (update-in entities [:transition :opacity] - 0.075))

              (done? [this screen entities]
                     (<= (get-in entities [:transition :opacity]) 0.0))

              (terminate [this screen entities]
                         (dissoc entities :transition))

              (can-skip? [this screen entities]
                            false)))

(defn do-dialogue [entities & pairs]
  (loop [pairs (partition 2 pairs)]
    (let [[[target line]] pairs
          next-speaker-is-different (not= target (ffirst (next pairs)))
          result (talk entities target line :stop? next-speaker-is-different)]
      (if (seq (rest pairs))
        (recur (rest pairs))
        result))))

(defn respond [entities line & more]
  (apply do-dialogue entities :ego line more))
