use serde_json::value::Value as Json;

use super::block_util::create_block;
use crate::block::{BlockContext, BlockParams};
use crate::context::Context;
use crate::error::RenderError;
use crate::helpers::{HelperDef, HelperResult};
use crate::json::value::to_json;
use crate::output::Output;
use crate::registry::Registry;
use crate::render::{Helper, RenderContext, Renderable};
use crate::util::copy_on_push_vec;

fn update_block_context<'reg>(
    block: &mut BlockContext<'reg>,
    base_path: Option<&Vec<String>>,
    relative_path: String,
    is_first: bool,
    value: &Json,
) {
    if let Some(ref p) = base_path {
        if is_first {
            *block.base_path_mut() = copy_on_push_vec(p, relative_path);
        } else if let Some(ptr) = block.base_path_mut().last_mut() {
            *ptr = relative_path;
        }
    } else {
        block.set_base_value(value.clone());
    }
}

fn set_block_param<'reg: 'rc, 'rc>(
    block: &mut BlockContext<'reg>,
    h: &Helper<'reg, 'rc>,
    base_path: Option<&Vec<String>>,
    k: &Json,
    v: &Json,
) -> Result<(), RenderError> {
    if let Some(bp_val) = h.block_param() {
        let mut params = BlockParams::new();
        if base_path.is_some() {
            params.add_path(bp_val, Vec::with_capacity(0))?;
        } else {
            params.add_value(bp_val, v.clone())?;
        }

        block.set_block_params(params);
    } else if let Some((bp_val, bp_key)) = h.block_param_pair() {
        let mut params = BlockParams::new();
        if base_path.is_some() {
            params.add_path(bp_val, Vec::with_capacity(0))?;
        } else {
            params.add_value(bp_val, v.clone())?;
        }
        params.add_value(bp_key, k.clone())?;

        block.set_block_params(params);
    }

    Ok(())
}

#[derive(Clone, Copy)]
pub struct EachHelper;

impl HelperDef for EachHelper {
    fn call<'reg: 'rc, 'rc>(
        &self,
        h: &Helper<'reg, 'rc>,
        r: &'reg Registry<'reg>,
        ctx: &'rc Context,
        rc: &mut RenderContext<'reg, 'rc>,
        out: &mut dyn Output,
    ) -> HelperResult {
        let value = h
            .param(0)
            .ok_or_else(|| RenderError::new("Param not found for helper \"each\""))?;

        let template = h.template();

        match template {
            Some(t) => match *value.value() {
                Json::Array(ref list) if !list.is_empty() => {
                    let block_context = create_block(&value)?;
                    rc.push_block(block_context);

                    let len = list.len();

                    let array_path = value.context_path();

                    for (i, v) in list.iter().enumerate().take(len) {
                        if let Some(ref mut block) = rc.block_mut() {
                            let is_first = i == 0usize;
                            let is_last = i == len - 1;

                            let index = to_json(i);
                            block.set_local_var("@first".to_string(), to_json(is_first));
                            block.set_local_var("@last".to_string(), to_json(is_last));
                            block.set_local_var("@index".to_string(), index.clone());

                            update_block_context(block, array_path, i.to_string(), is_first, &v);
                            set_block_param(block, h, array_path, &index, &v)?;
                        }

                        t.render(r, ctx, rc, out)?;
                    }

                    rc.pop_block();
                    Ok(())
                }
                Json::Object(ref obj) if !obj.is_empty() => {
                    let block_context = create_block(&value)?;
                    rc.push_block(block_context);

                    let mut is_first = true;
                    let obj_path = value.context_path();

                    for (k, v) in obj.iter() {
                        if let Some(ref mut block) = rc.block_mut() {
                            let key = to_json(k);

                            block.set_local_var("@first".to_string(), to_json(is_first));
                            block.set_local_var("@key".to_string(), key.clone());

                            update_block_context(block, obj_path, k.to_string(), is_first, &v);
                            set_block_param(block, h, obj_path, &key, &v)?;
                        }

                        t.render(r, ctx, rc, out)?;

                        if is_first {
                            is_first = false;
                        }
                    }

                    rc.pop_block();
                    Ok(())
                }
                _ => {
                    if let Some(else_template) = h.inverse() {
                        else_template.render(r, ctx, rc, out)?;
                    }
                    Ok(())
                }
            },
            None => Ok(()),
        }
    }
}

pub static EACH_HELPER: EachHelper = EachHelper;

#[cfg(test)]
mod test {
    use crate::json::value::to_json;
    use crate::registry::Registry;
    use serde_json::value::Value as Json;
    use std::collections::BTreeMap;
    use std::str::FromStr;

    #[test]
    fn test_each() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string(
                "t0",
                "{{#each this}}{{@first}}|{{@last}}|{{@index}}:{{this}}|{{/each}}"
            )
            .is_ok());
        assert!(handlebars
            .register_template_string("t1", "{{#each this}}{{@first}}|{{@key}}:{{this}}|{{/each}}")
            .is_ok());

        let r0 = handlebars.render("t0", &vec![1u16, 2u16, 3u16]);
        assert_eq!(
            r0.ok().unwrap(),
            "true|false|0:1|false|false|1:2|false|true|2:3|".to_string()
        );

        let mut m: BTreeMap<String, u16> = BTreeMap::new();
        m.insert("ftp".to_string(), 21);
        m.insert("http".to_string(), 80);
        let r1 = handlebars.render("t1", &m);
        assert_eq!(r1.ok().unwrap(), "true|ftp:21|false|http:80|".to_string());
    }

    #[test]
    fn test_each_with_parent() {
        let json_str = r#"{"a":{"a":99,"c":[{"d":100},{"d":200}]}}"#;

        let data = Json::from_str(json_str).unwrap();
        // println!("data: {}", data);
        let mut handlebars = Registry::new();

        // previously, to access the parent in an each block,
        // a user would need to specify ../../b, as the path
        // that is computed includes the array index: ./a.c.[0]
        assert!(handlebars
            .register_template_string("t0", "{{#each a.c}} d={{d}} b={{../a.a}} {{/each}}")
            .is_ok());

        let r1 = handlebars.render("t0", &data);
        assert_eq!(r1.ok().unwrap(), " d=100 b=99  d=200 b=99 ".to_string());
    }

    #[test]
    fn test_nested_each_with_parent() {
        let json_str = r#"{"a": [{"b": [{"d": 100}], "c": 200}]}"#;

        let data = Json::from_str(json_str).unwrap();
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string(
                "t0",
                "{{#each a}}{{#each b}}{{d}}:{{../c}}{{/each}}{{/each}}"
            )
            .is_ok());

        let r1 = handlebars.render("t0", &data);
        assert_eq!(r1.ok().unwrap(), "100:200".to_string());
    }

    #[test]
    fn test_nested_each() {
        let json_str = r#"{"a": [{"b": true}], "b": [[1, 2, 3],[4, 5]]}"#;

        let data = Json::from_str(json_str).unwrap();

        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string(
                "t0",
                "{{#each b}}{{#if ../a}}{{#each this}}{{this}}{{/each}}{{/if}}{{/each}}"
            )
            .is_ok());

        let r1 = handlebars.render("t0", &data);
        assert_eq!(r1.ok().unwrap(), "12345".to_string());
    }

    #[test]
    fn test_nested_array() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string("t0", "{{#each this.[0]}}{{this}}{{/each}}")
            .is_ok());

        let r0 = handlebars.render("t0", &(vec![vec![1, 2, 3]]));

        assert_eq!(r0.ok().unwrap(), "123".to_string());
    }

    #[test]
    fn test_empty_key() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string("t0", "{{#each this}}{{@key}}-{{value}}\n{{/each}}")
            .is_ok());

        let r0 = handlebars
            .render(
                "t0",
                &json!({
                    "foo": {
                        "value": "bar"
                    },
                    "": {
                        "value": "baz"
                    }
                }),
            )
            .unwrap();

        let mut r0_sp: Vec<_> = r0.split('\n').collect();
        r0_sp.sort();

        assert_eq!(r0_sp, vec!["", "-baz", "foo-bar"]);
    }

    #[test]
    fn test_each_else() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string("t0", "{{#each a}}1{{else}}empty{{/each}}")
            .is_ok());
        let m1 = btreemap! {
            "a".to_string() => Vec::<String>::new(),
        };
        let r0 = handlebars.render("t0", &m1).unwrap();
        assert_eq!(r0, "empty");

        let m2 = btreemap! {
            "b".to_string() => Vec::<String>::new()
        };
        let r1 = handlebars.render("t0", &m2).unwrap();
        assert_eq!(r1, "empty");
    }

    #[test]
    fn test_block_param() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string("t0", "{{#each a as |i|}}{{i}}{{/each}}")
            .is_ok());
        let m1 = btreemap! {
            "a".to_string() => vec![1,2,3,4,5]
        };
        let r0 = handlebars.render("t0", &m1).unwrap();
        assert_eq!(r0, "12345");
    }

    #[test]
    fn test_each_object_block_param() {
        let mut handlebars = Registry::new();
        let template = "{{#each this as |v k|}}\
                        {{#with k as |inner_k|}}{{inner_k}}{{/with}}:{{v}}|\
                        {{/each}}";
        assert!(handlebars.register_template_string("t0", template).is_ok());

        let m = btreemap! {
            "ftp".to_string() => 21,
            "http".to_string() => 80
        };
        let r0 = handlebars.render("t0", &m);
        assert_eq!(r0.ok().unwrap(), "ftp:21|http:80|".to_string());
    }

    #[test]
    fn test_each_object_block_param2() {
        let mut handlebars = Registry::new();
        let template = "{{#each this as |v k|}}\
                        {{#with v as |inner_v|}}{{k}}:{{inner_v}}{{/with}}|\
                        {{/each}}";

        assert!(handlebars.register_template_string("t0", template).is_ok());

        let m = btreemap! {
            "ftp".to_string() => 21,
            "http".to_string() => 80
        };
        let r0 = handlebars.render("t0", &m);
        assert_eq!(r0.ok().unwrap(), "ftp:21|http:80|".to_string());
    }

    fn test_nested_each_with_path_ups() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string(
                "t0",
                "{{#each a.b}}{{#each c}}{{../../d}}{{/each}}{{/each}}"
            )
            .is_ok());

        let data = btreemap! {
            "a".to_string() => to_json(&btreemap! {
                "b".to_string() => vec![btreemap!{"c".to_string() => vec![1]}]
            }),
            "d".to_string() => to_json(&1)
        };

        let r0 = handlebars.render("t0", &data);
        assert_eq!(r0.ok().unwrap(), "1".to_string());
    }

    #[test]
    fn test_nested_each_with_path_up_this() {
        let mut handlebars = Registry::new();
        let template = "{{#each variant}}{{#each ../typearg}}\
                        {{#if @first}}template<{{/if}}{{this}}{{#if @last}}>{{else}},{{/if}}\
                        {{/each}}{{/each}}";
        assert!(handlebars.register_template_string("t0", template).is_ok());
        let data = btreemap! {
            "typearg".to_string() => vec!["T".to_string()],
            "variant".to_string() => vec!["1".to_string(), "2".to_string()]
        };
        let r0 = handlebars.render("t0", &data);
        assert_eq!(r0.ok().unwrap(), "template<T>template<T>".to_string());
    }

    #[test]
    fn test_key_iteration_with_unicode() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string("t0", "{{#each this}}{{@key}}: {{this}}\n{{/each}}")
            .is_ok());
        let data = json!({
            "normal": 1,
            "你好": 2,
            "#special key": 3,
            "😂": 4,
            "me.dot.key": 5
        });
        let r0 = handlebars.render("t0", &data).ok().unwrap();
        assert!(r0.contains("normal: 1"));
        assert!(r0.contains("你好: 2"));
        assert!(r0.contains("#special key: 3"));
        assert!(r0.contains("😂: 4"));
        assert!(r0.contains("me.dot.key: 5"));
    }

    #[test]
    fn test_base_path_after_each() {
        let mut handlebars = Registry::new();
        assert!(handlebars
            .register_template_string("t0", "{{#each a}}{{this}}{{/each}} {{b}}")
            .is_ok());
        let data = json!({
            "a": [1, 2, 3, 4],
            "b": "good",
        });

        let r0 = handlebars.render("t0", &data).ok().unwrap();

        assert_eq!("1234 good", r0);
    }

    #[test]
    fn test_else_context() {
        let reg = Registry::new();
        let template = "{{#each list}}A{{else}}{{foo}}{{/each}}";
        let input = json!({"list": [], "foo": "bar"});
        let rendered = reg.render_template(template, &input).unwrap();
        assert_eq!("bar", rendered);
    }

    #[test]
    fn test_block_context_leak() {
        let reg = Registry::new();
        let template = "{{#each list}}{{#each inner}}{{this}}{{/each}}{{foo}}{{/each}}";
        let input = json!({"list": [{"inner": [], "foo": 1}, {"inner": [], "foo": 2}]});
        let rendered = reg.render_template(template, &input).unwrap();
        assert_eq!("12", rendered);
    }

    #[test]
    fn test_derived_array_as_block_params() {
        handlebars_helper!(range: |x: u64| (0..x).collect::<Vec<u64>>());
        let mut reg = Registry::new();
        reg.register_helper("range", Box::new(range));
        let template = "{{#each (range 3) as |i|}}{{i}}{{/each}}";
        let input = json!(0);
        let rendered = reg.render_template(template, &input).unwrap();
        assert_eq!("012", rendered);
    }

    #[test]
    fn test_derived_object_as_block_params() {
        handlebars_helper!(point: |x: u64, y: u64| json!({"x":x, "y":y}));
        let mut reg = Registry::new();
        reg.register_helper("point", Box::new(point));
        let template = "{{#each (point 0 1) as |i|}}{{i}}{{/each}}";
        let input = json!(0);
        let rendered = reg.render_template(template, &input).unwrap();
        assert_eq!("01", rendered);
    }

    #[test]
    fn test_derived_array_without_block_param() {
        handlebars_helper!(range: |x: u64| (0..x).collect::<Vec<u64>>());
        let mut reg = Registry::new();
        reg.register_helper("range", Box::new(range));
        let template = "{{#each (range 3)}}{{this}}{{/each}}";
        let input = json!(0);
        let rendered = reg.render_template(template, &input).unwrap();
        assert_eq!("012", rendered);
    }

    #[test]
    fn test_derived_object_without_block_params() {
        handlebars_helper!(point: |x: u64, y: u64| json!({"x":x, "y":y}));
        let mut reg = Registry::new();
        reg.register_helper("point", Box::new(point));
        let template = "{{#each (point 0 1)}}{{this}}{{/each}}";
        let input = json!(0);
        let rendered = reg.render_template(template, &input).unwrap();
        assert_eq!("01", rendered);
    }

    #[test]
    fn test_non_iterable() {
        let reg = Registry::new();
        let template = "{{#each this}}each block{{else}}else block{{/each}}";
        let input = json!("strings aren't iterable");
        let rendered = reg.render_template(template, &input).unwrap();
        assert_eq!("else block", rendered);
    }

    #[test]
    fn test_recursion() {
        let mut reg = Registry::new();
        assert!(reg
            .register_template_string(
                "walk",
                "(\
                    {{#each this}}\
                        {{#if @key}}{{@key}}{{else}}{{@index}}{{/if}}: \
                        {{this}} \
                        {{> walk this}}, \
                    {{/each}}\
                )",
            )
            .is_ok());

        let input = json!({
            "array": [42, {"wow": "cool"}, [[]]],
            "object": { "a": { "b": "c", "d": ["e"] } },
            "string": "hi"
        });
        let expected_output = "(\
            array: [42, [object], [[], ], ] (\
                0: 42 (), \
                1: [object] (wow: cool (), ), \
                2: [[], ] (0: [] (), ), \
            ), \
            object: [object] (\
                a: [object] (\
                    b: c (), \
                    d: [e, ] (0: e (), ), \
                ), \
            ), \
            string: hi (), \
        )";

        let rendered = reg.render("walk", &input).unwrap();
        assert_eq!(expected_output, rendered);
    }
}
