using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Speech.AudioFormat;
using System.Speech.Recognition;
using System.Text;

/// <summary>
/// Windows音声解析DLL CopyRight : MSLABO  Ver1.0
/// </summary>
/// <see cref="https://creativecommons.org/licenses/by-sa/4.0/"/>
/// <see cref="http://mslabo.sakura.ne.jp/WordPress/"/>
/// <remarks>
/// 本プログラムとドキュメント（以下、本APと呼称）の著作権はMSLABOにあります。
/// 本APのMSLABO作成部分は、以下のクリエィティブ・コモンズライセンスに従い提供されています。
/// また本APが利用しているJNA、PROCESSINGなどの各モジュールについては、各ソフトウェアの
/// ライセンス規約に従うものとします。
/// 利用にあたっては、必ず自己責任でお願い致します。
/// 本APの不具合については極力善処を行う予定ですが、これを保証するものではありません。
/// また本APに関する情報はすべてMSLABOのHPから一方的に公開するものとし、原則として、個別の
/// 問い合わせや要望は受け付けません。
/// 上記事項に同意頂ける方のみ、利用が可能です。
/// 本プログラムが、みなさまのPROCESSING利用の一助になれば幸いです。
/// </remarks>
namespace SystemSpeechNET
{
    public class Class1
    {
        /// <summary>
        /// WinMM API定義クラス
        /// </summary>
        /// <see cref="https://qiita.com/kob58im/items/aa6c7a4dc80946dbe3a7"/>
        /// <remarks>
        /// Nativeクラスは上記サイトの@kob58imさんのCODEを参考としました。
        /// この場を借りて、お礼申し上げます。
        /// </remarks>
        class Native
        {
            const int MAXPNAMELEN = 32;
            public const int MMSYSERR_NOERROR = 0;

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            public struct WaveInCaps
            {
                public Int16 wMid;
                public Int16 wPid;
                public Int32 vDriverVersion;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAXPNAMELEN)]
                public string szPname;
                public Int32 dwFormats;
                public Int16 wChannels;
                Int16 wReserved1;
            };

            [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern Int32 waveInGetNumDevs();


            [DllImport("winmm.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern Int32 waveInGetDevCaps(
               Int32 uDeviceID,
               ref WaveInCaps wic,
               Int32 cbwic
           );
        }

        /// <summary>
        /// 音声解析クラス
        /// </summary>
        public class SpeechNETClass
        {
            private const string DEF_LOGFILE = @"SystemSpeechNET.txt";
            private const string DEF_GRAMMAR_FILE = @"Grammar.txt";

            private static SpeechRecognitionEngine engine = null;
            private static string recognizedStr = "";
            private static JavaRecognizedCallBack javaRecognizedFunc = null;
            private static JavaHypothesizedCallBack javaHypothesizedFunc = null;
            private static string currentPath = "";
            private static string logFile = DEF_LOGFILE;
            private static string grmFile = DEF_GRAMMAR_FILE;
            private static bool logSw = false;
            private static string codeName = "UTF-8";
            private static Encoding logEncode = null;
            private static Dictionary<String, float> hypothesizedDic = new Dictionary<string, float>();

            #region "内部関数"
            /// <summary>
            /// 解析モードを設定する
            /// </summary>
            /// <param name="mode">解析モード。true=文法解析/false=自由会話解析</param>
            /// <returns>0以外：異常</returns>
            private static int DictationType(bool mode)
            {
                int ret;
                if (mode)
                {
                    //文法ファイルを使う
                    ret = FileGrammar();

                    if (ret != 0)
                    {
                        //文法ファイルが利用できないので、自由会話解析を行う
                        ret = DictationGrammar();
                    }
                }
                else
                {
                    //自由会話解析を行う
                    ret = DictationGrammar();
                }
                return ret;
            }

            /// <summary>
            /// ファイル読込み。
            /// </summary>
            /// <param name="path">ファイルパス</param>
            /// <param name="comment">コメント文字</param>
            /// <returns>読込み結果リスト</returns>
            private static List<string> ReadGrammarFile(string path, string comment)
            {
                //コメント以外の行を取得
                var lines = File.ReadAllLines(path, Encoding.Default)
                    .Where(line => !line.StartsWith(comment))
                    .Where(line => !String.IsNullOrEmpty(line))
                    .ToList();

                return lines;
            }

            /// <summary>
            /// WAVファイル検査
            /// </summary>
            /// <param name="filePath">WAVファイル</param>
            /// <returns>false:異常</returns>
            private static bool CheckWavFileFormat(string filePath)
            {
                FileInfo file = new FileInfo(filePath);
                if (file.Length < 16)
                {
                    WriteLog("WAVファイルサイズエラー");
                    return false;
                }

                byte[] riff = System.Text.Encoding.ASCII.GetBytes("RIFF");
                byte[] wave = System.Text.Encoding.ASCII.GetBytes("WAVEfmt");

                using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                {
                    var buffer = new byte[4];
                    fs.Read(buffer, 0, buffer.Length);

                    //先頭が[RIFF]か調べる
                    if (!buffer.SequenceEqual(riff))
                    {
                        //[RIFF]じゃない
                        WriteLog("WAVファイルヘッダーエラー");
                        return false;
                    }

                    //フォーマットタイプを調べる
                    fs.Seek(8, SeekOrigin.Begin);
                    buffer = new byte[7];
                    fs.Read(buffer, 0, buffer.Length);
                    if (!buffer.SequenceEqual(wave))
                    {
                        //[WAVEfmt]じゃない
                        WriteLog("WAVファイルフォーマットエラー");
                        return false;
                    }

                    return true;
                }
            }

            private static string ConvertEncoding(string src, System.Text.Encoding destEnc)
            {
                byte[] src_temp = System.Text.Encoding.ASCII.GetBytes(src);
                byte[] dest_temp = System.Text.Encoding.Convert(System.Text.Encoding.ASCII, destEnc, src_temp);
                string ret = destEnc.GetString(dest_temp);
                return ret;
            }

            /// <summary>
            /// 簡易ログ出力
            /// </summary>
            /// <param name="msg">出力メッセージ</param>
            private static void WriteLog(string msg)
            {
                if (logSw)
                {
                    CurrentPath();

                    DateTime dt = DateTime.Now;

                    string logTime = dt.ToString("yyyy/MM/dd HH:mm:ss fff");

                    // 文字コードを指定
                    string writeMsg = String.Format("{0}:{1}", logTime, msg);

                    // ファイルに書く
                    using (var writer = new StreamWriter(Path.Combine(currentPath, logFile), true, logEncode))
                    {
                        writer.WriteLine(writeMsg);
                    }

                    Console.OutputEncoding = logEncode;
                    Console.WriteLine( writeMsg );

                }
            }

            /// <summary>
            /// 解析エンジンClose
            /// </summary>
            private static void Close()
            {
                try {
                    engine.RecognizeAsyncCancel();
                    engine.SpeechRecognized -= RecogEngine_SpeechRecognized;
                    engine.SpeechHypothesized -= RecogEngine_SpeechHypothesized;
                    engine.SpeechRecognitionRejected -= SpeechRecognitionRejected;
                    //engine.AudioSignalProblemOccurred -= RecogEngine_ProblemOccurred;

                    engine.Dispose();
                    engine = null;
                }
                catch(Exception e)
                {
                    WriteLog("Close 例外:" + e.StackTrace + "/" + e.Message);
                    return;
                }

            }

            /// <summary>
            /// Dictationモードで解析する
            /// </summary>
            /// <returns>0以外：異常</returns>
            private static int DictationGrammar()
            {
                try
                {
                    //すべての文法を初期化
                    engine.UnloadAllGrammars();

                    engine.LoadGrammar(new DictationGrammar());

                    WriteLog("自由解析します");
                }
                catch (Exception e)
                {
                    WriteLog("DictationGrammar 例外:" + e.StackTrace + "/" + e.Message);
                    return -1;
                }
                return 0;
            }

            /// <summary>
            /// 指定ファイルを文法ファイルとして解析する
            /// </summary>
            /// <returns>0以外：異常</returns>
            private static int FileGrammar()
            {
                CurrentPath();

                string grammarPath = Path.Combine(currentPath, grmFile);
                if (!File.Exists(grammarPath))
                {
                    WriteLog(String.Format("文法ファイルが存在しません。ファイル={0}", grammarPath));
                    return -1;
                }

                try
                {
                    //すべての文法を初期化
                    engine.UnloadAllGrammars();

                    // 文法ファイルからコメントを除く行を取得                   
                    var choices = new Choices();
                    foreach (string line in ReadGrammarFile(grammarPath, "#"))
                    {
                        choices.Add(line);
                    }
                    var grammar = new Grammar(choices.ToGrammarBuilder());
                    engine.LoadGrammar(new Grammar(choices.ToGrammarBuilder()));

                    WriteLog(String.Format("{0}を文法ファイルとして解析します", grammarPath));
                }
                catch (Exception e)
                {
                    WriteLog("FileGrammar 例外:" + e.StackTrace + "/" + e.Message);
                    return -1;
                }
                return 0;
            }

            /// <summary>
            /// 標準入力マイクを使用
            /// </summary>
            /// <returns>0以外：異常</returns>
            private static int InputDefaultAudio()
            {
                //標準入力マイクがあるか確認する
                int n = Native.waveInGetNumDevs();
                if (n < 1)
                {
                    WriteLog("標準入力マイクが存在しません");
                    return -1;
                }

                var wic = new Native.WaveInCaps();
                int retCode = Native.waveInGetDevCaps(0, ref wic, Marshal.SizeOf(wic));
                if( retCode == Native.MMSYSERR_NOERROR)
                {
                    WriteLog(wic.szPname + "を入力デバイスとして利用します");
                }                

                try
                {
                    // 実行環境の標準の入力を音声認識エンジンの入力とする。
                    engine.SetInputToNull();
                    engine.SetInputToDefaultAudioDevice();
                }
                catch (Exception e)
                {
                    WriteLog("SetInputDefaultAudio 例外:" + e.StackTrace + "/" + e.Message);
                    return -1;
                }
                return 0;
            }

            /// <summary>
            /// カレントパスを設定する
            /// </summary>
            private static void CurrentPath()
            {
                if(String.IsNullOrEmpty(currentPath))
                {
                    currentPath = System.IO.Directory.GetCurrentDirectory();
                }                
            }

            /// <summary>
            /// 音声認識イベントハンドラ
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private static void RecogEngine_SpeechHypothesized(object sender, SpeechHypothesizedEventArgs e)
            {
                WriteLog("SpeechHypothesized");

                foreach (RecognizedPhrase phrase in e.Result.Alternates)
                {
                    string key = phrase.Text.Trim();
                    float confidence = phrase.Confidence;

                    WriteLog("  候補対象語句 : " + key);
                    WriteLog("  信頼性 スコア: " + confidence);

                    //解析語句を信頼性一覧に登録する
                    if (!hypothesizedDic.ContainsKey(key)){
                        //同じKEYが存在しない場合、新規登録
                        hypothesizedDic.Add(key, confidence);
                    } else
                    {
                        //既存のKEYがある場合、より信頼度の高いデータを登録
                        if( hypothesizedDic[key] < confidence)
                        {
                            hypothesizedDic.Remove(key);
                            hypothesizedDic.Add(key, confidence);
                        }                        
                    }                    
                }
            }

            /// <summary>
            /// 音声認識認識文法不一致イベントハンドラ
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private static void SpeechRecognitionRejected(object sender, SpeechRecognitionRejectedEventArgs e)
            {
                WriteLog("SpeechRejected:" + e.Result.Text);

                //Java側をコールバックする
                RejectedCallBack();

                hypothesizedDic.Clear();
            }

            /// <summary>
            /// 解析失敗時、それまで得ていた解析内容を戻す
            /// </summary>
            private static void RejectedCallBack()
            {
                if (javaHypothesizedFunc != null)
                {
                    //Java側をコールバックする
                    string retStr = "";
                    float confidence = 0;

                    if (hypothesizedDic.Count > 0)
                    {
                        //信頼性一覧を信頼度を元に降順ソートし
                        //最も信頼度の高い単語を返却する
                        var getVal = hypothesizedDic.OrderByDescending(x => x.Value).First();
                        retStr = getVal.Key;
                        confidence = getVal.Value;
                    }

                    javaHypothesizedFunc(ref retStr, ref confidence);
                }
            }

            /// <summary>
            /// 音声認識推定完了イベントハンドラ
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private static void RecogEngine_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
            {
                string nowRecogTxt = e.Result.Text.Trim();
                if (!String.IsNullOrEmpty(nowRecogTxt))
                {
                    string retStr = recognizedStr + nowRecogTxt;
                    WriteLog(String.Format("解析結果({0}):[{1}]", retStr.Length, retStr));

                    if (javaRecognizedFunc != null)
                    {
                        //Java側をコールバックする                        
                        int size = retStr.Length;

                        javaRecognizedFunc(ref retStr, ref size);
                        recognizedStr = "";
                    } else
                    {
                        recognizedStr = retStr + Environment.NewLine;
                    }
                }
            }
            #endregion


            #region "公開関数"
            /// <summary>
            /// ログとコンソールの文字コードを受け取る
            /// </summary>
            /// <param name="code">文字コード名</param>
            /// <param name="path">カレントパス</param>
            /// <returns>0以外：異常</returns>
            [DllExport]
            public static int SetLogEncode(string code, string path)
            {
                try{
                    codeName = code;
                    logEncode = Encoding.GetEncoding(codeName);

                    if (!String.IsNullOrEmpty(path) && Directory.Exists(path))
                    {
                        currentPath = path;
                    }
                } 
                catch( Exception )
                {
                    return -1;
                }
                return 0;
                
            }

            /// <summary>
            /// ログファイル名切り替え
            /// </summary>
            /// <param name="path">ログファイル名</param>
            [DllExport]
            public static void SetLogName(string fileName)
            {
                CurrentPath();

                fileName = Path.GetFileName(fileName);
                if (!String.IsNullOrEmpty(fileName))
                {
                    logFile = fileName;
                }
            }

            /// <summary>
            /// 文法ファイル名切り替え
            /// </summary>
            /// <param name="fileName">文法ファイル名</param>
            [DllExport]
            public static void SetGrammarName(string fileName)
            {
                CurrentPath();

                fileName = Path.GetFileName(fileName);
                if (!String.IsNullOrEmpty(fileName))
                {
                    grmFile = fileName;
                }
            }

            /// <summary>
            /// ログ出力切り替え
            /// </summary>
            /// <param name="sw">true:出力する false:出力しない</param>
            [DllExport]
            public static void SetLogSwitch(bool sw)
            {
                logSw = sw;
            }

            /// <summary>
            /// 解析エンジンClose
            /// </summary>
            [DllExport]
            public static void CloseEngine()
            {
                if (engine != null)
                {
                    Close();
                    WriteLog("解析エンジンをCloseしました");
                }
            }

            /// <summary>
            /// 解析エンジンOpen
            /// </summary>
            /// <param name="mode">解析モード。true=文法解析/false=自由会話解析</param>
            /// <returns>0以外：異常</returns>
            [DllExport]
            public static int OpenEngine(bool mode)
            {
                //解析語句信頼性一覧をクリア
                hypothesizedDic.Clear();

                if (engine != null)
                {
                    WriteLog("Open済みの解析エンジンを閉じて再Openします");
                    Close();
                }

                engine = new SpeechRecognitionEngine();

                try
                {
                    //音声認識認識文法不一致イベントハンドラを設定する
                    engine.SpeechRecognitionRejected += SpeechRecognitionRejected;

                    //音声認識推定完了イベントハンドラを設定する
                    engine.SpeechRecognized += RecogEngine_SpeechRecognized;

                    //音声認識イベントハンドラを設定する
                    engine.SpeechHypothesized += RecogEngine_SpeechHypothesized;

                    //解析モードを設定する
                    DictationType(mode);

                    //標準入力マイクを利用する
                    InputDefaultAudio();

                    WriteLog("解析エンジンをOpenしました");
                }
                catch (Exception e)
                {
                    WriteLog("Open 例外:" + e.StackTrace + "/" + e.Message);
                    engine.Dispose();
                    engine = null;
                    return -1;
                }

                return 0;
            }

            /// <summary>
            /// 解析タイプを設定する
            /// </summary>
            /// <param name="type">解析モード。true=文法解析/false=自由会話解析</param>
            /// <returns>0以外：異常</returns>            
            [DllExport]
            public static int SetDictationType(bool mode)
            {
                if (engine == null)
                {
                    WriteLog("解析エンジンが未Openです");
                    return -1;
                }

                return DictationType(mode);
            }

            /// <summary>
            /// 入力に標準入力マイクを利用する
            /// </summary>
            /// <returns>0以外：異常</returns>
            [DllExport]
            public static int SetInputDefaultAudio()
            {
                if (engine == null)
                {
                    WriteLog("解析エンジンが未Openです");
                    return -1;
                }

                return InputDefaultAudio();
            }

            /// <summary>
            /// 入力に指定WAVファイルを利用する
            /// </summary>
            /// <param name="filename">入力元WAVファイル</param>
            /// <returns>0以外：異常</returns>
            [DllExport]
            public static int SetInputWavFile(string filename)
            {
                if (engine == null)
                {
                    WriteLog("解析エンジンが未Openです");
                    return -1;
                }

                if (String.IsNullOrEmpty(filename)
                    || !File.Exists(filename)
                    || !Path.GetExtension(filename.ToLower()).Equals(".wav"))
                {
                    WriteLog("指定ファイルが存在しないか不正です");
                    return -1;
                }

                if (CheckWavFileFormat(filename) == false)
                {
                    WriteLog("指定ファイルがWAVファイルではありません");
                    return -1;
                }

                try
                {

                    engine.SetInputToWaveFile(filename);

                    //engine.SetInputToAudioStream(
                    //    File.OpenRead(filename),
                    //    new SpeechAudioFormatInfo(
                    //        44100, AudioBitsPerSample.Sixteen, AudioChannel.Stereo));

                    WriteLog(String.Format("{0}を入力元として利用します", filename));
                }
                catch (Exception e)
                {
                    WriteLog("SetInputWavFile 例外:" + e.StackTrace + "/" + e.Message);
                    return -1;
                }

                return 0;
            }

            /// <summary>
            /// 非同期解析開始
            /// </summary>
            /// <param name="singleMode">true:シングル解析 false：連続解析</param>
            /// <returns>0以外：異常</returns>
            [DllExport]
            public static int StartAsyncRecognized(bool singleMode)
            {
                if (engine == null)
                {
                    WriteLog("解析エンジンが未Openです");
                    return -1;
                }

                try
                {
                    if (singleMode)
                    {
                        //非同期の認識を１回だけ実行する
                        engine.RecognizeAsync(RecognizeMode.Single);
                    }
                    else
                    {
                        //非同期の認識を継続して実行する
                        engine.RecognizeAsync(RecognizeMode.Multiple);
                    }
                }
                catch (Exception e)
                {
                    WriteLog("StartAsyncRecognized 例外:" + e.StackTrace + "/" + e.Message);
                    return -2;
                }

                return 0;
            }

            /// <summary>
            /// 同期解析開始
            /// </summary>
            /// <param name="timeout">無音許可時間（秒）。0以下なら無限待ち</param>
            /// <returns>0以外：異常</returns>
            [DllExport]
            public static int StartRecognized(int timeout)
            {
                if (engine == null)
                {
                    WriteLog("解析エンジンが未Openです");
                    return -1;
                }

                try
                {
                    if (timeout > 0)
                    {
                        engine.InitialSilenceTimeout = TimeSpan.FromSeconds(timeout);
                    }

                    RecognitionResult result = engine.Recognize();
                    if (result == null)
                    {
                        WriteLog("StartRecognized 音声解析エラー");

                        //Java側を呼び出す
                        RejectedCallBack();

                        hypothesizedDic.Clear();
                        return -1;
                    }
 
                }
                catch (Exception e)
                {
                    WriteLog("StartRecognized 例外:" + e.StackTrace + "/" + e.Message);
                    return -1;
                }
                return 0;
            }

            /// <summary>
            /// 非同期連続解析停止
            /// </summary>
            /// <returns>0以外：異常</returns>
            [DllExport]
            public static int StopAsyncRecognized()
            {
                if (engine == null)
                {
                    WriteLog("解析エンジンが未Openです");
                    return -1;
                }

                try
                {
                    engine.RecognizeAsyncCancel();
                }
                catch (Exception e)
                {
                    WriteLog("StopAsyncRecognized 例外:" + e.StackTrace + "/" + e.Message);
                    return -1;
                }

                return 0;
            }

            /// <summary>
            /// 解析結果取得
            /// </summary>
            /// <returns>解析文字</returns>
            [DllExport]
            public static string GetSpeechRecognized()
            {
                string data = recognizedStr.Trim();
                recognizedStr = "";
                return data;
            }

            /// <summary>
            /// 最も信頼度の高い単語を返却する
            /// </summary>
            /// <returns>解析文字</returns>
            [DllExport]
            public static string GetSpeechHypothesized()
            {
                //信頼性一覧がカラ
                if (hypothesizedDic.Count < 1) return "";

                //信頼性一覧を信頼度を元に降順ソートし、最も信頼度の高い単語を返却する
                string retVal = hypothesizedDic.OrderByDescending(x => x.Value).First().Key;
                hypothesizedDic.Clear();
                return retVal;
            }

            public delegate void JavaRecognizedCallBack(ref string a, ref int b);
            public delegate void JavaHypothesizedCallBack(ref string a, ref float b);

            [DllExport]
            public static void SetHypothesizedCallBackFunc(IntPtr hWnd)
            {
                if (hWnd != null)
                {
                    javaHypothesizedFunc = (JavaHypothesizedCallBack)Marshal.GetDelegateForFunctionPointer(hWnd, typeof(JavaHypothesizedCallBack));
                }

                return;
            }

            /// <summary>
            /// 解析終了時コールバック先関数登録
            /// </summary>
            /// <param name="hWnd">コールバック先関数</param>
            [DllExport]
            public static void SetRecognizedCallbackFunc(IntPtr hWnd)
            {
                if (hWnd != null)
                {
                    javaRecognizedFunc = (JavaRecognizedCallBack)Marshal.GetDelegateForFunctionPointer(hWnd, typeof(JavaRecognizedCallBack));
                }

                return;
            }
            #endregion


        }
    }
}
