package classes.away3d.core.base
{
    import classes.away3d.arcane;
    import classes.away3d.containers.*;
    import classes.away3d.core.vos.*;
    import classes.away3d.events.*;
    import classes.away3d.materials.*;
    import classes.away3d.sprites.*;
    
    import flash.geom.*;
    import flash.utils.*;
    
    use namespace arcane;
	
    /**
    * 3d object containing face and segment elements 
    */
    public class Mesh extends Object3D
    {
    	/** @private */
		arcane var _geometry:Geometry;
		
    	private var _face:Face;
		private var _material:Material;
		private var _bitmapMaterial:BitmapMaterial;
		private var _back:Material;
		
		private function getMaxVert(i:uint):Number
		{
			var verts:Vector.<Number> = geometry.verts;
			var length:uint = verts.length;
			var maxVal:Number = -Infinity;
			while (i < length) {
				if (maxVal < verts[i])
					maxVal = verts[i];
				i += 3;
			}
			
			return maxVal;
		}
		
		private function getMinVert(i:uint):Number
		{
			var verts:Vector.<Number> = geometry.verts;
			var length:uint = verts.length;
			var minVal:Number = Infinity;
			while (i < length) {
				if (minVal > verts[i])
					minVal = verts[i];
				i += 3;
			}
			
			return minVal;
		}
		
		private function onMaterialUpdate(event:MaterialEvent):void
		{
			if (_session)
        		_session.updateSession();
			
			//cancel session update for moviematerials
			//if (event.material is MovieMaterial && _scene)
			//	delete _scene.updatedSessions[_session];
				
		}
		
        private function onMappingChange(event:ElementEvent):void
		{
			if (_session)	
        		_session.updateSession();
			
			if ((_face = event.element as Face)) {
				if (_face.material)
					_bitmapMaterial = _face.material as BitmapMaterial;
				else
					_bitmapMaterial = _material as BitmapMaterial;
				
				if (_bitmapMaterial)
					_bitmapMaterial.getFaceMaterialVO(_face.faceVO, this).invalidated = true;
			}
		}
        
        private function onGeometryUpdate(event:GeometryEvent):void
        {
        	if (_session)	
        		_session.updateSession();
        	
        	notifyDimensionsChange();
        }
        
        private function removeMaterial(mat:Material):void
        {
        	if (!mat)
        		return;
        	
            //remove update listener
			mat.removeOnMaterialUpdate(onMaterialUpdate);
        }
        
        private function addMaterial(mat:Material):void
        {
        	if (!mat)
        		return;
        	
        	//add update listener
			mat.addOnMaterialUpdate(onMaterialUpdate);
        }
        
        protected override function updateDimensions():void
        {
        	//update bounding radius
        	var vertices:Vector.<Vertex> = geometry.vertices;
        	var _length:int = vertices.length;
        	
        	if (_length) {
        		
	        	if (_scaleX < 0)
	        		_boundingScale = -_scaleX;
	        	else
	        		_boundingScale = _scaleX;
            	
            	if (_scaleY < 0 && _boundingScale < -_scaleY)
            		_boundingScale = -_scaleY;
            	else if (_boundingScale < _scaleY)
            		_boundingScale = _scaleY;
            	
            	if (_scaleZ < 0 && _boundingScale < -_scaleZ)
            		_boundingScale = -_scaleZ;
            	else if (_boundingScale < _scaleZ)
            		_boundingScale = _scaleZ;
            	
	        	var mradius:Number = 0;
	        	var vradius:Number;
	            var num:Vector3D;
	            for each (var vertex:Vertex in vertices) {
	            	num = vertex.position.subtract(_pivotPoint);
	                vradius = num.lengthSquared;
	                if (mradius < vradius)
	                    mradius = vradius;
	            }
	            if (mradius)
	           		_boundingRadius = Math.sqrt(mradius);
	           	else
	           		_boundingRadius = 0;
	             
	            //update max/min X
	            _maxX = getMaxVert(uint(0));
	            _minX = getMinVert(uint(0));
	            
	            //update max/min Y
	            _maxY = getMaxVert(uint(1));
	            _minY = getMinVert(uint(1));
	            
	            //update max/min Z
	            _maxZ = getMaxVert(uint(2));
	            _minZ = getMinVert(uint(2));
         	}
         	
            super.updateDimensions();
        }
        
        //TODO: create effective dispose mechanism for meshs
		/*
        private function clear():void
        {
            for each (var face:Face in _faces.concat([]))
                removeFace(face);
        }
        */
        
		/**
		 * String defining the source of the mesh.
		 * 
		 * If the mesh has been created internally, the string is used to display the package name of the creating object.
		 * Used to display information in the stats panel
		 * 
		 * @see away3d.core.stats.Stats
		 */
       	public var url:String;
		
		/**
		 * String defining the type of class used to generate the mesh.
		 * Used to display information in the stats panel
		 * 
		 * @see away3d.core.stats.Stats
		 */
       	public var type:String = "mesh";
       	
        /**
        * Defines a segment material to be used for outlining the 3d object.
        */
        public var outline:Material;
		
		/**
		 * Indicates whether both the front and reverse sides of a face should be rendered.
		 */
        public var bothsides:Boolean;
        
        /**
        * Placeholder for md2 frame indexes
        */
        public var indexes:Array;
        
        public function get verts():Vector.<Number>
        {
            return _geometry.verts;
        }
        
		/**
		 * Returns an array of the vertices contained in the mesh object.
		 */
        public function get vertices():Vector.<Vertex>
        {
            return _geometry.vertices;
        }
		/**
		 * Set a new array of vertices for the mesh object. Warning: make sure faces are first updated with the new vertexes.
		 * The array length must not exceed the length of the mesh vertices array.
		 */
        public function set vertices(v:Vector.<Vertex>):void
        {
            _geometry.vertices = v;
        }
        
		/**
		 * Returns an array of the indices contained in the mesh object.
		 */
        public function get indices():Vector.<int>
        {
            return _geometry.indices;
        }
        
		/**
		 * Returns an array of the start indices contained in the mesh object.
		 */
        public function get startIndices():Vector.<int>
        {
            return _geometry.startIndices;
        }
        
		/**
		 * Returns an array of the faceVOs contained in the mesh object.
		 */
        public function get faceVOs():Vector.<FaceVO>
        {
        	return _geometry.faceVOs;
        }
		
		/**
		 * Returns an array of the segmentVOs contained in the mesh object.
		 */
        public function get segmentVOs():Vector.<SegmentVO>
        {
        	return _geometry.segmentVOs;
        }
        
		/**
		 * Returns an array of the spriteVOs contained in the mesh object.
		 */
        public function get spriteVOs():Vector.<SpriteVO>
        {
        	return _geometry.spriteVOs;
        }
        
		/**
		 * Returns an array of the faces contained in the mesh object.
		 */
        public function get faces():Vector.<Face>
        {
            return _geometry.faces;
        }
		/**
		 * Returns an array of the segments contained in the mesh object.
		 */
        public function get segments():Vector.<Segment>
        {
            return _geometry.segments;
        }
		
		/**
		 * Returns an array of the 3d sprites contained in the mesh object.
		 */
        public function get sprites():Vector.<Sprite3D>
        {
            return _geometry.sprites;
        }
		
		/**
		 * Returns an array of all elements contained in the mesh object.
		 */
        public function get elements():Vector.<Element>
        {
            return _geometry.elements;
        }
        
        /**
        * Defines the geometry object used for the mesh.
        */
        public function get geometry():Geometry
        {
        	return _geometry;
        }
        
        public function set geometry(val:Geometry):void
        {
        	if (_geometry == val)
                return;
            
            if (_geometry != null) {
            	_geometry.removeOnMaterialUpdate(onMaterialUpdate);
            	_geometry.removeOnMappingChange(onMappingChange);
            	_geometry.removeOnGeometryUpdate(onGeometryUpdate);
            }
            
        	_geometry = val;
        	
            if (_geometry != null) {
            	_geometry.addOnMaterialUpdate(onMaterialUpdate);
            	_geometry.addOnMappingChange(onMappingChange);
            	_geometry.addOnGeometryUpdate(onGeometryUpdate);
            }
        }
            
		/**
		 * Defines the material used to render the faces, segments or 3d sprites in the geometry object.
		 * Individual material settings on faces, segments and 3d sprites will override this setting.
		 * 
		 * @see away3d.core.base.Face#material
		 */
        public function get material():Material
        {
        	return _material;
        }
        
        public function set material(val:Material):void
        {
        	if (_material == val && _material != null)
                return;
            
            removeMaterial(_material);
			
			if (val == null)
				val = new WireColorMaterial();
			
			//set material value
        	addMaterial(_material = val);
        	
        	if (_session)	
        		_session.updateSession();
        }
        
        /**
        * Defines a triangle material to be used for the backface of all faces in the 3d object.
        */
        public function get back():Material
        {
        	return _back;
        }
        
		public function set back(val:Material):void
        {
        	if (_back == val)
                return;
            
            removeMaterial(_back);
			
			//set back value
        	addMaterial(_back = val);
        }
		
		/**
		 * Creates a new <code>BaseMesh</code> object.
		 *
		 * @param	init			[optional]	An initialisation object for specifying default instance properties.
		 */
        public function Mesh(init:Object = null)
        {
            super(init);
                
            geometry = new Geometry();
            
            outline = ini.getMaterial("outline");
            material = ini.getMaterial("material");
            back = ini.getMaterial("back") as Material;
            bothsides = ini.getBoolean("bothsides", false);
        }
		
		/**
		 * Adds a face object to the mesh object.
		 * 
		 * @param	face	The face object to be added.
		 */
        public function addFace(face:Face):void
        {
            _geometry.addFace(face);
            
            if(this.parent)
            	this.parent.incrementPolyCount(1);
        }
		
		/**
		 * Removes a face object from the mesh object.
		 * 
		 * @param	face	The face object to be removed.
		 */
        public function removeFace(face:Face):void
        {
            _geometry.removeFace(face);
            
            if(this.parent)
            	this.parent.incrementPolyCount(-1);
        }
		
		/**
		 * Adds a segment object to the mesh object.
		 * 
		 * @param	segment	The segment object to be added.
		 */
        public function addSegment(segment:Segment):void
        {
            _geometry.addSegment(segment);
            
            if(this.parent)
            	this.parent.incrementPolyCount(1);
        }
		
		/**
		 * Removes a segment object from the mesh object.
		 * 
		 * @param	segment	The segment object to be removed.
		 */
        public function removeSegment(segment:Segment):void
        {
            _geometry.removeSegment(segment);
            
            if(this.parent)
            	this.parent.incrementPolyCount(-1);
        }
		
		/**
		 * Adds a sprite3d object to the mesh object.
		 * 
		 * @param	sprite3d	The sprite3d object to be added.
		 */
        public function addSprite(sprite3d:Sprite3D):void
        {
            _geometry.addSprite(sprite3d);
            
            if(this.parent)
            	this.parent.incrementPolyCount(1);
        }
        
		/**
		 * Removes a 3d sprite object from the mesh object.
		 * 
		 * @param	sprite3d	The 3d sprite object to be removed.
		 */
        public function removeSprite(sprite3d:Sprite3D):void
        {
            _geometry.removeSprite(sprite3d);
            
            if(this.parent)
            	this.parent.incrementPolyCount(-1);
        }
		
		/**
		 * Inverts the geometry of all face objects.
		 * 
		 * @see away3d.code.base.Face#invert()
		 */
        public function invertFaces():void
        {
            _geometry.invertFaces();
        }
        
		/**
		* Divides all faces objects of a Mesh into 4 equal sized face objects.
		* Used to segment a geometry in order to reduce affine persepective distortion.
		* 
		* @see away3d.primitives.SkyBox
		*/
        public function quarterFaces():void
        {
        	var cache:uint = this.faces.length;
        	
            _geometry.quarterFaces();
            
            if(this.parent)
            	this.parent.incrementPolyCount(this.faces.length - cache);
        }
		
		/**
		* Divides a face object into 4 equal sized face objects.
		* 
		 * @param	face	The face to split in 4 equal faces.
		*/
        public function quarterFace(face:Face):void
        {
            _geometry.quarterFace(face);
            
            if(this.parent)
            	this.parent.incrementPolyCount(3);
        }
        
		/**
		* Divides all faces objects of a Mesh into 3 face objects.
		* 
		*/
        public function triFaces():void
        {
        	var cache:uint = this.faces.length;
        	
            _geometry.triFaces();
            
            if(this.parent)
            	this.parent.incrementPolyCount(this.faces.length - cache);
        }
		
		/**
		* Divides a face object into 3 face objects.
		* 
		 * @param	face	The face to split 3 faces.
		*/
        public function triFace(face:Face):void
        {
            _geometry.triFace(face);
            
            if(this.parent)
            	this.parent.incrementPolyCount(2);
        }
		
		/**
		* Divides all faces objects of a Mesh into 2 face objects.
		* 
		* @param	side	The side of the faces to split in two. 0 , 1 or 2. (clockwize).
		*/
        public function splitFaces(side:int = 0):void
        {
        	var cache:uint = this.faces.length;
        	
			 _geometry.splitFaces(side);
			 
			 if(this.parent)
            	this.parent.incrementPolyCount(this.faces.length - cache);
        }
		/**
		* Divides a face object into 2 face objects.
		* 
		* @param	face	The face to split in 2 faces.
		* @param	side	The side of the face to split in two. 0 , 1 or 2. (clockwize).
		*/
		public function splitFace(face:Face, side:int = 0):void
        {
			 _geometry.splitFace(face, side);
			 
			 if(this.parent)
            	this.parent.incrementPolyCount(1);
        }
        
        /**
        * Updates the materials in the mesh object
        */
        public function updateMesh(view:View3D):void
        {
        	geometry.updateGeometry();
        	
        	geometry.updateMaterials(this, view);
        	
        	if (_material)
        		_material.updateMaterial(this, view);
        	if (back)
        		back.updateMaterial(this, view);
        }
        
		/**
		 * Duplicates the mesh properties to another 3d object.  Usage: existingObject = objectToClone.clone( existingObject ) as Mesh;
		 * 
		 * @param	object	[optional]	The new object instance into which all properties are copied. The default is <code>Mesh</code>.
		 * @return						The new object instance with duplicated properties applied.
		 */
        public override function clone(object:Object3D = null):Object3D
        {
            var mesh:Mesh = (object as Mesh) || new Mesh();
            super.clone(mesh);
            mesh.type = type;
            mesh.material = material;
            mesh.outline = outline;
            mesh.back = back;
            mesh.bothsides = bothsides;
            mesh.debugbb = debugbb;
			mesh.geometry = geometry;
			
            return mesh;
        }
        
		/**
		 * Duplicates the mesh properties to another 3d object, including geometry. Usage: var newObject:Mesh = oldObject.cloneAll( newObject ) as Mesh;
		 * 
		 * @param	object	[optional]	The new object instance into which all properties are copied. The default is <code>Mesh</code>.
		 * @return						The new object instance with duplicated properties applied.
		 */
        public function cloneAll(object:Object3D = null):Object3D
        {
            var mesh:Mesh = (object as Mesh) || new Mesh();
            super.clone(mesh);
            mesh.type = type;
            mesh.material = material;
            mesh.outline = outline;
            mesh.back = back;
            mesh.bothsides = bothsides;
            mesh.debugbb = debugbb;
			mesh.geometry = geometry.clone();
			
			return mesh;
        }
 		
 		/**
 		 * Returns a formatted string containing a self contained AS3 class definition that can be used to re-create the mesh.
 		 * 
 		 * @param	classname	[optional]	Defines the class name used in the output string. Defaults to <code>Away3DObject</code>.
 		 * @param	packagename	[optional]	Defines the package name used in the output string. Defaults to no package.
 		 * @param	round		[optional]	Rounds all values to 4 decimal places. Defaults to false.
 		 * @param	animated	[optional]	Defines whether animation data should be saved. Defaults to false.
 		 * 
 		 * @return	A string to be pasted into a new .as file
 		 */
 		public function asAS3Class(classname:String = null, packagename:String = "", round:Boolean = false, animated:Boolean = false):String
        {
            classname = classname || name || "Away3DObject";
			
            var source:String = "package "+packagename+"\n{\n\timport away3d.core.base.*;\n\timport away3d.core.utils.*;\n"+"\n\tpublic class "+classname+" extends Mesh\n\t{\n";
            source += "\t\tprivate var varr:Array = [];\n\t\tprivate var uvarr:Array = [];\n\t\tprivate var scaling:Number;\n";
			
			source += "\n\t\tprivate function v(x:Number,y:Number,z:Number):void\n\t\t{\n";
			source += "\t\t\tvarr.push(new Vertex(x*scaling, y*scaling, z*scaling));\n\t\t}\n\n";
			
            source += "\t\tprivate function uv(u:Number,v:Number):void\n\t\t{\n";
            source += "\t\t\tuvarr.push(new UV(u,v));\n\t\t}\n\n";
            source += "\t\tprivate function f(vn0:int, vn1:int, vn2:int, uvn0:int, uvn1:int, uvn2:int):void\n\t\t{\n";
            source += "\t\t\taddFace(new Face(varr[vn0],varr[vn1],varr[vn2], null, uvarr[uvn0],uvarr[uvn1],uvarr[uvn2]));\n\t\t}\n\n";
            source += "\t\tpublic function "+classname+"(init:Object = null)\n\t\t{\n\t\t\tsuper(init);\n\t\t\tinit = Init.parse(init);\n\t\t\tscaling = init.getNumber(\"scaling\", 1);\n\t\t\tbuild();\n\t\t\ttype = \""+classname+"\";\n\t\t}\n\n";
            source += "\t\tprivate function build():void\n\t\t{\n";
				
			var refvertices:Dictionary = new Dictionary();
            var verticeslist:Array = [];
            var remembervertex:Function = function(vertex:Vertex):void
            {
                if (refvertices[vertex] == null)
                {
                    refvertices[vertex] = verticeslist.length;
                    verticeslist.push(vertex);
                }
            };

            var refuvs:Dictionary = new Dictionary();
            var uvslist:Array = [];
            var rememberuv:Function = function(uv:UV):void
            {
                if (uv == null)
                    return;

                if (refuvs[uv] == null)
                {
                    refuvs[uv] = uvslist.length;
                    uvslist.push(uv);
                }
            };

            for each (var face:Face in _geometry.faces)
            {
                remembervertex(face._v0);
                remembervertex(face._v1);
                remembervertex(face._v2);
                rememberuv(face._uv0);
                rememberuv(face._uv1);
                rememberuv(face._uv2);
            }
 			
			var uv:UV;
			var v:Vertex;
			var myPattern:RegExp;
			var myPattern2:RegExp;
			
			for each (v in verticeslist)
				source += (round)? "\t\t\tv("+v._x.toFixed(4)+","+v._y.toFixed(4)+","+v._z.toFixed(4)+");\n" : "\t\t\tv("+v._x+","+v._y+","+v._z+");\n";
			 
			for each (uv in uvslist)
				source += (round)? "\t\t\tuv("+uv._u.toFixed(4)+","+uv._v.toFixed(4)+");\n"  :  "\t\t\tuv("+uv._u+","+uv._v+");\n";

			if(round){
				var tmp:String;
				myPattern2 = new RegExp(".0000","g");
			}
			
			var f:Face;	
			for each (f in _geometry.faces)
				source += "\t\t\tf("+refvertices[f._v0]+","+refvertices[f._v1]+","+refvertices[f._v2]+","+refuvs[f._uv0]+","+refuvs[f._uv1]+","+refuvs[f._uv2]+");\n";

			if (round) source = source.replace(myPattern2,"");
			
			source += "\n\t\t}\n\t}\n}";
			//here a setClipboard to avoid Flash slow trace window might be beter...
            return source;
        }
 		
 		/**
 		 * Returns an xml representation of the mesh
 		 * 
 		 * @return	An xml object containing mesh information
 		 */
        public function asXML():XML
        {
            var result:XML = <mesh></mesh>;

            var refvertices:Dictionary = new Dictionary();
            var verticeslist:Array = [];
            var remembervertex:Function = function(vertex:Vertex):void
            {
                if (refvertices[vertex] == null)
                {
                    refvertices[vertex] = verticeslist.length;
                    verticeslist.push(vertex);
                }
            };

            var refuvs:Dictionary = new Dictionary();
            var uvslist:Array = [];
            var rememberuv:Function = function(uv:UV):void
            {
                if (uv == null)
                    return;

                if (refuvs[uv] == null)
                {
                    refuvs[uv] = uvslist.length;
                    uvslist.push(uv);
                }
            };

            for each (var face:Face in _geometry.faces)
            {
                remembervertex(face._v0);
                remembervertex(face._v1);
                remembervertex(face._v2);
                rememberuv(face._uv0);
                rememberuv(face._uv1);
                rememberuv(face._uv2);
            }

            var vn:int = 0;
            for each (var v:Vertex in verticeslist)
            {
                v;//TODO : FDT Warning
                result.appendChild(<vertex id={vn} x={v._x} y={v._y} z={v._z}/>);
                vn++;
            }

            var uvn:int = 0;
            for each (var uv:UV in uvslist)
            {
                uv;//TODO : FDT Warning
                result.appendChild(<uv id={uvn} u={uv._u} v={uv._v}/>);
                uvn++;
            }

            for each (var f:Face in _geometry.faces)
            {
                f;//TODO : FDT Warning
                result.appendChild(<face v0={refvertices[f._v0]} v1={refvertices[f._v1]} v2={refvertices[f._v2]} uv0={refuvs[f._uv0]} uv1={refuvs[f._uv1]} uv2={refuvs[f._uv2]}/>);
            }
            return result;
        }
		
		/**
 		 * update vertex information.
 		 * 
 		 * @param		v						The vertex object to update
 		 * @param		x						The new x value for the vertex
 		 * @param		y						The new y value for the vertex
 		 * @param		z						The new z value for the vertex
		 * @param		refreshNormals	[optional]	Defines whether normals should be recalculated
 		 * 
 		 */
		public function updateVertex(v:Vertex, x:Number, y:Number, z:Number, refreshNormals:Boolean = false):void
		{
			_geometry.updateVertex(v, x, y, z, refreshNormals);
		}
		
		/**
 		* Apply the local rotations to the geometry without altering the appearance of the mesh
 		*/
		public override function applyRotations():void
		{
			var tmprotations:Vector3D = new Vector3D();

			for each (var vertex:Vertex in vertices) {
				 
				tmprotations.x = vertex.x;
				tmprotations.y = vertex.y;
				tmprotations.z = vertex.z;
				
				tmprotations = transform.deltaTransformVector(tmprotations);

				updateVertex(vertex, tmprotations.x, tmprotations.y, tmprotations.z, true);
			}
			
			rotationX = 0;
			rotationY = 0;
			rotationZ = 0;
		}
		
		/**
 		* Apply the given position to the geometry without altering the appearance of the mesh
 		*/
		public override function applyPosition(dx:Number, dy:Number, dz:Number):void
		{
			var x:Number;
			var y:Number;
			var z:Number;
			
			for each (var vertex:Vertex in vertices) {
				x = vertex.x;
				y = vertex.y;
				z = vertex.z;
				vertex.setValue(x - dx, y - dy, z - dz);
			}
			
			var dV:Vector3D = new Vector3D(dx, dy, dz);
            dV = transform.deltaTransformVector(dV);
            dV = dV.add(position);
            moveTo(dV.x, dV.y, dV.z);  
		}
		
		public function updateBounds():void
		{
			updateDimensions();
		}
		
    }
}
