(ns stencil.ast
  (:refer-clojure :exclude [partial])
  (:require [clojure.zip :as zip]
            [clojure.string :as string])
  (:use stencil.utils))

;;
;; Data structures
;;

(defprotocol ASTZipper
  (branch? [this] "Returns true if this node can possibly have children,
                   whether it currently does or not.")
  (children [this] "When called on a branch node, returns its children.")
  (make-node [this children] "Given a node (potentially with existing children)
                              and a seq of children that should totally replace
                              the existing children, make the new node."))

(defprotocol ASTNode
  (render [this ^StringBuilder sb context-stack]
    "Given a StringBuilder and the current context-stack, render this node to
     the result string in the StringBuilder."))

;; Section and InvertedSection need to keep track of the raw source code of
;; their contents, since lambdas need access to that. The attrs field lets them
;; keep track of that, with fields
;;      - content-start : position in source string of content start
;;      - content-end   : position in source string of end of content
;;      - content       : string holding the raw content
(defrecord Section [name attrs contents]
  ASTZipper
  (branch? [this] true)
  (children [this] contents)
  (make-node [this children] (Section. name attrs (vec children))))
;; ASTNode IS implemented, but not here. To avoid Clojure's circular
;; dependency inadequacies, we have to implement ASTNode at the top of
;; core.clj.
(defn section [name attrs contents]
  (Section. name attrs contents))

(defrecord InvertedSection [name attrs contents]
  ASTZipper
  (branch? [this] true)
  (children [this] contents)
  (make-node [this children] (InvertedSection. name attrs (vec children)))
  ASTNode
  (render [this sb context-stack]
    ;; Only render the section if the value is not present, false, or
    ;; an empty list.
    (let [ctx (first context-stack)
          ctx-val (context-get context-stack name)]
      ;; Per the spec, a function is truthy, so we should not render.
      (if (and (not (instance? clojure.lang.Fn ctx-val))
               (or (not ctx-val)
                   (and (sequential? ctx-val)
                        (empty? ctx-val))))
        (render contents sb context-stack)))))
(defn inverted-section [name attrs contents]
  (InvertedSection. name attrs contents))

;; Partials can be obligated to indent the entire contents of the sub-template's
;; output, so we hold on to any padding here and apply it after the sub-
;; template renders.
(defrecord Partial [name padding]
  ASTZipper
  (branch? [this] false)
  (children [this] nil)
  (make-node [this children] nil))
;; ASTNode IS implemented, but not here. To avoid Clojure's circular
;; dependency inadequacies, we have to implement ASTNode at the end of
;; loader.clj.
(defn partial [name padding] (Partial. name padding))

(defrecord EscapedVariable [name]
  ASTZipper
  (branch? [this] false)
  (children [this] nil)
  (make-node [this children] nil))
;; ASTNode IS implemented, but not here. To avoid Clojure's circular
;; dependency inadequacies, we have to implement ASTNode at the top of
;; core.clj.
(defn escaped-variable [name] (EscapedVariable. name))

(defrecord UnescapedVariable [name]
  ASTZipper
  (branch? [this] false)
  (children [this] nil)
  (make-node [this children] nil))
;; ASTNode IS implemented, but not here. To avoid Clojure's circular
;; dependency inadequacies, we have to implement ASTNode at the top of
;; core.clj.
(defn unescaped-variable [name] (UnescapedVariable. name))

(extend-protocol ASTZipper
  ;; Want to be able to just stick Strings in the AST.
  java.lang.String
  (branch? [this] false)
  (children [this] nil)
  (make-node [this children] nil)
  ;; Want to be able to use vectors to create lists in the AST.
  clojure.lang.PersistentVector
  (branch? [this] true)
  (children [this] this)
  (make-node [this children] (vec children)))

(extend-protocol ASTNode
  java.lang.String
  (render [this ^StringBuilder sb context-stack] (.append sb this))
  clojure.lang.PersistentVector
  (render [this sb context-stack]
    (dotimes [i (count this)]
      (render (nth this i) sb context-stack))))

;; Implement a Zipper over ASTZippers.

(defn ast-zip
  "Returns a zipper for ASTZippers, given a root ASTZipper."
  [root]
  (zip/zipper branch?
              children
              make-node
              root))