/* <copyright>
This file contains proprietary software owned by Motorola Mobility, Inc.<br/>
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/>
(c) Copyright 2011 Motorola Mobility, Inc.  All Rights Reserved.
</copyright> */


var GeomObj = require("js/lib/geom/geom-obj").GeomObj;
var Line = require("js/lib/geom/line").Line;
var Rectangle = require("js/lib/geom/rectangle").Rectangle;
var Circle = require("js/lib/geom/circle").Circle;
var MaterialsModel = require("js/models/materials-model").MaterialsModel;

var worldCounter = 0;

///////////////////////////////////////////////////////////////////////
// Class GLWorld
//      Manages display in a canvas
///////////////////////////////////////////////////////////////////////
var World = function GLWorld( canvas, use3D, preserveDrawingBuffer ) {
    ///////////////////////////////////////////////////////////////////////
    // Instance variables
    ///////////////////////////////////////////////////////////////////////

	// flag to do the drawing with WebGL
    this._useWebGL = false;
    if(use3D) {
        this._useWebGL = use3D;
    }

    this._canvas = canvas;
	if (this._useWebGL) {
        if(preserveDrawingBuffer) {
            this._glContext = canvas.getContext("experimental-webgl", {preserveDrawingBuffer: true});
        } else {
		    this._glContext = canvas.getContext("experimental-webgl");
        }
    } else {
		this._2DContext = canvas.getContext( "2d" );
    }
    
	this._viewportWidth = canvas.width;
    this._viewportHeight = canvas.height;

	// view parameters
	this._fov = 45.0;
	this._zNear = 0.1;
	this._zFar = 100.0;
	this._viewDist = 5.0;

	// default light parameters
	this._ambientLightColor  = [0.1, 0.1, 0.1,  1.0];
	this._diffuseLightColor  = [0.1, 0.1, 0.1,  1.0];
	this._specularLightColor = [0.6, 0.6, 0.6,  1.0];
	this._pointLightLoc = [0.0, 0.0, 0.05];

	// default material properties.  Material properties should be overridden
	// by the materials used by the objects
	this._materialShininess = 20.0;

    this._geomRoot = undefined;

	this._cameraMat = Matrix.I(4);
	this._cameraMat[14] = 5.0;
	this._cameraMatInv = Matrix.I(4);
	this._cameraMatInv[14] = -5.0;

    this._camera = null;
	// keep a flag indicating whether a render has been completed.
	// this allows us to turn off automatic updating if there are
	// no animated materials
	this._firstRender = true;

	this._worldCount = worldCounter;
	worldCounter++;

	// keep a counter for generating node names
	this._nodeCounter = 0;

    ///////////////////////////////////////////////////////////////////////
    // Property accessors
    ///////////////////////////////////////////////////////////////////////
    this.getGLContext		= function()		{  return this._glContext;			};
    this.setGLContext		= function(gl)		{  this._glContext = gl;			};

    this.get2DContext		= function()		{  return this._2DContext;			};
    this.set2DContext		= function(c)		{  this._2DContext = c;				};

    this.getCanvas			= function()		{  return this._canvas;				};
    this.setCanvas			= function(c)		{  this._canvas = c;				};

    this.getShaderProgram	= function()		{  return this._shaderProgram;		};

	this.getViewportWidth	= function()		{  return this._viewportWidth;		};
	this.getViewportHeight  = function()		{  return this._viewportHeight;		};

	this.getAspect			= function()		{  return this._viewportWidth/this._viewportHeight;  };

	this.getGeomRoot			= function()		{  return this._geomRoot;			};
	this.getZNear				= function()		{  return this._zNear;				};
	this.getZFar				= function()		{  return this._zFar;				};
	this.getFOV					= function()		{  return this._fov;				};

	this.getCamera				= function()		{  return this._camera;				};

	this.getCameraMat			= function()		{  return this._cameraMat.slice(0);	};
	this.setCameraMat			= function(c)		{  this._cameraMat = c.slice(0);  this._cameraMatInv = glmat4.inverse(c, []);  };

	this.getCameraMatInverse  = function()		{  return this._cameraMatInv.slice(0); };

	this.getViewDistance		= function()		{  return this._viewDist;			};

	this.getRootNode			= function()		{  return this._rootNode;			};
	this.setRootNode			= function(r)		{  this._rootNode = r;				};

	this.isWebGL				= function()		{  return this._useWebGL;			};

	this.getRenderer			= function()		{  return this.renderer;			};

    // Flag to play/pause animation at authortime
    this._previewAnimation = true;
  ////////////////////////////////////////////////////////////////////////////////////
  // RDGE
  // local variables
    this.myScene = null;
    this.elapsed = 0;
    this.light = null;
    this.light2 = null;
    this.fillShader = null;
    this.strokeShader = null;
    this.renderer = null;

	// keep an array of texture maps that need to be loaded
	this._texMapsToLoad = [];
	this._allMapsLoaded = true;

	// this is the node to which objects get hung
	this._rootNode = null;

	// set up the camera matrix
	var camMat = Matrix.I(4);
	camMat[14] = this.getViewDistance();
	this.setCameraMat( camMat );
    
    // post-load processing of the scene
    this.init = function() {
		var ctx1 = RDGE.globals.engine.ctxMan.handleToObject(this._canvas.rdgeCtxHandle),
			ctx2 = RDGE.globals.engine.getContext();
		if (ctx1 != ctx2)  console.log( "***** different contexts *****" );
		this.renderer = ctx1.renderer;
		this.renderer._world = this;
      
		// create a camera, set its perspective, and then point it at the origin
		var cam = new RDGE.camera();
		this._camera = cam;
		cam.setPerspective(this.getFOV(), this.getAspect(), this.getZNear(), this.getZFar());
		cam.setLookAt([0, 0, this.getViewDistance()], [0, 0, 0], RDGE.vec3.up());
        
		// make this camera the active camera
		this.renderer.cameraManager().setActiveCamera(cam);

		// change clear color
		//this.renderer.setClearFlags(RDGE.globals.engine.getContext().DEPTH_BUFFER_BIT);
		this.renderer.setClearColor([0.0, 0.0, 0.0, 0.0]);
		//this.renderer.NinjaWorld = this;
        
		// create an empty scene graph
		this.myScene = new RDGE.SceneGraph();
        
		// create some lights
		// light 1
		this.light = RDGE.createLightNode("myLight");
		this.light.setPosition([0,0,1.2]);
		this.light.setDiffuseColor([0.75,0.9,1.0,1.0]);
        
		// light 2
		this.light2 = RDGE.createLightNode("myLight2");
		this.light2.setPosition([-0.5,0,1.2]);
		this.light2.setDiffuseColor([1.0,0.9,0.75,1.0]);
        
		// create a light transform
		var lightTr = RDGE.createTransformNode("lightTr");
        
		// create and attach a material - materials hold the light data
		lightTr.attachMaterial(RDGE.createMaterialNode("lights"));
        
		// enable light channels 1, 2 - channel 0 is used by the default shader
		lightTr.materialNode.enableLightChannel(1, this.light);
		lightTr.materialNode.enableLightChannel(2, this.light2);
     
		// all added objects are parented to the light node
		this._rootNode = lightTr;
        
		// add the light node to the scene
		this.myScene.addNode(lightTr);
        
		// Add the scene to the engine - necessary if you want the engine to draw for you
		//RDGE.globals.engine.AddScene("myScene" + this._canvas.id, this.myScene);
		var name = this._canvas.getAttribute( "data-RDGE-id" ); 
		RDGE.globals.engine.AddScene("myScene" + name, this.myScene);
	};
    
	// main code for handling user interaction and updating the scene   
	this.update = function(dt) {
		if (!dt)  dt = 0.2;
        
		dt = 0.01;	// use our own internal throttle
		this.elapsed += dt;
        
		if (this._useWebGL) {
			// changed the global position uniform of light 0, another way to change behavior of a light
		    RDGE.rdgeGlobalParameters.u_light0Pos.set([5 * Math.cos(this.elapsed), 5 * Math.sin(this.elapsed), 20]);
        
			// orbit the light nodes around the boxes
			this.light.setPosition([1.2*Math.cos(this.elapsed*2.0), 1.2*Math.sin(this.elapsed*2.0), 1.2*Math.cos(this.elapsed*2.0)]);
			this.light2.setPosition([-1.2*Math.cos(this.elapsed*2.0), 1.2*Math.sin(this.elapsed*2.0), -1.2*Math.cos(this.elapsed)]);
		}
        
		this.updateMaterials( this.getGeomRoot(), this.elapsed );

		// now update all the nodes in the scene
		if (this._useWebGL)
			this.myScene.update(dt);
    };

    // defining the draw function to control how the scene is rendered      
	this.draw = function() {
		if (this._useWebGL) {
			RDGE.globals.engine.setContext( this._canvas.rdgeid );
			var ctx = RDGE.globals.engine.getContext();
			var renderer = ctx.renderer;
			if (renderer.unloadedTextureCount <= 0) {
				renderer.disableCulling();
				renderer._clear();
				this.myScene.render();

				if (this._firstRender) {
					if (this._canvas.task) {
						this._firstRender = false;

						if (!this.hasAnimatedMaterials() || !this._previewAnimation) {
							this._canvas.task.stop();
							//this._renderCount = 10;
						}
					}
				} else if (this._renderCount >= 0) {
					if (this._canvas.task) {
						this._renderCount--;
						if (this._renderCount <= 0) {
							this._canvas.task.stop();
                        }
					}
				}
			}
		} else {
			this.render();
		}
    };
	
    this.onRunState = function() {
//		console.log( "GLWorld.onRunState" );
		this.restartRenderLoop();
	};
	
    this.onLoadState = function() {
//		console.log( "GLWorld.onLoadState" );
	};

	this.textureToLoad = function( texture ) {
		if (!texture.previouslyReferenced) {
			var name = texture.lookUpName;
			texture._world = this;
			texture.callback = this.textureMapLoaded;
			this._texMapsToLoad[name] = true;
			this._allMapsLoaded = false;

			// stop the draw loop until all textures have been loaded
			this._canvas.task.stop();
		}
	};

	this.textureMapLoaded = function( texture ) {
		var world = texture._world;
		if (!world) {
			console.log( "**** loaded texture does not have world defined ****" );
			return;
		}

		var name = texture.lookUpName;
		if (!world._texMapsToLoad[name]) {
			console.log( "loaded an unregistered texture map: " + name );
		} else {
			//console.log( "loaded a registered texture map: " + name );
			world._texMapsToLoad[name] = undefined;
		}

		// check if all the texture maps are loaded.  if so, resume the render loop
		world._allMapsLoaded = world.allTextureMapsLoaded();
		if (world._allMapsLoaded) {
			world._canvas.task.start();
        }
	};

	this.allTextureMapsLoaded = function() {
		for (var name in this._texMapsToLoad) {
			var needsLoad = this._texMapsToLoad[name];
			if (needsLoad)  return false;
		}

		return true;
	};

	this.textureLoadedCallback = function( name ) {
//		console.log( "*** material texture loaded: " + name );

		var world = this._world;
		if (!world) {
			console.log( "**** world not defined for loaded texture map: " + name );
        } else {
			world.textureMapLoaded( name );
        }
	};
	
	this.hasAnimatedMaterials = function() {
		var root = this.getGeomRoot();
		var rtnVal = false;
		if (root) {
			rtnVal = this.hHasAnimatedMaterials( root );
        }

		return rtnVal;
	};

	this.hHasAnimatedMaterials = function( obj ) {
		if (obj) {
			if (obj.getFillMaterial()) {
				if (obj.getFillMaterial().isAnimated())  return true;
			}

			if (obj.getStrokeMaterial()) {
				if (obj.getStrokeMaterial().isAnimated())  return true;
			}


			// do the sibling
			var hasAnim = false;
			if  (obj.getNext())  hasAnim = this.hHasAnimatedMaterials( obj.getNext() );
			if (hasAnim)  return true;
			if  (obj.getChild())  hasAnim = this.hHasAnimatedMaterials( obj.getChild() );
			if (hasAnim)  return true;
		}

		return false;
	};

	this.generateUniqueNodeID = function() {
		var str = "" + this._nodeCounter;
		this._nodeCounter++;
		return str;
	};

    
    // start RDGE passing your runtime object, and false to indicate we don't need a an initialization state
    // in the case of a procedurally built scene an init state is not needed for loading data
	this._canvas.rdgeid = this._canvas.getAttribute( "data-RDGE-id" );
	if (this._useWebGL) {
		rdgeStarted = true;
		RDGE.globals.engine.unregisterCanvas( this._canvas );
		RDGE.globals.engine.registerCanvas(this._canvas, this);
		RDGE.RDGEStart( this._canvas );
		this._canvas.task.stop()
	}
};


///////////////////////////////////////////////////////////////////////
// Property Accessors
///////////////////////////////////////////////////////////////////////
World.prototype.getGeomRoot = function()  {
    return this._geomRoot;
};


///////////////////////////////////////////////////////////////////////
// Methods
///////////////////////////////////////////////////////////////////////
World.prototype.updateObject = function (obj) {
	if (!this._useWebGL)  return;

    var prims = obj.getPrimitiveArray();
	var materialNodes = obj.getMaterialNodeArray();
    if (prims.length != materialNodes.length)
        throw new Error("inconsistent material and primitive counts");
    var nPrims = prims.length;
    var ctrTrNode;
    if (nPrims > 0) {
        ctrTrNode = obj.getTransformNode();
		if (ctrTrNode == null) {
			ctrTrNode = RDGE.createTransformNode("objRootNode_" + this._nodeCounter++);
			this._rootNode.insertAsChild( ctrTrNode );
			obj.setTransformNode( ctrTrNode );
		}

		ctrTrNode.meshes.forEach(function(thisMesh) {
		    RDGE.globals.meshMan.deleteMesh(thisMesh.mesh.name);
		});
		ctrTrNode.meshes = [];

        ctrTrNode.attachMeshNode(this.renderer.id + "_prim_" + this._nodeCounter++, prims[0]);
        ctrTrNode.attachMaterial(materialNodes[0]);
    }
	
	var children = ctrTrNode.children;
    for (var i = 1; i < nPrims; i++) {
        // get the next primitive
        var prim = prims[i];

        // get a previously created transform node.  If the transform has not been created, create it
        var childTrNode;
		if (children && children.length >= i) {
			childTrNode = children[i-1].transformNode;

			childTrNode.meshes.forEach(function(thisMesh) {
			    RDGE.globals.meshMan.deleteMesh(thisMesh.mesh.name);
			});
			childTrNode.meshes = [];
		} else {
			childTrNode = RDGE.createTransformNode("objNode_" + this._nodeCounter++);
			ctrTrNode.insertAsChild(childTrNode);
		}

        // attach the instanced box goe
        childTrNode.attachMeshNode(this.renderer.id + "_prim_" + this._nodeCounter++, prim);
        childTrNode.attachMaterial(materialNodes[i]);
    }
};

World.prototype.addObject = function( obj ) {
    if (!obj)  return;

    try {
        // undefine all the links of the object
        obj.setChild( undefined );
        obj.setNext( undefined );
        obj.setPrev( undefined );
        obj.setParent( undefined );

        obj.setWorld( this );

        if (this._geomRoot == null) {
            this._geomRoot = obj;
        } else {
            var go = this._geomRoot;
            while (go.getNext())  go = go.getNext();
            go.setNext( obj );
            obj.setPrev( go );
        }

		// build the WebGL buffers
		if (this._useWebGL) {
			obj.buildBuffers();
			this.restartRenderLoop();
		}
    }

    catch(e) {
        alert( "Exception in GLWorld.addObject " + e );
    }
};

World.prototype.restartRenderLoop = function() {
	//console.log( "restartRenderLoop" );

	this._firstRender = true;
	this._renderCount  = -1;
	if (this._canvas.task) {
		if (this._allMapsLoaded) {
			//console.log( "starting task" );
			this._canvas.task.start();
		} else {
			//console.log( "stopping task" );
			this._canvas.task.stop();
		}
	}
};

//append to the list of objects if obj doesn't already exist
//if obj exists, then don't add to list of objects
World.prototype.addIfNewObject = function (obj) {
    if (!obj) return;

    try {
        obj.setWorld(this);

        if (this._geomRoot == null) {
            this._geomRoot = obj;
        } else if (this._geomRoot !== obj) {
            var go = this._geomRoot;
            while (go.getNext() && go.getNext() !== obj) {
                go = go.getNext();
            }

            if (go.getNext() === null) {
                // undefine all the links of the object
                obj.setChild(undefined);
                obj.setNext(undefined);
                obj.setPrev(undefined);
                obj.setParent(undefined);

                go.setNext(obj);
                obj.setPrev(go);

            }
        }

		// build the WebGL buffers
		if (this._useWebGL) {
			obj.buildBuffers();
			this.restartRenderLoop();
		}
    } catch (e) {
        alert("Exception in GLWorld.addIfNewObject " + e);
    }
};

World.prototype.clearTree = function() {
	if (this._useWebGL) {
		var root = this._rootNode;
		root.children = new Array();
		RDGE.globals.engine.unregisterCanvas( this._canvas.rdgeid );

		this.update( 0 );
		this.draw();
	}
};

World.prototype.updateMaterials = function( obj, time ) {
	if (!obj)  return;

	var matArray = obj.getMaterialArray();
	if (matArray) {
		var n = matArray.length;
		for (var i=0;  i<n;  i++) {
			matArray[i].update( time );
	}
	}

	this.updateMaterials( obj.getNext(),  time );
	this.updateMaterials( obj.getChild(), time );
};

// return the origin of the world in NDC
World.prototype.getNDCOrigin = function() {
  var pt = MathUtils.transformPoint( [0,0,0], this.getCameraMatInverse() );
  var projMat = Matrix.makePerspective( this.getFOV(), this.getAspect(), this.getZNear(), this.getZFar());
  var ndcPt = MathUtils.transformHomogeneousPoint( pt, projMat );

  return  MathUtils.applyHomogeneousCoordinate( ndcPt );
};

World.prototype.worldToScreen = function(v) {
	var pMatrix = Matrix.makePerspective( this._fov, this.getAspect(), this._zNear, this._zFar);
	var mvMatrix = this.getCameraMatInverse();
	//var tmpMat = pMatrix.multiply( mvMatrix );
	var tmpMat = glmat4.multiply(pMatrix, mvMatrix, []);
	//var v2 = tmpMat.multiply( v );
	var v2 = glmat4.multiplyVec3( tmpMat, v, []);
	var v3 = MathUtils.transformHomogeneousPoint( v, tmpMat );
	v3 = MathUtils.applyHomogeneousCoordinate( v3 );
	var x = v2[0],  y = v2[1],  z = v2[2];

	var h = this.getGLContext().viewportHeight/2.0, w = this.getGLContext().viewportWidth/2.0;
    var x2 = w * (1 + x), y2 = h * ( 1 - y );
    return [x2, y2, z, 1];
};

World.prototype.screenToView = function( x, y ) {
	var gl = this._glContext;
	var w = gl.viewportWidth/2.0,
		h = gl.viewportHeight/2.0;

	var xv = x/w - 1,
		yv = 1.0 - y/h;

	return [xv,yv];
};

World.prototype.screenToWorld = function( xScr,  yScr ) {
	var viewVec = this.screenToView( xScr, yScr );
	var xView = viewVec[0],  yView = viewVec[1];

	var tmp = this.worldToScreen( [0,0,0,1] );
	var zView = tmp[2];

	// get the perspective matrix
	var pMatrix = Matrix.makePerspective( this._fov, this.getAspect(), this._zNear, this._zFar);
	var mvMatrix = Matrix.I(4);   // this would be the inverse of the camera matrix (if we had one).

	//var tmpMat = pMatrix.multiply( mvMatrix );
	var tmpMat = glmat4.multiply( pMatrix, mvMatrix, []);
	//var tmpInv = tmpMat.inverse();
	var tmpInv = glmat4.inverse( tmpMat, []);
	var v3 = [xView,  yView, zView, 1 ];
	//var w = tmpInv.multiply( v3 );
	var w = glmat4.multiplyVec4( tmpInv, v3, []);
	w[0] /= w[3];
	w[1] /= w[3];
	w[2] /= w[3];
	w[3] = 1.0;

	return w;
};

World.prototype.GLToScreen = function( glPt ) {
};

World.prototype.ScreenToGL = function( scrPt ) {
};


World.prototype.resetMatrixStack = function() {
    this._matStack = new Array();
    this._matStack.push( Matrix.I(4) );
};

World.prototype.pushMatrix = function( mat ) {
    if (mat) {
        var mat2 = this.stackTop();
        if (mat2) {
			var mat12 = glmat4.multiply( mat, mat2, []);
			this._matStack.push( mat12 );
		}
    }
};

World.prototype.stackTop = function() {
    var mat;
    if (this._matStack && (this._matStack.length > 0)) {
        mat = this._matStack[ this._matStack.length-1];
    }

    return mat;
};

World.prototype.popMatrix = function() {
    if (this._matStack.length == 0) {
        throw "Invalid popMatrix!";
    }

    return this._matStack.pop();
};

World.prototype.setMVMatrix = function() {
    var mat = this.stackTop();
    if (mat) {
		var gl = this._glContext;

        //var mvMatrix = this._cameraMatInv.multiply(mat);
		var mvMatrix = glmat4.multiply( this._cameraMatInv, mat, []);
        //var mat2 = mat.multiply( this._cameraMatInv );
        gl.uniformMatrix4fv(this._shaderProgram.mvMatrixUniform, false, new Float32Array(mvMatrix));

        var normalMatrix = mat3.create();
        // RDGE.mat4.toInverseMat3(mvMatrix, normalMatrix);
        // RDGE.mat4.toInverseMat3(new Float32Array(mvMatrix.flatten()), normalMatrix);
        RDGE.mat4.toInverseMat3(new Float32Array(mvMatrix), normalMatrix);
        mat3.transpose(normalMatrix);
        gl.uniformMatrix3fv(this._shaderProgram.nMatrixUniform, false, normalMatrix);
    }
};

World.prototype.makePerspectiveMatrix = function() {
  return Matrix.makePerspective( this.getFOV(),  this.getAspect(),  this.getZNear(),  this.getZFar() );
};

World.prototype.render = function() {
	if (!this._useWebGL) {
		// clear the context
		var ctx = this.get2DContext();
		if (!ctx)  return;
		ctx.clearRect(0, 0, this.getViewportWidth(), this.getViewportHeight());
		
		// render the geometry
		var root = this.getGeomRoot();
		this.hRender( root );
	} else {
		RDGE.globals.engine.setContext( this._canvas.rdgeid );
		//this.draw();
		this.restartRenderLoop();
	}
};

World.prototype.hRender = function( obj ) {
	if (!obj)  return;
	obj.render();

	this.hRender( obj.getChild() );
	this.hRender( obj.getNext() );
};

World.prototype.setViewportFromCanvas = function(canvas) {
	this._viewportWidth = canvas.width;
	this._viewportHeight = canvas.height;

	if (this._useWebGL) {
		this._glContext.viewportWidth = canvas.width;
		this._glContext.viewportHeight = canvas.height;

		this.getCamera().setPerspective(this.getFOV(), this.getAspect(), this.getZNear(), this.getZFar());
		
		this.renderer.setViewPort(0, 0, canvas.width, canvas.height);
	}
};

World.prototype.getShapeFromPoint = function( offsetX, offsetY ) {
	var x = offsetX/this._canvas.width;
	var y = offsetY/this._canvas.height;

	var go = this._geomRoot;
	if(go.collidesWithPoint(x,y)) {
//		console.log("collision found");
		return go;
	}

	while (go.getNext()) {
		go = go.getNext();
		if(go.collidesWithPoint(x,y)) {
//			console.log("collision found");
			return go;
		}
	}
};



World.prototype.exportJSON = function () {
	// world properties
	var worldObj = 
	{
		'version'	: 1.1,
		'id'		: this.getCanvas().getAttribute( "data-RDGE-id" ),
		'fov'		: this._fov,
		'zNear'		: this._zNear,
		'zFar'		: this._zFar,
		'viewDist'	: this._viewDist,
		'webGL'		: this._useWebGL
	};

	// RDGE scenegraph
	if (this._useWebGL)
		worldObj.scenedata = this.myScene.exportJSON();

	// object data
	var strArray = [];
	this.exportObjectsJSON( this._geomRoot, worldObj );

	// You would think that the RDGE export function
	// would not be destructive of the data.  You would be wrong...
	// We need to rebuild everything
    if (this._useWebGL) {
        if (worldObj.children && (worldObj.children.length >= 1)) {
            this.rebuildTree(this._geomRoot);
            this.restartRenderLoop();
		}
	}

	// convert the object to a string
	var jStr = JSON.stringify( worldObj );

	// prepend some version information to the string.
	// this string is also used to differentiate between JSON
	// and pre-JSON versions of fileIO.
	// the ending ';' in the version string is necessary
	jStr = "v1.0;" + jStr;
	
	return jStr;
};

World.prototype.rebuildTree = function (obj) {
	if (!obj)  return;

	obj.buildBuffers();

	if (obj.getChild()) {
		 this.rebuildTree( obj.getChild () );
    }

	if (obj.getNext())
		this.rebuildTree( obj.getNext() );
};

World.prototype.exportObjectsJSON = function (obj, parentObj) {
	if (!obj)  return;

	var jObj = obj.exportJSON();
	if (!parentObj.children)  parentObj.children = [];
	parentObj.children.push( jObj );

	if (obj.getChild()) {
		 this.exportObjectsJSON( obj.getChild (), jObj  );
    }

	if (obj.getNext())
		this.exportObjectsJSON( obj.getNext(), parentObj );
};

World.prototype.findTransformNodeByMaterial = function( materialNode,  trNode ) {
	//if (trNode == null)  trNode = this._ctrNode;
	if (trNode == null)  trNode = this._rootNode;
	if ( trNode.transformNode && (materialNode == trNode.transformNode.materialNode))  return trNode;

	var rtnNode;
	if (trNode.children != null) {
		var nKids = trNode.children.length;
		for (var i=0;  i<nKids;  i++) {
			var child = trNode.children[i];
			rtnNode = this.findTransformNodeByMaterial( materialNode, child );
			if (rtnNode)  break;
		}
	}

	return rtnNode;
};

World.prototype.importJSON = function (jObj) {
    if (jObj.webGL) {
		// start RDGE
		rdgeStarted = true;
		var id = this._canvas.getAttribute( "data-RDGE-id" ); 
		this._canvas.rdgeid = id;
        RDGE.globals.engine.registerCanvas(this._canvas, this);
        RDGE.RDGEStart(this._canvas);
		this._canvas.task.stop()
	}

	// import the objects
	// there should be exactly one child of the parent object
	if (jObj.children)
	{
		for (var i=0;  i<jObj.children.length;  i++)
			this.importObjectsJSON( jObj.children[i] );
	}
	else
		throw new Error ("unrecoverable canvas import error - inconsistent root object: " + jObj.children );

    if (!this._useWebGL) {
		// render using canvas 2D
		this.render();
	}
	else
		this.restartRenderLoop();
};

World.prototype.importObjectsJSON = function (jObj, parentGeomObj) {
	// read the next object
	var gObj = this.importObjectJSON( jObj,  parentGeomObj );

	// determine if we have children
    if (jObj.children) {
		var nKids = jObj.children.length;
        for (var i = 0; i < nKids; i++) {
			var child = jObj.children[i];
			this.importObjectsJSON( child, gObj );
		}
	}
};

World.prototype.importObjectJSON = function( jObj, parentGeomObj )
{
	var type = jObj.type;
    var BrushStroke = require("js/lib/geom/brush-stroke").BrushStroke;
    var SubPath = require("js/lib/geom/sub-path").SubPath;
	var obj;
	switch (type)
	{
		case 1:
			obj = Object.create(Rectangle, {});
			obj.importJSON( jObj );
			break;

		case 2:		// circle
			obj = Object.create(Circle, {});
			obj.importJSON( jObj );
			break;

		case 3:		// line
            obj = Object.create(Line, {});
            obj.importJSON( jObj );
            break;

        case 5:     //cubic bezier
            obj = new SubPath();
            obj.importJSON(jObj);
            break;

        case 6:     //brush stroke
            obj = new BrushStroke();
            obj.importJSON(jObj);
            break;

		default:
			throw new Error( "Unrecognized object type: " + type );
			break;
	}

	if (obj)
		this.addObject( obj,  parentGeomObj );

	return obj;
};

if (typeof exports === "object") {
    exports.World = World;
}