use lazy_static::lazy_static;
use pest::iterators::Pair;
use pest::Parser;
use pest_derive::Parser;
use regex::Regex;
use tera::{to_value, Context, Map, Value};

use crate::context::RenderContext;
use errors::{bail, Error, Result};

// This include forces recompiling this source file if the grammar file changes.
// Uncomment it when doing changes to the .pest file
const _GRAMMAR: &str = include_str!("content.pest");

#[derive(Parser)]
#[grammar = "content.pest"]
pub struct ContentParser;

lazy_static! {
    static ref MULTIPLE_NEWLINE_RE: Regex = Regex::new(r"\n\s*\n").unwrap();
    static ref OUTER_NEWLINE_RE: Regex = Regex::new(r"^\s*\n|\n\s*$").unwrap();
}

fn replace_string_markers(input: &str) -> String {
    match input.chars().next().unwrap() {
        '"' => input.replace('"', ""),
        '\'' => input.replace('\'', ""),
        '`' => input.replace('`', ""),
        _ => unreachable!("How did you even get there"),
    }
}

fn parse_literal(pair: Pair<Rule>) -> Value {
    let mut val = None;
    for p in pair.into_inner() {
        match p.as_rule() {
            Rule::boolean => match p.as_str() {
                "true" => val = Some(Value::Bool(true)),
                "false" => val = Some(Value::Bool(false)),
                _ => unreachable!(),
            },
            Rule::string => val = Some(Value::String(replace_string_markers(p.as_str()))),
            Rule::float => {
                val = Some(to_value(p.as_str().parse::<f64>().unwrap()).unwrap());
            }
            Rule::int => {
                val = Some(to_value(p.as_str().parse::<i64>().unwrap()).unwrap());
            }
            _ => unreachable!("Unknown literal: {:?}", p),
        };
    }

    val.unwrap()
}

/// Returns (shortcode_name, kwargs)
fn parse_shortcode_call(pair: Pair<Rule>) -> (String, Map<String, Value>) {
    let mut name = None;
    let mut args = Map::new();

    for p in pair.into_inner() {
        match p.as_rule() {
            Rule::ident => {
                name = Some(p.as_span().as_str().to_string());
            }
            Rule::kwarg => {
                let mut arg_name = None;
                let mut arg_val = None;
                for p2 in p.into_inner() {
                    match p2.as_rule() {
                        Rule::ident => {
                            arg_name = Some(p2.as_span().as_str().to_string());
                        }
                        Rule::literal => {
                            arg_val = Some(parse_literal(p2));
                        }
                        Rule::array => {
                            let mut vals = vec![];
                            for p3 in p2.into_inner() {
                                match p3.as_rule() {
                                    Rule::literal => vals.push(parse_literal(p3)),
                                    _ => unreachable!(
                                        "Got something other than literal in an array: {:?}",
                                        p3
                                    ),
                                }
                            }
                            arg_val = Some(Value::Array(vals));
                        }
                        _ => unreachable!("Got something unexpected in a kwarg: {:?}", p2),
                    }
                }

                args.insert(arg_name.unwrap(), arg_val.unwrap());
            }
            _ => unreachable!("Got something unexpected in a shortcode: {:?}", p),
        }
    }
    (name.unwrap(), args)
}

fn render_shortcode(
    name: &str,
    args: &Map<String, Value>,
    context: &RenderContext,
    body: Option<&str>,
) -> Result<String> {
    let mut tera_context = Context::new();
    for (key, value) in args.iter() {
        tera_context.insert(key, value);
    }
    if let Some(ref b) = body {
        // Trimming right to avoid most shortcodes with bodies ending up with a HTML new line
        tera_context.insert("body", b.trim_end());
    }
    tera_context.extend(context.tera_context.clone());

    let template_name = format!("shortcodes/{}.html", name);

    let res = utils::templates::render_template(&template_name, &context.tera, tera_context, &None)
        .map_err(|e| Error::chain(format!("Failed to render {} shortcode", name), e))?;

    // Small hack to avoid having multiple blank lines because of Tera tags for example
    // A blank like will cause the markdown parser to think we're out of HTML and start looking
    // at indentation, making the output a code block.
    let res = MULTIPLE_NEWLINE_RE.replace_all(&res, "\n");

    let res = OUTER_NEWLINE_RE.replace_all(&res, "");

    Ok(res.to_string())
}

pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> {
    let mut res = String::with_capacity(content.len());

    let mut pairs = match ContentParser::parse(Rule::page, content) {
        Ok(p) => p,
        Err(e) => {
            let fancy_e = e.renamed_rules(|rule| match *rule {
                Rule::int => "an integer".to_string(),
                Rule::float => "a float".to_string(),
                Rule::string => "a string".to_string(),
                Rule::literal => "a literal (int, float, string, bool)".to_string(),
                Rule::array => "an array".to_string(),
                Rule::kwarg => "a keyword argument".to_string(),
                Rule::ident => "an identifier".to_string(),
                Rule::inline_shortcode => "an inline shortcode".to_string(),
                Rule::ignored_inline_shortcode => "an ignored inline shortcode".to_string(),
                Rule::sc_body_start => "the start of a shortcode".to_string(),
                Rule::ignored_sc_body_start => "the start of an ignored shortcode".to_string(),
                Rule::text => "some text".to_string(),
                Rule::EOI => "end of input".to_string(),
                Rule::double_quoted_string => "double quoted string".to_string(),
                Rule::single_quoted_string => "single quoted string".to_string(),
                Rule::backquoted_quoted_string => "backquoted quoted string".to_string(),
                Rule::boolean => "a boolean (true, false)".to_string(),
                Rule::all_chars => "a alphanumerical character".to_string(),
                Rule::kwargs => "a list of keyword arguments".to_string(),
                Rule::sc_def => "a shortcode definition".to_string(),
                Rule::shortcode_with_body => "a shortcode with body".to_string(),
                Rule::ignored_shortcode_with_body => "an ignored shortcode with body".to_string(),
                Rule::sc_body_end => "{% end %}".to_string(),
                Rule::ignored_sc_body_end => "{%/* end */%}".to_string(),
                Rule::text_in_body_sc => "text in a shortcode body".to_string(),
                Rule::text_in_ignored_body_sc => "text in an ignored shortcode body".to_string(),
                Rule::content => "some content".to_string(),
                Rule::page => "a page".to_string(),
                Rule::WHITESPACE => "whitespace".to_string(),
            });
            bail!("{}", fancy_e);
        }
    };

    // We have at least a `page` pair
    for p in pairs.next().unwrap().into_inner() {
        match p.as_rule() {
            Rule::text => res.push_str(p.as_span().as_str()),
            Rule::inline_shortcode => {
                let (name, args) = parse_shortcode_call(p);
                res.push_str(&render_shortcode(&name, &args, context, None)?);
            }
            Rule::shortcode_with_body => {
                let mut inner = p.into_inner();
                // 3 items in inner: call, body, end
                // we don't care about the closing tag
                let (name, args) = parse_shortcode_call(inner.next().unwrap());
                let body = inner.next().unwrap().as_span().as_str();
                res.push_str(&render_shortcode(&name, &args, context, Some(body))?);
            }
            Rule::ignored_inline_shortcode => {
                res.push_str(
                    &p.as_span().as_str().replacen("{{/*", "{{", 1).replacen("*/}}", "}}", 1),
                );
            }
            Rule::ignored_shortcode_with_body => {
                for p2 in p.into_inner() {
                    match p2.as_rule() {
                        Rule::ignored_sc_body_start | Rule::ignored_sc_body_end => {
                            res.push_str(
                                &p2.as_span()
                                    .as_str()
                                    .replacen("{%/*", "{%", 1)
                                    .replacen("*/%}", "%}", 1),
                            );
                        }
                        Rule::text_in_ignored_body_sc => res.push_str(p2.as_span().as_str()),
                        _ => unreachable!("Got something weird in an ignored shortcode: {:?}", p2),
                    }
                }
            }
            Rule::EOI => (),
            _ => unreachable!("unexpected page rule: {:?}", p.as_rule()),
        }
    }

    Ok(res)
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;
    use config::Config;
    use front_matter::InsertAnchor;
    use tera::Tera;

    macro_rules! assert_lex_rule {
        ($rule: expr, $input: expr) => {
            let res = ContentParser::parse($rule, $input);
            println!("{:?}", $input);
            println!("{:#?}", res);
            if res.is_err() {
                println!("{}", res.unwrap_err());
                panic!();
            }
            assert!(res.is_ok());
            assert_eq!(res.unwrap().last().unwrap().as_span().end(), $input.len());
        };
    }

    fn render_shortcodes(code: &str, tera: &Tera) -> String {
        let config = Config::default();
        let permalinks = HashMap::new();
        let context = RenderContext::new(&tera, &config, "", &permalinks, InsertAnchor::None);
        super::render_shortcodes(code, &context).unwrap()
    }

    #[test]
    fn lex_text() {
        let inputs = vec!["Hello world", "HEllo \n world", "Hello 1 2 true false 'hey'"];
        for i in inputs {
            assert_lex_rule!(Rule::text, i);
        }
    }

    #[test]
    fn lex_inline_shortcode() {
        let inputs = vec![
            "{{ youtube() }}",
            "{{ youtube(id=1, autoplay=true, url='hey') }}",
            "{{ youtube(id=1, \nautoplay=true, url='hey') }}",
        ];
        for i in inputs {
            assert_lex_rule!(Rule::inline_shortcode, i);
        }
    }

    #[test]
    fn lex_inline_ignored_shortcode() {
        let inputs = vec![
            "{{/* youtube() */}}",
            "{{/* youtube(id=1, autoplay=true, url='hey') */}}",
            "{{/* youtube(id=1, \nautoplay=true, \nurl='hey') */}}",
        ];
        for i in inputs {
            assert_lex_rule!(Rule::ignored_inline_shortcode, i);
        }
    }

    #[test]
    fn lex_shortcode_with_body() {
        let inputs = vec![
            r#"{% youtube() %}
            Some text
            {% end %}"#,
            r#"{% youtube(id=1,
            autoplay=true, url='hey') %}
            Some text
            {% end %}"#,
        ];
        for i in inputs {
            assert_lex_rule!(Rule::shortcode_with_body, i);
        }
    }

    #[test]
    fn lex_ignored_shortcode_with_body() {
        let inputs = vec![
            r#"{%/* youtube() */%}
            Some text
            {%/* end */%}"#,
            r#"{%/* youtube(id=1,
            autoplay=true, url='hey') */%}
            Some text
            {%/* end */%}"#,
        ];
        for i in inputs {
            assert_lex_rule!(Rule::ignored_shortcode_with_body, i);
        }
    }

    #[test]
    fn lex_page() {
        let inputs = vec![
            "Some text and a shortcode `{{/* youtube() */}}`",
            "{{ youtube(id=1, autoplay=true, url='hey') }}",
            "{{ youtube(id=1, \nautoplay=true, url='hey') }} that's it",
            r#"
            This is a test
            {% hello() %}
            Body {{ var }}
            {% end %}
            "#,
        ];
        for i in inputs {
            assert_lex_rule!(Rule::page, i);
        }
    }

    #[test]
    fn does_nothing_with_no_shortcodes() {
        let res = render_shortcodes("Hello World", &Tera::default());
        assert_eq!(res, "Hello World");
    }

    #[test]
    fn can_unignore_inline_shortcode() {
        let res = render_shortcodes("Hello World {{/* youtube() */}}", &Tera::default());
        assert_eq!(res, "Hello World {{ youtube() }}");
    }

    #[test]
    fn can_unignore_shortcode_with_body() {
        let res = render_shortcodes(
            r#"
Hello World
{%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#,
            &Tera::default(),
        );
        assert_eq!(res, "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}");
    }

    // https://github.com/Keats/gutenberg/issues/383
    #[test]
    fn unignore_shortcode_with_body_does_not_swallow_initial_whitespace() {
        let res = render_shortcodes(
            r#"
Hello World
{%/* youtube() */%}
Some body {{ hello() }}{%/* end */%}"#,
            &Tera::default(),
        );
        assert_eq!(res, "\nHello World\n{% youtube() %}\nSome body {{ hello() }}{% end %}");
    }

    #[test]
    fn can_parse_shortcode_arguments() {
        let inputs = vec![
            ("{{ youtube() }}", "youtube", Map::new()),
            ("{{ youtube(id=1, autoplay=true, hello='salut', float=1.2) }}", "youtube", {
                let mut m = Map::new();
                m.insert("id".to_string(), to_value(1).unwrap());
                m.insert("autoplay".to_string(), to_value(true).unwrap());
                m.insert("hello".to_string(), to_value("salut").unwrap());
                m.insert("float".to_string(), to_value(1.2).unwrap());
                m
            }),
            ("{{ gallery(photos=['something', 'else'], fullscreen=true) }}", "gallery", {
                let mut m = Map::new();
                m.insert("photos".to_string(), to_value(["something", "else"]).unwrap());
                m.insert("fullscreen".to_string(), to_value(true).unwrap());
                m
            }),
        ];

        for (i, n, a) in inputs {
            let mut res = ContentParser::parse(Rule::inline_shortcode, i).unwrap();
            let (name, args) = parse_shortcode_call(res.next().unwrap());
            assert_eq!(name, n);
            assert_eq!(args, a);
        }
    }

    #[test]
    fn can_render_inline_shortcodes() {
        let mut tera = Tera::default();
        tera.add_raw_template("shortcodes/youtube.html", "Hello {{id}}").unwrap();
        let res = render_shortcodes("Inline {{ youtube(id=1) }}.", &tera);
        assert_eq!(res, "Inline Hello 1.");
    }

    #[test]
    fn can_render_shortcodes_with_body() {
        let mut tera = Tera::default();
        tera.add_raw_template("shortcodes/youtube.html", "{{body}}").unwrap();
        let res = render_shortcodes("Body\n {% youtube() %}Hey!{% end %}", &tera);
        assert_eq!(res, "Body\n Hey!");
    }

    // https://github.com/Keats/gutenberg/issues/462
    #[test]
    fn shortcodes_with_body_do_not_eat_newlines() {
        let mut tera = Tera::default();
        tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap();
        let res = render_shortcodes("Body\n {% youtube() %}\nHello \n World{% end %}", &tera);
        assert_eq!(res, "Body\n Hello \n World");
    }

    #[test]
    fn outer_newlines_removed_from_shortcodes_with_body() {
        let mut tera = Tera::default();
        tera.add_raw_template("shortcodes/youtube.html", "  \n  {{body}}  \n  ").unwrap();
        let res = render_shortcodes("\n{% youtube() %}  \n  content  \n  {% end %}\n", &tera);
        assert_eq!(res, "\n  content  \n");
    }

    #[test]
    fn outer_newlines_removed_from_inline_shortcodes() {
        let mut tera = Tera::default();
        tera.add_raw_template("shortcodes/youtube.html", "  \n  Hello, Zola.  \n  ").unwrap();
        let res = render_shortcodes("\n{{ youtube() }}\n", &tera);
        assert_eq!(res, "\n  Hello, Zola.  \n");
    }
}
