#region Using ステートメント

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;

#endregion

namespace MikuMikuDance.XNA.Debug
{
    /// <summary>
    /// デバッグ用コマンドウィンドウクラス(伊藤 雄一(HN:ひげねこ)作)
    /// </summary>
    /// <remarks>
    /// ゲーム内で動作するデバックコマンドウィンドウUI部分
    /// キーボード入力によってコマンドを入力、実行することができる。
    /// Xbox 360でもUSBキーボードを接続することで動作可能。
    /// 
    /// 使用方法:
    /// 1)このコンポーネントをゲームに追加。
    /// 2)RegisterCommandメソッドを使ってコマンドを登録する
    /// 3)Tabキーでデバッグウィンドウの開閉しコマンド入力
    /// </remarks>
    public class DebugCommandUI : DrawableGameComponent, IDebugCommandHost
    {
        #region 定数宣言

        /// <summary>
        /// 最大行数
        /// </summary>
        const int MaxLineCount = 20;

        /// <summary>
        /// コマンドヒストリ数
        /// </summary>
        const int MaxCommandHistory = 32;

        /// <summary>
        /// コマンドプロンプト文字列
        /// </summary>
        const string Prompt = "CMD>";

        /// <summary>
        /// カーソル文字。ここではUnicodeのBlock Eleemntsからカーソルっぽいものを使用
        /// http://www.unicode.org/charts/PDF/U2580.pdf
        /// </summary>
        const string Cursor = "\u2582";

        #endregion

        #region プロパティ

        /// <summary>
        /// キー入力待機状態か
        /// </summary>
        public bool Focused { get { return state != State.Closed; } }

        #endregion

        #region フィールド

        // コマンドウィンドウのステート
        enum State
        {
            Closed,     // 閉じている
            Opening,    // 開いている途中
            Opened,     // 開いている(コマンド入力待機中)
            Closing     // 閉じている途中
        }

        /// <summary>
        /// コマンド実行用情報格納用のクラス
        /// </summary>
        class CommandInfo
        {
            public CommandInfo(
                string command, string description, DebugCommandExecute callback )
            {
                this.command = command;
                this.description = description;
                this.callback = callback;
            }

            // コマンド名
            public string command;

            // コマンド詳細
            public string description;

            // コマンド実行用のデリゲート
            public DebugCommandExecute callback;
        }

        // デバッグマネージャーへの参照
        private DebugManager debugManager;

        // 現在のステート
        private State state = State.Closed;

        // ステート移行用のタイマー
        private float stateTransition;

        // 登録されているEchoリスナー
        List<IDebugEchoListner> listenrs = new List<IDebugEchoListner>();

        // 登録されているコマンド
        private Dictionary<string, CommandInfo> commandTable =
                                                new Dictionary<string, CommandInfo>();

        // 現在入力中のコマンドライン文字列と、カーソル位置
        private string commandLine = String.Empty;
        private int cursorIndex = 0;

        // コマンドライン表示文字列
        private Queue<string> lines = new Queue<string>();

        // コマンド履歴用バッファ
        private List<string> commandHistory = new List<string>();

        // 現在選択されている履歴インデックス
        private int commandHistoryIndex;

        #region キーボード入力処理用の変数群

        //　前フレームのキーボードステート
        private KeyboardState prevKeyState;

        //　最後に押されたキー
        private Keys pressedKey;

        //　キーリピートタイマー
        private float keyRepeatTimer;

        // 最初のキー押下時のリピート時間(秒)
        private float keyRepeatStartDuration = 0.3f;

        // ２回目以降のキーリピート時間(秒)
        private float keyRepeatDuration = 0.03f;

        #endregion

        #endregion

        #region 初期化

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public DebugCommandUI( Game game )
            : base( game )
        {
            // サービスとして追加する
            Game.Services.AddService( typeof( IDebugCommandHost ), this );


            // 基本コマンドの追加

            // ヘルプコマンド
            // 登録されているコマンド情報の表示
            RegisterCommand( "help", "Show Command helps",
            delegate( IDebugCommandHost host, string command, IList<string> args )
            {
                int maxLen = 0;
                foreach ( CommandInfo cmd in commandTable.Values )
                    maxLen = Math.Max( maxLen, cmd.command.Length );

                string fmt = String.Format( "{{0,-{0}}}    {{1}}", maxLen );

                foreach ( CommandInfo cmd in commandTable.Values )
                {
                    Echo( String.Format( fmt, cmd.command, cmd.description ) );
                }
            } );

            // クリアスクリーン
            // コマンド画面クリア
            RegisterCommand( "cls", "Clear Screen",
            delegate( IDebugCommandHost host, string command, IList<string> args )
            {
                lines.Clear();
            } );

            // Echoコマンド
            RegisterCommand( "echo", "Display Messages",
            delegate( IDebugCommandHost host, string command, IList<string> args )
            {
                Echo( command.Substring( 5 ) );
            } );
            debugManager =
                Game.Services.GetService( typeof( DebugManager ) ) as DebugManager;

            //剛体表示
            RegisterCommand("rigid", "Show/Hide Rigid debug draw",
                (IDebugCommandHost host, string command, IList<string> args) => debugManager.DrawRigids = !debugManager.DrawRigids);
            //Bullet時間計測のOn/Off
            RegisterCommand("plog", "Enable/Disable Physics log",
                (IDebugCommandHost host, string command, IList<string> args) => debugManager.TimeRuler.EnablePhysics = !debugManager.TimeRuler.EnablePhysics);
            if ( debugManager == null )
                throw new InvalidOperationException( "DebugManagerが見つかりません。" );

            base.Initialize();
        }

        #endregion

        #region IDebugCommandHostインターフェースの実装
        /// <summary>
        /// コマンド登録
        /// </summary>
        /// <param name="command">コマンド名</param>
        /// <param name="description">説明</param>
        /// <param name="callback">コールバック用デリゲート</param>
        public void RegisterCommand(
            string command, string description, DebugCommandExecute callback )
        {
            string lowerCommand = command.ToLower();
            if ( commandTable.ContainsKey( lowerCommand ) )
            {
                throw new InvalidOperationException(
                    String.Format( "{0}は既に登録されています", command ) );
            }

            commandTable.Add(
                lowerCommand, new CommandInfo( command, description, callback ) );
        }
        /// <summary>
        /// コマンド解除
        /// </summary>
        /// <param name="command">コマンド名</param>
        public void UnregisterCommand( string command )
        {
            string lowerCommand = command.ToLower();
            if ( !commandTable.ContainsKey( lowerCommand ) )
            {
                throw new InvalidOperationException(
                    String.Format( "{0}は登録されていません", command ) );
            }

            commandTable.Remove( command );
        }
        /// <summary>
        /// コマンドの実行
        /// </summary>
        /// <param name="command">コマンド名</param>
        public void ExecuteCommand( string command )
        {
            // コマンドの実行
            char[] spaceChars = new char[] { ' ' };

            Echo( Prompt + command );

            command = command.TrimStart( spaceChars );

            List<string> args = new List<string>( command.Split( spaceChars ) );
            string cmdText = args[0];
            args.RemoveAt( 0 );

            CommandInfo cmd;
            if ( commandTable.TryGetValue( cmdText.ToLower(), out cmd ) )
            {
                try
                {
                    // 登録されているコマンドのデリゲートを呼び出す
                    cmd.callback( this, command, args );
                }
                catch ( Exception e )
                {
                    // 例外がコマンド実行中に発生
                    EchoError( "Unhandled Exception occured" );

                    string[] lines = e.Message.Split( new char[]{ '\n' } );
                    foreach ( string line in lines )
                        EchoError( line );
                }
            }
            else
            {
                Echo( "Unknown Command" );
            }

            // コマンドヒストリに追加する
            commandHistory.Add( command );
            while ( commandHistory.Count > MaxCommandHistory )
                commandHistory.RemoveAt( 0 );

            commandHistoryIndex = commandHistory.Count;
        }
        /// <summary>
        /// EchoListnerの登録
        /// </summary>
        /// <param name="listner">EchoListner</param>
        public void RegisterEchoListner( IDebugEchoListner listner )
        {
            listenrs.Add( listner );
        }

        /// <summary>
        /// EchoListnerの解除
        /// </summary>
        /// <param name="listner">EchoListner</param>
        public void UnregisterEchoListner(IDebugEchoListner listner)
        {
            listenrs.Remove( listner );
        }

        /// <summary>
        /// EchoListnerの呼び出し
        /// </summary>
        /// <param name="messageType">メッセージ種別</param>
        /// <param name="text">メッセージ挿入用</param>
        public void Echo(DebugCommandMessage messageType, string text)
        {
            lines.Enqueue( text );
            while ( lines.Count >= MaxLineCount )
                lines.Dequeue();

            // 登録されているリスナーを呼び出す
            foreach ( IDebugEchoListner listner in listenrs )
                listner.Echo( messageType, text );
        }
        /// <summary>
        /// Echo
        /// </summary>
        /// <param name="text">Echoするテキスト</param>
        public void Echo( string text )
        {
            Echo( DebugCommandMessage.Standard, text );
        }

        /// <summary>
        /// EchoWarning
        /// </summary>
        /// <param name="text">EchoWarningするテキスト</param>
        public void EchoWarning(string text)
        {
            Echo( DebugCommandMessage.Warning, text );
        }

        /// <summary>
        /// Echo
        /// </summary>
        /// <param name="text">Echoするテキスト</param>
        public void EchoError(string text)
        {
            Echo( DebugCommandMessage.Error, text );
        }

        #endregion

        #region 更新と描画
        /// <summary>
        /// 更新処理
        /// </summary>
        /// <param name="gameTime">GameTimeオブジェクト</param>
        public override void Update( GameTime gameTime )
        {
            KeyboardState keyState = Keyboard.GetState();

            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
            const float OpenSpeed = 8.0f;
            const float CloseSpeed = 8.0f;

            switch ( state )
            {
            case State.Closed:
                if ( keyState.IsKeyDown( Keys.Tab ) )
                    state = State.Opening;
                break;
            case State.Opening:
                stateTransition += dt * OpenSpeed;
                if ( stateTransition > 1.0f )
                {
                    stateTransition = 1.0f;
                    state = State.Opened;
                }
                break;
            case State.Opened:
                ProcessKeyInputs( dt );
                break;
            case State.Closing:
                stateTransition -= dt * CloseSpeed;
                if ( stateTransition < 0.0f )
                {
                    stateTransition = 0.0f;
                    state = State.Closed;
                }
                break;
            }

            prevKeyState = keyState;

            base.Update( gameTime );
        }

        /// <summary>
        /// キー入力処理
        /// </summary>
        /// <param name="dt"></param>
        public void ProcessKeyInputs( float dt )
        {
            KeyboardState keyState = Keyboard.GetState();
            Keys[] keys = keyState.GetPressedKeys();

            bool shift = keyState.IsKeyDown( Keys.LeftShift ) ||
                            keyState.IsKeyDown( Keys.RightShift );

            foreach ( Keys key in keys )
            {
                if ( !IsKeyPressed( key, dt ) ) continue;

                char ch;
                if ( KeyboardUtils.KeyToString( key, shift, out ch ) )
                {
                    // 通常文字の入力
                    commandLine = commandLine.Insert( cursorIndex, new string( ch, 1 ) );
                    cursorIndex++;
                }
                else
                {
                    switch ( key )
                    {
                    case Keys.Back:
                        if ( cursorIndex > 0 )
                            commandLine = commandLine.Remove( --cursorIndex, 1 );
                        break;
                    case Keys.Delete:
                        if ( cursorIndex < commandLine.Length )
                            commandLine = commandLine.Remove( cursorIndex, 1 );
                        break;
                    case Keys.Left:
                        if ( cursorIndex > 0 )
                            cursorIndex--;
                        break;
                    case Keys.Right:
                        if ( cursorIndex < commandLine.Length )
                            cursorIndex++;
                        break;
                    case Keys.Enter:
                        // コマンドの実行
                        ExecuteCommand( commandLine );
                        commandLine = string.Empty;
                        cursorIndex = 0;
                        break;
                    case Keys.Up:
                        // ヒストリ表示
                        if ( commandHistory.Count > 0 )
                        {
                            commandHistoryIndex =
                                Math.Max( 0, commandHistoryIndex - 1 );

                            commandLine = commandHistory[commandHistoryIndex];
                            cursorIndex = commandLine.Length;
                        }
                        break;
                    case Keys.Down:
                        // ヒストリ表示
                        if ( commandHistory.Count > 0 )
                        {
                            commandHistoryIndex = Math.Min( commandHistory.Count - 1,
                                                            commandHistoryIndex + 1 );
                            commandLine = commandHistory[commandHistoryIndex];
                            cursorIndex = commandLine.Length;
                        }
                        break;
                    case Keys.Tab:
                        state = State.Closing;
                        break;
                    }
                }
            }

        }

        /// <summary>
        /// キーリピート付きキー押下チェック
        /// </summary>
        /// <param name="key">キー</param>
        /// <param name="dt">チャダリング抑制秒数</param>
        /// <returns></returns>
        bool IsKeyPressed( Keys key, float dt )
        {
            // 前フレームでキーが押されていなければ、キーが押されていると判定
            if ( prevKeyState.IsKeyUp( key ) )
            {
                keyRepeatTimer = keyRepeatStartDuration;
                pressedKey = key;
                return true;
            }

            // 前フレームでキーが押されていた場合はリピート処理
            if ( key == pressedKey )
            {
                keyRepeatTimer -= dt;
                if ( keyRepeatTimer <= 0.0f )
                {
                    keyRepeatTimer += keyRepeatDuration;
                    return true;
                }
            }

            return false;
        }
        /// <summary>
        /// 描画処理
        /// </summary>
        /// <param name="gameTime">GameTimeオブジェクト</param>
        public override void Draw( GameTime gameTime )
        {
            // コマンドウィンドウが完全に閉じている場合は描画処理をしない
            if ( state == State.Closed )
                return;

            SpriteFont font = debugManager.DebugFont;
            SpriteBatch spriteBatch = debugManager.SpriteBatch;
            Texture2D whiteTexture = debugManager.WhiteTexture;

            // コマンドウィンドウのサイズ計算と描画
            float w = GraphicsDevice.Viewport.Width;
            float h = GraphicsDevice.Viewport.Height;
            float topMargin = h * 0.1f;
            float leftMargin = w * 0.1f;

            Rectangle rect = new Rectangle();
            rect.X = (int)leftMargin;
            rect.Y = (int)topMargin;
            rect.Width = (int)( w * 0.8f );
            rect.Height = (int)( MaxLineCount * font.LineSpacing );

            Matrix mtx = Matrix.CreateTranslation(
                        new Vector3( 0, -rect.Height * ( 1.0f - stateTransition ), 0 ) );

            spriteBatch.Begin( SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate,
                                SaveStateMode.SaveState, mtx );

            spriteBatch.Draw( whiteTexture, rect, new Color( 0, 0, 0, 200 ) );

            // 文字列の描画
            Vector2 pos = new Vector2( leftMargin, topMargin );
            foreach ( string line in lines )
            {
                spriteBatch.DrawString( font, line, pos, Color.White );
                pos.Y += font.LineSpacing;
            }

            // プロンプト文字列の描画
            string leftPart = Prompt + commandLine.Substring( 0, cursorIndex );
            Vector2 cursorPos = pos + font.MeasureString( leftPart );
            cursorPos.Y = pos.Y;

            spriteBatch.DrawString( font,
                String.Format( "{0}{1}", Prompt, commandLine ), pos, Color.White );
            spriteBatch.DrawString( font, Cursor, cursorPos, Color.White );

            spriteBatch.End();
            GraphicsDevice.RenderState.DepthBufferEnable = true;
            GraphicsDevice.RenderState.AlphaBlendEnable = false;
            GraphicsDevice.RenderState.AlphaTestEnable = false;
            GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
            GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;

        }

        #endregion

    }
}
