JanusFireboxParser = function() {
}

JanusFireboxParser.prototype.parse = function(source, baseurl, datapath) {
  var xml = this.parseXML(source, false, true); 
  var rooms = this.getAsArray(this.arrayget(xml, 'fireboxroom._children.room', {})); 
  var room = {_children: {}};
  for (var i = 0; i < rooms.length; i++) {
    var attrs = Object.keys(rooms[i]).filter(function(k) { return (k[0] != '_'); });
    attrs.forEach(function(k) {
      room[k] = rooms[i][k];
    });
    if (rooms[i]._children) {
      Object.keys(rooms[i]._children).forEach(function(k) {
        room._children[k] = rooms[i]._children[k];
      });
    }
  }
  var roomdata = this.getRoomData(xml, room, baseurl, datapath);
  roomdata.source = source;
  return roomdata;
}
JanusFireboxParser.prototype.getRoomData = function(xml, room, baseurl, datapath) {
  var assets = this.parseAssets(xml, baseurl, datapath);

  var parseNode = this.parseNode.bind(this);

  var roomdata = {
    assets: assets,
    room: this.parseNode(room)
  };
  for (var k in room._children) {
    var objects = this.getAsArray(room._children[k]);
    roomdata[k] = objects.map(parseNode);
  }

  // TODO - some rooms have orphan objects, outside of the <Room> tag. 
  // We should parse these too, but I don't have any examples to test with
/*
  var orphantypes = ['link', 'image', 'image3d', 'video', 'sound', 'text', 'paragraph', 'light', 'particle', 'ghost'];
  for (var i = 0; i < orphantypes.length; i++) {
    var type = orphantypes[i],
        orphans = this.getAsArray(this.arrayget(xml, 'fireboxroom._children.' + type));
    if (orphans && orphans.length > 0) {
      if (!roomdata[type]) roomdata[type] = [];
      roomdata[type].push.apply(roomdata[type], orphans);
    }
  };
*/
  return roomdata;
}
JanusFireboxParser.prototype.getAsArray = function(arr) {
  return (arr instanceof Array ? arr : [arr]);
}
JanusFireboxParser.prototype.parseAssets = function(xml, baseurl, datapath) {
  var assetxml = this.arrayget(xml, 'fireboxroom._children.assets.0', {}); 
  var objectassets = this.getAsArray(this.arrayget(assetxml, "_children.assetobject", [])); 
  var soundassets = this.getAsArray(this.arrayget(assetxml, "_children.assetsound", [])); 
  var imageassets = this.getAsArray(this.arrayget(assetxml, "_children.assetimage", [])); 
  var videoassets = this.getAsArray(this.arrayget(assetxml, "_children.assetvideo", [])); 
  var scriptassets = this.getAsArray(this.arrayget(assetxml, "_children.assetscript", [])); 
  var ghostassets = this.getAsArray(this.arrayget(assetxml, "_children.assetghost", [])); 
  var websurfaceassets = this.getAsArray(this.arrayget(assetxml, "_children.assetwebsurface", [])); 
  var shaderassets = this.getAsArray(this.arrayget(assetxml, "_children.assetshader", []));
  var fontassets = this.getAsArray(this.arrayget(assetxml, "_children.assetfont", []));
  var assetlist = [];
  if (!datapath) {
    datapath = 'http://web.janusvr.com/media';
  }
  var fixURLEncoding = this.fixURLEncoding.bind(this);
  imageassets.forEach(function(n) { 
    var src = fixURLEncoding(n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
    assetlist.push({
      assettype: 'image',
      name: n.id,
      src: src,
      baseurl: baseurl,
      hasalpha: ('hasalpha' in n ? n.hasalpha == "true" || n.hasalpha === true || n.hasalpha === 1 || n.hasalpha === '1' : null),
      proxy: n.proxy,
      vr180: n.vr180,
      sbs3d: n.sbs3d,
      ou3d: n.ou3d,
      flipy: n.flipy,
      reverse3d: n.reverse3d,
      tex_linear: n.tex_linear,
      equi: n.equi,
    });
  });
  videoassets.forEach(function(n) { 
    var src = fixURLEncoding(n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
    assetlist.push({ 
      assettype:'video', 
      name:n.id, 
      src: src, 
      loop: (n.loop && n.loop != 'false' && n.loop != '0'),
      vr180: n.vr180,
      sbs3d: n.sbs3d == 'true',  
      ou3d: n.ou3d == 'true',  
      hasalpha: n.hasalpha == 'true',
      flipy: n.flipy,
      reverse3d: n.reverse3d,
      auto_play: n.auto_play == 'true',
      baseurl: baseurl,
      proxy: n.proxy && n.proxy != 'false'
    }); 
  });
  soundassets.forEach(function(n) { 
    var src = fixURLEncoding(n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
    assetlist.push({ 
      assettype:'sound', 
      name:n.id, 
      src: src,
      baseurl: baseurl
    }); 
  });
  var websurfaces = {};
  websurfaceassets.forEach(function(n) {
    websurfaces[n.id] = n;
    assetlist.push(n);
  });
  scriptassets.forEach(function(n) { 
    var src = fixURLEncoding(n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
    assetlist.push({ 
      assettype:'script', 
      name: src,
      src: src,
      baseurl: baseurl
    }); 
  });
  shaderassets.forEach(function(n) {
    n.uniforms = (n._children && n._children.uniform ? n._children.uniform : false);
    assetlist.push({
      assettype: 'shader',
      name: n.id,
      shadertype: n.shadertype || 'default',
      fragment_src: n.src || n.fragment_src,
      vertex_src: n.vertex_src,
      hasalpha: n.hasalpha,
      uniforms: n.uniforms,
      baseurl: baseurl,
    });
  });
  fontassets.forEach(function(n) {
    assetlist.push({
      assettype: 'font',
      name: n.id,
      src: n.src,
      baseurl: baseurl,
    });
  });

  var objlist = []; 
  objectassets.forEach(function(n) { 
    if (n.src) {
      var src = fixURLEncoding(n.src.match(/^file:/) ? n.src.replace(/^file:/, datapath) : n.src);
      var mtlsrc = (n.mtl && n.mtl.match(/^file:/) ? n.mtl.replace(/^file:/, datapath) : n.mtl);
      if (mtlsrc && !mtlsrc.match(/^(https?:)?\/\//)) mtlsrc = baseurl + mtlsrc;
      var srcparts = src.split(' ');
      src = srcparts[0];
      assetlist.push({assettype: 'model', name: n.id, src: src, mtl: mtlsrc, tex_linear: n.tex_linear, tex0: n.tex || n.tex0 || srcparts[1], tex1: n.tex1 || srcparts[2], tex2: n.tex2 || srcparts[3], tex3: n.tex3 || srcparts[4]}); 
    }
  }); 
  var assets = {
    object: objectassets,
    image: imageassets,
    sound: soundassets,
    video: videoassets,
    websurfaces: websurfaces,
    scripts: scriptassets,
    ghosts: ghostassets,
    shaders: shaderassets,
    assetlist: assetlist
  };
  return assets;
}

JanusFireboxParser.prototype.getVectorValue = function(vector, defaultvalue) {
  if (typeof defaultvalue == 'undefined') {
    defaultvalue = null;//[0,0,0];
  }
  if (typeof vector == 'string') {
    return vector.trim().split(' ').map(parseFloat);
  } else if (vector instanceof THREE.Vector3) {
    return vector.toArray();
  } else if (typeof vector == 'undefined') {
    return defaultvalue;
  }
  return vector;
}
JanusFireboxParser.prototype.parseNode = function(n) {
  var nodeinfo = {};
  var attrs = Object.keys(n);
  attrs.forEach(function(k) {
    nodeinfo[k] = (n[k] == 'false' ? false : n[k]);
  });

  
  nodeinfo.pos = this.getVectorValue(n.pos);
  nodeinfo.scale = this.getVectorValue(n.scale, [1,1,1]);
  nodeinfo.orientation = this.getOrientation(n.xdir, n.ydir || n.up, n.zdir || n.fwd);
  nodeinfo.col = (n.col ? (n.col[0] == '#' ? [parseInt(n.col.substr(1,2), 16)/255, parseInt(n.col.substr(3, 2), 16)/255, parseInt(n.col.substr(5, 2), 16)/255] : n.col) : null);
  
  var minscale = 1e-20;
/*
  nodeinfo.scale[0] = Math.max(minscale, nodeinfo.scale[0]);
  nodeinfo.scale[1] = Math.max(minscale, nodeinfo.scale[1]);
  nodeinfo.scale[2] = Math.max(minscale, nodeinfo.scale[2]);
*/
  if (nodeinfo.scale[0] < minscale && nodeinfo.scale[0] > -minscale) nodeinfo.scale[0] = minscale;
  if (nodeinfo.scale[1] < minscale && nodeinfo.scale[1] > -minscale) nodeinfo.scale[1] = minscale;
  if (nodeinfo.scale[2] < minscale && nodeinfo.scale[2] > -minscale) nodeinfo.scale[2] = minscale;

  return nodeinfo;
}

JanusFireboxParser.prototype.getOrientation = function(xdir, ydir, zdir) {
  if (xdir) {
    xdir = (xdir instanceof THREE.Vector3 ? xdir : new THREE.Vector3().fromArray(xdir.split(' '))).normalize();
  } else {
    xdir = new THREE.Vector3(1,0,0);
  }
  if (ydir) {
    ydir = (ydir instanceof THREE.Vector3 ? ydir : new THREE.Vector3().fromArray(ydir.split(' '))).normalize();
  } else {
    ydir = new THREE.Vector3(0,1,0);
  }
  if (zdir) {
    zdir = (zdir instanceof THREE.Vector3 ? zdir : new THREE.Vector3().fromArray(zdir.split(' '))).normalize();
  } else {
    zdir = new THREE.Vector3(0,0,1);
  }

  var newydir = ydir.clone().sub(zdir).multiplyScalar(ydir.dot(zdir)).normalize();
  xdir.crossVectors(ydir, zdir).normalize();

  var mat4 = new THREE.Matrix4().makeBasis(xdir, ydir, zdir);
  var quat = new THREE.Quaternion();
  var pos = new THREE.Vector3();
  var scale = new THREE.Vector3();
  //quat.setFromRotationMatrix(mat4);
  mat4.decompose(pos, quat, scale);
  quat.normalize();
  //quat.multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0)));
  return quat;
}
JanusFireboxParser.prototype.parseXML = function(imgxml, leaf, forceLower) {
  var node, root, parent, xmldoc;
  if (imgxml.nodeName) {
    xmldoc = node = imgxml;
  } else {
    try {
      if (window.DOMParser) {
        var parser = new DOMParser();
        xmldoc = parser.parseFromString(imgxml,"application/xml");
        node = xmldoc.children[0];
      } else {
        node = new ActiveXObject("Microsoft.XMLDOM");
        node.async = "false";
        node.loadXML(imgxml).firstChild;
      }

      // Chrome doesn't throw an exception for malformed XML, so we look for a <parsererror> xml tag
      // Firefox now appears to follow the same behavior, but puts the <parsererror> as the top-level element instead of as a child of the last DOM element which parsed successfully
      var parsererrors = (node.tagName == 'parsererror' ? [node] : node.getElementsByTagName("parsererror"));
      if (parsererrors.length > 0) {
        // Extract the message from the first div child of the <parsererror> element
        var errorel = parsererrors[0].getElementsByTagName('div')[0] || parsererrors[0];
        throw new JanusFireboxParserException(errorel.innerHTML);
      }
    } catch (e) {
      throw new JanusFireboxParserException(e.message);
    }
  }
  root = {};
  if (!leaf) {
    var rootname = node.tagName;
    if (forceLower) rootname = rootname.toLowerCase();
    if (!root[rootname]) root[rootname] = {};
    parent = root[rootname];
    //node = parent[node.tagName];
  } else {
    parent = root;
  }
  if (node.attributes) {
    for (var i = 0; i < node.attributes.length; i++) {
      var name = node.attributes[i].nodeName;
      if (forceLower) name = name.toLowerCase();
      var value = node.attributes[i].nodeValue;
      parent[name] = value;
    }
  }
  if (node.childNodes) {
    for (var j = 0; j < node.childNodes.length; j++) {
      var child = node.childNodes[j];
      var nodename = child.nodeName;
      if (forceLower) nodename = nodename.toLowerCase();
      if (!parent._children) parent._children = {};
      if (!parent._children[nodename]) {
        parent._children[nodename] = [];
      } else if (parent._children[nodename].constructor !== Array) {
        parent._children[nodename] = [parent._children[nodename]];
      }
      if (node.getElementsByTagName(child.tagName).length > 0) {
        parent._children[nodename].push(this.parseXML(child, true, forceLower));
      } else if (child.nodeName) {
        if (child.nodeName == "#text" || child.nodeName == "#cdata-section") {
          // this gets confused if you have multiple text/cdata nodes...
          if (!child.nodeValue.match(/^[\s\n]*$/m)) {
            parent._content = child.nodeValue;
          }
        } else {
          parent._children[nodename] = parent._children[nodename].concat(this.parseXML(child, true, forceLower));
        }
      }
    }
  }
  return root;
};

/**
 * Retrieves specified dot-separated value from a multilevel object element 
 *
 * @function JanusFireboxParser.arrayget
 * @param {object} obj
 * @param {string} name
 * @param {object|number|string} [defval] default value if none found
*/
JanusFireboxParser.prototype.arrayget = function(obj, name, defval) {
  var ptr = obj;
  var x = name.split(".");
  for (var i = 0; i < x.length; i++) {
    if (ptr==null || (!(ptr[x[i]] instanceof Array) && !(ptr[x[i]] instanceof Object) && i != x.length-1)) {
      ptr = null;
      break;
    }
    ptr = ptr[x[i]];
  }
  if (typeof ptr == "undefined" || ptr === null) {
    return (typeof defval == "undefined" ? null : defval);
  }
  return ptr;
};

/**
 * Fixes encoding of URL strings if they weren't properly encoded
 *
 * @function JanusFireboxParser.fixURLEncoding
 * @param {string} url
 * @returns {string} encoded URL
*/
JanusFireboxParser.prototype.fixURLEncoding = function(url) {
  var fixed = url;
  if (url.indexOf(' ') != -1) {
    fixed = encodeURI(url);
  }
  return fixed;
};

/**
 * Exception class for XML parse errors
 */
function JanusFireboxParserException(msg) {
  this.message = msg;
}
