/**
 * PMDController for MikuMikuDance Model+Motion
 * モデルの変形制御
 *
 * @author b2ox
 */
package org.b2ox.pv3d.MikuMikuDance
{
	import org.b2ox.pv3d.*;
	import org.b2ox.pv3d.MikuMikuDance.*;
	import org.papervision3d.core.controller.*;
	import org.papervision3d.core.math.*;
	import org.papervision3d.objects.*;

	public class PMDController implements IObjectController
	{
		private var _model:MikuMikuDance;

		private var _vertex_count:int;
		private var _original_vertices:Vector.<Number3D> = null;
		private var _work_vertices:Vector.<Number3D> = null;
		private var _bone:Vector.<PMDBone>;
		private var _bindOfVerts:Vector.<VertexBinding>;
		private var _root_bone:Vector.<PMDBone>;
		private var _name2bone:Object;
		private var _IKbones:Vector.<PMDBone>;

		private var _skin:Vector.<PMDSkin>;
		private var _name2skin:Object;
		private var _baseSkin:PMDSkin = null;

		public function PMDController(model:MikuMikuDance)
		{
			_model = model;
			_bone = new Vector.<PMDBone>();
			_IKbones = new Vector.<PMDBone>();
			_skin = new Vector.<PMDSkin>();

			_root_bone = new Vector.<PMDBone>();
			_name2bone = new Object();
			_name2skin = new Object();
		}

		//------------------------------------
		/**
		 * IObjectControllerにおいて実装すべきメソッド
		 */ 
		public function update():void
		{
			resetVertices();
			effectSkins(_model.geometry.vertices);
			effectBones(_model.geometry.vertices);
		}

		/**
		 * モデルの頂点を初期状態に戻す
		 */
		public function resetVertices():void
		{
			for (var i:int = 0; i < _vertex_count; i++)
				Utils.copyVertex3DfromNumber3D(_model.geometry.vertices[i], _original_vertices[i]);
		}

		/**
		 * _work_verticesをゼロベクトルでクリアする
		 */
		private function clearWorkVertices():void
		{
			for (var i:int = 0; i < _vertex_count; i++) _work_vertices[i].reset();
		}

		/**
		 * _work_verticesの内容をtargetに反映する
		 * @param	target
		 */
		private function effectWorkVertices(target:Array):void
		{
			for (var i:int = 0; i < _vertex_count; i++)
				Utils.copyVertex3DfromNumber3D( target[i], _work_vertices[i]);
		}

		/**
		 * ボーンの位置・回転、表情のウェイトを初期状態に戻す
		 */
		public function resetParams():void 
		{
			resetBoneParams();
			resetSkinWeights();
		}

		/**
		 * ボーンの位置・回転を初期状態に戻す
		 */
		public function resetBoneParams():void 
		{
			for each (var bone:PMDBone in _bone) bone.reset();
		}

		/**
		 * 表情のウェイトを初期状態(=0)にする
		 */
		public function resetSkinWeights():void 
		{
			for each (var skin:PMDSkin in _skin) skin.weight = 0;
		}

		/**
		 * 指定タイプの表情のウェイトだけを初期状態(=0)にする
		 */
		public function resetSkinWeightsByType(skinType:int):void 
		{
			for each (var skin:PMDSkin in _skin) if (skin.type == skinType) skin.weight = 0;
		}

		//------------------------------------
		/**
		 * 頂点に対する影響ボーンの初期化
		 * @param	vertex_count
		 */
		public function initBind(vertex_count:int):void
		{
			_vertex_count = vertex_count;
			_bindOfVerts = new Vector.<VertexBinding>(vertex_count, true);
			_original_vertices = new Vector.<Number3D>(vertex_count, true);
			_work_vertices = new Vector.<Number3D>(vertex_count, true);
			for (var i:int; i < vertex_count; i++) _work_vertices[i] = new Number3D();
		}

		/**
		 * 初期状態の頂点を登録
		 * @param	i
		 * @param	x
		 * @param	y
		 * @param	z
		 */
		public function regVertex(i:int, x:Number, y:Number, z:Number):void
		{
			_original_vertices[i] = new Number3D(x,y,z);
		}

		/**
		 * 頂点に対する影響ボーンの登録
		 * @param	i
		 * @param	bone0
		 * @param	bone1
		 * @param	weight
		 * @param	edge
		 */
		public function regBind(i:int, bone0:int, bone1:int, weight:int, edge:int):void
		{
			_bindOfVerts[i] = new VertexBinding( bone0, bone1, weight, edge );
		}

		/**
		 * ボーンの追加
		 * @param	bone
		 */
		public function addBone(bone:PMDBone):void
		{
			_bone.push(bone);
			bone.mmd = _model;
			_name2bone[bone.name] = bone;
			if (bone.isRootBone) // ルートの場合
			{
				_root_bone.push(bone);
			} else {
				_bone[bone.parentID].addChildBone(bone);
			}
		}

		/**
		 * ボーンと影響頂点をバインド
		 */
		public function bindBones():void
		{
			var i:int;
			var hash:Object;
			var w:Number;
			// MMDでは各頂点に影響ボーン情報を付けているが、ボーンに影響頂点を対応づけるほうが計算しやすい?
			_bone.fixed = true;
			var bone_count:int = _bone.length;
			for (i = 0; i < _vertex_count; i++) {
				hash = _bindOfVerts[i];
				w = hash.weight / 100.0;
				_bone[hash.bone0].bindVertex(i, w);
				if (hash.bone1 < bone_count) _bone[hash.bone1].bindVertex(i, 1.0-w);
			}
			_bindOfVerts = null; // 計算後は要らないので削除
		}

		/**
		 * VMDcontrollerから利用する
		 * 
		 * @param	boneName
		 * @param	q
		 * @param	dv
		 */
		public function setVMDBoneParam(boneName:String, q:Quaternion, dv:Number3D):void
		{
			var bone:PMDBone = _name2bone[boneName];
			if (bone == null) return;
			if (q != null) bone.rotation = q;
			if (dv != null) bone.move(dv);
		}

		/**
		 * 現在のボーン状態に基づいてボーン変形を適用する
		 * 
		 * @param	target
		 */
		public function effectBones(target:Array=null):void
		{
			var bone:PMDBone;
			if (target == null) target = _model.geometry.vertices;
			for each (bone in _bone) bone.calcTransformWorld();
			for each (bone in _IKbones) bone.calcIK();
			clearWorkVertices()
			for each (bone in _bone) bone.effectBone(_work_vertices, target);
			effectWorkVertices(target);
		}

		/**
		 * IKボーン情報の登録
		 * 
		 * @param	boneID
		 * @param	iterations
		 * @param	weight
		 * @param	chain
		 */
		public function regIKparams(boneID:int, iterations:int, weight:Number, chain:Array):void
		{
			trace("IKbone: "+boneID+" "+_bone[boneID].name+" itr:"+iterations+" weight:"+weight+" chain:["+chain+"]]");
			_IKbones.push(_bone[boneID]);
			_bone[boneID].regIKparams(iterations, weight, chain.map(function (v:int, i:int, ary:Array):PMDBone { return _bone[v]; } ));
		}

		//------------------------------------
		/**
		 * 表情の追加
		 * @param	skin
		 */
		public function addSkin(skin:PMDSkin):void
		{
			if (skin.type == PMDSkin.TYPE_BASE)
			{
				if (_baseSkin != null) throw new Error("ベーススキンは1個しか登録できません。");
				_baseSkin = skin;
			} else if (_baseSkin == null) throw new Error("まずベーススキンを登録しないと一般のスキンは登録できません。");
			skin.calcTargetIndices(_baseSkin);
			_skin.push(skin);
			_name2skin[skin.name] = skin;
		}

		/**
		 * 表情のウェイトを設定
		 * @param	skinName
		 * @param	weight
		 */
		public function setSkinWeight(skinName:String, weight:Number):void
		{
			var skin:PMDSkin = _name2skin[skinName];
			if (skin != null) skin.weight = weight;
		}

		/**
		 * 表情の適用
		 * @param	target: 頂点配列
		 */
		public function effectSkins(target:Array=null):void
		{
			if (target == null) target = _model.geometry.vertices;

			var skin:PMDSkin;
			var flag:Boolean = true; // weightが全部0なら適用する必要なし
			for each (skin in _skin)
				if (skin.type != PMDSkin.TYPE_BASE && skin.weight > 0) { flag = false; break; }
			if (flag) return;

			_baseSkin.effect(target, null);
			for each (skin in _skin) skin.effectPlus(target);
		}

		//-------------------------------------------------
		/**
		 * ボーンにアクセサリを取り付ける
		 * @param	boneName
		 * @param	mdl
		 * @param	mdlName
		 * @return
		 */
		public function attachModel(boneName:String, mdl:DisplayObject3D, mdlName:String):Boolean
		{
			for each (var bone:PMDBone in _bone)
			{
				if ( bone.name == boneName )
				{
					bone.attachModel(mdl, mdlName);
					return true;
				}
			}
			return false;
		}
		/**
		 * ボーンからアクセサリを取り外す
		 * @param	boneName
		 * @param	mdlName
		 * @return
		 */
		public function removeModel(boneName:String, mdlName:String):void
		{
			for each (var bone:PMDBone in _bone)
			{
				if ( bone.name == boneName )
				{
					bone.removeModel(mdlName);
					return;
				}
			}
		}

		//-------------------------------------------------
		// 以下は主にデバッグ用
		public function skinTypeOf(skinName:String):int
		{
			var skin:PMDSkin = _name2skin[skinName];
			if (skin != null) return skin.type;
			return -1;
		}

		public function hasBone(boneName:String):Boolean { return (null != _name2bone[boneName]); }
		public function hasSkin(skinName:String):Boolean { return (null != _name2skin[skinName]); }

		public function showBone():void
		{
			for each (var bone:PMDBone in _bone) { bone.showCube(); bone.calcTransformWorld(); }
		}
	}
}

class VertexBinding
{
	public var bone0:int, bone1:int, weight:int, edge:int;
	public function VertexBinding(bone0:int, bone1:int, weight:int, edge:int)
	{
		this.bone0 = bone0;
		this.bone1 = bone1;
		this.weight = weight;
		this.edge = edge;
	}
}
