use proc_macro::{Literal, Term, TokenNode, TokenStream};
use proc_macro::quote;

use maud_htmlescape::Escaper;

pub struct Renderer {
    output: TokenNode,
    stmts: Vec<TokenStream>,
    tail: String,
}

impl Renderer {
    /// Creates a new `Renderer`.
    pub fn new() -> Renderer {
        let output = TokenNode::Term(Term::intern("__maud_output"));
        Renderer {
            output: output,
            stmts: Vec::new(),
            tail: String::new(),
        }
    }

    /// Creates a new `Renderer` under the same context as `self`.
    pub fn fork(&self) -> Renderer {
        Renderer {
            output: self.output.clone(),
            stmts: Vec::new(),
            tail: String::new(),
        }
    }

    /// Flushes the tail buffer, emitting a single `.push_str()` call.
    fn flush(&mut self) {
        if !self.tail.is_empty() {
            let expr = {
                let output = self.output.clone();
                let string = TokenNode::Literal(Literal::string(&self.tail));
                quote!($output.push_str($string);)
            };
            self.stmts.push(expr);
            self.tail.clear();
        }
    }

    /// Reifies the `Renderer` into a block of markup.
    pub fn into_expr(mut self, size_hint: usize) -> TokenStream {
        let Renderer { output, stmts, .. } = { self.flush(); self };
        let size_hint = TokenNode::Literal(Literal::u64(size_hint as u64));
        let stmts = stmts.into_iter().collect::<TokenStream>();
        quote!({
            extern crate maud;
            let mut $output = String::with_capacity($size_hint as usize);
            $stmts
            maud::PreEscaped($output)
        })
    }

    /// Reifies the `Renderer` into a raw list of statements.
    pub fn into_stmts(mut self) -> TokenStream {
        let Renderer { stmts, .. } = { self.flush(); self };
        stmts.into_iter().collect()
    }

    /// Pushes a statement, flushing the tail buffer in the process.
    pub fn push<T>(&mut self, stmt: T) where T: Into<TokenStream> {
        self.flush();
        self.stmts.push(stmt.into())
    }

    /// Pushes a literal string to the tail buffer.
    fn push_str(&mut self, s: &str) {
        self.tail.push_str(s);
    }

    /// Appends a literal string.
    pub fn string(&mut self, s: &str) {
        self.push_str(&html_escape(s));
    }

    /// Appends the result of an expression.
    pub fn splice(&mut self, expr: TokenStream) {
        let output = self.output.clone();
        self.push(quote!({
            extern crate maud;
            // Create a local trait alias so that autoref works
            trait Render: maud::Render {
                fn render_to(&self, output: &mut String) {
                    maud::Render::render_to(self, output);
                }
            }
            impl<T: maud::Render> Render for T {}
            $expr.render_to(&mut $output);
        }));
    }

    pub fn element_open_start(&mut self, name: &str) {
        self.push_str("<");
        self.push_str(name);
    }

    pub fn attribute_start(&mut self, name: &str) {
        self.push_str(" ");
        self.push_str(name);
        self.push_str("=\"");
    }

    pub fn attribute_empty(&mut self, name: &str) {
        self.push_str(" ");
        self.push_str(name);
    }

    pub fn attribute_end(&mut self) {
        self.push_str("\"");
    }

    pub fn element_open_end(&mut self) {
        self.push_str(">");
    }

    pub fn element_close(&mut self, name: &str) {
        self.push_str("</");
        self.push_str(name);
        self.push_str(">");
    }

    /// Emits an `if` expression.
    ///
    /// The condition is a token tree (not an expression) so we don't
    /// need to special-case `if let`.
    pub fn emit_if(&mut self, if_cond: TokenStream, if_body: TokenStream,
                   else_body: Option<TokenStream>) {
        let stmt = match else_body {
            None => quote!(if $if_cond { $if_body }),
            Some(else_body) => quote!(if $if_cond { $if_body } else { $else_body }),
        };
        self.push(stmt);
    }
}

fn html_escape(s: &str) -> String {
    use std::fmt::Write;
    let mut buffer = String::new();
    Escaper::new(&mut buffer).write_str(s).unwrap();
    buffer
}