using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using MikuMikuDance.XNA.Model.ModelData;
using System.ComponentModel;

// TODO: replace these with the processor input and output types.
using TInput = MikuMikuDance.XNA.Model.MMDModelIntermediate;
using TOutput = MikuMikuDance.XNA.Model.MMDModelContext;

namespace MikuMikuDance.XNA.Model
{
    /// <summary>
    /// MikuMikuDanceのモデルデータをXNAにインポートするためのプロセッサ
    /// </summary>
    [ContentProcessor(DisplayName = "MikuMikuDanceモデル : MikuMikuDance for XNA")]
    public class MMDProcessor : ContentProcessor<TInput, TOutput>
    {
        // 頂点数制限無いくさい？
        // このプロセッサでは頂点テクスチャを使うので最大ボーン数は256個になる
        //const int MaxBones = 256;
        /// <summary>
        /// カスタムエフェクト使用フラグ
        /// </summary>
        [DefaultValue(false)]
        [DisplayName("カスタムエフェクトの使用")]
        [Description("MikuMikuDanceModel用エフェクト意外のエフェクトを使いたいときにtrueを指定")]
        public bool UseCustomEffect { get; set; }

        /// <summary>
        /// カスタムエフェクトファイル名
        /// </summary>
        [DefaultValue("")]
        [DisplayName("カスタムエフェクトファイル")]
        [Description("MikuMikuDanceModel用エフェクト意外のエフェクトを使いたいときにファイル名を指定")]
        public string CustomEffectName { get; set; }

        /// <summary>
        /// コンテンツ変換
        /// </summary>
        /// <param name="input">MMDモデル for XNA中間クラス</param>
        /// <param name="context">コンテントプロセッサ</param>
        /// <returns>MMDモデル for XNAコンテンツ</returns>
        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            //返却物
            TOutput result = new MMDModelContext();

            //ひにけにXNAの頂点テクスチャでスキンアニメーションを参考
            //ひにけにXNAの手法ではボーン数が255が限界なので、65535に拡張できるように改良する必要あり

            //念のためスキンアニメーションに向いたものかどうかチェックする
            ValidateMesh(input.RootNode, context, null);
            //スケルトンを探す
            BoneContent skeleton = MeshHelper.FindSkeleton(input.RootNode);
            if (skeleton == null)
                throw new InvalidContentException("スケルトンが見つかりません");
            // モデル内のメッシュがそれぞれ違うローカル座標を持っていると扱いが
            // 面倒なので、全部焼いちゃう(メッシュの変換座標をあらかじめ頂点
            // データに適用すること)
            FlattenTransforms(input.RootNode, skeleton);

            // バインド・ポーズとスケルトン構造を読み込む
            IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);

            /*if (bones.Count > MaxBones)
            {
                throw new InvalidContentException(string.Format(
                    "このスケルトンには{0}個のボーンがあります。最大ボーン数は{1}です。",
                    bones.Count, MaxBones));
            }*/

            List<MMDBoneData> BoneDataProcessed = new List<MMDBoneData>();
            Dictionary<string, int> NamePosDictionary = new Dictionary<string, int>();
            int Pos = 0;
            foreach (BoneContent bone in bones)
            {
                //スキンアニメーション用データを取得
                MMDBoneData mmdbone = new MMDBoneData();
                mmdbone.BindPose = QuatTransform.FromMatrix(bone.Transform);
                mmdbone.InverseBindPose = QuatTransform.FromMatrix(Matrix.Invert(bone.AbsoluteTransform));
                mmdbone.SkeletonHierarchy = bones.IndexOf(bone.Parent as BoneContent);//親を登録(無い場合は-1)
                if (!NamePosDictionary.ContainsKey(bone.Name) && bone.Name != "")
                    NamePosDictionary.Add(bone.Name, Pos);
                BoneDataProcessed.Add(mmdbone);
                Pos++;
            }
            //他のボーン情報を統合
            foreach (var i in input.Bones)
            {
                //ボーンの新たな位置を取得
                Pos = NamePosDictionary[i.Name];
                //情報をスキンアニメーションデータと統合
                BoneDataProcessed[Pos].BoneType = i.BoneType;
                if (i.IKParentBoneIndex == 0)
                    BoneDataProcessed[Pos].IKParentBoneIndex = 0;
                else
                    BoneDataProcessed[Pos].IKParentBoneIndex = (ushort)NamePosDictionary[input.Bones[i.IKParentBoneIndex].Name];
                BoneDataProcessed[Pos].Name = i.Name;
                if (i.IK != null)
                {//IKデータがあるならそれもコピー
                    BoneDataProcessed[Pos].IK = new MMDIKData();
                    BoneDataProcessed[Pos].IK.ControlWeight = i.IK.ControlWeight;
                    BoneDataProcessed[Pos].IK.IKBoneIndex = (ushort)NamePosDictionary[input.Bones[i.IK.IKBoneIndex].Name];
                    BoneDataProcessed[Pos].IK.IKChildBones = new List<ushort>();
                    foreach (var j in i.IK.IKChildBones)
                        BoneDataProcessed[Pos].IK.IKChildBones.Add((ushort)NamePosDictionary[input.Bones[j].Name]);
                    BoneDataProcessed[Pos].IK.IKTargetBoneIndex = (ushort)NamePosDictionary[input.Bones[i.IK.IKTargetBoneIndex].Name];
                    BoneDataProcessed[Pos].IK.Iteration = i.IK.Iteration;
                }
            }
            //ベースボーン情報を設定
            BoneDataProcessed[0].Name = "base";
            BoneDataProcessed[0].BoneType = MMDBoneType.RotateAndMove;
            BoneDataProcessed[0].IKParentBoneIndex = 0;

            //MMDにはアニメーション情報は無いので、ここは飛ばす
            //// アニメーションデータをランタイム用フォーマットに変換する
            //Dictionary<string, AnimationClip> animationClips;
            //animationClips = ProcessAnimations(skeleton.Animations, bones);

            //モデル情報をプロセッサーにかける
            OpaqueDataDictionary param = new OpaqueDataDictionary();
            if (UseCustomEffect && CustomEffectName != null)
                param.Add("EffectFile", CustomEffectName);
            else
                param.Add("EffectFile", "");
            result.ModelData = context.Convert<NodeContent, ModelContent>(input.RootNode, "SkinnedModelProcessor", param);
            //result.ModelData = context.Convert<NodeContent, ModelContent>(input.RootNode, "ModelProcessor");
            
            // MMDModelDataにプロセス済みのボーンを格納する
            result.Bones = BoneDataProcessed;
            
            //他のモデル情報をコピー
            //result.IKs = input.IKs;
            result.Skins = input.Skins;
            result.NumVertexForFace = input.NumVertexForFace;
            result.Rigids = input.Rigids;
            result.Joints = input.Joints;
            //剛体の情報修正
            for (int i = 0; i < result.Rigids.Count; i++)
            {
                if (result.Rigids[i].RelatedBoneIndex != 0xffff)
                    result.Rigids[i].RelatedBoneIndex = (ushort)NamePosDictionary[input.Bones[result.Rigids[i].RelatedBoneIndex].Name];
            }
            //返却
            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);
            }
        }
    }
}