// utilities
/**********************************************************/
function blockmap_get_lines(text)
{
	var all_lines = text.split("\n");
	var n = all_lines.length;
	var lines = [ ];
	var line = null;
	for (var i=0; i<n; ++i)
	{
		line = all_lines[i].replace(/[ \t]+/g, " ").replace(/\s\s*$/, "");
		if (line.length > 0)
		{
			lines.push(line);
		}
	}
	return lines;
}
/**********************************************************/





// node
/**********************************************************/
function blockmap_node()
{
	this.id               = 0;
	this.parend_index     = -1;
	this.children_indices = [ -1, -1, -1, -1 ];
	this.strips           = 0;
	this.box              = new SglBox3();
	this.tex              = null;
}

blockmap_node.prototype =
{
	leaf : function()
	{
		for (var i=0; i<4; ++i)
		{
			if (this.children_indices[i] >= 0) return false;
		}
		return true;
	}
};
/**********************************************************/





// tree
/**********************************************************/
function blockmap_tree(url, async, onload_callback)
{
	this.ready       = true;
	this.nodes_count = 0;
	this.top_size    = 0;
	this.walls_width = 0;
	this.root_index  = -1;
	this.nodes       = null;
	this.url         = null;

	if (url)
	{
		this.load(url, async, onload_callback);
	}
}

blockmap_tree.prototype =
{
	destroy : function()
	{
		this.ready       = true;
		this.nodes_count = 0;
		this.top_size    = 0;
		this.walls_width = 0;
		this.root_index  = -1;
		this.nodes       = null;
		this.url         = null;
	},

	load : function(url, async, onload_callback)
	{
		this.destroy();
		if (!url) return false;

		this.ready = false;
		this.url   = url;
		var that = this;

		var do_async = (async) ? (async) : (true);
		var req = new XMLHttpRequest();

		if (do_async)
		{
			req.onreadystatechange = function ()
			{
				that.process_load(req);
				if (onload_callback)
				{
					onload_callback(that);
				}
			};
		}

		req.open("GET", url, do_async);
		req.send(null);

		if (!do_async)
		{
			this.do_load(req.responseText);
			if (onload_callback)
			{
				onload_callback(that);
			}
		}

		return true;
	},

	process_load : function(req)
	{
		if (req.readyState == 4)
		{
			this.do_load(req.responseText);
		}
	},

	do_load : function(text)
	{
		this.ready = true;

		var lines = blockmap_get_lines(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 nodes_count = parseInt(tokens[0]);
		if (nodes_count <= 0) return;

		if (n <= i) return;
		tokens = lines[i++].split(" ");
		if (tokens.length < 2) return;
		var top_size    = parseInt(tokens[0]);
		if (top_size <= 0) return;
		var walls_width = parseInt(tokens[1]);
		if (walls_width <= 0) return;

		if (n <= i) return;
		tokens = lines[i++].split(" ");
		if (tokens.length < 1) return;
		var root_index = parseInt(tokens[0]);
		if ((root_index < 0) || (root_index >= nodes_count)) return;

		var nodes = new Array(nodes_count);
		for (var k=0; k<nodes_count; ++k)
		{
			var node = new blockmap_node();

			if (n <= i) return;
			tokens = lines[i++].split(" ");
			if (tokens.length < 1) return;
			node.id = parseInt(tokens[0]);
			if (node.id <= 0) return;

			if (n <= i) return;
			tokens = lines[i++].split(" ");
			if (tokens.length < 1) return;
			node.parent_index = parseInt(tokens[0]);
			if ((node.parent_index < -1) || (node.parent_index >= nodes_count)) return;

			if (n <= i) return;
			tokens = lines[i++].split(" ");
			if (tokens.length < 4) return;
			for (var c=0; c<4; ++c)
			{
				node.children_indices[c] = parseInt(tokens[c]);
				if ((node.children_indices[c] < -1) || (node.children_indices[c] >= nodes_count)) return;
			}

			if (n <= i) return;
			tokens = lines[i++].split(" ");
			if (tokens.length < 1) return;
			node.strips = parseInt(tokens[0]);
			if (node.strips < 0) return;

			if (n <= i) return;
			tokens = lines[i++].split(" ");
			if (tokens.length < 6) return;
			node.box.min[0] = parseFloat(tokens[0]);
			node.box.min[1] = parseFloat(tokens[1]);
			node.box.min[2] = parseFloat(tokens[2]);
			node.box.max[0] = parseFloat(tokens[3]);
			node.box.max[1] = parseFloat(tokens[4]);
			node.box.max[2] = parseFloat(tokens[5]);

			nodes[k] = node;
		}

		this.nodes_count = nodes_count;
		this.top_size    = top_size;
		this.walls_width = walls_width;
		this.root_index  = root_index;
		this.nodes       = nodes;
	},

	empty : function()
	{
		return (this.nodes_count <= 0);
	},

	root : function()
	{
		var idx = this.root_index;
		if (idx < 0) return null;
		return this.nodes[idx];
	},

	parent : function(n)
	{
		var idx = n.parent_index;
		if (idx < 0) return null;
		return this.nodes[idx];
	},

	child: function(n, i)
	{
		var idx = n.children_indices[i];
		if (idx < 0) return null;
		return this.nodes[idx];
	}
};
/**********************************************************/





// frustum plane
/**********************************************************/
function blockmap_frustum_plane()
{
	this.normal      = [ 0.0, 0.0, 1.0 ];
	this.offset      = 0.0;
	this.test_vertex = [ 0, 0, 0 ];
}

blockmap_frustum_plane.prototype =
{
	setup : function(nx, ny, nz, off)
	{
		var s = 1.0 / Math.sqrt(nx*nx + ny*ny + nz*nz);

		this.normal[0] = nx * s;
		this.normal[1] = ny * s;
		this.normal[2] = nz * s;

		this.offset = off * s;

		this.test_vertex[0] = (this.normal[0] >= 0.0) ? (1) : (0);
		this.test_vertex[1] = (this.normal[1] >= 0.0) ? (1) : (0);
		this.test_vertex[2] = (this.normal[2] >= 0.0) ? (1) : (0);
	}
};
/**********************************************************/





// renderer
/**********************************************************/
function blockmap_renderer(gl, url, async, onload_callback)
{
	// webgl object
	/******************************************************/
	this.gl = gl;
	/******************************************************/



	// box mesh
	/******************************************************/
	var positions = 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 triangles_indices = 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 edges_indices = 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, positions);

	box.addIndexedPrimitives("triangles", gl.TRIANGLES, triangles_indices);
	box.addIndexedPrimitives("edges",     gl.LINES,     edges_indices);

	this.box = box;
	/******************************************************/



	// invalid texture
	/******************************************************/
	this.invalid_texture = null;
	/******************************************************/



	// box rendering
	/******************************************************/
	var box_vsrc  = sglLoadFile("examples/box.v.glsl");
	var box_fsrc  = sglLoadFile("examples/box.f.glsl");
	var box_prg   = new SglProgram(gl, [box_vsrc], [box_fsrc]);
	log(box_prg.log);

	this.box_program  = box_prg;
	this.box_renderer = new SglMeshGLRenderer(this.box_program);
	/******************************************************/



	// blockmap rendering
	/******************************************************/
	var bmap_vsrc  = sglLoadFile("examples/blockmap.v.glsl");
	var bmap_fsrc  = sglLoadFile("examples/blockmap.f.glsl");
	var bmap_prg   = new SglProgram(gl, [bmap_vsrc], [bmap_fsrc]);
	log(bmap_prg.log);

	this.bmap_program  = bmap_prg;
	this.bmap_renderer = new SglMeshGLRenderer(this.bmap_program);
	/******************************************************/



	// internal state
	/******************************************************/
	this.tree             = new blockmap_tree();
	this.root             = null;
	this.nodes_to_render  = [ ];
	this.viewport         = [ 0, 0, 1, 1 ];
	this.viewer_position  = [ 0.0, 0.0, 0.0 ];
	this.mvp_matrix       = sglIdentityM4();
	this.normal_matrix    = sglIdentityM4();
	this.billboard_matrix = sglIdentityM4();
	this.frustum_planes   = new Array(6);
	for (var i=0; i<6; ++i)
	{
		this.frustum_planes[i] = new blockmap_frustum_plane();
	}
	/******************************************************/



	// public state
	/******************************************************/
	this.url               = null;
	this.error             = 1.0;
	this.boxes_enabled     = true;
	this.blockmaps_enabled = true;
	/******************************************************/



	// load
	/******************************************************/
	this.load(url, async, onload_callback);
	/******************************************************/
}

blockmap_renderer.prototype =
{
	destroy : function()
	{
		for (attr in this)
		{
			if (this[attr].destroy)
			{
				this[attr].destroy();
			}
			this[attr] = null;
		}
	},

	reset : function()
	{
		this.tree             = new blockmap_tree();
		this.root             = null;
		this.nodes_to_render  = [ ];
		this.viewport         = [ 0, 0, 1, 1 ];
		this.viewer_position  = [ 0.0, 0.0, 0.0 ];
		this.mvp_matrix       = sglIdentityM4();
		this.normal_matrix    = sglIdentityM4();
		this.billboard_matrix = sglIdentityM4();
		this.frustum_planes   = new Array(6);
		for (var i=0; i<6; ++i)
		{
			this.frustum_planes[i] = new blockmap_frustum_plane();
		}

		this.error             = 1.0;
		this.boxes_enabled     = true;
		this.blockmaps_enabled = true;
	},

	load : function(url, async, onload_callback)
	{
		this.reset();

		this.url = url;

		var that = this;
		var on_tree_load = function()
		{
			that.tree_loaded();
			if (onload_callback)
			{
				onload_callback(that);
			}
		}

		var tree_url = url + "/tree.txt";
		this.tree.load(tree_url, async, on_tree_load);
	},

	empty : function()
	{
		return this.tree.empty();
	},

	normalized_box_transform : function()
	{
		var root = this.tree.root();
		if (!root) return sglIdentityM4();

		var bc = root.box.center;
		var bs = root.box.size;

		var max_dim = bs[0];
		if (max_dim < bs[1]) max_dim = bs[1];
		if (max_dim < bs[2]) max_dim = bs[2];

		var scale = (max_dim > 0.0) ? (1.0 / max_dim) : (1.0);
		scale *= 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]);

		var xform = sglMulM4(r, sglMulM4(t1, sglMulM4(s, t2)));
		return xform;
	},

	render : function(xform_stack, viewport)
	{
		if (!this.boxes_enabled && !this.blockmaps_enabled) return;

		this.root = this.tree.root();
		if (!this.root) return;

		this.begin_render(xform_stack, viewport);
			this.do_render();
		this.end_render();

		this.root = null;
	},

	tree_loaded : function()
	{
		var gl = this.gl;

		// invalid texture
		/******************************************************/
		var w = this.tree.top_size * 2 + this.tree.walls_width;
		var h = this.tree.top_size;

		var size   = w * h * 4;
		var texels = new Array(size);
		for (var i=0; i<size; ++i) {
			texels[i] = 0;
		}

		this.invalid_texture = new SglTexture2D(gl, gl.RGBA, w, h, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(texels), { minFilter : gl.NEAREST, magFilter : gl.NEAREST});
		/******************************************************/

		this.load_nodes_textures();
	},

	load_nodes_textures : function(n)
	{
		var root = this.tree.root();
		if (root)
		{
			this.load_node_texture_rec(root);
		}
	},

	request_node_texture : function(n)
	{
		var gl  = this.gl;
		var url = this.url + "/" + n.id + ".png";
		n.tex = new SglTexture2D(gl, url, { minFilter : gl.NEAREST, magFilter : gl.NEAREST});
	},

	load_node_texture_rec : function(n)
	{
		this.request_node_texture(n);
		var c = null;
		for (var i=0; i<4; ++i)
		{
			c = this.tree.child(n, i);
			if (!c) continue;
			this.load_node_texture_rec(c);
		}
	},

	begin_render : function(xform, viewport)
	{
		var pr  = xform.projectionMatrix;
		var mv  = xform.modelViewMatrix;

		var rot =
		[
			sglGetColM4(mv, 0),
			sglGetColM4(mv, 1),
			sglGetColM4(mv, 2)
		];

		var sc =
		[
			sglLengthV4(rot[0]),
			sglLengthV4(rot[1]),
			sglLengthV4(rot[2])
		];

		var sc_rcp =
		[
			1.0 / sc[0],
			1.0 / sc[1],
			1.0 / sc[2]
		];

		var s_mat     = sglScalingM4C(sc[0], sc[1], sc[2]);
		var s_mat_inv = sglScalingM4C(sc_rcp[0], sc_rcp[1], sc_rcp[2]);

		rot[0] = sglMulV4S(rot[0], sc_rcp[0]);
		rot[1] = sglMulV4S(rot[1], sc_rcp[1]);
		rot[2] = sglMulV4S(rot[2], sc_rcp[2]);

		var r_mat_inv = sglIdentityM4();
		sglSetRowM4V(r_mat_inv, 0, rot[0]);
		sglSetRowM4V(r_mat_inv, 1, rot[1]);
		sglSetRowM4V(r_mat_inv, 2, rot[2]);

		this.viewport          = viewport.slice(0, 4);
		this.viewer_position   = xform.modelSpaceViewerPosition;
		this.mvp_matrix        = xform.modelViewProjectionMatrix;
		this.normal_matrix     = xform.viewSpaceNormalMatrix;
		this.billboard_matrix  = sglMulM4(s_mat_inv, sglMulM4(r_mat_inv, s_mat));

		var m = this.mvp_matrix;
		var q = [ m[3], m[7], m[11] ];

		this.frustum_planes[0].setup(q[ 0] - m[ 0], q[ 1] - m[ 4], q[ 2] - m[ 8], m[15] - m[12]);
		this.frustum_planes[1].setup(q[ 0] + m[ 0], q[ 1] + m[ 4], q[ 2] + m[ 8], m[15] + m[12]);
		this.frustum_planes[2].setup(q[ 0] - m[ 1], q[ 1] - m[ 5], q[ 2] - m[ 9], m[15] - m[13]);
		this.frustum_planes[3].setup(q[ 0] + m[ 1], q[ 1] + m[ 5], q[ 2] + m[ 9], m[15] + m[13]);
		this.frustum_planes[4].setup(q[ 0] - m[ 2], q[ 1] - m[ 6], q[ 2] - m[10], m[15] - m[14]);
		this.frustum_planes[5].setup(q[ 0] + m[ 2], q[ 1] + m[ 6], q[ 2] + m[10], m[15] + m[14]);
	},

	end_render : function()
	{
		;
	},

	do_render : function()
	{
		if (!this.collect_nodes_to_render())
		{
			return;
		}

		if (this.boxes_enabled)
		{
			this.render_nodes_boxes();
		}

		if (this.blockmaps_enabled)
		{
			this.render_nodes_blockmaps();
		}
	},

	collect_nodes_to_render : function()
	{
		this.nodes_to_render = [ ];
		if (this.root)
		{
			this.collect_nodes_to_render_rec(this.root, false);
		}
		return (this.nodes_to_render.length > 0);
	},

	collect_nodes_to_render_rec : function(n, parent_fully_visible)
	{
		var fully_visible = parent_fully_visible;
		if (!parent_fully_visible)
		{
			var vis = this.test_node_visibility(n);
			if (!vis) return;
			fully_visible = (vis > 0);
		}

		var err = this.calculate_node_error(n);
		if ((err <= 1.0) || (n.leaf()))
		{
			this.nodes_to_render.push(n);
			return;
		}

		var c = null;
		for (var i=0; i<4; ++i)
		{
			c = this.tree.child(n, i);
			if (!c) continue;
			this.collect_nodes_to_render_rec(c, fully_visible);
		}
	},

	test_node_visibility : function(n)
	{
		var fully_visible = 1;

		var bminmax = [ n.box.min, n.box.max ];

		var fp = null;
		for (var i=0; i<6; ++i)
		{
			fp = this.frustum_planes[i];
			if
			(
				((fp.normal[0] * bminmax[fp.test_vertex[0]][0]) +
				 (fp.normal[1] * bminmax[fp.test_vertex[1]][1]) +
				 (fp.normal[2] * bminmax[fp.test_vertex[2]][2]) +
				 (fp.offset)) < 0.0
			)
			{
				return 0;
			}

			if
			(
				((fp.normal[0] * bminmax[1 - fp.test_vertex[0]][0]) +
				 (fp.normal[1] * bminmax[1 - fp.test_vertex[1]][1]) +
				 (fp.normal[2] * bminmax[1 - fp.test_vertex[2]][2]) +
				 (fp.offset)) < 0.0
			)
			{
				fully_visible = -1;
			}
		}

		return fully_visible;
	},

	calculate_node_error : function(n)
	{
		var bc = n.box.center;
		var bs = n.box.size;

		var hs = bs[0] * 0.5;

		var p0 = [ bc[0] - hs, bc[1], bc[2], 0.0 ];
		var p1 = [ bc[0] - hs, bc[1], bc[2], 1.0 ];
		var p2 = [ bc[0] + hs, bc[1], bc[2], 1.0 ];

		p1 = sglProjectV4(sglMulM4V4(this.billboard_matrix, p1));
		p1 = sglAddV4(p1, p0);
		p1 = sglProjectV4(sglMulM4V4(this.mvp_matrix, p1));

		p2 = sglProjectV4(sglMulM4V4(this.billboard_matrix, p2));
		p2 = sglAddV4(p2, p0);
		p2 = sglProjectV4(sglMulM4V4(this.mvp_matrix, p2));

		p1[0] = this.viewport[2] * 0.5 * p1[0] + this.viewport[0] + this.viewport[2] * 0.5;
		//p1[1] = this.viewport[3] * 0.5 * p1[1] + this.viewport[1] + this.viewport[3] * 0.5;

		p2[0] = this.viewport[2] * 0.5 * p2[0] + this.viewport[0] + this.viewport[2] * 0.5;
		//p2[1] = this.viewport[3] * 0.5 * p2[1] + this.viewport[1] + this.viewport[3] * 0.5;

		var size  = sglAbs(p2[0] - p1[0]);
		var error = (size / this.tree.top_size) / this.error;

		return error;
	},

	render_nodes_boxes : function()
	{
		this.begin_nodes_boxes();
		var n = this.nodes_to_render.length;
		for (var i=0; i<n; ++i)
		{
			this.render_node_box(this.nodes_to_render[i]);
		}
		this.end_nodes_boxes();
	},

	begin_nodes_boxes : function()
	{
		this.gl.lineWidth(3.0);

		this.box_renderer.begin();
		this.box_renderer.setUniforms
		({
			u_model_view_projection_matrix : this.mvp_matrix,
			u_color                        : [ 0.6, 0.2, 0.2 ]
		})
		this.box_renderer.beginMesh(this.box);
		this.box_renderer.beginPrimitives("edges");
	},

	end_nodes_boxes : function()
	{
		this.box_renderer.endPrimitives();
		this.box_renderer.endMesh();
		this.box_renderer.end();

		this.gl.lineWidth(1.0);
	},

	render_node_box : function(n)
	{
		this.box_renderer.setUniforms
		({
			u_world_box_min : n.box.min,
			u_world_box_max : n.box.max
		});
		this.box_renderer.render();
	},

	render_nodes_blockmaps : function()
	{
		this.begin_nodes_blockmaps();
		var n = this.nodes_to_render.length;
		for (var i=0; i<n; ++i)
		{
			this.render_node_blockmap(this.nodes_to_render[i]);
		}
		this.end_nodes_blockmaps();
	},

	begin_nodes_blockmaps : function()
	{
		var img_size = new Array(4);
		img_size[0] = this.tree.top_size * 2.0 + this.tree.walls_width;
		img_size[1] = this.tree.top_size;
		img_size[2] = 1.0 / img_size[0];
		img_size[3] = 1.0 / img_size[1];

		this.bmap_renderer.begin();
		this.bmap_renderer.setUniforms
		({
			u_model_view_projection_matrix : this.mvp_matrix,
			u_normal_matrix                : this.normal_matrix,
			u_world_viewer_position        : this.viewer_position,
			u_blockmap_image_size          : img_size
		})
		this.bmap_renderer.beginMesh(this.box);
		this.bmap_renderer.beginPrimitives("triangles");
	},

	end_nodes_blockmaps : function()
	{
		this.gl.bindTexture(this.gl.TEXTURE_2D, null);

		this.bmap_renderer.endPrimitives();
		this.bmap_renderer.endMesh();
		this.bmap_renderer.end();
	},

	render_node_blockmap : function(n)
	{
		var bm_size = new Array(4);
		bm_size[0] = this.tree.top_size / (this.tree.top_size * 2.0 + this.tree.walls_width);
		bm_size[1] = this.tree.top_size / this.tree.top_size;
		bm_size[2] = this.tree.top_size / n.strips;
		bm_size[3] = 1.0 / bm_size[2];

		var sector = [ 0.0, 0.0, 1.0 ];

		this.bmap_renderer.setUniforms
		({
			u_world_box_min          : n.box.min,
			u_world_box_max          : n.box.max,
			u_blockmap_size          : bm_size,
			u_sector                 : sector
		});

		var tex =this.invalid_texture;
		if (n.tex) {
			if (n.tex.isValid) {
				tex = n.tex;
			}
		}

		this.bmap_renderer.setSamplers
		({
			s_blockmap_sampler : tex
		});

		this.bmap_renderer.render();
	}
};
/**********************************************************/

