/*
 * Copyright 2007-2008 (c) rch850,tarotarorg
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package org.tarotaro.flash.pv3d {
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.events.*;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	import org.papervision3d.core.geom.TriangleMesh3D;
	import org.papervision3d.core.geom.renderables.Triangle3D;
	import org.papervision3d.core.geom.renderables.Vertex3D;
	import org.papervision3d.core.math.Matrix3D;
	import org.papervision3d.core.math.NumberUV;
	import org.papervision3d.core.proto.GeometryObject3D;
	import org.papervision3d.core.proto.MaterialObject3D;
	import org.papervision3d.events.FileLoadEvent;
	import org.papervision3d.materials.utils.MaterialsList;
	import org.papervision3d.objects.DisplayObject3D;
	import org.tarotaro.flash.pv3d.ZipLoader;
	
	/**
	* メタセコイアのファイル（.mqo）を読み込むためのクラス。
	*
	* var mqo = new Metasequoia();
	* mqo.addEventListener(...);
	* mqo.load("hoge.mqo");
	*/
	public class Metasequoia extends TriangleMesh3D {
		/**
		 * ファイルの文字コード。よほどのことが無い限り shift_jis だと思います。
		 */
		public var charset:String = "shift_jis";
		
		/**
		 * 面の両側にマテリアルを貼るかどうかを指定します。
		 */
		public var doubleSided:Boolean = false;
		
		/**
		 * InteractiveScene3DEventを受け取るかどうかを設定します。
		 */
		public var interactive:Boolean = false;
		
		private var _loader:ZipLoader;
		private var _filename:String;
		private var _materialsToLoad:int =0;
		private var _materialNames:Array;
		private var _scale:Number = 1;
		private var _skipMaterial:Boolean = false;
		/**
		* Metasequoiaファイル内のオブジェクト一覧
		*/
		private var _objectlist:Array;

		/**
		* モデルの階層
		*/
		private var _depth:Number;

		/**
		* コンストラクタ
		*/
		function Metasequoia() {
			init();
			super(null, new Array(), new Array(), null);
		}
		
		/**
		* @param file 読み込むファイルの URL。絶対パスで指定してください。
		* @param scale 読み込むときの拡大率。1 が原寸大です。
		*/
		public function load(file:String, scale:Number = 1, skipMaterial:Boolean = false):void {
			this._skipMaterial = skipMaterial;
			init();
			this.name = file;
			_filename = file;
			_scale = scale;
			loadMetasequoia();
		}

		public function set materialBase(value:Metasequoia):void 
		{
			this.materials = value.materials;
			this._materialNames = value._materialNames;
		}
		/**
		 * 名前に該当するオブジェクトをMetasequoia内から検索する
		 * @param	name
		 * @return
		 */
		public function getObjectByName(name:String):Metasequoia {
			//自分の直下の子に該当するオブジェクトが存在するかどうか検索
			var obj:Metasequoia = this.children[name] as Metasequoia;
			if ( obj ) {
				return obj;
			} else {
				//ない場合、子孫を探索
				for (var n:Object in this.children) {
					var mqo:Metasequoia = this.children[n] as Metasequoia;
					if ( mqo ) {
						var obj2:Metasequoia = mqo.getObjectByName(name);
						if ( obj2 ) {
							return obj2;
						}
					}
				}
			}
			return null;
			
		}

		private function init():void {
			_objectlist = new Array();
			_objectlist.push(this);
			this._depth = -1;
			if (!this._skipMaterial) {
				this.materials = new MaterialsList();
			}
		}

		
		private function loadMetasequoia():void {
			_loader = new ZipLoader();
			_loader.dataFormat = URLLoaderDataFormat.BINARY;
			_loader.addEventListener(Event.COMPLETE, completeHandler);
			_loader.addEventListener(IOErrorEvent.IO_ERROR, defaultHandler);
			_loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, defaultHandler);
			_loader.addEventListener(ProgressEvent.PROGRESS, defaultHandler);
			_loader.load(new URLRequest(_filename));
		}
		
		private function completeHandler(evt:Event):void {
			var byteArray:ByteArray = ByteArray(_loader.data);
			buildMetasequoia(byteArray.readMultiByte(byteArray.length, charset));
			dispatchEvent(evt.clone());
		}
		
		private function defaultHandler(evt:Event):void {
			dispatchEvent(evt.clone());
		}
		
		private function buildMetasequoia(plainText:String):void {
			var lines:Array = plainText.split("\r\n");
			//trace("num lines = " + lines.length);
			var l:int = 0;
			
			// Material チャンクを読み込む
			l = parseMaterialChunk(lines, 0);
			
			// Object チャンクを読み込めなくなるまで読み込む
			while (l != -1) {
				l = parseObjectChunk(lines, l);
			}
			
			geometry.ready = true;
			
		}
		
		/**
		* Material チャンクの開始行を返します。
		* 見つからなかった場合には -1 を返します。
		*/
		private function getMaterialChunkLine(lines:Array, startLine:int = 0):int {
			for (var i:uint = startLine; i < lines.length; ++i) {
				if (lines[i].indexOf("Material") == 0) {
					return int(i);
				}
			}
			return -1;
		}
		
		/**
		* Material チャンクを読み込み、その最後の行番号を返します。
		* エラーが起こった場合は -1 を返します。
		*/
		private function parseMaterialChunk(lines:Array, startLine:int):int {
			var l:int = getMaterialChunkLine(lines, startLine);
			if (l == -1) {
				return -1;
			}
			
			// 解析中の行の文字列
			var line:String = lines[l];
			
			// マテリアル数を取得
			var num:Number = parseInt(line.substr(9));
			if (isNaN(num)) {
				return -1;
			}
            //ロードするマテリアル数を設定
            _materialsToLoad = num;

			++l;
			
			// } で閉じているところの行番号
			var endLine:int = l + int(num);
			if (this._skipMaterial) {
				endLine += 1;
			} else {
				_materialNames = new Array();
				// mqo ファイルのあるディレクトリのパス
				var path:String = _filename.slice(0, _filename.lastIndexOf("/") + 1);
				
				for (; l < endLine; ++l) {
					var material:MetasequoiaMaterial;
					line = lines[l];

					//マテリアルの作成開始
					material = new MetasequoiaMaterial();

					//マテリアルロード完了イベントの設定
					material.addEventListener (FileLoadEvent.LOAD_COMPLETE , materialLoadCompleteHandler);
					//マテリアルのロード開始
					material.parseLine(path , line);
					material.smooth = true;
					material.doubleSided = this.doubleSided;
					material.interactive = this.interactive;
					
					materials.addMaterial(material, material.name);
					_materialNames.push(material.name);
				}
			}
			return endLine;
		}
	
		private function materialLoadCompleteHandler(evt:FileLoadEvent):void {
			_materialsToLoad--;
			if (_materialsToLoad == 0)
			{
				//COLLADA のソースにあった謎の一行。不具合の元になるのでコメントアウト
				//materials = new MaterialsList();
				dispatchEvent(new FileLoadEvent(FileLoadEvent.COLLADA_MATERIALS_DONE));
			}
		}
		
		private function materialLoadErrorHandler(evt:FileLoadEvent):void {
			_materialsToLoad--;
			if(_materialsToLoad == 0){
				dispatchEvent(new FileLoadEvent(FileLoadEvent.COLLADA_MATERIALS_DONE));
			}
		}
		
		/**
		* Object チャンクの開始行を返します。
		* 見つからなかった場合には -1 を返します。
		*/
		private function getObjectChunkLine(lines:Array, startLine:int = 0):int {
			for (var i:uint = startLine; i < lines.length; ++i) {
				if (lines[i].indexOf("Object") == 0) {
					return int(i);
				}
			}
			return -1;
		}
		
		/**
		* Object チャンクを読み込み、その最後の行番号を返します。
		* エラーが起こった場合は -1 を返します。
		*/
		private function parseObjectChunk(lines:Array, startLine:int):int {
			//これから解析するObjectチャンクのオブジェクトを確保
			var obj:Metasequoia = new Metasequoia();
			obj._scale = this._scale;
			
			var l:int = getObjectChunkLine(lines, startLine);
			if (l == -1) {
				return -1;
			}
			
			// 解析中の行の文字列
			var line:String = lines[l];
			
			// オブジェクト名を取得
			var objectName:String = line.substring(8, line.indexOf("\"", 8));
			obj.name = objectName;
			++l;
			
			// vertex チャンクを検索
			var vline:int = getChunkLine(lines, "vertex", l);
			if (vline == -1) {
				return -1;
			}
			
			// プロパティを読み込む
			var properties:Dictionary = new Dictionary();
			for (; l < vline; ++l) {
				line = lines[l];
				var props:Array = RegExp(/^\s*([\w]+)\s+(.*)$/).exec(line);
				properties[props[1]] = props[2];
			}
			obj._depth = parseInt(properties["depth"] , 10) | 0;
			obj.visible = (properties["visible"] == "15");

			line = lines[l];
			l = vline + 1;

			//親オブジェクトを探す
			for(var i:Number = this._objectlist.length -1 ; i >= 0 ; i--) {
				var parentCandidate:Metasequoia = this._objectlist[i] as Metasequoia;
				if (parentCandidate) {
					//_depthが自分より1少ない→親オブジェクト
					if (parentCandidate._depth == (obj._depth - 1)) {
						//親に自分をaddChildさせる
						parentCandidate.addChild(obj);
						break;
					}
				}
			}

            var vertices:Array = obj.geometry.vertices ;
            var faces:Array = obj.geometry.faces;

			// 頂点数を取得
			var numVertices:int = parseInt(line.substring(line.indexOf("vertex") + 7));
			var vertexEndLine:int = l + numVertices;
	
			// vertex チャンクを読み込む
			for (; l < vertexEndLine; ++l) {
				line = lines[l];
				var coords:Array = line.match(/(-?\d+\.\d+)/g);
				var x:Number = parseFloat(coords[0]) * _scale;
				var y:Number = parseFloat(coords[1]) * _scale;
				var z:Number = -parseFloat(coords[2]) * _scale;
				vertices.push(new Vertex3D(x, y, z));
			}
			
			// face チャンクを検索
			l = getChunkLine(lines,  "face", l);
			if (l == -1) {
				return -1;
			}
			line = lines[l++];
			
			// 面数を取得
			var numFaces:int = parseInt(line.substring(line.indexOf("face") + 5));
			var faceEndLine:int = l + numFaces;
			
			// face チャンクを読み込む
			for (; l < faceEndLine; ++l) {
				parseFace(obj, lines[l], properties);
			}
			
			//オブジェクト一覧に追加する
            obj.geometry.ready = true;
			this._objectlist.push(obj);
			//次の行番号を返す
			return l;
		}
		
		private function parseFace(obj:Metasequoia , line:String, properties:Dictionary):void {
			var faces:Array = obj.geometry.faces;
			var vertices:Array = obj.geometry.vertices;
			var vstr:String = getParam(line, "V");
			var mstr:String = getParam(line, "M");
			var uvstr:String = getParam(line, "UV");
			
			var v:Array = (vstr != null) ? vstr.match(/\d+/g) : [];
			var uv:Array = (uvstr != null) ? uvstr.match(/-?\d+\.\d+/g) : [];
			var a:Vertex3D;
			var b:Vertex3D;
			var c:Vertex3D;
			var d:Vertex3D;
			var material:MaterialObject3D;
			var uvA:NumberUV;
			var uvB:NumberUV;
			var uvC:NumberUV;
			var uvD:NumberUV;
			var face:Triangle3D;
			
			if (v.length == 3) {
				c = vertices[parseInt(v[0])];
				b = vertices[parseInt(v[1])];
				a = vertices[parseInt(v[2])];
						
				if (mstr != null) {
					material = materials.getMaterialByName(_materialNames[parseInt(mstr)]);
				}

				if (uv.length != 0) {
					uvC = new NumberUV(parseFloat(uv[0]), 1 - parseFloat(uv[1]));
					uvB = new NumberUV(parseFloat(uv[2]), 1 - parseFloat(uv[3]));
					uvA = new NumberUV(parseFloat(uv[4]), 1 - parseFloat(uv[5]));
					face = new Triangle3D(obj, [a, b, c], material, [uvA, uvB, uvC]);
				} else {
					face = new Triangle3D(obj, [a, b, c], material,
						[new NumberUV(0, 0), new NumberUV(1, 0), new NumberUV(0, 1)]);
				}
				
				faces.push(face);
				
				if (properties["mirror"] == "1") {
					var mirrorAxis:int = parseInt(properties["mirror_axis"]);
					a = mirrorVertex(a, mirrorAxis);
					b = mirrorVertex(b, mirrorAxis);
					c = mirrorVertex(c, mirrorAxis);
					vertices.push(a);
					vertices.push(b);
					vertices.push(c);
					face = new Triangle3D(obj, [c, b, a], material, face.uv.reverse());
					faces.push(face);
				}
			} else if (v.length == 4) {
				d = vertices[parseInt(v[0])];
				c = vertices[parseInt(v[1])];
				b = vertices[parseInt(v[2])];
				a = vertices[parseInt(v[3])];
				
				if (mstr != null) {
					material = materials.getMaterialByName(_materialNames[parseInt(mstr)]);
				}

				if (uv.length != 0) {
					uvD = new NumberUV(parseFloat(uv[0]), 1 - parseFloat(uv[1]));
					uvC = new NumberUV(parseFloat(uv[2]), 1 - parseFloat(uv[3]));
					uvB = new NumberUV(parseFloat(uv[4]), 1 - parseFloat(uv[5]));
					uvA = new NumberUV(parseFloat(uv[6]), 1 - parseFloat(uv[7]));
				} else {
					uvD = new NumberUV(0, 0);
					uvC = new NumberUV(1, 0);
					uvB = new NumberUV(0, 1);
					uvA = new NumberUV(1, 1);
				}
				face = new Triangle3D(obj, [a, b, c], material, [uvA, uvB, uvC]);
				faces.push(face);
				face = new Triangle3D(obj, [c, d, a], material, [uvC, uvD, uvA]);
				faces.push(face);
				
				if (properties["mirror"] == "1") {
					mirrorAxis = parseInt(properties["mirror_axis"]);
					a = mirrorVertex(a, mirrorAxis);
					b = mirrorVertex(b, mirrorAxis);
					c = mirrorVertex(c, mirrorAxis);
					d = mirrorVertex(d, mirrorAxis);
					vertices.push(a);
					vertices.push(b);
					vertices.push(c);
					vertices.push(d);
					face = new Triangle3D(obj, [c, b, a], material, [uvC, uvB, uvA]);
					faces.push(face);
					face = new Triangle3D(obj, [a, d, c], material, [uvA, uvD, uvC]);
					faces.push(face);
				}
			}
		}
		
		/**
		* 頂点を軸に沿って反転させたものを返します。
		*/
		private static function mirrorVertex(v:Vertex3D, axis:int):Vertex3D {
			return new Vertex3D(
				((axis & 1) != 0) ? -v.x : v.x,
				((axis & 2) != 0) ? -v.y : v.y,
				((axis & 4) != 0) ? -v.z : v.z);
		}
		
		/**
		* Object チャンクの開始行を返します。
		*/
		private static function getChunkLine(lines:Array, chunkName:String, startLine:int = 0):int {
			for (var i:uint = startLine; i < lines.length; ++i) {
				if (lines[i].indexOf(chunkName) != -1) {
					return int(i);
				}
			}
			return -1;
		}
		
		/**
		* line 内で paramName(...) という形式で指定されているパラメータを返します。
		*/
		private static function getParam(line:String, paramName:String):String {
			var prefix:String = paramName + "(";
			var prefixLen:int = prefix.length;
			
			var begin:int = line.indexOf(prefix, 0);
			if (begin == -1) {
				return null;
			}
			var end:int = line.indexOf(")", begin + prefixLen);
			if (end == -1){
				return null;
			}
			return line.substring(begin + prefixLen, end);
		}
	}
}
