using System;
using System.Collections.Generic;
using System.ComponentModel;
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 TInput = MikuMikuDance.XNA.Model.MMDImporterOutput;
using TOutput = MikuMikuDance.XNA.Model.MMDModelContent;

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>
    {

        bool useCustomEffect = false;
        /// <summary>
        /// カスタムエフェクト使用フラグ
        /// </summary>
        [DefaultValue(false)]
        [DisplayName("カスタムエフェクトの使用")]
        [Description("MikuMikuDanceModel用エフェクト意外のエフェクトを使いたいときにtrueを指定")]
        public bool UseCustomEffect { get { return useCustomEffect; } set { useCustomEffect = value; } }

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

        bool generateMipmaps = true;
        /// <summary>
        /// ミップマップの生成
        /// </summary>
        [DefaultValue(true)]
        [DisplayName("ミップマップの生成")]
        [Description("有効な場合、モデルのテクスチャーに対して完全なミップマップ チェーンが生成されます。既存のミップマップは置換されません。")]
        public virtual bool GenerateMipmaps { get { return generateMipmaps; } set { generateMipmaps = value; } }

        bool resizeTexturesToPowerOfTwo = true;
        /// <summary>
        /// テクスチャサイズを2の累乗にリサイズ
        /// </summary>
        [DefaultValue(true)]
        [DisplayName("テクスチャー サイズを2の累乗にリサイズ")]
        [Description("有効な場合、モデルのテクスチャーは次に大きな2の累乗のサイズに変更され、可能な限りの互換性を保ちます。多くのグラフィック カードは、サイズが2の累乗でないテクスチャーに対応していません。")]
        public virtual bool ResizeTexturesToPowerOfTwo { get { return resizeTexturesToPowerOfTwo; } set { resizeTexturesToPowerOfTwo = value; } }

        TextureProcessorOutputFormat textureFormat = TextureProcessorOutputFormat.DxtCompressed;
        /// <summary>
        /// テクスチャーフォーマット
        /// </summary>
        [DefaultValue(TextureProcessorOutputFormat.DxtCompressed)]
        [DisplayName("テクスチャー フォーマット")]
        [Description("処理されるテクスチャーの SurfaceFormat 型を指定します。テクスチャーのフォーマットは、無変換、Color (32 ビット RGBA)、またはDXT圧縮形式に変換されます。")]
        public virtual TextureProcessorOutputFormat TextureFormat { get { return textureFormat; } set { textureFormat = value; } }
        
        bool m_UsePartCenterZSort = false;
        /// <summary>
        /// パーツ中心を利用したモデル内Zソート法
        /// </summary>
        [DefaultValue(false)]
        [DisplayName("パーツ中心Zソート法")]
        [Description("有効な場合、モデルの各材質は各材質の中心位置を基準としたZソート法で描画されます。半透明材質やテクスチャを持つモデルが上手く表示されないときにtrueにすると解決するかもしれません")]
        public bool UsePartCenterZSort { get { return m_UsePartCenterZSort; } set { m_UsePartCenterZSort = value; } }

        bool m_UseMaterialPalette = true;
        /// <summary>
        /// マテリアルパレット使用フラグ
        /// </summary>
        [DefaultValue(true)]
        [DisplayName("マテリアルパレットの使用")]
        [Description("有効な場合、モデルの各材質は整理され、GPU命令発行数を抑えることができます。但し、半透明な材質がある場合は上手く表示されない可能性があります")]
        public bool UseMaterialPalette { get { return m_UseMaterialPalette; } set { m_UseMaterialPalette = value; } }
        /// <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 MMDModelContent();
            //MMDBuilder(旧インポート処理)を使って変換
            MikuMikuDance.XNA.Model.MMDModelIntermediate modelIntermediate;
            MMDBuilder builder = new MMDBuilder();
            builder.UseMaterialPalette = UseMaterialPalette;
            modelIntermediate = builder.Build(input.filename, input.model);

            //念のためスキンアニメーションに向いたものかどうかチェックする
            ValidateMesh(modelIntermediate.RootNode, context, null);
            //スケルトンを探す
            BoneContent skeleton = MeshHelper.FindSkeleton(modelIntermediate.RootNode);
            if (skeleton == null)
                throw new InvalidContentException("スケルトンが見つかりません");
            // モデル内のメッシュがそれぞれ違うローカル座標を持っていると扱いが
            // 面倒なので、全部焼いちゃう(メッシュの変換座標をあらかじめ頂点
            // データに適用すること)
            FlattenTransforms(modelIntermediate.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にプロセス済みのボーンを格納する
            result.Bones = BoneDataProcessed;
            
            //MMDにはアニメーション情報は無いので、ここは飛ばす
            //// アニメーションデータをランタイム用フォーマットに変換する
            //Dictionary<string, AnimationClip> animationClips;
            //animationClips = ProcessAnimations(skeleton.Animations, bones);

            //モデル情報をプロセッサーにかける
            SkinnedModelProcessor skinnedProcessor = new SkinnedModelProcessor();
            //パラメータ設定
            if (UseCustomEffect && CustomEffectName != null)
                skinnedProcessor.EffectFile = CustomEffectName;
            else
                skinnedProcessor.EffectFile =  "";
            skinnedProcessor.GenerateMipmaps = GenerateMipmaps;
            skinnedProcessor.ResizeTexturesToPowerOfTwo = ResizeTexturesToPowerOfTwo;
            skinnedProcessor.TextureFormat = TextureFormat;
            skinnedProcessor.ColorKeyEnabled = false;
            //プロセス
            result.ModelData = skinnedProcessor.Process(modelIntermediate.RootNode, context);
            OpaqueDataDictionary pParams = new OpaqueDataDictionary();
            pParams.Add("EffectFile", "");
            pParams.Add("Scale", 2);
            //result.ModelData = context.Convert<NodeContent, ModelContent>(input.RootNode, "SkinnedModelProcessor", pParams);
            
            //頂点テクスチャ用に頂点情報を整理する
            //フォーマット：
            //頂点表情セクタのヘッダ(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 < modelIntermediate.Skins.Length; i++)
            {
                if (modelIntermediate.Skins[i].SkinType == 0)
                    continue;//baseは無視
                for (int j = 0; j < modelIntermediate.Skins[i].SkinVerts.Length; j++)
                {
                    VertSet vset = new VertSet();
                    vset.FaceNumber = fNumber;
                    vset.VertNumber = modelIntermediate.Skins[i].SkinVerts[j].VertIndex;
                    vset.vert = new Vector4(MMDMath.VectorFromArray(modelIntermediate.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 < modelIntermediate.Skins.Length; i++)
            {
                if (modelIntermediate.Skins[i].SkinType == 0)
                {
                    for (int j = 0; j < modelIntermediate.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 < modelIntermediate.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 < modelIntermediate.NumVertexForFace || s2 < modelIntermediate.NumVertexForFace || c1 < 0 || c2 < 0)
                    throw new System.ApplicationException((modelIntermediate.NumVertexForFace - s1).ToString() + "," + (modelIntermediate.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 = modelIntermediate.Skins[faces[k]].Name;//ここでIndexOutがでるか……？
                        }
                        catch (System.ArgumentOutOfRangeException)
                        {
                            throw new System.ApplicationException(faces[k].ToString() + ">=" + modelIntermediate.Skins.Length.ToString());
                        }
                        valCount++;
                    }
                }
                for (int j = s2; j < s2 + valCount; j++)
                {
                    Vector4 temp = result.FaceData[j];
                }
            }
#endif
            //CPU表情アニメーション用の情報も確保
            Factorization(modelIntermediate.NumVertexForFace, out dataspace, out TexX, out TexY);
            result.FaceVertTextureSizeX = modelIntermediate.NumVertexForFace;//TexX;
            result.FaceVertTextureSizeY = 1;//TexY;

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

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