extern crate treexml;

#[macro_use]
extern crate serde_json;

use serde_json::{Map, Number, Value};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum XMLNodeType {
    Empty,
    Text,
    Attributes,
    TextAndAttributes,
    Parent,
    SemiStructured,
}

fn scan_xml_node(e: &treexml::Element) -> XMLNodeType {
    if e.children.is_empty() {
        if e.text.is_none() && e.cdata.is_none() {
            if e.attributes.is_empty() {
                XMLNodeType::Empty
            } else {
                XMLNodeType::Attributes
            }
        } else {
            if e.attributes.is_empty() {
                XMLNodeType::Text
            } else {
                XMLNodeType::TextAndAttributes
            }
        }
    } else {
        if e.text.is_some() || e.cdata.is_some() {
            XMLNodeType::SemiStructured
        } else {
            XMLNodeType::Parent
        }
    }
}

fn parse_text(text: &str) -> Value {
    match text.parse::<f64>() {
        Ok(v) => {
            match Number::from_f64(v) {
                Some(v) => {
                    return Value::Number(v);
                }
                _ => {}
            }
        }
        _ => {}
    }

    match text.parse::<bool>() {
        Ok(v) => {
            return Value::Bool(v);
        }
        _ => {}
    }

    Value::String(text.into())
}

fn parse_text_contents(e: &treexml::Element) -> Value {
    let text = format!("{}{}",
                       &e.text.clone().unwrap_or(String::new()),
                       &e.cdata.clone().unwrap_or(String::new()));
    parse_text(&text)
}

fn convert_node_aux(e: &treexml::Element) -> Option<Value> {
    match scan_xml_node(e) {
        XMLNodeType::Parent => {
            let mut data = Map::new();
            let mut firstpass = std::collections::HashSet::new();
            let mut vectorized = std::collections::HashSet::new();

            for c in &e.children {
                match convert_node_aux(c) {
                    Some(v) => {
                        if !firstpass.contains(&c.name) {
                            data.insert(c.name.clone(), v);
                            firstpass.insert(c.name.clone());
                        } else {
                            if !vectorized.contains(&c.name) {
                                let elem = data.remove(&c.name).unwrap();
                                data.insert(c.name.clone(), Value::Array(vec![elem, v]));
                                vectorized.insert(c.name.clone());
                            } else {
                                data.get_mut(&c.name).unwrap().as_array_mut().unwrap().push(v);
                            }
                        }
                    }
                    _ => {}
                }
            }
            Some(Value::Object(data))
        }
        XMLNodeType::Text => {
            Some(parse_text_contents(e))
        }
        XMLNodeType::Attributes => {
            Some(Value::Object(e.attributes
                .clone()
                .into_iter()
                .map(|(k, v)| (format!("@{}", k), parse_text(&v)))
                .collect()))
        }
        XMLNodeType::TextAndAttributes => {
            Some(Value::Object(e.attributes
                .clone()
                .into_iter()
                .map(|(k, v)| (format!("@{}", k), parse_text(&v)))
                .chain(vec![("#text".to_string(), parse_text_contents(&e))])
                .collect()))
        }
        _ => None,
    }
}

pub fn node2object(e: &treexml::Element) -> Map<String, Value> {
    let mut data = Map::new();
    data.insert(e.name.clone(), convert_node_aux(e).unwrap_or(Value::Null));
    data
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn node2object_empty() {
        let fixture = treexml::Element::new("e");
        let scan_result = XMLNodeType::Empty;
        let conv_result = json!({
            "e": null
        });

        assert_eq!(scan_result, scan_xml_node(&fixture));
        assert_eq!(conv_result, Value::Object(node2object(&fixture)));
    }

    #[test]
    fn node2object_text() {
        let mut fixture = treexml::Element::new("player");
        fixture.text = Some("Kolya".into());
        let scan_result = XMLNodeType::Text;
        let conv_result = json!({"player": "Kolya"});

        assert_eq!(scan_result, scan_xml_node(&fixture));
        assert_eq!(conv_result, Value::Object(node2object(&fixture)));
    }

    #[test]
    fn node2object_attributes() {
        let mut fixture = treexml::Element::new("player");
        fixture.attributes.insert("score".into(), "9000".into());
        let scan_result = XMLNodeType::Attributes;
        let conv_result = json!({"player": json!({"@score": 9000.0})});

        assert_eq!(scan_result, scan_xml_node(&fixture));
        assert_eq!(conv_result, Value::Object(node2object(&fixture)));
    }

    #[test]
    fn node2object_text_and_attributes() {
        let mut fixture = treexml::Element::new("player");
        fixture.text = Some("Kolya".into());
        fixture.attributes.insert("score".into(), "9000".into());
        let scan_result = XMLNodeType::TextAndAttributes;
        let conv_result = json!({
            "player": json!({"#text": "Kolya", "@score": 9000.0})
        });

        assert_eq!(scan_result, scan_xml_node(&fixture));
        assert_eq!(conv_result, Value::Object(node2object(&fixture)));
    }

    #[test]
    fn node2object_parent() {
        let mut fixture = treexml::Element::new("ServerData");
        fixture.children.push({
            let mut node = treexml::Element::new("Player");
            node.text = Some("Kolya".into());
            node
        });
        fixture.children.push({
            let mut node = treexml::Element::new("Player");
            node.text = Some("Petya".into());
            node
        });
        fixture.children.push({
            let mut node = treexml::Element::new("Player");
            node.text = Some("Misha".into());
            node
        });
        let scan_result = XMLNodeType::Parent;
        let conv_result = json!({
            "ServerData": json!({ "Player": [ "Kolya", "Petya", "Misha" ] })
        });

        assert_eq!(scan_result, scan_xml_node(&fixture));
        assert_eq!(conv_result, Value::Object(node2object(&fixture)));
    }
}
