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 = MikuMikuDance.XNA.Model.MMDModelContent;
using MikuMikuDance.XNA.Model.ModelData;
using MikuMikuDance.XNA.Model;
using System.ComponentModel;
using MikuMikuDance.XNA.Motion.MotionData;
using System.Diagnostics;

namespace MikuMikuDance.XNA.ModelReach
{

    /// <summary>
    /// MikuMikuDanceのモデルデータをXNAにインポートするためのプロセッサ(Reach用)
    /// </summary>
    [ContentProcessor(DisplayName = "MikuMikuDanceモデル(Reach) : MikuMikuDance for XNA")]
    public class MMDReachProcessor : ContentProcessor<TInput, TOutput>
    {
        const int MaxBones = 100;
        
        
        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">NodeContentクラス</param>
        /// <param name="context">コンテントプロセッサ</param>
        /// <returns>MMDモデル for XNAコンテンツ</returns>
        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            //返却物
            TOutput result = new MMDModelContent();
            //MMDモデル用のタグを探す
            MMDModelIntermediateTag tag = null;
            foreach (var i in input.OpaqueData)
            {
                if (i.Key == "mmdtag")
                {
                    //見つかったのでMMDモデル用のタグに変換を試みる
                    tag = i.Value as MMDModelIntermediateTag;
                    break;
                }
            }
            //タグはモデルから除去しておく
            if (tag != null)
                input.OpaqueData.Remove("mmdtag");
            
            //念のためスキンアニメーションに向いたものかどうかチェックする
            ValidateMesh(input, context, null);
            //スケルトンを探す
            BoneContent skeleton = MeshHelper.FindSkeleton(input);
            if (skeleton == null)
                throw new InvalidContentException("スケルトンが見つかりません");
            // モデル内のメッシュがそれぞれ違うローカル座標を持っていると扱いが
            // 面倒なので、全部焼いちゃう(メッシュの変換座標をあらかじめ頂点
            // データに適用すること)
            FlattenTransforms(input, skeleton);

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

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

            //メッシュからアニメーション情報を取得
            result.Motions = ProcessAnimations(skeleton.Animations, bones);

            //ボーン処理
            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);
                mmdbone.Name = bone.Name;
                BoneDataProcessed[Pos] = mmdbone;
                Pos++;
            }
            
            //他のボーン情報を統合
            if (tag != null)
            {
                foreach (var i in tag.Bones)
                {
                    //ボーンの新たな位置を取得
                    Pos = NamePosDictionary[i.Name];
                    //情報をスキンアニメーションデータと統合
                    BoneDataProcessed[Pos].BoneType = i.BoneType;
                    if (i.IKParentBoneIndex == 0)
                        BoneDataProcessed[Pos].IKParentBoneIndex = 0;
                    else
                        BoneDataProcessed[Pos].IKParentBoneIndex = (ushort)NamePosDictionary[tag.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[tag.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[tag.Bones[i.IK[iknum].IKChildBones[j]].Name];

                            IK.IKTargetBoneIndex = (ushort)NamePosDictionary[tag.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;
            }
            else
            {//タグなしモデル
                for (int i = 0; i < BoneDataProcessed.Length;i++ )
                {
                    //タグなしモデルはIK無し
                    BoneDataProcessed[i].HasIK = false;
                    BoneDataProcessed[i].IK = null;
                    //ボーンタイプは回転移動
                    BoneDataProcessed[i].BoneType = MMDBoneType.RotateAndMove;
                    //親IKボーンは無し
                    BoneDataProcessed[i].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;
            //モデル情報をプロセッサーにかける
            SkinnedModelProcessorReach skinnedProcessor = new SkinnedModelProcessorReach();
            //パラメータ設定
            skinnedProcessor.GenerateMipmaps = GenerateMipmaps;
            skinnedProcessor.ResizeTexturesToPowerOfTwo = ResizeTexturesToPowerOfTwo;
            skinnedProcessor.TextureFormat = TextureFormat;
            skinnedProcessor.ColorKeyEnabled = false;
            //プロセス
            result.ModelData = skinnedProcessor.Process(input, context);
            //表情なし
            result.FaceData = null;
            result.FaceDataTextureSizeX = 0;
            result.FaceDataTextureSizeY = 0;
            result.FaceVertTextureSizeX = 0;
            result.FaceVertTextureSizeY = 0;

            //他のモデル情報をコピー
            if (tag != null)
            {
                //表情なし
                result.Skins = null;
                result.NumVertexForFace = 0;
                result.Rigids = tag.Rigids;
                result.Joints = tag.Joints;
                //剛体の情報修正
                for (int i = 0; i < result.Rigids.Length; i++)
                {
                    if (result.Rigids[i].RelatedBoneIndex != 0xffff)
                        result.Rigids[i].RelatedBoneIndex = (ushort)NamePosDictionary[tag.Bones[result.Rigids[i].RelatedBoneIndex].Name];
                }
            }
            else
            {
                //タグなしモデルは剛体無し
                result.Skins = null;
                result.NumVertexForFace = 0;
                result.Rigids = new MMDRigid[0];
                result.Joints = new MMDJoint[0];
            }
            //Zオーダー用オブジェクトも無し
            result.PartCenters = null;
            result.UsePartCenterZSort = false;
            //Reachモード
            result.IsReach = true;
            //返却
            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);
            }
        }
        /// <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;
        }


    }
}