use serde_yaml::Value;

use crate::query::document::{
    CountOp, DeleteOp, FieldOp, FieldPath, Filter, FindOp, InclusionAnchor, KeyOp, Operation,
    ReferenceAnchor, Update, UpdateOp, YamlType,
};

pub trait WithFilter: Sized {
    fn with_filter(filter: Filter) -> Self;
}

impl WithFilter for FindOp {
    fn with_filter(f: Filter) -> Self {
        FindOp::new().filter(f)
    }
}

impl WithFilter for CountOp {
    fn with_filter(f: Filter) -> Self {
        CountOp::new().filter(f)
    }
}

impl WithFilter for DeleteOp {
    fn with_filter(f: Filter) -> Self {
        DeleteOp::new(f)
    }
}

pub fn filter<T: WithFilter>(f: Filter) -> T {
    T::with_filter(f)
}

pub fn find(op: FindOp) -> Operation {
    Operation::Find(op)
}

pub fn count(op: CountOp) -> Operation {
    Operation::Count(op)
}

pub fn delete(op: DeleteOp) -> Operation {
    Operation::Delete(op)
}

pub fn update(op: UpdateOp) -> Operation {
    Operation::Update(op)
}

pub fn update_op(f: Filter, doc: Update) -> UpdateOp {
    UpdateOp::new(f, doc)
}

pub fn and(filters: Vec<Filter>) -> Filter {
    Filter::And(filters)
}

pub fn or(filters: Vec<Filter>) -> Filter {
    Filter::Or(filters)
}

pub fn nor(filters: Vec<Filter>) -> Filter {
    Filter::Nor(filters)
}

pub fn eq(path: &str, v: impl Into<Value>) -> Filter {
    Filter::eq(path, v)
}

pub fn ne(path: &str, v: impl Into<Value>) -> Filter {
    Filter::ne(path, v)
}

pub fn gt(path: &str, v: impl Into<Value>) -> Filter {
    Filter::gt(path, v)
}

pub fn gte(path: &str, v: impl Into<Value>) -> Filter {
    Filter::gte(path, v)
}

pub fn lt(path: &str, v: impl Into<Value>) -> Filter {
    Filter::lt(path, v)
}

pub fn lte(path: &str, v: impl Into<Value>) -> Filter {
    Filter::lte(path, v)
}

pub fn exists(path: &str, present: bool) -> Filter {
    Filter::exists(path, present)
}

fn field_op(path: &str, op: FieldOp) -> Filter {
    Filter::Field {
        path: FieldPath::from_dotted(path),
        op,
    }
}

pub fn in_(path: &str, values: impl IntoIterator<Item = impl Into<Value>>) -> Filter {
    field_op(
        path,
        FieldOp::In(values.into_iter().map(Into::into).collect()),
    )
}

pub fn nin(path: &str, values: impl IntoIterator<Item = impl Into<Value>>) -> Filter {
    field_op(
        path,
        FieldOp::Nin(values.into_iter().map(Into::into).collect()),
    )
}

pub fn all(path: &str, values: impl IntoIterator<Item = impl Into<Value>>) -> Filter {
    field_op(
        path,
        FieldOp::All(values.into_iter().map(Into::into).collect()),
    )
}

pub fn size(path: &str, n: u64) -> Filter {
    field_op(path, FieldOp::Size(n))
}

pub fn type_of(path: &str, types: impl IntoIterator<Item = YamlType>) -> Filter {
    field_op(path, FieldOp::Type(types.into_iter().collect()))
}

pub fn key(op: KeyOp) -> Filter {
    Filter::Key(op)
}

pub fn key_eq(k: impl Into<String>) -> Filter {
    Filter::Key(KeyOp::eq(k))
}

pub fn key_ne(k: impl Into<String>) -> Filter {
    Filter::Key(KeyOp::ne(k))
}

pub fn key_in(keys: &[&str]) -> Filter {
    Filter::Key(KeyOp::in_(keys))
}

pub fn key_nin(keys: &[&str]) -> Filter {
    Filter::Key(KeyOp::nin(keys))
}

pub fn includes(anchor: InclusionAnchor) -> Filter {
    Filter::Includes(Box::new(anchor))
}

pub fn included_by(anchor: InclusionAnchor) -> Filter {
    Filter::IncludedBy(Box::new(anchor))
}

pub fn references(anchor: ReferenceAnchor) -> Filter {
    Filter::References(Box::new(anchor))
}

pub fn referenced_by(anchor: ReferenceAnchor) -> Filter {
    Filter::ReferencedBy(Box::new(anchor))
}

pub fn inclusion(key: impl Into<String>, max_depth: u32) -> InclusionAnchor {
    InclusionAnchor::with_max(key, max_depth)
}

pub fn inclusion_range(key: impl Into<String>, min_depth: u32, max_depth: u32) -> InclusionAnchor {
    InclusionAnchor::new(key, min_depth, max_depth)
}

pub fn reference(key: impl Into<String>, max_distance: u32) -> ReferenceAnchor {
    ReferenceAnchor::with_max(key, max_distance)
}

pub fn reference_range(
    key: impl Into<String>,
    min_distance: u32,
    max_distance: u32,
) -> ReferenceAnchor {
    ReferenceAnchor::new(key, min_distance, max_distance)
}
