use crate::parser::model::Filter;
use crate::query::queryable::Queryable;
use crate::query::state::{Data, Pointer, State};
use crate::query::Query;

impl Query for Filter {
    fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> {
        let root = state.root;
        state.flat_map(|p| {
            if p.is_internal() {
                Data::Value(self.filter_item(p, root).into())
            } else if let Some(items) = p.inner.as_array() {
                Data::Refs(
                    items
                        .into_iter()
                        .enumerate()
                        .filter(|(_, item)| self.filter_item(Pointer::empty(*item), root))
                        .map(|(idx, item)| Pointer::idx(item, p.path.clone(), idx))
                        .collect(),
                )
            } else if let Some(items) = p.inner.as_object() {
                Data::Refs(
                    items
                        .into_iter()
                        .filter(|(_, item)| self.filter_item(Pointer::empty(*item), root))
                        .map(|(key, item)| Pointer::key(item, p.path.clone(), key))
                        .collect(),
                )
            } else {
                return Data::Nothing;
            }
        })
    }
}

impl Filter {
    fn process_elem<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> {
        let process_cond = |filter: &Filter| {
            filter
                .process(state.clone())
                .ok_val()
                .and_then(|v| v.as_bool())
                .unwrap_or_default()
        };
        match self {
            Filter::Or(ors) => State::bool(ors.iter().any(process_cond), state.root),
            Filter::And(ands) => State::bool(ands.iter().all(process_cond), state.root),
            Filter::Atom(atom) => atom.process(state),
        }
    }

    fn filter_item<'a, T: Queryable>(&self, item: Pointer<'a, T>, root: &T) -> bool {
        self.process_elem(State::data(root, Data::Ref(item.clone())))
            .ok_val()
            .and_then(|v| v.as_bool())
            .unwrap_or_default()
    }
}

#[cfg(test)]
mod tests {
    use crate::query::js_path;
    use serde_json::json;

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

        assert_eq!(
            js_path("$.a[? @ > 1]", &json),
            Ok(vec![
                (&json!(2), "$['a'][1]".to_string()).into(),
                (&json!(3), "$['a'][2]".to_string()).into(),
            ])
        );
    }

    #[test]
    fn existence() {
        let json = json!({
          "a": {
            "a":{"b":1},
            "c": {
              "b": 2
            },
            "d": {
              "b1": 3
            }
          }
        });
        assert_eq!(
            js_path("$.a[?@.b]", &json),
            Ok(vec![
                (&json!({"b":1}), "$['a']['a']".to_string()).into(),
                (&json!({"b":2}), "$['a']['c']".to_string()).into(),
            ])
        );
    }

    #[test]
    fn existence_or() {
        let json = json!({
          "a": {
            "a":{"b":1},
            "c": {
              "b": 2
            },
            "d": {
              "b1": 3
            },
            "e": {
              "b2": 3
            }
          }
        });
        assert_eq!(
            js_path("$.a[?@.b || @.b1]", &json),
            Ok(vec![
                (&json!({"b":1}), "$['a']['a']".to_string()).into(),
                (&json!({"b":2}), "$['a']['c']".to_string()).into(),
                (&json!({"b1":3}), "$['a']['d']".to_string()).into(),
            ])
        );
    }
}
