using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

using TInput = Microsoft.Xna.Framework.Content.Pipeline.Graphics.NodeContent;
using TOutput = System.Collections.Generic.Dictionary<string, MikuMikuDance.XNA.Motion.MotionData.MMDMotionData>;
using MikuMikuDance.XNA.Motion.MotionData;
using System.Diagnostics;

namespace MikuMikuDance.XNA.Motion
{
    /// <summary>
    /// モデル付属のモーションデータデータをMMDモーションとしてXNAにインポートするためのプロセッサ
    /// </summary>
    /// <remarks>MMDXではLoadModelMotionで読み出す</remarks>
    [ContentProcessor(DisplayName = "モデルのモーション用プロセッサ : MikuMikuDance for XNA")]
    public class MMDModelMotionProcessor : ContentProcessor<TInput, TOutput>
    {

        /// <summary>
        /// コンテンツ変換
        /// </summary>
        /// <param name="input">モデルモーションデータ</param>
        /// <param name="context">コンテントプロセッサ</param>
        /// <returns>MMDモーションデータ for XNA</returns>
        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            ValidateMesh(input, context, null);

            // スケルトンを探す
            BoneContent skeleton = MeshHelper.FindSkeleton(input);

            if (skeleton == null)
                throw new InvalidContentException("スケルトンが見つかりません");

            // モデル内のメッシュがそれぞれ違うローカル座標を持っていると扱いが
            // 面倒なので、全部焼いちゃう(メッシュの変換座標をあらかじめ頂点
            // データに適用すること)
            FlattenTransforms(input, skeleton);

            // バインド・ポーズとスケルトン構造を読み込む
            IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);
            
            //メッシュからアニメーション情報の検索
            TOutput result;

            result = ProcessAnimations(skeleton.Animations, bones);


            return result;
        }

        /// <summary>
        /// アニメーション情報をMMD用に変換
        /// </summary>
        static Dictionary<string, MMDMotionData> ProcessAnimations(AnimationContentDictionary animations, IList<BoneContent> bones)
        {
            if (animations.Count == 0)
                throw new InvalidContentException("このモデルにはアニメーション情報が含まれていません");
            // ボーン名からインデックスに変換する辞書テーブルを作る
            Dictionary<string, int> boneMap = new Dictionary<string, int>();
            for (int i = 0; i < bones.Count; i++)
            {
                string boneName = bones[i].Name;
                if (!string.IsNullOrEmpty(boneName))
                    boneMap.Add(boneName, i);
            }

            Dictionary<string, MMDMotionData> result = new Dictionary<string, MMDMotionData>();
            foreach (var animation in animations)
            {
                MMDMotionData motion = ProcessAnimation(animation.Value, boneMap, bones);
                result.Add(animation.Key, motion);
            }
            return result;
        }

        static MMDMotionData ProcessAnimation(AnimationContent animation, Dictionary<string, int> boneMap, IList<BoneContent> bones)
        {
            List<MMDBoneMotion> keyframes = new List<MMDBoneMotion>();

            //アニメーションチャンネルを処理
            foreach (var channel in animation.Channels)
            {
                if (!boneMap.ContainsKey(channel.Key))
                    continue;
                // キーフレームの変換
                foreach (AnimationKeyframe keyframe in channel.Value)
                {
                    //MMDのモーション値*バインドポーズ＝モデルのモーション値なので、バインドポーズの逆をかける
                    Matrix InvBind = Matrix.Invert(bones[boneMap[channel.Key]].Transform);
                    Matrix transform = keyframe.Transform * InvBind;

                    // 行列の分解
                    Quaternion rotation;
                    Vector3 translation;
                    Vector3 scale;
                    transform.Decompose(out scale, out rotation, out translation);
                    rotation.Normalize();

                    keyframes.Add(new MMDBoneMotion()
                    {
                        BoneName = channel.Key,
                        FrameNo = (uint)Math.Round((double)keyframe.Time.Ticks * 60.0 / (double)Stopwatch.Frequency),
                        Location = translation,
                        Quatanion = rotation,
                        Curve = CreateIdentityCurve()
                    });
                }
            }
            //ソート
            keyframes.Sort((x, y) => (int)((long)x.FrameNo - (long)y.FrameNo));
            //エラーチェック
            if (keyframes.Count == 0)
                throw new InvalidContentException(
                    "キーフレームの無いアニメーションです。");
            //MMDMotionDataに落し込み
            MMDMotionData result = new MMDMotionData();
            //キーフレームリストを配列に変換
            result.BoneMotions = keyframes.ToArray();
            //その他のものはから配列にする
            result.FaceMotions = new MMDFaceMotion[0];
            result.CameraMotions = new MMDCameraMotion[0];
            result.LightMotions = new MMDLightMotion[0];
            return result;

        }

        private static BezierCurve[] CreateIdentityCurve()
        {
            BezierCurve[] result = new BezierCurve[4];
            for (int i = 0; i < result.Length; i++)
            {
                result[i].v1 = new Vector2(0.25f, 0.25f);
                result[i].v2 = new Vector2(0.75f, 0.75f);
            }
            return result;
        }


        /// <summary>
        /// このメッシュはスキンアニメーションに向いたものかチェックする
        /// </summary>
        static void ValidateMesh(NodeContent node, ContentProcessorContext context,
                                 string parentBoneName)
        {
            MeshContent mesh = node as MeshContent;

            if (mesh != null)
            {
                // メッシュの整合性を調べる
                if (parentBoneName != null)
                {
                    context.Logger.LogWarning(null, null,
                        "{0}メッシュは{1}ボーンの子です。" +
                        "SkinnedModelProcessorはメッシュがボーンの子であるケースに" +
                        "対応していません。",
                        mesh.Name, parentBoneName);
                }

                if (!MeshHasSkinning(mesh))
                {
                    context.Logger.LogWarning(null, null,
                        "{0}メッシュはスキニング情報がないので変換しません",
                        mesh.Name);

                    mesh.Parent.Children.Remove(mesh);
                    return;
                }
            }
            else if (node is BoneContent)
            {
                // このノードがボーンなら、ボーン内を調査中であることを覚えておく
                parentBoneName = node.Name;
            }

            // 再帰的処理(調査中にノードが消去されるので、
            // 子供達のコピーを走査する)
            foreach (NodeContent child in new List<NodeContent>(node.Children))
                ValidateMesh(child, context, parentBoneName);
        }


        /// <summary>
        /// メッシュがスキニング情報をもっているか調べる
        /// </summary>
        static bool MeshHasSkinning(MeshContent mesh)
        {
            foreach (GeometryContent geometry in mesh.Geometry)
            {
                if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights()))
                    return false;
            }

            return true;
        }


        /// <summary>
        /// 全てが同じ座標空間になるように、不必要な変換行列を
        /// モデル・ジオメトリに焼き付ける
        /// </summary>
        static void FlattenTransforms(NodeContent node, BoneContent skeleton)
        {
            foreach (NodeContent child in node.Children)
            {
                // スケルトンは処理しない
                if (child == skeleton)
                    continue;

                // ローカル変換行列をジオメトリに焼きつける
                MeshHelper.TransformScene(child, child.Transform);

                // 焼き付けたので、ローカル座標変換行列は
                // 単位行列(Matrix.Identity)になる
                child.Transform = Matrix.Identity;

                // 再帰呼び出し
                FlattenTransforms(child, skeleton);
            }
        }

    }
}