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

namespace MikuMikuDance.XNA.ShadowMap
{
    /// <summary>
    /// パースペクティブシャドウマップ用シャドウマップマネージャ。MMDのmode1に相当?(多分PSM法)
    /// </summary>
    /// <remarks>ゲーム中での運用方法はIShadowMapmanagerを継承したクラスを作成し、ゲームに合わせて調整すること</remarks>
    public class PerspectiveShadowMap : IShadowMapManager
    {
        //シャドウマップのサイズ。精度を調整する場合はこの値を変えること
        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];

        /// <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 PerspectiveShadowMap(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 = Vector2.Zero;
            return texture;
        }
        /// <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)
        {
            //表示行列設定
            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);
        }
    }
}
