(ns uxbox.util.geom.matrix
  (:require [cuerdas.core :as str]
            [uxbox.util.math :as mth]
            [uxbox.util.geom.point :as gpt]))

(defrecord Matrix [a b c d tx ty])

(defprotocol ICoerce
  "Matrix coersion protocol."
  (-matrix [v] "Return a matrix instance."))

(extend-type Matrix
  cljs.core/IDeref
  (-deref [v]
    (mapv #(get v %) [:a :c :b :d :tx :ty]))

  Object
  (toString [v]
    (->> (str/join "," @v)
         (str/format "matrix(%s)"))))

(extend-protocol ICoerce
  nil
  (-matrix [_]
    (Matrix. 1 0 0 1 0 0))

  Matrix
  (-matrix [v] v)

  cljs.core/PersistentVector
  (-matrix [v]
    (let [[a b c d tx ty] v]
      (Matrix. a b c d tx ty)))

  cljs.core/IndexedSeq
  (-matrix [v]
    (let [[a b c d tx ty] v]
      (Matrix. a b c d tx ty))))

(defn matrix?
  "Return true if `v` is Matrix instance."
  [v]
  (instance? Matrix v))

(defn matrix
  "Create a new matrix instance."
  ([]
   (Matrix. 1 0 0 1 0 0))
  ([v]
   (-matrix v))
  ([a b c d tx ty]
   (Matrix. a b c d tx ty)))

(defn rotate
  "Apply rotation transformation to the matrix."
  ([m angle]
   (let [center (gpt/point 0 0)]
     (rotate m angle center)))
  ([m angle center]
   (let [angle (mth/radians angle)
         x (:x center)
         y (:y center)
         cos (mth/cos angle)
         sin (mth/sin angle)
         nsin (- sin)
         tx (- x (+ (* x cos)) (* y sin))
         ty (- y (- (* x sin)) (* y cos))
         a (+ (* cos (:a m)) (* sin (:b m)))
         b (+ (* nsin (:a m)) (* cos (:b m)))
         c (+ (* cos (:c m)) (* sin (:d m)))
         d (+ (* nsin (:c m)) (* cos (:d m)))
         tx' (+ (:tx m) (* tx (:a m)) (* ty (:b m)))
         ty' (+ (:ty m) (* tx (:c m)) (* ty (:d m)))]
     (Matrix. a b c d tx' ty'))))

(defn scale
  "Apply scale transformation to the matrix."
  ([m v] (scale m v v))
  ([m x y]
   (assoc m
          :a (* (:a m) x)
          :c (* (:c m) x)
          :b (* (:b m) y)
          :d (* (:d m) y))))

(defn translate
  "Apply translate transformation to the matrix."
  ([m pt]
   (let [pt (gpt/-point pt)]
     (assoc m
            :tx (+ (:tx m) (* (:x pt) (:a m)) (* (:y pt) (:b m)))
            :ty (+ (:ty m) (* (:x pt) (:c m)) (* (:y pt) (:d m))))))
  ([m x y]
   (translate m (gpt/point x y))))

(defn append
  ([m om]
   (let [a1 (:a m)
         b1 (:b m)
         c1 (:c m)
         d1 (:d m)
         a2 (:a om)
         b2 (:b om)
         c2 (:c om)
         d2 (:d om)
         tx1 (:tx m)
         ty1 (:ty m)
         tx2 (:tx om)
         ty2 (:ty om)]
     (Matrix.
      (+ (* a2 a1) (* c2 b1))
      (+ (* b2 a1) (* d2 b1))
      (+ (* a2 c1) (* c2 d1))
      (+ (* b2 c1) (* d2 d1))
      (+ tx1 (* tx2 a1) (* ty2 b1))
      (+ ty1 (* tx2 c1) (* ty2 d1)))))
  ([m om & others]
   (reduce append (append m om) others)))
