use std::collections::{HashMap, VecDeque};

use serde::Serialize;
use serde_json::value::{to_value, Value as Json};
use serde_json::Map;

use crate::block::{BlockContext, BlockParamHolder};
use crate::error::{RenderError, RenderErrorReason};
use crate::grammar::Rule;
use crate::json::path::{merge_json_path, PathSeg};
use crate::json::value::ScopedJson;
use crate::util::extend;

pub type Object = HashMap<String, Json>;

/// The context wrap data you render on your templates.
///
#[derive(Debug, Clone)]
pub struct Context {
    data: Json,
}

#[derive(Debug)]
enum ResolvedPath<'a> {
    // FIXME: change to borrowed when possible
    // full path
    AbsolutePath(Vec<String>),
    // relative path and path root
    RelativePath(Vec<String>),
    // relative path against block param value
    BlockParamValue(Vec<String>, &'a Json),
    // relative path against derived value,
    LocalValue(Vec<String>, &'a Json),
}

fn parse_json_visitor<'a>(
    relative_path: &[PathSeg],
    block_contexts: &'a VecDeque<BlockContext<'_>>,
    always_for_absolute_path: bool,
) -> ResolvedPath<'a> {
    let mut path_context_depth: usize = 0;
    let mut with_block_param = None;
    let mut from_root = false;

    // peek relative_path for block param, @root and  "../../"
    for path_seg in relative_path {
        match path_seg {
            PathSeg::Named(the_path) => {
                if let Some((holder, base_path)) = get_in_block_params(block_contexts, the_path) {
                    with_block_param = Some((holder, base_path));
                }
                break;
            }
            PathSeg::Ruled(the_rule) => match the_rule {
                Rule::path_root => {
                    from_root = true;
                    break;
                }
                Rule::path_up => path_context_depth += 1,
                _ => break,
            },
        }
    }

    let mut path_stack = Vec::with_capacity(relative_path.len() + 5);
    match with_block_param {
        Some((BlockParamHolder::Value(ref value), _)) => {
            merge_json_path(&mut path_stack, &relative_path[(path_context_depth + 1)..]);
            ResolvedPath::BlockParamValue(path_stack, value)
        }
        Some((BlockParamHolder::Path(ref paths), base_path)) => {
            extend(&mut path_stack, base_path);
            if !paths.is_empty() {
                extend(&mut path_stack, paths);
            }
            merge_json_path(&mut path_stack, &relative_path[(path_context_depth + 1)..]);

            ResolvedPath::AbsolutePath(path_stack)
        }
        None => {
            if path_context_depth > 0 {
                let blk = block_contexts
                    .get(path_context_depth)
                    .or_else(|| block_contexts.front());

                if let Some(base_value) = blk.and_then(|blk| blk.base_value()) {
                    merge_json_path(&mut path_stack, relative_path);
                    ResolvedPath::LocalValue(path_stack, base_value)
                } else {
                    if let Some(base_path) = blk.map(BlockContext::base_path) {
                        extend(&mut path_stack, base_path);
                    }
                    merge_json_path(&mut path_stack, relative_path);
                    ResolvedPath::AbsolutePath(path_stack)
                }
            } else if from_root {
                merge_json_path(&mut path_stack, relative_path);
                ResolvedPath::AbsolutePath(path_stack)
            } else if always_for_absolute_path {
                if let Some(base_value) = block_contexts.front().and_then(|blk| blk.base_value()) {
                    merge_json_path(&mut path_stack, relative_path);
                    ResolvedPath::LocalValue(path_stack, base_value)
                } else {
                    if let Some(base_path) = block_contexts.front().map(BlockContext::base_path) {
                        extend(&mut path_stack, base_path);
                    }
                    merge_json_path(&mut path_stack, relative_path);
                    ResolvedPath::AbsolutePath(path_stack)
                }
            } else {
                merge_json_path(&mut path_stack, relative_path);
                ResolvedPath::RelativePath(path_stack)
            }
        }
    }
}

fn get_data<'a>(d: Option<&'a Json>, p: &str) -> Result<Option<&'a Json>, RenderError> {
    let result = match d {
        Some(Json::Array(l)) => p
            .parse::<usize>()
            .map(|idx_u| l.get(idx_u))
            .map_err(|_| RenderErrorReason::InvalidJsonIndex(p.to_owned()))?,
        Some(Json::Object(m)) => m.get(p),
        Some(_) => None,
        None => None,
    };
    Ok(result)
}

fn get_in_block_params<'a>(
    block_contexts: &'a VecDeque<BlockContext<'_>>,
    p: &str,
) -> Option<(&'a BlockParamHolder, &'a Vec<String>)> {
    for bc in block_contexts {
        let v = bc.get_block_param(p);
        if v.is_some() {
            return v.map(|v| (v, bc.base_path()));
        }
    }

    None
}

pub(crate) fn merge_json(base: &Json, addition: &HashMap<&str, &Json>) -> Json {
    // if no addition json provided, just return the original one
    if addition.is_empty() {
        return base.clone();
    }

    let mut base_map = match base {
        Json::Object(ref m) => m.clone(),
        Json::Array(ref a) => {
            let mut base_map = Map::new();
            for (idx, value) in a.iter().enumerate() {
                base_map.insert(idx.to_string(), value.clone());
            }
            base_map
        }
        Json::String(ref s) => {
            let mut base_map = Map::new();
            for (idx, value) in s.chars().enumerate() {
                base_map.insert(idx.to_string(), Json::String(value.to_string()));
            }
            base_map
        }
        _ => Map::new(),
    };

    for (k, v) in addition {
        base_map.insert((*k).to_string(), (*v).clone());
    }

    Json::Object(base_map)
}

impl Context {
    /// Create a context with null data
    pub fn null() -> Context {
        Context { data: Json::Null }
    }

    /// Create a context with given data
    pub fn wraps<T: Serialize>(e: T) -> Result<Context, RenderError> {
        to_value(e)
            .map_err(|e| RenderErrorReason::SerdeError(e).into())
            .map(|d| Context { data: d })
    }

    /// Navigate the context with relative path and block scopes
    pub(crate) fn navigate<'rc>(
        &'rc self,
        relative_path: &[PathSeg],
        block_contexts: &VecDeque<BlockContext<'_>>,
        recursive_lookup: bool,
    ) -> Result<ScopedJson<'rc>, RenderError> {
        // always use absolute at the moment until we get base_value lifetime issue fixed
        let resolved_visitor = parse_json_visitor(relative_path, block_contexts, true);

        match resolved_visitor {
            ResolvedPath::AbsolutePath(mut paths) => {
                // Only attempt a recursive resolution if it looks like a simple
                // named parameter, rather than an explicitly pathed parameter like
                // "./foo" or "../foo".
                let allow_recursive =
                    recursive_lookup && matches!(relative_path, [PathSeg::Named(_)]);
                if allow_recursive {
                    // Paths is probably something like: ["children", "0", "name"].
                    // This block of code will first try to resolve that set of
                    // paths, but if no value is found, it will then successively
                    // mutate that list to remove the parent element (the penultimate
                    // one) and resolving each in turn:
                    // ["children", "name"]
                    // ["name"]
                    while !paths.is_empty() {
                        let mut ptr = Some(self.data());
                        for p in &paths {
                            ptr = match get_data(ptr, p) {
                                Ok(p) => p,
                                Err(err) => {
                                    use crate::RenderErrorReason::InvalidJsonIndex;
                                    if matches!(err.reason(), InvalidJsonIndex(_)) {
                                        ptr = None;
                                        break;
                                    }
                                    return Err(err);
                                }
                            };
                        }

                        match ptr {
                            Some(v) => {
                                return Ok(ScopedJson::Context(v, paths));
                            }
                            None => {
                                let paths_len = paths.len();
                                if paths_len == 1 {
                                    return Ok(ScopedJson::Missing);
                                }

                                paths.remove(paths_len - 2);
                            }
                        }
                    }
                    Ok(ScopedJson::Missing)
                } else {
                    let mut ptr = Some(self.data());
                    for p in &paths {
                        ptr = get_data(ptr, p)?;
                    }
                    Ok(ptr.map_or_else(|| ScopedJson::Missing, |v| ScopedJson::Context(v, paths)))
                }
            }
            ResolvedPath::RelativePath(_paths) => {
                // relative path is disabled for now
                unreachable!()
                // let mut ptr = block_contexts.front().and_then(|blk| blk.base_value());
                // for p in paths.iter() {
                //     ptr = get_data(ptr, p)?;
                // }

                // Ok(ptr
                //     .map(|v| ScopedJson::Context(v, paths))
                //     .unwrap_or_else(|| ScopedJson::Missing))
            }
            ResolvedPath::BlockParamValue(paths, value)
            | ResolvedPath::LocalValue(paths, value) => {
                let mut ptr = Some(value);
                for p in &paths {
                    ptr = get_data(ptr, p)?;
                }
                Ok(ptr.map_or_else(|| ScopedJson::Missing, |v| ScopedJson::Derived(v.clone())))
            }
        }
    }

    /// Return the Json data wrapped in context
    pub fn data(&self) -> &Json {
        &self.data
    }

    /// Return the mutable reference to Json data wrapped in context
    pub fn data_mut(&mut self) -> &mut Json {
        &mut self.data
    }
}

impl From<Json> for Context {
    fn from(data: Json) -> Context {
        Context { data }
    }
}

#[cfg(test)]
mod test {
    use crate::json::value;
    use crate::{BlockParams, Path};

    use super::*;

    fn navigate_from_root<'rc>(
        ctx: &'rc Context,
        path: &str,
    ) -> Result<ScopedJson<'rc>, RenderError> {
        let relative_path = Path::parse(path).unwrap();
        ctx.navigate(relative_path.segs().unwrap(), &VecDeque::new(), false)
    }

    #[derive(Serialize)]
    struct Address {
        city: String,
        country: String,
    }

    #[derive(Serialize)]
    struct Person {
        name: String,
        age: i16,
        addr: Address,
        titles: Vec<String>,
    }

    #[test]
    fn test_render() {
        let v = "hello";
        let ctx = Context::wraps(v.to_string()).unwrap();
        assert_eq!(
            navigate_from_root(&ctx, "this").unwrap().render(),
            v.to_string()
        );
    }

    #[test]
    fn test_navigation() {
        let addr = Address {
            city: "Beijing".to_string(),
            country: "China".to_string(),
        };

        let person = Person {
            name: "Ning Sun".to_string(),
            age: 27,
            addr,
            titles: vec!["programmer".to_string(), "cartographer".to_string()],
        };

        let ctx = Context::wraps(person).unwrap();
        assert_eq!(
            navigate_from_root(&ctx, "./addr/country").unwrap().render(),
            "China".to_string()
        );
        assert_eq!(
            navigate_from_root(&ctx, "addr.[country]").unwrap().render(),
            "China".to_string()
        );

        let v = true;
        let ctx2 = Context::wraps(v).unwrap();
        assert_eq!(
            navigate_from_root(&ctx2, "this").unwrap().render(),
            "true".to_string()
        );

        assert_eq!(
            navigate_from_root(&ctx, "titles.[0]").unwrap().render(),
            "programmer".to_string()
        );

        assert_eq!(
            navigate_from_root(&ctx, "age").unwrap().render(),
            "27".to_string()
        );
    }

    #[test]
    fn test_this() {
        let mut map_with_this = Map::new();
        map_with_this.insert("this".to_string(), value::to_json("hello"));
        map_with_this.insert("age".to_string(), value::to_json(5usize));
        let ctx1 = Context::wraps(&map_with_this).unwrap();

        let mut map_without_this = Map::new();
        map_without_this.insert("age".to_string(), value::to_json(4usize));
        let ctx2 = Context::wraps(&map_without_this).unwrap();

        assert_eq!(
            navigate_from_root(&ctx1, "this").unwrap().render(),
            "[object]".to_owned()
        );
        assert_eq!(
            navigate_from_root(&ctx2, "age").unwrap().render(),
            "4".to_owned()
        );
    }

    #[test]
    fn test_merge_json() {
        let map = json!({ "age": 4 });
        let s = "hello".to_owned();
        let arr = json!(["a", "b"]);
        let mut hash = HashMap::new();
        let v = value::to_json("h1");
        hash.insert("tag", &v);

        let ctx_a1 = Context::wraps(merge_json(&map, &hash)).unwrap();
        assert_eq!(
            navigate_from_root(&ctx_a1, "age").unwrap().render(),
            "4".to_owned()
        );
        assert_eq!(
            navigate_from_root(&ctx_a1, "tag").unwrap().render(),
            "h1".to_owned()
        );

        let ctx_a2 = Context::wraps(merge_json(&value::to_json(&s), &hash)).unwrap();
        assert_eq!(
            navigate_from_root(&ctx_a2, "this").unwrap().render(),
            "[object]".to_owned()
        );
        assert_eq!(
            navigate_from_root(&ctx_a2, "tag").unwrap().render(),
            "h1".to_owned()
        );
        assert_eq!(
            navigate_from_root(&ctx_a2, "0").unwrap().render(),
            "h".to_owned()
        );
        assert_eq!(
            navigate_from_root(&ctx_a2, "1").unwrap().render(),
            "e".to_owned()
        );

        let ctx_a3 = Context::wraps(merge_json(&value::to_json(&arr), &hash)).unwrap();
        assert_eq!(
            navigate_from_root(&ctx_a3, "tag").unwrap().render(),
            "h1".to_owned()
        );
        assert_eq!(
            navigate_from_root(&ctx_a3, "0").unwrap().render(),
            "a".to_owned()
        );
        assert_eq!(
            navigate_from_root(&ctx_a3, "1").unwrap().render(),
            "b".to_owned()
        );

        let ctx_a4 = Context::wraps(merge_json(&value::to_json(&s), &HashMap::new())).unwrap();
        assert_eq!(
            navigate_from_root(&ctx_a4, "this").unwrap().render(),
            "hello".to_owned()
        );
    }

    #[test]
    fn test_key_name_with_this() {
        let m = json!({
            "this_name": "the_value"
        });
        let ctx = Context::wraps(m).unwrap();
        assert_eq!(
            navigate_from_root(&ctx, "this_name").unwrap().render(),
            "the_value".to_string()
        );
    }

    use serde::ser::Error as SerdeError;
    use serde::{Serialize, Serializer};

    struct UnserializableType {}

    impl Serialize for UnserializableType {
        fn serialize<S>(&self, _: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            Err(SerdeError::custom("test"))
        }
    }

    #[test]
    fn test_serialize_error() {
        let d = UnserializableType {};
        assert!(Context::wraps(d).is_err());
    }

    #[test]
    fn test_root() {
        let m = json!({
            "a" : {
                "b" : {
                    "c" : {
                        "d" : 1
                    }
                }
            },
            "b": 2
        });
        let ctx = Context::wraps(m).unwrap();
        let mut block = BlockContext::new();
        *block.base_path_mut() = ["a".to_owned(), "b".to_owned()].to_vec();

        let mut blocks = VecDeque::new();
        blocks.push_front(block);

        assert_eq!(
            ctx.navigate(
                Path::parse("@root/b").unwrap().segs().unwrap(),
                &blocks,
                false
            )
            .unwrap()
            .render(),
            "2".to_string()
        );
    }

    #[test]
    fn test_block_params() {
        let m = json!([{
            "a": [1, 2]
        }, {
            "b": [2, 3]
        }]);

        let ctx = Context::wraps(m).unwrap();
        let mut block_params = BlockParams::new();
        block_params
            .add_path("z", ["0".to_owned(), "a".to_owned()].to_vec())
            .unwrap();
        block_params.add_value("t", json!("good")).unwrap();

        let mut block = BlockContext::new();
        block.set_block_params(block_params);

        let mut blocks = VecDeque::new();
        blocks.push_front(block);

        assert_eq!(
            ctx.navigate(
                Path::parse("z.[1]").unwrap().segs().unwrap(),
                &blocks,
                false
            )
            .unwrap()
            .render(),
            "2".to_string()
        );
        assert_eq!(
            ctx.navigate(Path::parse("t").unwrap().segs().unwrap(), &blocks, false)
                .unwrap()
                .render(),
            "good".to_string()
        );
    }
}
