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

namespace MikuMikuDance.XNA.ShadowMap
{
    /// <summary>
    /// デプスバッファシャドウマップ用シャドウマップマネージャ。
    /// </summary>
    /// <remarks>
    /// MMDにはこれに相当するモードはない。
    /// ぶっちゃけると汚いので使い物にならない、没クラス。
    /// </remarks>
    internal class DepthBufferShadowMap : IShadowMapManager
    {
        //シャドウ距離(mmdのシャドウ距離の90ぐらいに相当)
        float m_ShadowDist = 250f;// 88.70f;
        /// <summary>
        /// シャドウ距離
        /// </summary>
        public float ShadowDist { get { return m_ShadowDist; } set { m_ShadowDist = value; } }

        float m_LightDist = 50;
        /// <summary>
        /// ライト距離
        /// </summary>
        public float LightDist { get { return m_LightDist; } set { m_LightDist = 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>
        /// <param name="SizeMul">何倍サイズのシャドウマップを生成するか</param>
        public DepthBufferShadowMap(GraphicsDevice graphicsDevice, float SizeMul)
        {
            if (SizeMul < 0.01)
                throw new ApplicationException("SizeMulが負または小さすぎます");
            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];
            PresentationParameters pp = graphicsDevice.PresentationParameters;
            int width = (int)(pp.BackBufferWidth * SizeMul);
            int height = (int)(pp.BackBufferHeight * SizeMul);

            for (int i = 0; i < NumBuf; i++)
            {
                //浮動小数点テクスチャ作成
                shadowRenderTarget[i] = new RenderTarget2D(graphicsDevice,
                                                        width,
                                                        height,
                                                        1, shadowMapFormat);

                //シャドウマップ用のデプスバッファ取得
                shadowDepthBuffer[i] = new DepthStencilBuffer(graphicsDevice,
                                                        width,
                                                        height,
                                                        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 / (float)texture.Width, 0.5f / (float)texture.Height);
            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();
            
            //ライトの位置計算
            Vector3 lightPosition = lightDir * LightDist;
            
            
            //ライトの位置を元にしたビューマトリックス生成
            View = Matrix.CreateLookAt(lightPosition, lightPosition - lightDir, Vector3.Up);
            
            //ライトのプロジェクションマトリックスを使う
            //平行光源の場合は視錐台のプロジェクションを使う。これによりパースペクティブシャドウマップになる
            float aspectRatio = (float)RenderTarget.Width / (float)RenderTarget.Height;
            Proj = Matrix.CreatePerspectiveFieldOfView(Camera.FieldOfView, aspectRatio, Camera.Near, ShadowDist*10);
        }
    }
}