use maud_htmlescape::Escaper;
use proc_macro2::{
    Delimiter,
    Group,
    Literal,
    Span,
    Ident,
    TokenStream,
    TokenTree,
};
use quote::quote;
use proc_macro_error::SpanRange;

use crate::ast::*;

pub fn generate(markups: Vec<Markup>, output_ident: TokenTree) -> TokenStream {
    let mut build = Builder::new(output_ident.clone());
    Generator::new(output_ident).markups(markups, &mut build);
    build.finish()
}

struct Generator {
    output_ident: TokenTree,
}

impl Generator {
    fn new(output_ident: TokenTree) -> Generator {
        Generator { output_ident }
    }

    fn builder(&self) -> Builder {
        Builder::new(self.output_ident.clone())
    }

    fn markups(&self, markups: Vec<Markup>, build: &mut Builder) {
        for markup in markups {
            self.markup(markup, build);
        }
    }

    fn markup(&self, markup: Markup, build: &mut Builder) {
        match markup {
            Markup::Block(Block { markups, outer_span }) => {
                if markups.iter().any(|markup| matches!(*markup, Markup::Let { .. })) {
                    build.push_tokens(self.block(Block { markups, outer_span }));
                } else {
                    self.markups(markups, build);
                }
            },
            Markup::Literal { content, .. } => build.push_escaped(&content),
            Markup::Symbol { symbol } => self.name(symbol.into(), build),
            Markup::Splice { expr, .. } => build.push_tokens(self.splice(expr.into())),
            Markup::Element { name, attrs, body } => self.element(name.into(), attrs, body, build),
            Markup::Let { tokens, .. } => build.push_tokens(tokens.into()),
            Markup::Special { segments } => {
                for segment in segments {
                    build.push_tokens(self.special(segment));
                }
            },
            Markup::Match { head, arms, arms_span, .. } => {
                build.push_tokens({
                    let body = arms
                        .into_iter()
                        .map(|arm| self.match_arm(arm))
                        .collect();
                    let mut body = TokenTree::Group(Group::new(Delimiter::Brace, body));
                    body.set_span(arms_span.collapse());
                    let head: TokenStream = head.into();
                    quote!(#head #body)
                });
            },
        }
    }

    fn block(&self, Block { markups, outer_span }: Block) -> TokenStream {
        let mut build = self.builder();
        self.markups(markups, &mut build);
        let mut block = TokenTree::Group(Group::new(Delimiter::Brace, build.finish()));
        block.set_span(outer_span.collapse());
        TokenStream::from(block)
    }

    fn splice(&self, expr: TokenStream) -> TokenStream {
        let output_ident = self.output_ident.clone();
        quote!({
            // Create a local trait alias so that autoref works
            trait Render: maud::Render {
                fn __maud_render_to(&self, output_ident: &mut ::std::string::String) {
                    maud::Render::render_to(self, output_ident);
                }
            }
            impl<T: maud::Render> Render for T {}
            #expr.__maud_render_to(&mut #output_ident);
        })
    }

    fn element(
        &self,
        name: TokenStream,
        attrs: Attrs,
        body: ElementBody,
        build: &mut Builder,
    ) {
        build.push_str("<");
        self.name(name.clone(), build);
        self.attrs(attrs, build);
        build.push_str(">");
        if let ElementBody::Block { block } = body {
            self.markups(block.markups, build);
            build.push_str("</");
            self.name(name, build);
            build.push_str(">");
        }
    }

    fn name(&self, name: TokenStream, build: &mut Builder) {
        let string = name.into_iter().map(|token| token.to_string()).collect::<String>();
        build.push_escaped(&string);
    }

    fn attrs(&self, attrs: Attrs, build: &mut Builder) {
        for Attribute { name, attr_type } in desugar_attrs(attrs) {
            match attr_type {
                AttrType::Normal { value } => {
                    build.push_str(" ");
                    self.name(name.into(), build);
                    build.push_str("=\"");
                    self.markup(value, build);
                    build.push_str("\"");
                },
                AttrType::Empty { toggler: None } => {
                    build.push_str(" ");
                    self.name(name.into(), build);
                },
                AttrType::Empty { toggler: Some(toggler) } => {
                    let head = desugar_toggler(toggler);
                    build.push_tokens({
                        let mut build = self.builder();
                        build.push_str(" ");
                        self.name(name.into(), &mut build);
                        let body = build.finish();
                        quote!(#head { #body })
                    })
                },
            }
        }
    }

    fn special(&self, Special { head, body, .. }: Special) -> TokenStream {
        let body = self.block(body);
        let head: TokenStream = head.into();
        quote!(#head #body)
    }

    fn match_arm(&self, MatchArm { head, body }: MatchArm) -> TokenStream {
        let body = self.block(body);
        let head: TokenStream = head.into();
        quote!(#head #body)
    }
}

////////////////////////////////////////////////////////

fn desugar_attrs(attrs: Attrs) -> Vec<Attribute> {
    let mut classes_static = vec![];
    let mut classes_toggled = vec![];
    let mut ids = vec![];
    let mut attributes = vec![];
    for attr in attrs {
        match attr {
            Attr::Class { name, toggler, .. } => {
                if let Some(toggler) = toggler {
                    classes_toggled.push((name, toggler));
                } else {
                    classes_static.push(name);
                }
            },
            Attr::Id { name, .. } => ids.push(name),
            Attr::Attribute { attribute } => attributes.push(attribute),
        }
    }
    let classes = desugar_classes_or_ids("class", classes_static, classes_toggled);
    let ids = desugar_classes_or_ids("id", ids, vec![]);
    classes.into_iter().chain(ids).chain(attributes).collect()
}

fn desugar_classes_or_ids(
    attr_name: &'static str,
    values_static: Vec<Markup>,
    values_toggled: Vec<(Markup, Toggler)>,
) -> Option<Attribute> {
    if values_static.is_empty() && values_toggled.is_empty() {
        return None;
    }
    let mut markups = Vec::new();
    let mut leading_space = false;
    for name in values_static {
        markups.extend(prepend_leading_space(name, &mut leading_space));
    }
    for (name, toggler) in values_toggled {
        let body = Block {
            markups: prepend_leading_space(name, &mut leading_space),
            outer_span: toggler.cond_span,
        };
        let head = desugar_toggler(toggler);
        markups.push(Markup::Special {
            segments: vec![Special { at_span: SpanRange::call_site(), head: head.into(), body }],
        });
    }
    Some(Attribute {
        name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))).into(),
        attr_type: AttrType::Normal {
            value: Markup::Block(Block {
                markups,
                outer_span: SpanRange::call_site(),
            }),
        },
    })
}

fn prepend_leading_space(name: Markup, leading_space: &mut bool) -> Vec<Markup> {
    let mut markups = Vec::new();
    if *leading_space {
        markups.push(Markup::Literal {
            content: " ".to_owned(),
            span: name.span(),
        });
    }
    *leading_space = true;
    markups.push(name);
    markups
}

fn desugar_toggler(Toggler { cond, cond_span }: Toggler) -> TokenStream {
    let mut cond = TokenStream::from(cond);
    // If the expression contains an opening brace `{`,
    // wrap it in parentheses to avoid parse errors
    if cond.clone().into_iter().any(is_braced_block) {
        let mut wrapped_cond = TokenTree::Group(Group::new(Delimiter::Parenthesis, cond));
        wrapped_cond.set_span(cond_span.collapse());
        cond = TokenStream::from(wrapped_cond);
    }
    quote!(if #cond)
}

fn is_braced_block(token: TokenTree) -> bool {
    matches!(token, TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace)
}

////////////////////////////////////////////////////////

struct Builder {
    output_ident: TokenTree,
    tokens: Vec<TokenTree>,
    tail: String,
}

impl Builder {
    fn new(output_ident: TokenTree) -> Builder {
        Builder {
            output_ident,
            tokens: Vec::new(),
            tail: String::new(),
        }
    }

    fn push_str(&mut self, string: &str) {
        self.tail.push_str(string);
    }

    fn push_escaped(&mut self, string: &str) {
        use std::fmt::Write;
        Escaper::new(&mut self.tail).write_str(string).unwrap();
    }

    fn push_tokens(&mut self, tokens: TokenStream) {
        self.cut();
        self.tokens.extend(tokens);
    }

    fn cut(&mut self) {
        if self.tail.is_empty() {
            return;
        }
        let push_str_expr = {
            let output_ident = self.output_ident.clone();
            let string = TokenTree::Literal(Literal::string(&self.tail));
            quote!(#output_ident.push_str(#string);)
        };
        self.tail.clear();
        self.tokens.extend(push_str_expr);
    }

    fn finish(mut self) -> TokenStream {
        self.cut();
        self.tokens.into_iter().collect()
    }
}