/**
* PMDBone for MikuMikuDance Model+Motion
* ボーン情報の格納とボーン変形の計算など
*
* @author b2ox
*/
package org.b2ox.pv3d.MikuMikuDance
{
	import org.b2ox.pv3d.*;
	import org.papervision3d.*;
	import org.papervision3d.core.math.*;
	import org.papervision3d.events.*;
	import org.papervision3d.materials.*;
	import org.papervision3d.materials.utils.*;
	import org.papervision3d.objects.*;
	import org.papervision3d.objects.primitives.*;

	public class PMDBone
	{
		private var _boneName:String, _parentID:int, _tailID:int, _boneType:int;
		private var _ikParentID:int;
		private var _translation:Number3D;
		private var _translation_orig:Number3D;
		private var _translation_world:Number3D;
		private var _rotation:Quaternion;
		private var _rotation_world:Quaternion;
		private var _bindVerts:Vector.<BindingInfo>;
		private var _parentBone:PMDBone;
		private var _children:Vector.<PMDBone>;
		private var _isRootBone: Boolean;
		private var _isKnee: Boolean;
		private var _mmd: MikuMikuDance;
		private var _attachment: DisplayObject3D;

		private var tmpQ:Quaternion;
		private var tmpV1:Number3D, tmpV2:Number3D, tmpV3:Number3D;

		public function PMDBone(bone_name:String, parent_id:int, tail_id:int, bone_type:int, ik_parent_id:int):void
		{
			_boneName = bone_name;
			_parentID = parent_id; 
			_tailID = tail_id;
			_boneType = bone_type;
			_ikParentID = ik_parent_id;
			_isKnee = bone_name == "右ひざ" || bone_name == "左ひざ";
			_isRootBone = parent_id == 65535; // parent_idが65535はルートボーン

			_translation = new Number3D();
			_translation_orig = new Number3D();
			_translation_world = new Number3D();

			_rotation = new Quaternion();
			_rotation_world = new Quaternion();

			_attachment = new DisplayObject3D();

			_bindVerts = new Vector.<BindingInfo>();
			_children = new Vector.<PMDBone>();
			_parentBone = null;
			tmpQ = new Quaternion();
			tmpV1 = new Number3D();
			tmpV2 = new Number3D();
			tmpV3 = new Number3D();
		}

		//-------------------------------------------------
		// 各種プロパティ
		public function get name():String { return _boneName; }
		public function get parentID():int { return _parentID; }
		public function get isRootBone():Boolean { return _isRootBone; }
		public function get translation():Number3D { return _translation; }
		public function get translationWorld():Number3D { return _translation_world; }
		public function set rotation(q:Quaternion):void { Utils.copyQuaternion(_rotation, q); }
		public function get rotation():Quaternion { return _rotation; }
		public function get rotationWorld():Quaternion { return _rotation_world; }

		public function set mmd(m:MikuMikuDance):void { _mmd = m; _mmd.addChild(_attachment); }

		//-------------------------------------------------
		/**
		 * 初期位置の設定
		 * @param	x
		 * @param	y
		 * @param	z
		 */
		public function initTranslation(x:Number, y:Number, z:Number):void
		{
			_translation.reset(x,y,z);
			_translation_orig.reset(x, y, z);
		}

		/**
		 * 初期位置からの変位を設定
		 */
		public function move(dv:Number3D):void
		{
			_translation.copyFrom(_translation_orig);
			_translation.plusEq(dv);
		}

		/**
		 * 初期位置・回転に戻す
		 */
		public function reset():void
		{
			_translation.copyFrom(_translation_orig);
			Utils.resetQuaternion(_rotation,0,0,0,1);
		}

		//-------------------------------------------------
		/**
		 * 子ボーンを追加
		 * @param	bone
		 */
		public function addChildBone(bone:PMDBone):void
		{
			bone._parentBone = this;
			_children.push(bone);
		}

		/**
		 * 現在のボーンに頂点をバインドする
		 * @param	vertexID
		 * @param	weight
		 */
		public function bindVertex(vertexID:int, weight:Number):void
		{
			_bindVerts.push(new BindingInfo(vertexID, weight));
		}

		/**
		 * ボーンのワールド座標での位置・回転を計算する
		 * 親ボーンから順に計算する必要有り。ただし、PMDでは親ボーンから順に並んで格納されているので特に気にする必要なし
		 */
		public function calcTransformWorld():void
		{
			if (_isRootBone)
			{
				Utils.copyQuaternion(_rotation_world, _rotation);
				_translation_world.copyFrom(_translation);
			} else {
				Utils.copyQuaternion(_rotation_world, _rotation);
				_rotation_world.mult(_parentBone._rotation_world);
				_rotation_world.normalize();

				_translation_world.copyFrom(_translation);
				// 第二引数を _parentBone._translation にしてしまうとボーンの変位がおかしくなる
				Utils.transformVector(_translation_world, _parentBone._translation_orig, _parentBone._translation_world, _parentBone._rotation_world);
			}
			_attachment.position = _translation_world;
			Utils.eulerOfQuaternion(tmpV1, _rotation_world);
			var b:Boolean = Papervision3D.useDEGREES;
			Papervision3D.useDEGREES = false;
			_attachment.rotationX = tmpV1.x;
			_attachment.rotationY = tmpV1.y;
			_attachment.rotationZ = tmpV1.z;
			Papervision3D.useDEGREES = b;
		}

		/**
		 * ボーン変形を影響頂点に適用する
		 * 
		 * @param	target
		 * @param	source
		 */
		public function effectBone(target:Vector.<Number3D>, source:Array):void
		{
			var transformer:Function = Utils.getVectorTransformer(_translation, _translation_world, _rotation_world);
			for each (var obj:Object in _bindVerts)
				transformer(target[obj.vertexID], source[obj.vertexID], obj.weight);
		}

		//-------------------------------------------------
		// IK計算用
		private var _IKiterations:int = 0;
		private var _IKweight:Number = 0;
		private var _IKchain:Array = null;
		private const AproxEpsilon:Number = 0.00001;
		private const LimitAngle:Number = 0.002;
		private var _isIKbone:Boolean = false;
		private var _isIKeffector:Boolean = false;
		private var _isIKchain:Boolean = false;
		public function regIKparams(iterations:int, weight:Number, chain:Array):void
		{
			_IKiterations = iterations;
			_IKweight = weight * Math.PI;
			_IKchain = chain;
			_isIKbone = true;
			_IKchain[0]._isIKeffector = true;
			for each (var bone:PMDBone in chain) bone._isIKchain = true;
		}

		/**
		 * IK計算(CCD-IKのつもり)
		 */
		public function calcIK():void
		{
			if (_isIKbone != true) return; // IKボーンでなければ無視
			var effector:PMDBone = _IKchain[0];
			var n:int = _IKchain.length;
			for (var k:int = 0; k < _IKiterations; k++)
			{
				for (var i:int = 1; i < n; i++)
				{
					// _translation_worldが目標点
					tmpV1.copyFrom(_translation_world);
					tmpV1.minusEq(effector._translation_world);
					if (tmpV1.isModuloLessThan(AproxEpsilon)) return; // エフェクタが十分近ければ終了

					var bone:PMDBone = _IKchain[i];

					// tmpV1: boneからeffectorへの単位ベクトル
					tmpV1.copyFrom(effector._translation_world);
					tmpV1.minusEq(bone._translation_world);
					tmpV1.normalize();

					// tmpV2: boneからIKboneへの単位ベクトル
					tmpV2.copyFrom(_translation_world);
					tmpV2.minusEq(bone._translation_world);
					tmpV2.normalize();

					// theta: tmpV1,tmpV2のなす角
					var theta:Number = Math.acos(Number3D.dot(tmpV1, tmpV2));
					if (AproxEpsilon < Math.abs(theta)) theta = (theta < -_IKweight) ? -_IKweight : ((theta > _IKweight) ? _IKweight : theta);

					// tmpV3 = tmpV1 x tmpV2
					Number3D.cross(tmpV1, tmpV2, tmpV3);
					if (Math.abs(tmpV3.modulo) < AproxEpsilon) continue;
					Utils.applyQuaternionLeft(tmpV3, bone._rotation_world);
					tmpV3.normalize();

					// tmpQ: tmpV1からtmpV2への回転
					tmpQ.setFromAxisAngle(tmpV3.x, tmpV3.y, tmpV3.z, theta);
					bone._rotation.mult(tmpQ);
					bone._rotation.normalize();

					// 膝関節では角度制限する
					if (bone._isKnee)
					{
						Utils.eulerOfQuaternion(tmpV1, bone._rotation);
						theta = tmpV1.z;
						theta = (theta > Math.PI) ? Math.PI : ((theta < LimitAngle) ? LimitAngle : theta);
						bone._rotation.setFromEuler(0, 0, theta)
					}

					// 先端方向に向かって回転の再計算
					for (var j:int = i; j >= 0; j--) _IKchain[j].calcTransformWorld();
				}
			}
		}

		//-------------------------------------------------
		/**
		 * ボーンにアクセサリを取り付ける
		 * @param	mdl
		 * @param	mdlName
		 */
		public function attachModel(mdl:DisplayObject3D, mdlName:String):void
		{
			_attachment.addChild(mdl, mdlName);
		}
		/**
		 * ボーンかアクセサリを取り外す
		 * @param	mdlName
		 */
		public function removeModel(mdlName:String):void
		{
			_attachment.removeChildByName(mdlName);
		}

		//-------------------------------------------------
		// デバッグ用
		public function showCube():void
		{
			var col:int = (255 << 16) | (255 << 8) | 255
			var size:int = 0;
			if (_isIKbone) { col = (100 << 16) | (250 << 8) | 100; size = 20; }
			if (_isIKchain) { col = (100 << 16) | (100 << 8) | 250; size = 20; }
			if (_isIKeffector) { col = (250 << 16) | (100 << 8) | 100; size = 20; }
			if (size == 0) return;
			var cube:Cube = new Cube(new MaterialsList( { all: new ColorMaterial(col,1,true) } ), size, size, size);
			attachModel(cube, "cube:" + _boneName);
			// クリックしたらボーン情報をtraceで出力
			function clicked(ev:InteractiveScene3DEvent):void {
				trace("Clicked: " + _boneName + " (parent: " + (_parentBone == null ? "none" : _parentBone.name) + ")");
				trace("   rot: " + _rotation + " trans: " + _translation + " init: " + _translation_orig);
				trace("   global rot: " + _rotation_world + " trans: " + _translation_world);
				trace("   (tailID, IKparentID, boneType) = (" + [_tailID, _ikParentID, _boneType] + ")");
			}
			cube.addEventListener(InteractiveScene3DEvent.OBJECT_CLICK, clicked );
		}
	}
}

class BindingInfo
{
	public var vertexID:int;
	public var weight:Number;
	public function BindingInfo(vertexID:int, weight:Number):void
	{
		this.vertexID = vertexID;
		this.weight = weight;
	}
}