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

namespace MikuMikuDance.XNA.Motion
{
    /// <summary>
    /// アニメーションを再生する際の情報
    /// </summary>
    public struct AnimationStartInfo
    {
        /// <summary>
        /// ループプレイ
        /// </summary>
        public bool bLoopPlay;
        /// <summary>
        /// ループ再生時のワールド座標系調整処理フラグ
        /// </summary>
        public RepeatBehaviorFlag flag;
        /// <summary>
        /// 調整の基準とするボーン名
        /// </summary>
        public string boneName;
        /// <summary>
        /// 再生開始時のスムーシング処理を行うフラグ
        /// </summary>
        /// <remarks>このフラグをOnにすると各モーションの1つ目のキーフレームが現在のボーンにあわせて書き換えられます</remarks>
        public bool Smooth;
    }
    /// <summary>
    /// モーションファイルからアニメーションを再生するアニメーションプレイヤー
    /// </summary>
    public class AnimationPlayer
    {
        /// <summary>
        /// (内部用)モデルTransform用のボーン名
        /// </summary>
        public const string transBoneName = "__TRANS__";
        //親のモデル
        MMDReachModel 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];
        MultiMotionTrack[] mmdMMotion = new MultiMotionTrack[MikuMikuDanceXNA.MotionTrackCap];
        //Updateしたボーン
        bool[] BoneUpdated;
        //再生時に剛体リセットが指定され、剛体の更新が必要かどうかのフラグ
        bool NeedRigidUpdate = false;
        /// <summary>
        /// 一秒あたりのフレーム数
        /// </summary>
        public int FramePerSecond { get; set; }
        
        /// <summary>
        /// 親のモデルクラスからアニメーションプレイヤーを生成
        /// </summary>
        /// <param name="model">親のモデルクラス</param>
        internal AnimationPlayer(MMDReachModel model)//内部からのみ
        {
            for (int i = 0; i < MikuMikuDanceXNA.MotionTrackCap; i++)
            {
                mmdNMotion[i] = new NormalMotionTrack();
                mmdNMotion[i].TrackNum = i;
                mmdMotion[i] = mmdNMotion[i];
                mmdBMotion[i] = new BakedMotionTrack();
                mmdBMotion[i].TrackNum = i;
                mmdMMotion[i] = new MultiMotionTrack();
                mmdMMotion[i].TrackNum = i;
            }
            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);
        }
        /// <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);
        }
        /// <summary>
        /// 指定したトラックにモーションをセット
        /// </summary>
        /// <param name="index">トラック番号(0～MikuMikuDanceXNA.MotionTrackCap)</param>
        /// <param name="motions">セットするマルチモーションデータ</param>
        /// <param name="SetStartPos">0フレームのデータでポーズを初期化する</param>
        /// <remarks>マルチモーションデータはモーションを幾つか繋げて1セットとしたもの</remarks>
        public void SetMotion(int index, MultiMotionInfo[] motions, bool SetStartPos)
        {
            mmdMMotion[index].Clear();
            mmdMMotion[index].Setup(motions);
            mmdMotion[index] = mmdMMotion[index];
            if (SetStartPos)
                Update(index, true);
        }
        /// <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 ClearAllMotion()
        {
            for (int i = 0; i < mmdNMotion.Length; i++)
            {
                mmdNMotion[i].Clear();
                mmdMotion[i] = mmdNMotion[i];
            }
        }
        /// <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="bLoopPlay">ループプレイ</param>
        public void Start(int index, bool bLoopPlay)
        {
            Start(index, new AnimationStartInfo { bLoopPlay = bLoopPlay, flag = RepeatBehaviorFlag.ResetRigid, boneName = "センター", Smooth = false });
        }
        /// <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, new AnimationStartInfo { bLoopPlay = bLoopPlay, flag = flag, boneName = "センター", Smooth = false });
        }
        /// <summary>
        /// モーションを再生する
        /// </summary>
        /// <param name="index">モーショントラック番号</param>
        /// <param name="startinfo">再生時の情報</param>
        public void Start(int index, AnimationStartInfo startinfo)
        {
            if (mmdMotion[index].IsEmpty)
                throw new ApplicationException("モーションデータがセットされてないのにStartを実行した");
            motionBehavior[index].Init(startinfo.flag, startinfo.boneName, mmdModel.BoneManager);
            mmdMotion[index].Start();
            mmdMotion[index].IsLoopPlay = startinfo.bLoopPlay;
            if ((startinfo.flag & RepeatBehaviorFlag.ResetRigid) != 0)
                NeedRigidUpdate = true;
            if (startinfo.Smooth)
                mmdMotion[index].Smooth(mmdModel.BoneManager);
        }
        /// <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="index">トラック番号</param>
        /// <param name="frame">シーク位置</param>
        public void Seek(int index, decimal frame)
        {
            mmdMotion[index].Seek(frame);
        }
        /// <summary>
        /// リバースフラグ設定
        /// </summary>
        /// <param name="index">トラック番号</param>
        /// <param name="bReverse">リバース設定フラグ(trueなら逆再生)</param>
        public void SetReverse(int index, bool bReverse)
        {
            mmdMotion[index].Reverse = bReverse;
        }
        /// <summary>
        /// リバースフラグ取得
        /// </summary>
        /// <param name="index">トラック番号</param>
        /// <returns>リバース設定フラグ(trueなら逆再生)</returns>
        public bool GetReverse(int index)
        {
            return mmdMotion[index].Reverse;
        }
        /// <summary>
        /// アニメーションの更新
        /// </summary>
        /// <param name="motionIndex">更新モーション番号。-1なら全て更新</param>
        /// <param name="ForcedUpdate">再生していなくても強制更新するフラグ</param>
        /// <remarks>モデルのUpdateから呼ばれるので、ユーザーが呼ぶ必要はありません</remarks>
        internal bool Update(int motionIndex, bool ForcedUpdate)
        {
            bool result = false;
            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))
                    {//ループ再生時のワールド座標系調整処理
                        if ((motionBehavior[i].Flag & RepeatBehaviorFlag.ModifyByMove) != 0)
                        {
                            decimal NowFrame = mmdMotion[i].NowFrame;//現在のフレームを退避
                            mmdMotion[i].NowFrame = mmdMotion[i].MaxFrame;//最終フレームで調整
                            mmdMotion[i].ApplyMotion(mmdModel.BoneManager, mmdModel.FaceManager, mmdModel.mmdXNA,ref mmdModel.Transform, 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.Transform *= new QuatTransform(rot, new Vector3(x, y, z));
                            //フレームを元に戻す
                            mmdMotion[i].NowFrame = NowFrame;
                        }
                        if ((motionBehavior[i].Flag & RepeatBehaviorFlag.ResetRigid) != 0)
                            result = true;
                    }
                    mmdMotion[i].ApplyMotion(mmdModel.BoneManager, mmdModel.FaceManager,mmdModel.mmdXNA, ref mmdModel.Transform, ref BoneUpdated);
                }
            }

            //IKボーンの処理前にモデルのワールド座標系更新
            //mmdModel.BoneManager.UpdateWorldTransforms();
            //IKボーンの処理
            foreach (var i2 in IKBones)
            {
                if (BoneUpdated[i2])
                    mmdModel.BoneManager.SolveIK(i2, mmdModel.BoneManager.Bones[i2].BoneTransform);
            }
            if (NeedRigidUpdate)
            {
                result = true;
                NeedRigidUpdate = false;
            }
            //こちらではボーンマネージャのUpdateは行わない
            return result;
        }
        /// <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();
        }
        
    }
}
