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

namespace MikuMikuDance.XNA.Model
{
    static class InnerProcessorPMDReach
    {
        //最大ボーン数(XNA4.0のReachに合わせる)
        const int MaxBones = 72;
        /// <summary>
        /// ミップマップの生成
        /// </summary>
        public static bool GenerateMipmaps = true;

        /// <summary>
        /// テクスチャサイズを2の累乗にリサイズ
        /// </summary>
        public static bool ResizeTexturesToPowerOfTwo = true;

        /// <summary>
        /// テクスチャーフォーマット
        /// </summary>
        public static TextureProcessorOutputFormat TextureFormat = TextureProcessorOutputFormat.DxtCompressed;
        /// <summary>
        /// パーツ中心を利用したモデル内Zソート法
        /// </summary>
        public static bool UsePartCenterZSort = false;

        internal static MMDModelContent Process(NodeContent RootNode, MMDModelIntermediateTag modelIntermediate, ContentProcessorContext context)
        {
            MMDModelContent modeltag = new MMDModelContent();

            //念のためスキンアニメーションに向いたものかどうかチェックする
            ValidateMesh(RootNode, context, null);
            //スケルトンを探す
            BoneContent skeleton = MeshHelper.FindSkeleton(RootNode);
            if (skeleton == null)
                throw new InvalidContentException("スケルトンが見つかりません");
            // モデル内のメッシュがそれぞれ違うローカル座標を持っていると扱いが
            // 面倒なので、全部焼いちゃう(メッシュの変換座標をあらかじめ頂点
            // データに適用すること)
            FlattenTransforms(RootNode, skeleton);
            // バインド・ポーズとスケルトン構造を読み込む
            IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);
            if (bones.Count > MaxBones)
            {
                throw new InvalidContentException(string.Format(
                    "このスケルトンには{0}個のボーンがあります。最大ボーン数は{1}です。",
                    bones.Count, MaxBones));
            }

            MMDBoneData[] BoneDataProcessed = new MMDBoneData[bones.Count];
            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[Pos] = mmdbone;
                Pos++;
            }
            //他のボーン情報を統合
            foreach (var i in modelIntermediate.Bones)
            {
                //ボーンの新たな位置を取得
                Pos = NamePosDictionary[i.Name];
                //情報をスキンアニメーションデータと統合
                BoneDataProcessed[Pos].BoneType = i.BoneType;
                if (i.IKParentBoneIndex == 0)
                    BoneDataProcessed[Pos].IKParentBoneIndex = 0;
                else
                    BoneDataProcessed[Pos].IKParentBoneIndex = (ushort)NamePosDictionary[modelIntermediate.Bones[i.IKParentBoneIndex].Name];
                BoneDataProcessed[Pos].Name = i.Name;
                if (i.HasIK)
                {//IKデータがあるならそれもコピー
                    BoneDataProcessed[Pos].HasIK = true;
                    BoneDataProcessed[Pos].IK = new List<MMDIKData>();
                    for (int iknum = 0; iknum < i.IK.Count; iknum++)
                    {
                        MMDIKData IK = new MMDIKData();
                        IK.ControlWeight = i.IK[iknum].ControlWeight;
                        IK.IKBoneIndex = (ushort)NamePosDictionary[modelIntermediate.Bones[i.IK[iknum].IKBoneIndex].Name];
                        IK.IKChildBones = new ushort[i.IK[iknum].IKChildBones.Length];
                        for (int j = 0; j < i.IK[iknum].IKChildBones.Length; j++)
                            IK.IKChildBones[j] = (ushort)NamePosDictionary[modelIntermediate.Bones[i.IK[iknum].IKChildBones[j]].Name];

                        IK.IKTargetBoneIndex = (ushort)NamePosDictionary[modelIntermediate.Bones[i.IK[iknum].IKTargetBoneIndex].Name];
                        IK.Iteration = i.IK[iknum].Iteration;
                        BoneDataProcessed[Pos].IK.Add(IK);
                    }
                }
            }

            //ベースボーン情報を設定
            BoneDataProcessed[0].Name = "base";
            BoneDataProcessed[0].BoneType = MMDBoneType.RotateAndMove;
            BoneDataProcessed[0].IKParentBoneIndex = 0;
            //IKChildデータをMMDXで処理しやすいように修正
            for (int i = 0; i < BoneDataProcessed.Length; i++)
            {
                if (BoneDataProcessed[i].HasIK)
                {
                    for (int iknum = 0; iknum < BoneDataProcessed[i].IK.Count; iknum++)
                    {
                        List<ushort> childList = new List<ushort>();
                        ushort j = BoneDataProcessed[i].IK[iknum].IKTargetBoneIndex;
                        while (true)
                        {
                            childList.Add(j);
                            //親ボーン取得
                            if (BoneDataProcessed[j].SkeletonHierarchy == -1)
                                break;//あれ？親が無い？
                            j = (ushort)BoneDataProcessed[j].SkeletonHierarchy;//boneのindexはWORD
                            //IK影響下じゃないボーンが見つかるまで探索(固定点決めるようにiKじゃないボーンも一つ入れておく)
                            bool flag = false;
                            foreach (var k in BoneDataProcessed[i].IK[iknum].IKChildBones)
                            {
                                if (k == j)
                                {
                                    flag = true;
                                    break;
                                }
                            }
                            if (!flag)
                                break;
                        }
                        BoneDataProcessed[i].IK[iknum].IKChildBones = childList.ToArray();
                    }
                }
            }
            // MMDModelDataにプロセス済みのボーンを格納する
            modeltag.Bones = BoneDataProcessed;

            //モデル情報をプロセッサーにかける
            SkinnedModelProcessor skinnedProcessor = new SkinnedModelProcessor();
            //パラメータ設定
            //Reachはエフェクト固定
            /*if (UseCustomEffect && CustomEffectName != null)
                skinnedProcessor.EffectFile = CustomEffectName;
            else
                skinnedProcessor.EffectFile = "";*/
            //リーチ用のエフェクトを指定
            skinnedProcessor.IsReach = true;
            //その他パラメータ指定
            skinnedProcessor.GenerateMipmaps = GenerateMipmaps;
            skinnedProcessor.ResizeTexturesToPowerOfTwo = ResizeTexturesToPowerOfTwo;
            skinnedProcessor.TextureFormat = TextureFormat;
            skinnedProcessor.ColorKeyEnabled = false;
            //プロセス
            modeltag.ModelData = skinnedProcessor.Process(RootNode, context);

            //他のモデル情報をコピー
            //result.IKs = input.IKs;
            modeltag.Skins = null;
            modeltag.NumVertexForFace = 0;
            modeltag.Rigids = modelIntermediate.Rigids;
            modeltag.Joints = modelIntermediate.Joints;
            //剛体の情報修正
            for (int i = 0; i < modeltag.Rigids.Length; i++)
            {
                if (modeltag.Rigids[i].RelatedBoneIndex != 0xffff)
                    modeltag.Rigids[i].RelatedBoneIndex = (ushort)NamePosDictionary[modelIntermediate.Bones[modeltag.Rigids[i].RelatedBoneIndex].Name];
            }
            //Zオーダー用オブジェクトのコピー
            modeltag.PartCenters = modelIntermediate.PartCenters;
            modeltag.UsePartCenterZSort = UsePartCenterZSort;

            //返却
            return modeltag;
        }

        /// <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);
            }
        }
    }
}
