#![recursion_limit = "128"]
extern crate proc_macro;
extern crate proc_macro2;
extern crate syn;
#[macro_use]
extern crate quote;
extern crate regex;
#[macro_use]
extern crate log;

extern crate xml;
use proc_macro2::*;
use xml::reader::{EventReader, XmlEvent, ParserConfig};
use std::iter::once;
use std::io::{BufRead, Read};
use regex::Regex;

#[proc_macro_attribute]
pub fn template(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
    env_logger::try_init().unwrap_or(());
    let attr = proc_macro2::TokenStream::from(attr);
    let item = proc_macro2::TokenStream::from(item);


    let mut file_ = String::new();
    let mut attr = attr.into_iter();

    let mut re = regex::Regex::new(r"(\{\{)|(\}\})|(\{([^\}\n]+)\})").expect("regex");

    while let Some(a) = attr.next() {
        if format!("{}", a) == "path" {
            if let (Some(a), Some(b)) = (attr.next(), attr.next()) {
                if format!("{}", a) == "=" {
                    file_ = format!("{}", b)
                }
            }
        } else if format!("{}", a) == "regex" {
            if let (Some(a), Some(b)) = (attr.next(), attr.next()) {
                if format!("{}", a) == "=" {
                    re = regex::Regex::new(&format!("{}", b)).expect("regex");
                }
            }
        } else {
            println!("unknown attribute {:?}", a);
        }
    }
    let cargo_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
    let mut file = std::path::Path::new(&cargo_dir).join("templates");
    file.push(file_.trim_matches('"'));
    info!("parsing {:?}", file);

    let template = {
        let mut f = std::fs::File::open(&file).unwrap();
        let mut template = String::new();
        f.read_to_string(&mut template).unwrap();
        template
    };

    // Replacing all & (except in "&blabla;") with &amp;
    let re_amp = regex::Regex::new(r"(&[a-zA-Z]+;)|&").unwrap();
    let template = re_amp.replace_all(&template, |cap: &regex::Captures| {
        if let Some(c) = cap.get(1) {
            c.as_str().to_string()
        } else {
            "&amp;".to_string()
        }
    });


    let mut name = None;
    let mut item = item.into_iter();
    let mut item_ = Vec::new();
    let mut spec = proc_macro2::TokenStream::new();
    let mut spec2 = proc_macro2::TokenStream::new();
    let mut is_name = true;
    let mut last_was_name = false;
    loop {
        match item.next() {
            Some(TokenTree::Ident(id)) => {
                if id.to_string() == "struct" || id.to_string() == "enum" {
                    let it = item.next().unwrap();
                    name = Some(syn::Ident::new(&format!("{}", it), it.span()));
                    item_.push(TokenTree::Ident(id));
                    item_.push(it);
                    last_was_name = true;
                } else {
                    item_.push(TokenTree::Ident(id));
                }
            }
            None => break,
            Some(TokenTree::Punct(p)) => {
                // println!("punct {:?} {:?}", p, last_was_name);
                if last_was_name {
                    if p.to_string() == "<" {
                        let mut level = 1;
                        spec.extend(once(TokenTree::Punct(p.clone())));
                        spec2.extend(once(TokenTree::Punct(p.clone())));
                        item_.push(TokenTree::Punct(p));
                        loop {
                            match item.next() {
                                Some(TokenTree::Punct(p)) => {
                                    let pp = p.to_string();
                                    spec.extend(once(TokenTree::Punct(p.clone())));
                                    item_.push(TokenTree::Punct(p.clone()));
                                    if pp == ">" {
                                        level -= 1;
                                        if level <= 0 {
                                            spec2.extend(once(TokenTree::Punct(p.clone())));
                                            break
                                        }
                                    } else if pp == "<" {
                                        level += 1;
                                    } else if pp == ":" {
                                        is_name = false;
                                    } else if pp == "," && level == 1 {
                                        spec2.extend(once(TokenTree::Punct(p.clone())));
                                        is_name = true;
                                    } else if is_name {
                                        spec2.extend(once(TokenTree::Punct(p.clone())));
                                    }
                                }
                                Some(it) => {
                                    spec.extend(once(it.clone()));
                                    if is_name {
                                        spec2.extend(once(it.clone()));
                                    }
                                    item_.push(it)
                                }
                                None => break,
                            }
                        }
                    } else {
                        item_.push(TokenTree::Punct(p));
                    }
                } else {
                    item_.push(TokenTree::Punct(p))
                }
            }
            Some(it) => {
                item_.push(it)
            }
        }
    }
    let name = name.unwrap();

    use std::iter::FromIterator;
    let item = proc_macro2::TokenStream::from_iter(item_);

    let tokens = walk(&re, std::io::Cursor::new(template.as_bytes()));
    let tok: TokenStream = tokens.parse().unwrap();
    let file = file.to_str().unwrap();
    let tokens = quote! {
        impl #spec cuach::Render for #name #spec2 {
            fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), anyhow::Error> {
                let _ = include_bytes!(#file);
                use std::fmt::Write;
                use cuach::Render;
                #tok
                Ok(())
            }
        }
        #item
    };
    debug!("{}", tokens);
    proc_macro::TokenStream::from(tokens)
}

use std::fmt::Write;
fn walk<B: BufRead>(re: &Regex, file: B) -> String {
    let mut tokens = String::new();
    let t0 = std::io::Cursor::new("<template>");
    let t1 = std::io::Cursor::new("</template>");
    let config = ParserConfig::new()
        .ignore_comments(false);
    let parser = EventReader::new_with_config(t0.chain(file).chain(t1), config);
    let mut level = 0;
    for e in parser {
        match e {
            Ok(XmlEvent::StartElement { name, attributes, .. }) => {
                if level == 0 {
                    // the "<template>" tag.
                    level += 1;
                    continue
                }
                level += 1;
                let mut s = format!("<{}", name.local_name);
                for attr in attributes.iter() {
                    write!(&mut s, " {}=\"{}\"", attr.name.local_name, attr.value).unwrap();
                }
                if name.local_name == "br" {
                    write!(&mut s, "/>").unwrap();
                } else {
                    write!(&mut s, ">").unwrap();
                }
                // Replace {bla} with a formatting string.
                let arguments = arguments(re, &s);
                for args in arguments {
                    match args {
                        Arg::Text(t) => writeln!(tokens, "w.write_str({:?})?;", t).unwrap(),
                        Arg::Arg(a) => writeln!(tokens, "(\n{}\n).render_into(w)?;", a).unwrap(),
                    }
                }
            }
            Ok(XmlEvent::EndElement { name }) => {
                level -= 1;
                if level == 0 {
                    // the "</template>" tag.
                    continue
                }
                if name.local_name != "br" {
                    writeln!(tokens, "w.write_str(\"</{}>\")?;", name.local_name).unwrap()
                }
            }
            Ok(XmlEvent::Comment(contents)) => {
                let contents = contents.replace("&amp;", "&");
                tokens.push_str(&contents);
                tokens.push_str("\n");
            }
            Ok(XmlEvent::Characters(contents)) => {
                let arguments = arguments(re, &contents);
                for args in arguments {
                    match args {
                        Arg::Text(t) => {
                            let re = regex::Regex::new(r"\s+").expect("regex");
                            let t = re.replace_all(t, " ");
                            writeln!(tokens, "w.write_str({:?})?;", t).unwrap()
                        },
                        Arg::Arg(a) => writeln!(tokens, "({}).render_into(w)?;", a).unwrap(),
                    }
                }
            }
            Ok(XmlEvent::Whitespace(w)) => {
                if !w.chars().any(|x| x == '\n') {
                    writeln!(tokens, r#"w.write_str(" ")?;"#).unwrap()
                }
            }
            Ok(XmlEvent::StartDocument { .. }) => {}
            Ok(XmlEvent::EndDocument) => {}
            Ok(XmlEvent::ProcessingInstruction { .. }) => {}
            Ok(XmlEvent::CData(_)) => {}
            Err(e) => {
                panic!("{:?}", e);
            }
        }
    }
    tokens
}

enum Arg<'a> {
    Text(&'a str),
    Arg(&'a str),
}
fn arguments<'a>(re: &Regex, s: &'a str) -> Vec<Arg<'a>> {
    let mut arguments = Vec::new();
    let mut start = 0;
    for cap in re.captures_iter(s) {
        debug!("cap = {:?}", cap);
        if let Some(cap2) = cap.get(1) {
            arguments.push(Arg::Text("{"));
            start = cap2.end()
        } else if let Some(cap2) = cap.get(2) {
            arguments.push(Arg::Text("}"));
            start = cap2.end()
        } else if let Some(cap2) = cap.get(3) {
            let (a, _) = s.split_at(cap2.end());
            let (a, _) = a.split_at(cap2.start());
            if cap2.start() > start {
                arguments.push(Arg::Text(a.split_at(start).1))
            }
            arguments.push(Arg::Arg(cap.get(4).unwrap().as_str()));
            start = cap2.end();
        }
    }
    if start < s.len() {
        arguments.push(Arg::Text(s.split_at(start).1))
    }
    arguments
}
