using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MikuMikuDance.XNA.Stages;

namespace MikuMikuDance.XNA.ShadowMap
{
    /// <summary>
    /// Light Space Perspective Shadow Map法(LispSM法, LSPSM法)によるシャドウマップマネージャ。
    /// Michael Wimmer et al.より(http://www.cg.tuwien.ac.at/research/vr/lispsm/)
    /// MMDのmode2シャドウに相当
    /// </summary>
    /// <remarks>MMDとなるべく近くなるように実装したが、要調整</remarks>
    public class LispShadowMap : IShadowMapManager
    {
        const float errTolerance = 0.01f;
        //シャドウマップのサイズ。精度を調整する場合はこの値を変えること
        const int shadowMapWidthHeight = 2048;
        //シャドウ距離(mmdのシャドウ距離の90ぐらいに相当)
        float m_ShadowDist = 150f;// 88.70f;
        /// <summary>
        /// シャドウ距離
        /// </summary>
        public float ShadowDist { get { return m_ShadowDist; } set { m_ShadowDist = value; } }

        //バッファ数
        const int NumBuf = 2;
        int bufIndex = 0;
        RenderTarget2D[] shadowRenderTarget;
        DepthStencilBuffer[] shadowDepthBuffer;
        //視錐台のコーナー
        Vector3[] frustumCorners = new Vector3[BoundingBox.CornerCount];
        Vector3[] frustumCornersCopy = new Vector3[BoundingBox.CornerCount];

        /// <summary>
        /// レンダリングターゲット
        /// </summary>
        public RenderTarget2D RenderTarget { get { return shadowRenderTarget[bufIndex]; } }
        /// <summary>
        /// デプスバッファ
        /// </summary>
        public DepthStencilBuffer DepthBuffer { get { return shadowDepthBuffer[bufIndex]; } }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="graphicsDevice">グラフィックデバイス</param>
        public LispShadowMap(GraphicsDevice graphicsDevice)
        {
            SurfaceFormat shadowMapFormat = SurfaceFormat.Unknown;

            // 32bitか、16bitかどちらに対応しているかチェック
            if (GraphicsAdapter.DefaultAdapter.CheckDeviceFormat(DeviceType.Hardware,
                               GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Format,
                               TextureUsage.Linear, QueryUsages.None,
                               ResourceType.RenderTarget, SurfaceFormat.Single) == true)
            {
                shadowMapFormat = SurfaceFormat.Single;
            }
            else if (GraphicsAdapter.DefaultAdapter.CheckDeviceFormat(
                               DeviceType.Hardware,
                               GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Format,
                               TextureUsage.Linear, QueryUsages.None,
                               ResourceType.RenderTarget, SurfaceFormat.HalfSingle)
                               == true)
            {
                shadowMapFormat = SurfaceFormat.HalfSingle;
            }
            shadowRenderTarget = new RenderTarget2D[NumBuf];
            shadowDepthBuffer = new DepthStencilBuffer[NumBuf];
            for (int i = 0; i < NumBuf; i++)
            {
                //浮動小数点テクスチャ作成
                shadowRenderTarget[i] = new RenderTarget2D(graphicsDevice,
                                                        shadowMapWidthHeight,
                                                        shadowMapWidthHeight,
                                                        1, shadowMapFormat);

                //シャドウマップ用のデプスバッファ取得
                shadowDepthBuffer[i] = new DepthStencilBuffer(graphicsDevice,
                                                           shadowMapWidthHeight,
                                                           shadowMapWidthHeight,
                                                           DepthFormat.Depth24);
            }
        }
        /// <summary>
        /// フリップ
        /// </summary>
        public void Flip()
        {
            if (++bufIndex >= NumBuf)
                bufIndex = 0;
        }

        DepthStencilBuffer oldDepthBuf = null;
        /// <summary>
        /// シャドウマップの描画の開始
        /// </summary>
        /// <param name="graphicDevice">グラフィックデバイス</param>
        public void BeginShadowMap(GraphicsDevice graphicDevice)
        {
            if (oldDepthBuf != null)
                throw new ApplicationException("BeginShadowMapが二回呼び出されました");
            //フリップ(前フレームの処理が終わってなかった場合、エラーとなるため)
            Flip();
            //レンダリングターゲット切り替え
            graphicDevice.SetRenderTarget(0, RenderTarget);
            //深度バッファ退避
            oldDepthBuf = graphicDevice.DepthStencilBuffer;
            //深度バッファセット
            graphicDevice.DepthStencilBuffer = DepthBuffer;
        }
        /// <summary>
        /// シャドウマップの描画の終了
        /// </summary>
        /// <param name="graphicDevice">グラフィックデバイス</param>
        public void EndShadowMap(GraphicsDevice graphicDevice)
        {
            EndShadowMap(null, graphicDevice);
        }
        /// <summary>
        /// シャドウマップの描画の終了
        /// </summary>
        /// <param name="nextTarget">次に使用するレンダリングターゲット</param>
        /// <param name="graphicDevice">グラフィックデバイス</param>
        public void EndShadowMap(RenderTarget2D nextTarget, GraphicsDevice graphicDevice)
        {
            if (oldDepthBuf == null)
                throw new ApplicationException("BeginShadowMapが呼び出される前にEndShadowMapが呼び出されました");
            //レンダリングターゲット切り替え
            graphicDevice.SetRenderTarget(0, nextTarget);
            //深度バッファを戻す
            graphicDevice.DepthStencilBuffer = oldDepthBuf;
            oldDepthBuf = null;
        }

        /// <summary>
        /// シャドウマップ取得
        /// </summary>
        /// <return>テクスチャ</return>
        public Texture2D GetShadowMap(out Vector2 offset)
        {
            Texture2D texture;
            texture = shadowRenderTarget[bufIndex].GetTexture();
            offset = new Vector2(0.5f / shadowMapWidthHeight, 0.5f / shadowMapWidthHeight);
            return texture;
        }
        void transformVecPoints(Vector3[] pos, Matrix transform)
        {
            Vector4 temp, temp2;
            for (int i = 0; i < pos.Length; i++)
            {
                Vector4.Transform(ref pos[i], ref transform, out temp);
                Vector4.Divide(ref temp, temp.W, out temp2);
                pos[i] = new Vector3(temp2.X, temp2.Y, temp2.Z);
            }
        }
        
        bool CalcLispSM(IMMDCamera Camera, IMMDLightManager LightManager, GraphicsDevice graphics, out Matrix View, out Matrix Proj)
        {
            Vector3 eyePos = Camera.CameraPos;
            Vector3 target = Camera.CameraTarget;
            Vector3 viewDir;
            Vector3.Subtract(ref target, ref eyePos, out viewDir);
            Vector3 lightDir = LightManager.KeyLight.Direction;
            lightDir.Normalize();
            viewDir.Normalize();
            Vector3 left, up;
            Matrix lightView, lightProj, lispMat;
            //sinGammaの計算(歪ませるパラメータ)
            float dotProd;
            double sinGamma;
            Vector3.Dot(ref viewDir, ref lightDir, out dotProd);
            sinGamma = Math.Sqrt(1.0 - (double)dotProd * (double)dotProd);
            //表示カメラの視線ベクトルとライトカメラの視線ベクトルの外積を求める
            Vector3.Cross(ref lightDir, ref viewDir, out left);
            //縮退チェック。縮退するなら通常のPSM法に切り替え
            if (left.Length() < errTolerance)
            {
                View = Matrix.Identity;
                Proj = Matrix.Identity;
                return false;
            }
            //それにさらに外積をかけて、二つの視線ベクトルの平面上かつライトベクトルに垂直なベクトルができあがる
            Vector3.Cross(ref left, ref lightDir, out up);
            up.Normalize();
            //仮のライトビューを作成
            lightView = Matrix.CreateLookAt(eyePos, eyePos + lightDir, up);
            //lightView = Matrix.CreateLookAt(eyePos, eyePos + lightDir, up);
            //視錐台から錐台のコーナー情報取得
            Camera.GetFrustumCorners(graphics, Camera.Far, frustumCorners);
            //コピーを取る
            Array.Copy(frustumCorners, frustumCornersCopy, frustumCorners.Length);
            //仮のライトビューにあわせて回転
            transformVecPoints(frustumCorners, lightView);
            //錐台を含む最小の箱型空間を生成
            BoundingBox lightBox = BoundingBox.CreateFromPoints(frustumCorners);

            {
                //factor値?の計算
                double factor = 1.0 / sinGamma;
                //z_n値(歪ませ座標系におけるz方向のnear?)
                double z_n = factor * Camera.Near;
                //y方向の距離を計算
                //double d = lightBox.Max.Y - lightBox.Min.Y;//サンプルのままのコード
                double d = ShadowDist;//これシャドウ距離なのか？
                //z_f値(歪ませ座標系におけるz方向のfar?)の計算
                double z_f = z_n + d * sinGamma;
                //n値(near?)の計算
                double n = (z_n + Math.Sqrt(z_f * z_n)) / sinGamma;
                //f値(far?)の計算
                double f = n + d;

                //new observer point n-1 behind eye position
                //新たな視点をn-1離れたところにする？
                Vector3 pos = eyePos - up * ((float)n - Camera.Near);
                //ビューを作成
                View = Matrix.CreateLookAt(pos, pos + lightDir, up);
                //Projection生成マトリクスを作成                    (↓この行列はOpenGL用。XNAは列と行が逆)
                lispMat = Matrix.Identity;                          // a = (f+n)/(f-n); b = -2*f*n/(f-n);
                lispMat.M22 = (float)((f + n) / (f - n));	        // [ 1 0 0 0] 
                lispMat.M42 = (float)(-2.0 * f * n / (f - n));      // [ 0 a 0 b]
                lispMat.M24 = 1;                				    // [ 0 0 1 0]
                lispMat.M44 = 0;    			                    // [ 0 1 0 0]

                 //仮のプロジェクションを作成(OpenGLとかける順序逆)
                Matrix.Multiply(ref View, ref lispMat, out lightProj);
                //仮のプロジェクションを用いて変換
                transformVecPoints(frustumCornersCopy, lightProj);
                //最大の箱を作成
                lightBox = BoundingBox.CreateFromPoints(frustumCornersCopy);
            }
            //refit to unit cube
            //this operation calculates a scale translate matrix that
            //maps the two extreme points min and max into (-1,-1,-1) and (1,1,1)
            //よく分からない……なんだこれ？
            //多分正射影行列で視野範囲を(-1,-1,-1) ～ (1,1,1)のボックスに収める？
            //XNAは(-1,-1,0) ～ (1,1,1)の範囲なのでこうなるのか？
            Vector3 BoxSize = lightBox.Max - lightBox.Min;
            lightProj = Matrix.CreateOrthographic(BoxSize.X, BoxSize.Y,0/* -BoxSize.Z*/, BoxSize.Z);
            
            //最終的なProjectionを計算(OpenGLとかける順序逆)
            Matrix.Multiply(ref lispMat, ref lightProj, out Proj);
            return true;
        }

        /// <summary>
        /// シャドウマップ生成用の光源から見たViewProjマトリクスの作成
        /// </summary>
        /// <param name="Camera">カメラ</param>
        /// <param name="LightManager">ライトマネージャ</param>
        /// <param name="graphics">グラフィックデバイス</param>
        /// <param name="View">ViewMatrixの出力</param>
        /// <param name="Proj">ProjMatrixの出力</param>
        public void CreateLightViewProjMatrix(IMMDCamera Camera, IMMDLightManager LightManager, GraphicsDevice graphics, out Matrix View, out Matrix Proj)
        {
            if (CalcLispSM(Camera, LightManager, graphics, out View, out Proj))
                return;
            //表示行列設定
            Vector3 lightDir = -LightManager.KeyLight.Direction;
            lightDir.Normalize();
            //光の方向に回転する回転行列を生成
            Matrix lightRotation = Matrix.CreateLookAt(Vector3.Zero, -lightDir, Vector3.Up);
            //視錐台から錐台のコーナー情報取得
            Camera.GetFrustumCorners(graphics, ShadowDist, frustumCorners);
            //光の方向に回転
            for (int i = 0; i < frustumCorners.Length; i++)
                frustumCorners[i] = Vector3.Transform(frustumCorners[i], lightRotation);
            //錐台を含む最小の箱型空間を生成
            BoundingBox lightBox = BoundingBox.CreateFromPoints(frustumCorners);
            //箱型空間のサイズ取得
            Vector3 boxSize = lightBox.Max - lightBox.Min;
            Vector3 halfBoxSize = boxSize * 0.5f;

            //ライトの位置計算
            //平行光源におけるライトの位置は箱型空間のバックパネル中央。
            Vector3 lightPosition = lightBox.Min + halfBoxSize;
            lightPosition.Z = lightBox.Min.Z;

            //ライトの位置を元の座標系に戻す
            lightPosition = Vector3.Transform(lightPosition, Matrix.Invert(lightRotation));

            //ライトの位置を元にしたビューマトリックス生成
            View = Matrix.CreateLookAt(lightPosition, lightPosition - lightDir, Vector3.Up);

            //ライトのプロジェクションマトリックスを使う
            //パースペクティブシャドウマップの場合は視錐台のプロジェクションを使う。これによりパースペクティブシャドウマップになる
            Proj = Matrix.CreateOrthographic(boxSize.X, boxSize.Y, -boxSize.Z, boxSize.Z);
        }
    }

}
