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;

using TInput = MikuMikuDance.XNA.Model.MMDModelIntermediate;
using TOutput = MikuMikuDance.XNA.Model.MMDModelContext;
using System;

namespace MikuMikuDance.XNA.Model
{
    class VertSet
    {
        //頂点番号
        public uint VertNumber;
        //頂点移動量
        public Vector4 vert;
        //表情番号
        public int FaceNumber;
    }
    /// <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;
            
            //頂点テクスチャ用に頂点情報を整理する
            //フォーマット：
            //頂点表情セクタのヘッダ(Vector4に割り当て)
            //struct VertToFace{
            //	float start;//base頂点→表情番号が書かれたフィールドの開始点(Vector4個数単位)
            //	float count;//base頂点→表情番号が書かれたフィールドの個数(表情数ではなくVector4の個数)
            //  float vstart;//base頂点→表情ごとの移動量が書かれたフィールドの開始点
            //  float vcount;//base頂点→表情ごとの移動量が書かれたフィールドの個数(count*4-αと同じになるはずだが)
            //};
            //頂点→表情番号フィールド(float:Vector4に4個づつ割り当て)
            //base頂点→表情ごとの移動量(Vector4。wは0)。indexは頂点→表情番号フィールドと一致
            //まずはリストアップ

            SortedDictionary<uint, List<VertSet>> vertdataTemp = new SortedDictionary<uint, List<VertSet>>();
            int fNumber = 0;
            for (int i = 0; i < input.Skins.Count; i++)
            {
                if (input.Skins[i].SkinType == 0)
                    continue;//baseは無視
                for (int j = 0; j < input.Skins[i].SkinVerts.Length; j++)
                {
                    VertSet vset = new VertSet();
                    vset.FaceNumber = fNumber;
                    vset.VertNumber = input.Skins[i].SkinVerts[j].VertIndex;
                    vset.vert = new Vector4(MMDMath.VectorFromArray(input.Skins[i].SkinVerts[j].Pos), 0);
                    if (vertdataTemp.ContainsKey(vset.VertNumber))
                        vertdataTemp[vset.VertNumber].Add(vset);
                    else
                    {
                        vertdataTemp.Add(vset.VertNumber, new List<VertSet>());
                        vertdataTemp[vset.VertNumber].Add(vset);
                    }
                }
                ++fNumber;
            }
            //表情に使われないbase頂点番号を追加
            for (int i = 0; i < input.Skins.Count; i++)
            {
                if (input.Skins[i].SkinType == 0)
                {
                    for (int j = 0; j < input.Skins[i].SkinVerts.Length; j++)
                    {
                        VertSet vset = new VertSet();
                        vset.FaceNumber = -1;//関連表情無し
                        vset.VertNumber = (uint)j;//表情頂点の定義はbase表情のindexなので、これに相当
                        vset.vert = Vector4.Zero;
                        if (!vertdataTemp.ContainsKey(vset.VertNumber))
                        {
                            vertdataTemp.Add(vset.VertNumber, new List<VertSet>());
                            vertdataTemp[vset.VertNumber].Add(vset);
                        }
                    }
                    break;
                }
            }
            //ヘッダと頂点→表情番号フィールドの構成
            Vector4[] vertHeader = new Vector4[vertdataTemp.Count];
            List<Vector4> VertData = new List<Vector4>();
            int count = 0;
            int VecPos = vertdataTemp.Count;
            foreach (var i in vertdataTemp)
            {
                vertHeader[count].X = VecPos;
                int size = 0;
                for (int j = 0; j < i.Value.Count; j += 4)
                {
                    //float[]でアクセスするとX,Y,Z,Wの順……
                    VertData.Add(new Vector4(i.Value[j].FaceNumber,
                        (i.Value.Count > j + 1) ? i.Value[j + 1].FaceNumber : -1,
                        (i.Value.Count > j + 2) ? i.Value[j + 2].FaceNumber : -1,
                        (i.Value.Count > j + 3) ? i.Value[j + 3].FaceNumber : -1
                        ));
                    ++size;
                }
                vertHeader[count].Y = size;
                VecPos += size;
                ++count;
            }
            //base頂点→表情ごとの移動量フィールドの構成
            count = 0;
            foreach (var i in vertdataTemp)
            {
                vertHeader[count].Z = VecPos;
                vertHeader[count].W = i.Value.Count;
                for (int j = 0; j < i.Value.Count; j++)
                    VertData.Add(i.Value[j].vert);
                VecPos += i.Value.Count;
                ++count;
            }
            //ヘッダとあわせてデータの完成
            VertData.InsertRange(0, vertHeader);
            //頂点テクスチャ最適化可能なテクスチャのサイズと穴埋め分の取得
            int dataspace, TexX, TexY;
            Factorization(VertData.Count, out dataspace, out TexX, out TexY);
            for (int i = 0; i < dataspace; i++)
                VertData.Add(Vector4.Zero);//穴埋め
            VecPos += dataspace;
            //完成したデータを格納
            result.FaceData = VertData.ToArray();
            result.FaceDataTextureSizeX = TexX;
            result.FaceDataTextureSizeY = TexY;
            //チェックコード
#if DEBUG
            if (VecPos != result.FaceData.Length)
                throw new System.ApplicationException((result.FaceData.Length - VecPos).ToString());
            if (result.FaceDataTextureSizeX * result.FaceDataTextureSizeY != result.FaceData.Length)
                throw new System.ApplicationException((result.FaceData.Length - result.FaceDataTextureSizeX * result.FaceDataTextureSizeY).ToString());
            for (int i = 0; i < input.NumVertexForFace; i++)
            {
                int s1, c1, s2, c2;
                s1 = (int)result.FaceData[i].X;
                c1 = (int)result.FaceData[i].Y;
                s2 = (int)result.FaceData[i].Z;
                c2 = (int)result.FaceData[i].W;
                if (s1 < input.NumVertexForFace || s2 < input.NumVertexForFace || c1 < 0 || c2 < 0)
                    throw new System.ApplicationException((input.NumVertexForFace - s1).ToString() + "," + (input.NumVertexForFace - s2).ToString() + "," + c1.ToString() + "," + c2.ToString());
                /*if (i == 600)
                    throw new System.Exception();*/
                //表情番号チェック
                int valCount = 0;
                for (int j = s1; j < s1 + c1; j++)
                {
                    int[] faces = new int[4] {  (int)result.FaceData[j].X, (int)result.FaceData[j].Y, (int)result.FaceData[j].Z,(int)result.FaceData[j].W };
                    for (int k = 0; k < 4; k++)
                    {
                        if (faces[k] == -1)
                            continue;
                        try
                        {
                            string temp = input.Skins[faces[k]].Name;//ここでIndexOutがでるか……？
                        }
                        catch (System.ArgumentOutOfRangeException)
                        {
                            throw new System.ApplicationException(faces[k].ToString() + ">=" + input.Skins.Count.ToString());
                        }
                        valCount++;
                    }
                }
                for (int j = s2; j < s2 + valCount; j++)
                {
                    Vector4 temp = result.FaceData[j];
                }
            }
#endif
            //CPU表情アニメーション用の情報も確保
            Factorization(input.NumVertexForFace, out dataspace, out TexX, out TexY);
            result.FaceVertTextureSizeX = input.NumVertexForFace;//TexX;
            result.FaceVertTextureSizeY = 1;//TexY;

            //他のモデル情報をコピー
            //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;
        }

        private void Factorization(int p, out int dataspace, out int x, out int y)
        {
            int i = (int)Math.Floor(Math.Sqrt((double)p));
            int end = (int)Math.Max(Math.Floor(Math.Sqrt((double)p) / 1.5), Math.Ceiling((float)p / 1024.0f));
            dataspace = int.MaxValue;
            x = i;
            for (; i >= end; i--)
            {
                y = (int)Math.Ceiling((double)p / (double)i);
                if (y * i - p < dataspace)
                {
                    dataspace = y * i - p;
                    x = i;
                }
                if (dataspace == 0)
                    break;
            }
            if (dataspace == int.MaxValue)
            {
                if (p > 1000 * 1000)
                    throw new InvalidContentException("表情頂点データの加工に失敗しました。表情の頂点数が多すぎます");
                else
                    throw new ApplicationException("表情頂点データの加工に失敗しました");
            }
            y = (p + dataspace) / x;
        }

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