use std::fmt;

use sxd_document::QName;

use ::context;
use ::nodeset::{self, OrderedNodes};

pub trait NodeTest: fmt::Debug {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>);
}

impl<T: ?Sized> NodeTest for Box<T>
    where T: NodeTest,
{
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        (**self).test(context, result)
    }
}

pub type SubNodeTest = Box<NodeTest + 'static>;

#[derive(Debug, Clone, PartialEq)]
pub struct NameTest {
    pub prefix: Option<String>,
    pub local_part: String,
}

impl NameTest {
    fn matches(&self, context: &context::Evaluation, node_name: QName) -> bool {
        let is_wildcard = self.local_part == "*";

        let test_uri = self.prefix.as_ref().map(|p| {
            // TODO: Error for undefined prefix
            context.namespace_for(p)
                .expect("No namespace for prefix")
        });

        match (is_wildcard, test_uri) {
            (true, None) => true,
            (true, Some(..)) => test_uri == node_name.namespace_uri(),
            _ => {
                test_uri == node_name.namespace_uri() &&
                    self.local_part == node_name.local_part()
            },
        }
    }
}

#[derive(Debug)]
pub struct Attribute {
    name_test: NameTest,
}

impl Attribute {
    pub fn new(name: NameTest) -> Attribute {
        Attribute {
            name_test: name,
        }
    }
}

impl NodeTest for Attribute {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        if let nodeset::Node::Attribute(ref a) = context.node {
            if self.name_test.matches(context, a.name()) {
                result.add(context.node);
            }
        }
    }
}

#[derive(Debug)]
pub struct Namespace {
    name_test: NameTest,
}

impl Namespace {
    pub fn new(name: NameTest) -> Namespace {
        Namespace {
            name_test: name,
        }
    }
}

impl NodeTest for Namespace {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        if let nodeset::Node::Namespace(ref ns) = context.node {
            if self.name_test.matches(context, QName::new(ns.prefix())) {
                result.add(context.node);
            }
        }
    }
}

#[derive(Debug)]
pub struct Element {
    name_test: NameTest,
}

impl Element {
    pub fn new(name: NameTest) -> Element {
        Element {
            name_test: name,
        }
    }
}

impl NodeTest for Element {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        if let nodeset::Node::Element(ref e) = context.node {
            if self.name_test.matches(context, e.name()) {
                result.add(context.node);
            }
        }
    }
}

#[allow(missing_copy_implementations)]
#[derive(Debug)]
pub struct Node;

impl NodeTest for Node {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        result.add(context.node);
    }
}

#[allow(missing_copy_implementations)]
#[derive(Debug)]
pub struct Text;

impl NodeTest for Text {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        if let nodeset::Node::Text(_) = context.node {
            result.add(context.node);
        }
    }
}

#[allow(missing_copy_implementations)]
#[derive(Debug)]
pub struct Comment;

impl NodeTest for Comment {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        if let nodeset::Node::Comment(_) = context.node {
            result.add(context.node);
        }
    }
}

#[derive(Debug)]
pub struct ProcessingInstruction {
    target: Option<String>,
}

impl ProcessingInstruction {
    pub fn new(target: Option<String>) -> ProcessingInstruction {
        ProcessingInstruction { target: target }
    }
}

impl NodeTest for ProcessingInstruction {
    fn test<'c, 'd>(&self, context: &context::Evaluation<'c, 'd>, result: &mut OrderedNodes<'d>) {
        if let nodeset::Node::ProcessingInstruction(pi) = context.node {
            match self.target {
                Some(ref name) if name == &pi.target() => result.add(context.node),
                Some(_)                                => {},
                None                                   => result.add(context.node),
            }
        }
    }
}

#[cfg(test)]
mod test {
    use std::borrow::ToOwned;

    use sxd_document::{Package, QName};
    use sxd_document::dom::{self, Document};

    use ::context::{self, Context};
    use ::nodeset::OrderedNodes;

    use super::*;

    struct Setup<'d> {
        doc: Document<'d>,
        context: Context<'d>,
    }

    impl<'d> Setup<'d> {
        fn new(package: &'d Package) -> Setup {
            Setup {
                doc: package.as_document(),
                context: Context::without_core_functions(),
            }
        }

        fn register_prefix(&mut self, prefix: &str, namespace_uri: &str) {
            self.context.set_namespace(prefix, namespace_uri);
        }

        fn context_for_attribute<'n, N>(&'d self, name: N, val: &str)
                                        -> (dom::Attribute<'d>, context::Evaluation<'d, 'd>)
            where N: Into<QName<'n>>
        {
            let e = self.doc.create_element("element");
            let a = e.set_attribute_value(name, val);
            let c = context::Evaluation::new(&self.context, a.into());
            (a, c)
        }

        fn context_for_ns_attribute(&'d mut self, prefix: &str, nsuri: &str, local: &str, value: &str)
                                    -> (dom::Attribute<'d>, context::Evaluation<'d, 'd>)
        {
            self.register_prefix(prefix, nsuri);
            self.context_for_attribute((nsuri, local), value)
        }

        fn context_for_element<'n, N>(&'d self, name: N)
                                  -> (dom::Element<'d>, context::Evaluation<'d, 'd>)
            where N: Into<QName<'n>>
        {
            let e = self.doc.create_element(name);
            let c = context::Evaluation::new(&self.context, e.into());
            (e, c)
        }

        fn context_for_ns_element(&'d mut self, prefix: &str, nsuri: &str, local: &str)
                                  -> (dom::Element<'d>, context::Evaluation<'d, 'd>)
        {
            self.register_prefix(prefix, nsuri);
            self.context_for_element((nsuri, local))
        }
    }

    fn run_attribute<'d>(context: &context::Evaluation<'d, 'd>, prefix: Option<&str>, local: &str)
                       -> OrderedNodes<'d>
    {
        let mut result = OrderedNodes::new();
        let name = NameTest {
            prefix: prefix.map(|p| p.to_owned()),
            local_part: local.to_owned()
        };
        let test = Attribute::new(name);
        test.test(context, &mut result);
        result
    }

    #[test]
    fn attribute_test_selects_attributes_with_matching_names() {
        let package = Package::new();
        let setup = Setup::new(&package);
        let (attribute, context) = setup.context_for_attribute("hello", "world");

        let result = run_attribute(&context, None, "hello");
        assert_eq!(ordered_nodes![attribute], result);
    }

    #[test]
    fn attribute_test_does_not_select_other_names() {
        let package = Package::new();
        let setup = Setup::new(&package);
        let (_, context) = setup.context_for_attribute("goodbye", "world");

        let result = run_attribute(&context, None, "hello");
        assert_eq!(ordered_nodes![], result);
    }

    #[test]
    fn attribute_test_supports_a_wildcard_match() {
        let package = Package::new();
        let setup = Setup::new(&package);
        let (attribute, context) = setup.context_for_attribute("whatever", "value");

        let result = run_attribute(&context, None, "*");
        assert_eq!(ordered_nodes![attribute], result);
    }

    #[test]
    fn attribute_test_matches_on_namespace_and_name() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        setup.register_prefix("another", "different-uri");
        let (attribute, context) = setup.context_for_ns_attribute("prefix", "namespace", "name", "value");

        let result = run_attribute(&context, Some("prefix"), "name");
        assert_eq!(ordered_nodes![attribute], result);

        let result = run_attribute(&context, Some("prefix"), "wrong-name");
        assert_eq!(ordered_nodes![], result);

        let result = run_attribute(&context, Some("another"), "name");
        assert_eq!(ordered_nodes![], result);
    }

    #[test]
    fn attribute_test_matches_on_namespace_when_wildcard_with_prefix() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        setup.register_prefix("another", "different-uri");
        let (attribute, context) = setup.context_for_ns_attribute("prefix", "namespace", "name", "value");

        let result = run_attribute(&context, Some("prefix"), "*");
        assert_eq!(ordered_nodes![attribute], result);

        let result = run_attribute(&context, Some("another"), "*");
        assert_eq!(ordered_nodes![], result);
    }

    #[test]
    fn attribute_test_ignores_namespace_when_wildcard_without_prefix() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        let (attribute, context) = setup.context_for_ns_attribute("prefix", "namespace", "name", "value");

        let result = run_attribute(&context, None, "*");
        assert_eq!(ordered_nodes![attribute], result);
    }

    #[test]
    fn attribute_test_does_not_match_when_attribute_has_namespace_but_without_prefix() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        let (_, context) = setup.context_for_ns_attribute("prefix", "namespace", "name", "value");

        let result = run_attribute(&context, None, "name");
        assert_eq!(ordered_nodes![], result);
    }

    fn run_element<'d>(context: &context::Evaluation<'d, 'd>, prefix: Option<&str>, local: &str)
                       -> OrderedNodes<'d>
    {
        let mut result = OrderedNodes::new();
        let name = NameTest {
            prefix: prefix.map(|p| p.to_owned()),
            local_part: local.to_owned()
        };
        let test = Element::new(name);
        test.test(context, &mut result);
        result
    }

    #[test]
    fn element_test_selects_nodes_with_matching_names() {
        let package = Package::new();
        let setup = Setup::new(&package);
        let (element, context) = setup.context_for_element("hello");

        let result = run_element(&context, None, "hello");
        assert_eq!(ordered_nodes![element], result);
    }

    #[test]
    fn element_test_does_not_select_other_names() {
        let package = Package::new();
        let setup = Setup::new(&package);
        let (_, context) = setup.context_for_element("goodbye");

        let result = run_element(&context, None, "hello");
        assert_eq!(ordered_nodes![], result);
    }

    #[test]
    fn element_test_supports_a_wildcard_match() {
        let package = Package::new();
        let setup = Setup::new(&package);
        let (element, context) = setup.context_for_element("hello");

        let result = run_element(&context, None, "*");
        assert_eq!(ordered_nodes![element], result);
    }

    #[test]
    fn element_test_matches_on_namespace_and_name() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        setup.register_prefix("another", "different-uri");
        let (element, context) = setup.context_for_ns_element("prefix", "uri", "name");

        let result = run_element(&context, Some("prefix"), "name");
        assert_eq!(ordered_nodes![element], result);

        let result = run_element(&context, Some("prefix"), "wrong-name");
        assert_eq!(ordered_nodes![], result);

        let result = run_element(&context, Some("another"), "name");
        assert_eq!(ordered_nodes![], result);
    }

    #[test]
    fn element_test_matches_on_namespace_when_wildcard_with_prefix() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        setup.register_prefix("another", "different-uri");
        let (element, context) = setup.context_for_ns_element("prefix", "uri", "name");

        let result = run_element(&context, Some("prefix"), "*");
        assert_eq!(ordered_nodes![element], result);

        let result = run_element(&context, Some("another"), "*");
        assert_eq!(ordered_nodes![], result);
    }

    #[test]
    fn element_test_ignores_namespace_when_wildcard_without_prefix() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        let (element, context) = setup.context_for_ns_element("prefix", "uri", "name");

        let result = run_element(&context, None, "*");
        assert_eq!(ordered_nodes![element], result);
    }

    #[test]
    fn element_test_does_not_match_when_element_has_namespace_but_without_prefix() {
        let package = Package::new();
        let mut setup = Setup::new(&package);
        let (_, context) = setup.context_for_ns_element("prefix", "uri", "name");

        let result = run_element(&context, None, "name");
        assert_eq!(ordered_nodes![], result);
    }
}
