function MultiResNode() {
	this.id              = 0;
	this.parentIndex     = -1;
	this.childrenIndices = [ -1, -1, -1, -1 ];
	this.projectedSize   = 1.0;
	this.zScale          = 1.0;
	this.box             = new SglBox3();
	this.priority        = { timestamp: -1, error: Number.MAX_VALUE };
	this.req             = null;
	this.data            = null;
	this.isLeaf          = true;
}

/*
MultiResNode.prototype = {
	get isLeaf() {
		for (var i=0; i<4; ++i) {
			if (this.childrenIndices[i] >= 0) return false;
		}
		return true;
	}
};
*/

function MultiResTree(url, onloadCallback) {
	this.ready      = true;
	this.nodesCount = 0;
	this.rootIndex  = -1;
	this.nodes      = null;
	this.url        = null;
	this.tileSize   = 0;
	this.scale      = [ 1.0, 1.0, 1.0 ];
	this.offset     = [ 0.0, 0.0, 0.0 ];

	if (url) {
		this.load(url, onloadCallback);
	}
}

MultiResTree.prototype = {
	_load : function(text) {
		this.ready = true;

		var lines = sglGetLines(text);
		var n = lines.length;
		var tokens = null;
		var i = 0;

		if (n <= i) return;
		tokens = lines[i++].split(" ");
		if (tokens.length < 1) return;
		var magic = tokens[0];
		if (magic != "MTERR") return;

		if (n <= i) return;
		tokens = lines[i++].split(" ");
		if (tokens.length < 2) return;
		var nodesCount = parseInt(tokens[0]);
		if (nodesCount <= 0) return;
		var rootIndex = parseInt(tokens[1]);
		if ((rootIndex < 0) || (rootIndex >= nodesCount)) return;

		if (n <= i) return;
		tokens = lines[i++].split(" ");
		if (tokens.length < 1) return;
		var tileSize = parseInt(tokens[0]);
		if (tileSize <= 0) return;

		if (n <= i) return;
		tokens = lines[i++].split(" ");
		if (tokens.length < 3) return;
		var scale = [ 1.0, 1.0, 1.0 ];
		scale[0] = parseFloat(tokens[0]);
		scale[1] = parseFloat(tokens[1]);
		scale[2] = parseFloat(tokens[2]);

		if (n <= i) return;
		tokens = lines[i++].split(" ");
		if (tokens.length < 3) return;
		var offset = [ 0.0, 0.0, 0.0 ];
		offset[0] = parseFloat(tokens[0]);
		offset[1] = parseFloat(tokens[1]);
		offset[2] = parseFloat(tokens[2]);

		var nodes = new Array(nodesCount);
		for (var k=0; k<nodesCount; ++k)
		{
			var node = new MultiResNode();

			if (n <= i) return;
			tokens = lines[i++].split(" ");
			if (tokens.length < 14) return;

			node.id = parseInt(tokens[0]);
			if (node.id <= 0) return;

			node.parentIndex = parseInt(tokens[1]);
			if ((node.parentIndex < -1) || (node.parentIndex >= nodesCount)) return;

			for (var c=0; c<4; ++c) {
				node.childrenIndices[c] = parseInt(tokens[c+2]);
				if ((node.childrenIndices[c] < -1) || (node.childrenIndices[c] >= nodesCount)) return;
			}

			node.projectedSize = parseFloat(tokens[6]);

			node.zScale = parseFloat(tokens[7]);

			node.box.min[0] = parseFloat(tokens[ 8]);
			node.box.min[1] = parseFloat(tokens[ 9]);
			node.box.min[2] = parseFloat(tokens[10]);
			node.box.max[0] = parseFloat(tokens[11]);
			node.box.max[1] = parseFloat(tokens[12]);
			node.box.max[2] = parseFloat(tokens[13]);

			node.isLeaf = true;
			for (var j=0; j<4; ++j) {
				if (node.childrenIndices[j] >= 0) {
					node.isLeaf = false;
					break;
				}
			}

			nodes[k] = node;
		}

		this.nodesCount = nodesCount;
		this.rootIndex  = rootIndex;
		this.tileSize   = tileSize;
		this.scale      = scale;
		this.offset     = offset;
		this.nodes      = nodes;
	},

	destroy : function() {
		var n = null;
		for (var i in this.nodes) {
			n = this.nodes[i];
			if (n.req) {
				n.req.onLoad = null;
				n.req = null;
			}
			if (n.data) {
				destroyData(n.data);
				n.data = null;
			}
		}

		this.ready      = true;
		this.nodesCount = 0;
		this.rootIndex  = -1;
		this.nodes      = null
		this.url        = null;
		this.tileSize   = 0;
		this.scale      = [ 1.0, 1.0, 1.0 ];
	},

	load : function(url, onloadCallback)
	{
		this.destroy();
		if (!url) return false;

		this.ready = false;
		this.url   = url;

		var cb = null;
		if (onloadCallback) {
			var that = this;
			cb = function (text) {
				that._load(text);
				onloadCallback(that);
			};
		}

		var txt = sglLoadFile(this.url, cb);
		if (txt) {
			this._load(txt);
		}

		return true;
	},

	get isEmpty() {
		return (this.nodesCount <= 0);
	},

	get root() {
		var idx = this.rootIndex;
		if (idx < 0) return null;
		return this.nodes[idx];
	},

	parent : function(n) {
		var idx = n.parentIndex;
		if (idx < 0) return null;
		return this.nodes[idx];
	},

	child: function(n, i) {
		var idx = n.childrenIndices[i];
		if (idx < 0) return null;
		return this.nodes[idx];
	}
};

function _MultiResAssert(cond) {
	if (cond) return;
	alert("MultiResAssert FAILED : " + cond);
}

function _MultiResCompareNodes(a, b) {
	// higher timestamp first
	if (a.priority.timestamp != b.priority.timestamp) {
		return (b.priority.timestamp - a.priority.timestamp);
	}

	// higher error first
	if (a.priority.error != b.priority.error) {
		return (b.priority.error - a.priority.error);
	}

	// lower id first
	return (a.id - b.id);
};

function _generateGridVerticesPositions(size) {
	var positions = new Array((size+1) * (size+1) * 3);
	var k = 0;
	for (var z=0; z<(size+1); ++z) {
		var hz = sglClamp(z, 1, size-1) - 1;
		var fz = 0.5 - hz/(size-2);
		for (var x=0; x<(size+1); ++x) {
			var fy = ((z == 0) || (z == size) || (x == 0) || (x == size)) ? (0.0) : (1.0);
			var hx = sglClamp(x, 1, size-1) - 1;
			var fx = -0.5 + hx/(size-2);
			positions[k++] = fx;
			positions[k++] = fz;
			positions[k++] = fy;
		}
	}

	return positions;
}

function _generateGridTrianglesListIndicesRec(indices, x0, x1, z0, z1, size, cacheSize) {
	if ((x1 - x0 + 1) < cacheSize) {
		if ((2 * (x1 - x0) + 1) > cacheSize) {
			for (var x=x0; x<x1; ++x) {
				indices.push(x + 0);
				indices.push(x + 0);
				indices.push(x + 1);
			}
		}

		for (var z=z0; z<z1; ++z) {
			for (var x=x0; x<x1; ++x) {
				indices.push((size + 1) * (z + 0) + (x + 0));
				indices.push((size + 1) * (z + 1) + (x + 0));
				indices.push((size + 1) * (z + 0) + (x + 1));

				indices.push((size + 1) * (z + 0) + (x + 1));
				indices.push((size + 1) * (z + 1) + (x + 0));
				indices.push((size + 1) * (z + 1) + (x + 1));
			}
		}
	}
	else {
		var xm = x0 + cacheSize - 2;
		_generateGridTrianglesListIndicesRec(indices, x0, xm, z0, z1, size, cacheSize);
		_generateGridTrianglesListIndicesRec(indices, xm, x1, z0, z1, size, cacheSize);
	}
}

function _generateGridTrianglesListIndices(size, cacheSize) {
	var indices = new Array();
	_generateGridTrianglesListIndicesRec(indices, 0, size, 0, size, size, cacheSize);
	return indices;
}

function _generateGridEdgesListIndices(trianglesIndices) {
	var n = trianglesIndices.length;

	var edges   = { };
	var indices = [ ];

	var i0, i1, i2;
	var e01, e12, e20;

	for (var i=0; i<n; i+=3) {
		i0 = trianglesIndices[i+0];
		i1 = trianglesIndices[i+1];
		i2 = trianglesIndices[i+2];

		if ((i0 != i1) && (i1 != i2) && (i2 != i0)) {
			e01 = ((i0 < i1) ? (i0 + "_" + i1) : (i1 + "_" + i0));
			e12 = ((i1 < i2) ? (i1 + "_" + i2) : (i2 + "_" + i1));
			e20 = ((i2 < i0) ? (i2 + "_" + i0) : (i0 + "_" + i2));

			if (!edges[e01]) {
				indices.push(i0);
				indices.push(i1);
				edges[e01] = true;
			}
			if (!edges[e12]) {
				indices.push(i1);
				indices.push(i2);
				edges[e12] = true;
			}
			if (!edges[e20]) {
				indices.push(i2);
				indices.push(i0);
				edges[e20] = true;
			}
		}
	}

	return indices;
}

function _generateGridMeshJS(size, cacheSize) {
	var positions = _generateGridVerticesPositions(size+2);
	var triangles = _generateGridTrianglesListIndices(size+2, cacheSize);
	var edges     = _generateGridEdgesListIndices(triangles);

	var mesh = new SglMeshJS();
	mesh.addVertexAttribute("position", 3, positions);
	mesh.addIndexedPrimitives("triangles", SGL_TRIANGLES_LIST, triangles);
	mesh.addIndexedPrimitives("edges",     SGL_LINES_LIST,     edges);

	return mesh;
}

function _generateGridMeshGL(gl, size, cacheSize) {
	var mjs  = _generateGridMeshJS(size, cacheSize);
	var mglT = sglPackMeshJStoGL(gl, mjs, "triangles", 64000);
	var mglE = sglPackMeshJStoGL(gl, mjs, "edges",     64000);

	return { triangleMesh : mglT, edgeMesh : mglE };
}

function MultiResRenderer(gl, url, cacheSizeInBytes, onLoad) {
	var cb = null;
	if (onLoad) {
		var that = this;
		cb = function(tree) {
			that._setupTree();
			onLoad(tree);
		};
	}

	this.gl    = gl;
	this._tree = new MultiResTree(url + "/tree.txt", cb);
	this._url  = url;
	this._timestamp = 0;
	this._frustum = new SglFrustum();
	this._maxError = 1.0;
	this._cache = [ ];
	this._cacheSizeInBytes = cacheSizeInBytes;
	this._maxCacheSize = 1;
	this._maxOngoingRequests = 2;
	this._currentOngoingRequests = 0;
	this._toRequest = [ ];
	this._toRenderFullRes = [ ];
	this._toRenderHalfRes = [ ];
	this._readyItems = [ ];

	this.renderData  = true;
	this.renderBoxes = false;
	this.wireframe   = false;

	// box mesh
	/******************************************************/
	var boxPositions = new Float32Array([
		-0.5, -0.5,  0.5,
		 0.5, -0.5,  0.5,
		-0.5,  0.5,  0.5,
		 0.5,  0.5,  0.5,
		-0.5, -0.5, -0.5,
		 0.5, -0.5, -0.5,
		-0.5,  0.5, -0.5,
		 0.5,  0.5, -0.5
	 ]);

	var trianglesIndices = new Uint16Array([
		0, 1, 2,  2, 1, 3,  // front
		5, 4, 7,  7, 4, 6,  // back
		4, 0, 6,  6, 0, 2,  // left
		1, 5, 3,  3, 5, 7,  // right
		2, 3, 6,  6, 3, 7,  // top
		4, 5, 0,  0, 5, 1   // bottom
	]);

	var edgesIndices = new Uint16Array([
		0, 1, 1, 3, 3, 2, 2, 0,  // front
		5, 4, 4, 6, 6, 7, 7, 5,  // back
		0, 4, 1, 5, 3, 7, 2, 6   // middle
	]);

	var box = new SglMeshGL(gl);

	box.addVertexAttribute("position", 3, boxPositions);
	box.addIndexedPrimitives("triangles", gl.TRIANGLES, trianglesIndices);
	box.addIndexedPrimitives("edges",     gl.LINES,     edgesIndices);

	this._boxMesh = box;
	/******************************************************/

	/******************************************************/
	this._tileFullMesh = null;
	this._tileHalfMesh = null;
	/******************************************************/

	// box rendering
	/******************************************************/
	var vs = sglLoadFile("examples/tmrtextured.v.glsl");
	var fs = sglLoadFile("examples/tmrtextured.f.glsl");
	var prg = new SglProgram(gl, [vs], [fs]);
	log(prg.log);

	this._program  = prg;
	this._renderer = new SglMeshGLRenderer(this._program);
	/******************************************************/

	// box rendering
	/******************************************************/
	var bvs = sglLoadFile("examples/box.v.glsl");
	var bfs = sglLoadFile("examples/box.f.glsl");
	var bprg = new SglProgram(gl, [bvs], [bfs]);
	log(bprg.log);

	this._boxProgram  = bprg;
	this._boxRenderer = new SglMeshGLRenderer(this._boxProgram);
	/******************************************************/

	this._treeTransform = sglIdentityM4();
	this._normalizedTreeTransform = sglIdentityM4();

	if (this._tree.ready) {
		this._setupTree();
	}
}

MultiResRenderer.prototype = {
	_createData : function(hmapImg, colorImg) {
		var heightTexOpts = {
			minFilter : this.gl.NEAREST,
			magFilter : this.gl.NEAREST,
			generateMipmap : false
		};

		var colorTexOpts = {
			//minFilter : this.gl.LINEAR_MIPMAP_LINEAR,
			minFilter : this.gl.LINEAR,
			magFilter : this.gl.LINEAR,
			//generateMipmap : true
			generateMipmap : false
		};

		var data = {
			hmap    : new SglTexture2D(this.gl, hmapImg,  heightTexOpts),
			texture : new SglTexture2D(this.gl, colorImg, colorTexOpts)
		};
		return data;
	},

	_destroyData : function(data) {
		data.hmap.destroy();
		data.hmap = null;
		data.texture.destroy();
		data.texture = null;
	},

	_requestNode : function(n) {
		if (n.req) return;

		var that  = this;
		var url   = this._url + "/" + n.id;
		var hmapImg  = new SglImageRequest(url + "_h.png");
		var colorImg = new SglImageRequest(url + "_c.png");

		var watcher = new SglRequestWatcher([hmapImg, colorImg], function(w) {
			that._currentOngoingRequests--;
			that._readyItems.push(n);
		});
		n.req = watcher;

		this._currentOngoingRequests++;
		watcher.send();
	},

	_collectNodesRec : function(n, parentFullyVisible) {
		var result = {
			needed : false,
			usable : ((n.data) ? (true) : (false))
		};

		// test visibility only if parent is not completely inside frustum
		if (!parentFullyVisible) {
			var visStatus = this._frustum.boxVisibility(n.box.min, n.box.max);
			if (visStatus == SGL_OUTSIDE_FRUSTUM) return result;
			parentFullyVisible = (visStatus == SGL_INSIDE_FRUSTUM);
		}

		// node is visible and thus needed
		result.needed = true;

		// calculate error
		var prjSize = this._frustum.projectedSegmentSize(n.box.center, n.box.size[0]);
		var error   = prjSize / n.projectedSize;

		n.priority.timestamp = this._timestamp;
		n.priority.error     = error;

		// if node data is not available, request node and return
		if (!result.usable) {
			if (!n.req) {
				this._toRequest.push(n);
				//this._toRequest[n.id] = n;
			}
			return result;
		}

		// if node is below error, render it in full resolution and return
		if ((error < this._maxError) || n.isLeaf) {
			this._toRenderFullRes.push(n);
			//this._toRenderFullRes[n.id] = n;
			return result;
		}

		// recurse down to children
		var c = null;
		var neededCount = 0;
		var usableCount = 0;
		var cr = null;
		var missingChildren = [ ];

		for (var i=0; i<4; ++i) {
			c = this._tree.child(n, i);
			if (!c) continue;
			cr = this._collectNodesRec(c, parentFullyVisible);
			if (cr.usable) usableCount++;
			if (cr.needed) {
				neededCount++;
				if (!cr.usable) {
					missingChildren.push(i);
				}
			}
		}

		//_MultiResAssert(neededCount > 0);

		if (neededCount > 0) {
			if (usableCount <= 0) {
				// no needed child is available, render node in full resolution
				this._toRenderFullRes.push(n);
				//this._toRenderFullRes[n.id] = n;
			}
			else if (missingChildren.length > 0) {
				// some child is needed but not usable, render their sectors in half resolution (wrt this node)
				this._toRenderHalfRes.push({ n: n, children: missingChildren });
				//this._toRenderHalfRes[n.id] = { n: n, missingChildren: missingChildren };
			}
			// else all needed children can be rendered
		}

		// the node is needed and available
		return result;
	},

	_collectNodes : function() {
		this._toRequest = [ ];
		this._toRenderFullRes = [ ];
		this._toRenderHalfRes = [ ];

		var root = this._tree.root;
		if (!root) return;

		this._collectNodesRec(root, false);
	},

	_renderNodeFullRes : function(n) {
		var uniforms = {
			u_world_box_min : n.box.min,
			u_world_box_max : n.box.max,
			u_z_scale       : n.zScale
		};

		var samplers = {
			u_hmap    : n.data.hmap,
			u_texture : n.data.texture
		};

		this._renderer.setUniforms(uniforms);
		this._renderer.setSamplers(samplers);
		this._renderer.render();
	},

	_renderNodesFullRes : function(mvp) {
		var mesh = null;
		var primitives = null;

		if (this.wireframe) {
			mesh = this._tileFullMesh.edgeMesh;
			primitives = "edges";
		}
		else {
			mesh = this._tileFullMesh.triangleMesh;
			primitives = "triangles";
		}

		var uniforms = {
			u_model_view_projection_matrix : mvp,
			u_scale_bias                   : [ 1.0, 1.0, 0.0, 0.0 ],
			u_texcoord_scale_bias          : [ (this._tree.tileSize / (this._tree.tileSize + 2.0)), (1.0 / (this._tree.tileSize + 2.0)) ]
		};

		this._renderer.begin();
			this._renderer.setUniforms(uniforms);
			this._renderer.beginMesh(mesh);
				this._renderer.beginPrimitives(primitives);
				var n = null;
				for (var i in this._toRenderFullRes) {
					n = this._toRenderFullRes[i];
					this._renderNodeFullRes(n);
				}
				this._renderer.endPrimitives();
			this._renderer.endMesh();
		this._renderer.end();
	},

	_renderNodeHalfRes : function(n, children) {
		var uniforms = {
			u_world_box_min : null,
			u_world_box_max : null,
			u_scale_bias    : null,
			u_z_scale       : 1.0
		};

		var samplers = {
			u_hmap    : n.data.hmap,
			u_texture : n.data.texture
		};

		this._renderer.setSamplers(samplers);

		var scaleBias = [
			[ 0.5, 0.5, 0.0, 0.0 ],
			[ 0.5, 0.5, 0.5, 0.0 ],
			[ 0.5, 0.5, 0.0, 0.5 ],
			[ 0.5, 0.5, 0.5, 0.5 ]
		];

		var c = null;
		for (var i in children) {
			c = this._tree.child(n, children[i]);
			uniforms.u_world_box_min = [ c.box.min[0], c.box.min[1], n.box.min[2] ];
			uniforms.u_world_box_max = [ c.box.max[0], c.box.max[1], n.box.max[2] ];
			uniforms.u_scale_bias    = scaleBias[children[i]];
			uniforms.u_z_scale       = n.zScale;
			this._renderer.setUniforms(uniforms);
			this._renderer.render();
		}
	},

	_renderNodesHalfRes : function(mvp) {
		var mesh = null;
		var primitives = null;

		if (this.wireframe) {
			mesh = this._tileHalfMesh.edgeMesh;
			primitives = "edges";
		}
		else {
			mesh = this._tileHalfMesh.triangleMesh;
			primitives = "triangles";
		}

		var uniforms = {
			u_model_view_projection_matrix : mvp,
			u_texcoord_scale_bias          : [ (this._tree.tileSize / (this._tree.tileSize + 2.0)), (1.0 / (this._tree.tileSize + 2.0)) ]
		};

		this._renderer.begin();
			this._renderer.setUniforms(uniforms);
			this._renderer.beginMesh(mesh);
				this._renderer.beginPrimitives(primitives);
				var nc = null;
				for (var i in this._toRenderHalfRes) {
					nc = this._toRenderHalfRes[i];
					this._renderNodeHalfRes(nc.n, nc.children);
				}
				this._renderer.endPrimitives();
			this._renderer.endMesh();
		this._renderer.end();
	},

	_renderNodes : function(mvp) {
		this._renderNodesFullRes(mvp);
		this._renderNodesHalfRes(mvp);
	},

	_renderNodeFullResBox : function(n) {
		var sz = n.box.size;

		var uniforms = {
			u_world_box_min : sglAddV3S(n.box.min, sz[0] * 0.05),
			u_world_box_max : sglSubV3S(n.box.max, sz[0] * 0.05),
			u_color         : ((n.isLeaf) ? ([1.0, 0.0, 1.0]) : ([0.0, 1.0, 0.0]))
		};

		this._boxRenderer.setUniforms(uniforms);
		this._boxRenderer.render();
	},

	_renderNodesFullResBoxes : function(mvp) {
		var uniforms = {
			u_model_view_projection_matrix : mvp
		};

		this._boxRenderer.begin();
			this._boxRenderer.setUniforms(uniforms);
			this._boxRenderer.beginMesh(this._boxMesh);
				this._boxRenderer.beginPrimitives("edges");
				var n = null;
				for (var i in this._toRenderFullRes) {
					n = this._toRenderFullRes[i];
					this._renderNodeFullResBox(n);
				}
				this._boxRenderer.endPrimitives();
			this._boxRenderer.endMesh();
		this._boxRenderer.end();
	},

	_renderNodeHalfResBox : function(n, children) {
		var uniforms = {
			u_world_box_min : null,
			u_world_box_max : null,
			u_color         : null
		};

		var c  = null;
		var sx = null;
		for (var i in children) {
			c = this._tree.child(n, children[i]);
			sz = c.box.size;
			uniforms.u_world_box_min = sglAddV3S(c.box.min, sz[0] * 0.05);
			uniforms.u_world_box_max = sglSubV3S(c.box.max, sz[0] * 0.05);
			uniforms.u_color         = ((c.isLeaf) ? ([1.0, 1.0, 0.0]) : ([1.0, 1.0, 1.0]));
			this._boxRenderer.setUniforms(uniforms);
			this._boxRenderer.render();
		}
	},

	_renderNodesHalfResBoxes : function(mvp) {
		var uniforms = {
			u_model_view_projection_matrix : mvp
		};

		this._boxRenderer.begin();
			this._boxRenderer.setUniforms(uniforms);
			this._boxRenderer.beginMesh(this._boxMesh);
				this._boxRenderer.beginPrimitives("edges");
				var nc = null;
				for (var i in this._toRenderHalfRes) {
					nc = this._toRenderHalfRes[i];
					this._renderNodeHalfResBox(nc.n, nc.children);
				}
				this._boxRenderer.endPrimitives();
			this._boxRenderer.endMesh();
		this._boxRenderer.end();
	},

	_renderNodesBoxes : function(mvp) {
		this.gl.lineWidth(2.0);
		this.gl.depthFunc(this.gl.LEQUAL);
		this._renderNodesFullResBoxes(mvp);
		this._renderNodesHalfResBoxes(mvp);
		this.gl.depthFunc(this.gl.LESS);
		this.gl.lineWidth(1.0);
	},

	_doRender : function(projectionMatrix, modelViewMatrix, viewport) {
		var mv = sglMulM4(modelViewMatrix, this._treeTransform);

		this._timestamp++;
		this._frustum.setup(projectionMatrix, mv, viewport);

		this._collectNodes();
		var mvp = sglMulM4(projectionMatrix, mv);

		if (this.renderData) {
			this._renderNodes(mvp);
		}

		if (this.renderBoxes) {
			this._renderNodesBoxes(mvp);
		}

		//this.gl.finish();
	},

	_updateCache : function() {
		var combined = this._cache.concat(this._readyItems);
		this._readyItems = [ ];
		combined.sort(_MultiResCompareNodes);

		var cl = combined.length;
		var k  = (this._maxCacheSize < cl) ? (this._maxCacheSize) : (cl);

		this._cache = combined.splice(0, k);
		var outOfCache = combined;

		var n = null;
		var r = null;
		var failed = [ ];

		for (var i=0; i<k; ++i) {
			n = this._cache[i];
			r = n.req;
			n.req = null;

			if (!r) continue;
			if (!r.succeeded) {
				failed.push(i);
				continue;
			}

			n.data = this._createData(r.requests[0].image, r.requests[1].image);
		}

		var fc = failed.length;
		var reinsertedCount = 0;

		k = outOfCache.length;
		for (var i=0; i<k; ++i) {
			n = outOfCache[i];
			r = n.req;
			n.req = null;

			if (r) continue;

			if (reinsertedCount < fc) {
				this._cache[failed[reinsertedCount]] = n;
				reinsertedCount++;
			}
			else {
				this._destroyData(n.data);
				n.data = null;
			}
		}

		if (reinsertedCount > 0) {
			this._cache.sort(_MultiResCompareNodes);
		}
	},

	_requestNodes : function() {
		var cs = this._cache.length;
		var lastItem = (cs > 0) ? (this._cache[cs-1]) : (null);

		this._toRequest.sort(_MultiResCompareNodes);
		var requestableCount = this._maxOngoingRequests - this._currentOngoingRequests;

		var requested = 0;
		var k = this._toRequest.length;
		var n = null;
		var canRequest = false;

		for (var i=0; ((i<k) && (requested<requestableCount)); ++i) {
			n = this._toRequest[i];
			if (n.req) continue; // ongoing;

			canRequest = true;
			if ((lastItem != null) && (cs >= this._maxCacheSize)) {
				canRequest = (_MultiResCompareNodes(n, lastItem) < 0);
			}
			if (canRequest) {
				this._requestNode(n);
				requested++
			}
		}
	},

	_calculateNormalizedTreeTransform : function() {
		var root = this._tree.root;
		if (!root) return sglIdentityM4();

		var rbox = new SglBox3();
		rbox.min = sglAddV3(sglMulV3(root.box.min, this._tree.scale), this._tree.offset);
		rbox.max = sglAddV3(sglMulV3(root.box.max, this._tree.scale), this._tree.offset);

		this._treeTransform = sglMulM4(sglTranslationM4V(this._tree.offset), sglScalingM4V(this._tree.scale));

		var bc = rbox.center;
		var bs = rbox.size;

		var maxDim = bs[0];
		if (maxDim < bs[1]) maxDim = bs[1];
		if (maxDim < bs[2]) maxDim = bs[2];

		var scale = (maxDim > 0.0) ? (1.0 / maxDim) : (1.0);

		var r  = sglRotationAngleAxisM4C(sglDegToRad(-90.0), 1.0, 0.0, 0.0);
		var t1 = sglTranslationM4C(0.0, 0.0, bs[2] / 2.0 * scale);
		var s  = sglScalingM4C(scale, scale, scale);
		var t2 = sglTranslationM4C(-bc[0], -bc[1], -bc[2]);

		this._normalizedTreeTransform = sglMulM4(r, sglMulM4(t1, sglMulM4(s, t2)));
	},

	_setupTree : function() {
		var root = this._tree.root;
		if (!root) return;

		var gl = this.gl;

		var hsize = ((this._tree.tileSize + 1) * (this._tree.tileSize + 1)) * 4;
		var csize = ((this._tree.tileSize + 2) * (this._tree.tileSize + 2)) * 4;

		this._maxCacheSize = sglFloor(this._cacheSizeInBytes / (hsize + csize));
		if (this._maxCacheSize <= 0) this._maxCacheSize = 1;

		this._tileFullMesh = _generateGridMeshGL(gl, this._tree.tileSize,     16);
		this._tileHalfMesh = _generateGridMeshGL(gl, this._tree.tileSize / 2, 16);

		this._calculateNormalizedTreeTransform();
	},

	render : function(projectionMatrix, modelViewMatrix, viewport) {
		if (!this._tree.root) return;

		this._doRender(projectionMatrix, modelViewMatrix, viewport);
		this._updateCache();
		this._requestNodes();
	},

	get normalizedTreeTransform() {
		return this._normalizedTreeTransform;
	}
};
