/*
    XML2OBJ {Class}

    Transforme un xml en object js parametrable

    <monNoeud monAttribut="toto">monTexte</monNoeud>

    @use 
        new XML2OBJ("/mon/fichier.xml")
        new XML2OBJ("/mon/fichier.xml", {arrayForcedNode:"item|article"})
        new XML2OBJ(responseXML, {arrayForcedNode:["item", "article"], arrayForcedAttribute:"attr"})

    @param  xml {string|document} 
        si string: alors url chargement synchrone du xml (ne marche pas sous webKit)
        si document:  responseXML au onSuccess d'un appel AJAX 

    @param options {object}

        @property arrayForcedNode {regexp|array|string}

            si regexp: un object RegExp 
            si string: de la forme 'elm' matchera <elm> et pas <elms> 
            si array: de la forme ['elm' , 'node', 'root'] matchera strictement les noeuds

            avec arrayForcedNode = "*" toutes les proprietes de l'objet representant des noeuds seront array,
            mais pas les attributes

        @property arrayForcedAttribute {string}
            par defaut, les attributs des nodes deviennent des propriétés de l'object monNoeud.monAttribut = "toto"
            si sette, les attributs se poussent dans un array monNoeud[arrayForcedAttribute][monAttribut] = "toto"

        @ property nodeTextPropertyName {string}
            par defaut, le textContent du noeud se trouve dans monNoeud.txt = monTexte
            si sette devient monNoeud.nodeTextPropertyName = monTexte

        @property tagNamePropertyName {string}
            chaque objet recoit une propriete contenant le nom du noeud, 
            valeur par defaut "tag", a ne modifier qu'en cas de conflit.

        @property rootTagPropertyName {string}
            variable contenat le nom du noeud root, par defaut = root
            var json = XML2OBJ(responseXML);
            le noeud racine du XML se pointe comme ceci : json[json.root];



        JSON = {
            root : "structures",
            structures : {...},
            url: "http://mon.xml"
        }
        nous pouvons donc cibler l'element racine comme ceci :  JSON[JSON.root]

    @method getTEXT : retourne l'objet JSON en string (eval())


    @return instance 

    les methodes privees _load et _createDocument sont fortement inspirées de xml.js -  http://www.davidflanagan.com/javascript5/
    sourceCode of the Bible, o'Reilly's JS Definitive Guide by David Flanagan
*/

var XML2OBJ = function (){
    this.initialize.apply(this, arguments);
}

XML2OBJ.prototype = {

    constructor : XML2OBJ,

    options : {
        arrayForcedNode: "^$",
        objectForcedAttribute: false, 
        arrayForcedAttribute: false,
        nodeTextPropertyName: "txt",
        tagNamePropertyName: "tag",
        rootTagPropertyName: "root"
    },

    initialize: function (sXml, oOptions){

        // setOptions
        this.setOptions(oOptions);

        // creation d'un doc xml
        this.xml = this._load(sXml);

        // vars
        this.cache = {};
        this[this.options.rootTagPropertyName] = this.xml.documentElement.nodeName; // this.root

        // lancement recursion
        this._build(this.xml, this);

        //console.log(this[this.root]);
    },

    //retourne l'objet genere
    getJSON : function (){
        if(!this.json){
            this.json = {
                root : this.root
            };
            this._build(this.xml, this.json);
        }
        return this.json;
    },

    // retourne une chaine text (facon json)
    getTXT : function (oNode){
        if(!oNode) oNode = this.json;
        return this._jsonToString(oNode);
    },

    // construction du json recursive
    _build : function (eNode, oParent){
        for(var i=0,l=eNode.childNodes.length;i<l;i++){
            var node = eNode.childNodes[i];
            if(node.nodeType != 1) continue;
            var oNode = this._createObject(node, oParent);
            this._build(node, oNode);
        }
    },

    // ajout de l'object node dans le json
    _inject: function (oParent, oNode ){

        var forceArray = this.options["arrayForcedNode"].test(oNode[this.options.tagNamePropertyName]);

        // si la propriete n'existe pas on l'a cree 
        if(!oParent[oNode[this.options.tagNamePropertyName]] && !forceArray) {
            oParent[oNode[this.options.tagNamePropertyName]] = oNode;
        }
        // si la propriete n'existe pas, et que node est un arrayForcedNode on cree un array 
        else if(!oParent[oNode[this.options.tagNamePropertyName]] && forceArray) {
            oParent[oNode[this.options.tagNamePropertyName]] = [oNode];
        }       
        // si parent exite comme str , transforme en [] puis push
        else if (oParent[oNode[this.options.tagNamePropertyName]] && !oParent[oNode[this.options.tagNamePropertyName]].push){
            oParent[oNode[this.options.tagNamePropertyName]] = [oParent[oNode[this.options.tagNamePropertyName]], oNode];
        }
        // si parent existe comme [], push
        else if (oParent[oNode[this.options.tagNamePropertyName]] && oParent[oNode[this.options.tagNamePropertyName]].push){
            oParent[oNode[this.options.tagNamePropertyName]].push(oNode);
        }

    },

    // create a object describing the node
    _createObject: function (eNode, oParent){

        var $obj = {};

        // avec arrayForcedAttribute on cree un tableau dans $obj
        var target = this.options.arrayForcedAttribute ? $obj[this.options.arrayForcedAttribute] = {} : $obj
        for(var i=0,A=eNode.attributes,l=A.length;i<l;i++){ 
            target[A[i].nodeName] = 
                this.options.objectForcedAttribute ?
                    this._createAttributeObject(A[i], A[i].nodeValue) :
                    this._evalToBoolean(A[i].nodeValue);
        }

        // stock le contenu text de la balise ou "" si le noeud a des enfants
        var ctn = eNode.text || eNode.textContent;
        //TODO target = var this.options.objectForcedString ? $obj[eNode.nodeName][this.options.nodeTextPropertyName] : $obj[this.options.nodeTextPropertyName];
        $obj[this.options.nodeTextPropertyName] = this._hasChild(eNode) ? "" : ctn;

        // stock le tag
        $obj[this.options['tagNamePropertyName']] = eNode.tagName;
        this._inject(oParent, $obj);
        return $obj;
    },

    _createAttributeObject: function (oNode, sValue){
        var $obj = {};
        $obj[this.options.nodeTextPropertyName] = sValue;
        return $obj;
    },

    // recursive, trasfo de object -> string
    _jsonToString: function (oNode){
        var results = [];
        for(var object in oNode){
            var value = "";
            if(typeof oNode[object] == "string"){
                value = '"'+object+'":"'+this._parseStringProp(oNode[object])+'"';
            }
            else if(typeof oNode[object] == "number"){
                value = '"'+object+'":'+oNode[object];
            }
            else if(oNode[object] && typeof oNode[object].push == "function"){
                var val = [];
                var oSelf = this;
                oNode[object].each(
                    function (el){
                        val.push(oSelf._jsonToString(el));
                    }
                );
                value = '"'+object+'":' + '[' + val.join(', ') + ']'
            }
            else if(typeof oNode[object] == "object"){
                value = '"'+object+'": '+ this._jsonToString(oNode[object]);
            }
            if (typeof value !== "undefined" && value != "") results.push(value);
        };
        return '{' + results.join(', ') + '}';

    },

    // remplace le " par '', le " etant le delimiteur de chaine
    _parseStringProp: function (sProp){
        if(!this.parseRegExp) this.parseRegExp = new RegExp('(")', "g");
        return sProp.replace(this.parseRegExp, "''");
    },


    // tester vitesse avec childNodes
    _hasChild: function (eNode){
        return eNode.getElementsByTagName("*").length == 0 ? false : true;
    },


    // simply load the xml
    _load : function(url) {
        // Create a new document the previously defined function
        // will not work on webKit
        if(typeof url == "string"){
            var xmldoc = this._createDocument();  
            xmldoc.async = false;  // We want to load synchronously
            xmldoc.load(url);      // Load and parse
        }
        else {
          //alert(/MSIE 9/.test(navigator.userAgent))
            if(Browser.Engine.webkit || window.opera || /MSIE 9/.test(navigator.userAgent)){
                var xmldoc = url;
            }
            else {
                var xmldoc = this._createDocument();  
                xmldoc.appendChild(url.documentElement);
            }
        }
        return xmldoc;        // Return the document
    },

    _evalToBoolean: function (str){
        return this.options.evalBoolean && str && str.match(/^\s*true|false\s*$/) ? eval(str) : str;
    },

    // create an empty document
    _createDocument : function(rootTagName, namespaceURL) {
        if (!rootTagName) rootTagName = "";
        if (!namespaceURL) namespaceURL = "";

        if (document.implementation && document.implementation.createDocument) {
            // This is the W3C standard way to do it
            return document.implementation.createDocument(namespaceURL, rootTagName, null);
        }
        else { // This is the IE way to do it
            // Create an empty document as an ActiveX object
            // If there is no root element, this is all we have to do
            var doc = new ActiveXObject("MSXML2.DOMDocument");

            // If there is a root tag, initialize the document
            if (rootTagName) {
                // Look for a namespace prefix
                var prefix = "";
                var tagname = rootTagName;
                var p = rootTagName.indexOf(':');
                if (p != -1) {
                    prefix = rootTagName.substring(0, p);
                    tagname = rootTagName.substring(p+1);
                }

                // If we have a namespace, we must have a namespace prefix
                // If we don't have a namespace, we discard any prefix
                if (namespaceURL) {
                    if (!prefix) prefix = "a0"; // What Firefox uses
                }
                else prefix = "";

                // Create the root element (with optional namespace) as a
                // string of text
                var text = "<" + (prefix?(prefix+":"):"") +  tagname +
                    (namespaceURL
                     ?(" xmlns:" + prefix + '="' + namespaceURL +'"')
                     :"") +
                    "/>";
                // And parse that text into the empty document
                doc.loadXML(text);
            }
            return doc;
        }
    },

    setOptions: function (oOpts){
        // permet de passer les options du prototype vers l'instance
        var $obj = this.options;
        this.options = {}
        for(var prop in $obj) this.options[prop] = $obj[prop];
        for(var prop in oOpts) this.options[prop] = oOpts[prop];
        // 
        // traitement du arrayForcedNode vers regExp
        if(this.options["arrayForcedNode"] == "*") this.options["arrayForcedNode"] = "[a-zA-Z]*";
        this.options["arrayForcedNode"] = this.toRegExp(this.options["arrayForcedNode"], "");
    },

    toRegExp: function (src, opt){
        if(src.exec) return src;
        var $str = "\\b"+src+"\\b";
        if(src && src.push) $str = src.join("|");
        if(!src) $str = "^$";
        return new RegExp($str, opt)
    },

    toHTML: function (){

    }

}
