using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MikuMikuDance.XNA.Model;
using MikuMikuDance.XNA.Model.ModelData;
using System.Threading;

namespace MikuMikuDance.XNA.Motion
{
    
    /// <summary>
    /// モーションファイルからアニメーションを再生するアニメーションプレイヤー
    /// </summary>
    public class AnimationPlayer
    {
        //親のモデル
        MMDModel mmdModel;
        int[] IKBones;//あらかじめ処理済みのIKボーン一覧
        //現在のモーション一覧
        MotionTrack[] mmdMotion = new MotionTrack[MikuMikuDanceXNA.MotionTrackCap];
        RepeatBehavior[] motionBehavior = new RepeatBehavior[MikuMikuDanceXNA.MotionTrackCap];
        //オブジェクトプール...何で要るの……(XBoxのGCのせいだ)
        NormalMotionTrack[] mmdNMotion = new NormalMotionTrack[MikuMikuDanceXNA.MotionTrackCap];
        BakedMotionTrack[] mmdBMotion = new BakedMotionTrack[MikuMikuDanceXNA.MotionTrackCap];
        //Updateしたボーン
        bool[] BoneUpdated;
        /// <summary>
        /// 一秒あたりのフレーム数
        /// </summary>
        public int FramePerSecond { get; set; }
        
        /// <summary>
        /// 親のモデルクラスからアニメーションプレイヤーを生成
        /// </summary>
        /// <param name="model">親のモデルクラス</param>
        internal AnimationPlayer(MMDModel model)//内部からのみ
        {
            for (int i = 0; i < MikuMikuDanceXNA.MotionTrackCap; i++)
            {
                mmdNMotion[i] = new NormalMotionTrack();
                mmdMotion[i] = mmdNMotion[i];
                mmdBMotion[i] = new BakedMotionTrack();
            }
            mmdModel = model;
            BoneUpdated = new bool[mmdModel.BoneManager.Bones.Length];
            FramePerSecond = 30;//MMDは30フレーム/秒が標準
            //IKボーン探し
            List<int> Iks = new List<int>();
            for (int i = 0; i < model.BoneManager.Bones.Length; i++)
            {
                if (model.BoneManager.Bones[i].BoneData.HasIK)
                    Iks.Add(i);
            }
            IKBones = Iks.ToArray();
        }

        /// <summary>
        /// 開いているトラックにモーションをセット
        /// </summary>
        /// <param name="motion">セットするMMDモーションデータ</param>
        /// <param name="SetStartPos">0フレームのデータでポーズを初期化する</param>
        /// <returns>セットしたトラックの番号。開いているトラックが無ければ-1</returns>
        public int SetMotion(MMDMotion motion, bool SetStartPos)
        {
            int Index = -1;
            for (Index = 0; Index < MikuMikuDanceXNA.MotionTrackCap; Index++)
                if (mmdMotion[Index].IsEmpty)
                    break;
            if (Index >= MikuMikuDanceXNA.MotionTrackCap)
                return -1;
            SetMotion(Index, motion, SetStartPos);
            return Index;
        }
        /// <summary>
        /// 指定したトラックにモーションをセット
        /// </summary>
        /// <param name="index">トラック番号(0～MikuMikuDanceXNA.MotionTrackCap)</param>
        /// <param name="motion">セットするMMDモーションデータ</param>
        /// <param name="SetStartPos">0フレームのデータでポーズを初期化する</param>
        public void SetMotion(int index, MMDMotion motion, bool SetStartPos)
        {
            //オブジェクトプールのモーショントラックにデータをセット
            mmdNMotion[index].Clear();
            mmdNMotion[index].Motion = motion;
            mmdNMotion[index].IsEmpty = false;
            mmdMotion[index] = mmdNMotion[index];//オブジェクトプールから引っ張ってくる
            if (SetStartPos)
                Update(index, true);
            mmdModel.physics.ResetRigid(mmdModel);
        }
        /// <summary>
        /// 開いているトラックにモーションをセット
        /// </summary>
        /// <param name="motion">セットするベイク済みMMDモーションデータ</param>
        /// <param name="SetStartPos">0フレームのデータでポーズを初期化する</param>
        /// <returns>セットしたトラックの番号。開いているトラックが無ければ-1</returns>
        /// <remarks>ベイク済みモーションはベイクに使用したモデルのみ使用可能。それ以外はボーンが同じ構造なら動作するかも……？</remarks>
        public int SetMotion(MMDBakedMotion motion, bool SetStartPos)
        {
            int Index = -1;
            for (Index = 0; Index < MikuMikuDanceXNA.MotionTrackCap; Index++)
                if (mmdMotion[Index].IsEmpty)
                    break;
            if (Index >= MikuMikuDanceXNA.MotionTrackCap)
                return -1;
            SetMotion(Index, motion, SetStartPos);
            return Index;
        }
        /// <summary>
        /// 指定したトラックにモーションをセット
        /// </summary>
        /// <param name="index">トラック番号(0～MikuMikuDanceXNA.MotionTrackCap)</param>
        /// <param name="motion">セットするベイク済みMMDモーションデータ</param>
        /// <param name="SetStartPos">0フレームのデータでポーズを初期化する</param>
        /// <remarks>ベイク済みモーションはベイクに使用したモデルのみ使用可能。それ以外はボーンが同じ構造なら動作するかも……？</remarks>
        public void SetMotion(int index, MMDBakedMotion motion, bool SetStartPos)
        {
            //オブジェクトプールのモーショントラックにデータをセット
            mmdBMotion[index].Clear();
            mmdBMotion[index].Motion = motion;
            mmdBMotion[index].IsEmpty = false;
            mmdMotion[index] = mmdBMotion[index];//オブジェクトプールから引っ張ってくる
            if (SetStartPos)
                Update(index, true);
            mmdModel.physics.ResetRigid(mmdModel);
        }
        /// <summary>
        /// 指定したトラックからベイク済みモーションを取得
        /// </summary>
        /// <param name="index">トラック番号(0～MikuMikuDanceXNA.MotionTrackCap)</param>
        /// <returns>ベイク済みMMDモーションデータ</returns>
        public MMDBakedMotion GetBakedMotion(int index)
        {
            BakedMotionTrack bMotion = mmdMotion[index] as BakedMotionTrack;
            if (bMotion == null)
                return null;
            return bMotion.Motion;
        }
        /// <summary>
        /// モーションをストップする
        /// </summary>
        public void Stop(int index)
        {
            mmdMotion[index].Stop();
        }
        /// <summary>
        /// 全てのモーションをストップする
        /// </summary>
        public void StopAll()
        {
            for (int i = 0; i < mmdMotion.Length; i++)
            {
                Stop(i);
            }
        }
        /// <summary>
        /// モーションを再生する
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <param name="gameTime">GameTimeオブジェクト</param>
        /// <param name="bLoopPlay">ループプレイ</param>
        [Obsolete()]
        public void Start(int index, GameTime gameTime, bool bLoopPlay)
        {
            Start(index, bLoopPlay, RepeatBehaviorFlag.None,"センター");
        }
        /// <summary>
        /// モーションを再生する
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <param name="bLoopPlay">ループプレイ</param>
        public void Start(int index, bool bLoopPlay)
        {
            Start(index,  bLoopPlay, RepeatBehaviorFlag.None, "センター");
        }
        /// <summary>
        /// モーションを再生する
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <param name="bLoopPlay">ループプレイ</param>
        /// <param name="flag">ループ再生時のワールド座標系調整処理フラグ</param>
        /// <remarks>ループ再生時にセンターボーンの移動量/回転量でモデルの座標系を修正します</remarks>
        public void Start(int index,  bool bLoopPlay, RepeatBehaviorFlag flag)
        {
            Start(index,  bLoopPlay, flag, "センター");
        }
        /// <summary>
        /// モーションを再生する
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <param name="bLoopPlay">ループプレイ</param>
        /// <param name="flag">ループ再生時のワールド座標系調整処理フラグ</param>
        /// <param name="boneName">調整の基準とするボーン名</param>
        /// <remarks>ループ再生時に指定ボーンの移動量/回転量でモデルの座標系を修正します</remarks>
        public void Start(int index, bool bLoopPlay, RepeatBehaviorFlag flag, string boneName)
        {
            if (mmdMotion[index].IsEmpty)
                throw new ApplicationException("モーションデータがセットされてないのにStartを実行した");
            motionBehavior[index].Init(flag, boneName, mmdModel.BoneManager);
            mmdMotion[index].Start();
            mmdMotion[index].IsLoopPlay = bLoopPlay;
        }
        /// <summary>
        /// 全てのモーションを再生する
        /// </summary>
        /// <param name="gameTime">GameTimeオブジェクト</param>
        /// <param name="bLoopPlay">ループプレイ</param>
        [Obsolete()]
        public void StartAll(GameTime gameTime, bool bLoopPlay)
        {
            StartAll(bLoopPlay);
        }
        /// <summary>
        /// 全てのモーションを再生する
        /// </summary>
        /// <param name="bLoopPlay">ループプレイ</param>
        public void StartAll(bool bLoopPlay)
        {
            for (int i = 0; i < mmdMotion.Length; i++)
            {
                if (mmdMotion[i].IsEmpty)
                    continue;
                Start(i, bLoopPlay);
            }
        }
        /// <summary>
        /// モーションをリセットする
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        public void Reset(int index)
        {
            mmdMotion[index].Reset();
        }
        /// <summary>
        /// モーションの再生状態を取得
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <returns>再生中ならtrue</returns>
        public bool IsPlay(int index)
        {
            return (mmdMotion[index].IsPlay);
        }
        /// <summary>
        /// モーションがループプレイかどうかを調べる
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <returns>ループプレイならtrue</returns>
        public bool IsLoopPlay(int index)
        {
            return mmdMotion[index].IsLoopPlay;
        }

        /// <summary>
        /// モーションのフレーム数を取得
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <returns>モーションフレーム数</returns>
        public long GetMaxFrame(int index)
        {
            return mmdMotion[index].MaxFrame;
        }
        /// <summary>
        /// モーションのフレーム数を設定
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <param name="NewValue">モーションフレーム数</param>
        public void SetMaxFrame(int index, long NewValue)
        {
            mmdMotion[index].MaxFrame = NewValue;
        }
        /// <summary>
        /// 現在の再生フレームを取得
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <returns>現在の再生フレーム</returns>
        public decimal GetNowFrame(int index)
        {
            return mmdMotion[index].NowFrame;
        }

        /// <summary>
        /// アニメーションの更新
        /// </summary>
        /// <param name="motionIndex">更新モーション番号。-1なら全て更新</param>
        /// <param name="ForcedUpdate">再生していなくても強制更新するフラグ</param>
        /// <remarks>モデルのUpdateから呼ばれるので、ユーザーが呼ぶ必要はありません</remarks>
        internal void Update(int motionIndex, bool ForcedUpdate)
        {
            Array.Clear(BoneUpdated, 0, BoneUpdated.Length);
            for (int i = (motionIndex < 0 ? 0 : motionIndex); i < (motionIndex < 0 ? mmdMotion.Length : motionIndex + 1); i++)
            {
                //再生中かどうかチェック
                if (!mmdMotion[i].IsEmpty && (mmdMotion[i].IsPlay || ForcedUpdate))
                {
                    //現在の再生フレームを更新
                    if (mmdMotion[i].UpdateFrame(mmdModel.Player.FramePerSecond) && (motionBehavior[i].Flag & RepeatBehaviorFlag.ModifyByMove) != 0)
                    {//ループ再生時のワールド座標系調整処理
                        decimal NowFrame = mmdMotion[i].NowFrame;//現在のフレームを退避
                        mmdMotion[i].NowFrame = mmdMotion[i].MaxFrame;//最終フレームで調整
                        mmdMotion[i].ApplyMotion(mmdModel.BoneManager, mmdModel.FaceManager, ref BoneUpdated);
                        Matrix targetBone;
                        targetBone = mmdModel.BoneManager.Bones[motionBehavior[i].BoneIndex].BoneTransform.CreateMatrix() * Matrix.Invert(mmdModel.BoneManager.Bones[motionBehavior[i].BoneIndex].BoneData.BindPose.CreateMatrix());//修正対象のボーン座標を取得
                        Vector3 temp1, temp2;
                        //座標系の修正
                        float x = 0, y = 0, z = 0;
                        Quaternion rot = Quaternion.Identity;
                        if ((motionBehavior[i].Flag & RepeatBehaviorFlag.ModifyByX) == RepeatBehaviorFlag.ModifyByX)
                            x = targetBone.Translation.X;
                        if ((motionBehavior[i].Flag & RepeatBehaviorFlag.ModifyByY) == RepeatBehaviorFlag.ModifyByY)
                            y = targetBone.Translation.Y;
                        if ((motionBehavior[i].Flag & RepeatBehaviorFlag.ModifyByZ) == RepeatBehaviorFlag.ModifyByZ)
                            z = targetBone.Translation.Z;
                        if ((motionBehavior[i].Flag & RepeatBehaviorFlag.ModifyByR) == RepeatBehaviorFlag.ModifyByR)
                            targetBone.Decompose(out temp1, out rot, out temp2);
                        mmdModel.World *= Matrix.CreateFromQuaternion(rot) * Matrix.CreateTranslation(x, y, z);
                        //フレームを元に戻す
                        mmdMotion[i].NowFrame = NowFrame;
                    }
                    mmdMotion[i].ApplyMotion(mmdModel.BoneManager, mmdModel.FaceManager, ref BoneUpdated);
                }
            }

            //IKボーンの処理前にモデルのワールド座標系更新
            //mmdModel.BoneManager.UpdateWorldTransforms();
            //IKボーンの処理
            foreach (var i2 in IKBones)
            {
                if (BoneUpdated[i2])
                    mmdModel.BoneManager.SolveIK(i2, mmdModel.BoneManager.Bones[i2].BoneTransform);
            }
            /*for (int i2 = 0; i2 < mmdModel.BoneManager.Bones.Length; i2++)
            {
                if (mmdModel.BoneManager.Bones[i2].BoneData.HasIK && BoneUpdated[i2])
                    mmdModel.BoneManager.SolveIK(i2, mmdModel.BoneManager.Bones[i2].BoneTransform);
            }*/
            //こちらではボーンマネージャのUpdateは行わない
        }
        /// <summary>
        /// モーションをベイクする
        /// </summary>
        /// <param name="index">ベイクするモーションのインデックス番号</param>
        /// <remarks>ベイクは予め補完計算、IK計算を行い、処理速度を向上させる</remarks>
        public void BakeMotion(int index)
        {
            //空orベイク済みなら処理を行わない
            if (mmdMotion[index].Type == TrackType.BakedTrack || mmdMotion[index].IsEmpty)
                return;
            //トラック準備
            NormalMotionTrack normal = mmdMotion[index] as NormalMotionTrack;


            //退避用の配列にボーン情報と表情を退避
            QuatTransform[] PopedBones = new QuatTransform[mmdModel.BoneManager.Bones.Length];
            for (int i = 0; i < mmdModel.BoneManager.Bones.Length; i++)
            {
                PopedBones[i] = mmdModel.BoneManager.Bones[i].BoneTransform;
            }
            Vector4[] PopedFaces = new Vector4[mmdModel.FaceManager.FaceRates.Length];
            Array.Copy(mmdModel.FaceManager.FaceRates, PopedFaces, PopedFaces.Length);
            for (int i = 0; i < mmdModel.FaceManager.FaceRates.Length; i++)
                mmdModel.FaceManager.FaceRates[i].X = -1;

            //AnimationBakerに処理を投げる
#if XBOX360
            int NumThread = MikuMikuDanceXNA.NumThread;
#else
            int NumThread = mmdModel.mmdXNA.NumThread;
#endif
            MMDBakedMotion bakedData = AnimationBaker.Bake(NumThread, normal, mmdModel.BoneManager, mmdModel.FaceManager);
            BakedMotionTrack result = new BakedMotionTrack();
            result.Motion = bakedData;


            result.IsEmpty = false;
            //ベイク済みモーションを再セット
            mmdMotion[index] = result;
            //退避させておいたボーン情報を回復
            for (int i = 0; i < mmdModel.BoneManager.Bones.Length; i++)
            {
                mmdModel.BoneManager.Bones[i].BoneTransform = PopedBones[i];
            }
            Array.Copy(PopedFaces, mmdModel.FaceManager.FaceRates, PopedFaces.Length);
            mmdModel.BoneManager.UpdateWorldTransforms();
        }
        
    }
}
