#include "stdafx.h"
#include <dmksctrl.h>
#include <process.h>
#include "Port.h"
#include <mmsystem.h>

namespace GuruGuruSmf { namespace Dxmus
{

    // コンストラクタ
    Dls::Dls(IDirectMusicLoader8* ldr)
    {
        loader = ldr;
        collection = 0;
    }

    // デストラクタ
    Dls::~Dls()
    {
        if (collection){
            collection->Release();
            collection = 0;
        }
    }

    // ファイルから読み込み
    GGSERROR Dls::LoadFromFile(wchar_t *fileName)
    {
        HRESULT hr;
        hr = loader->LoadObjectFromFile(CLSID_DirectMusicCollection, IID_IDirectMusicCollection8,
                                fileName, (LPVOID*)&collection );
        if(FAILED(hr))  return GgsError::FileLoadFailed;    // 読み込み失敗

        return GgsError::NoError;   // 成功
    }

    // 音色取り出し
    int Dls::GetInstrument(int voice, IDirectMusicInstrument8** instrument)
    {
        if(collection == 0) return -1;

        return collection->GetInstrument(voice, instrument);
    }

    void Port::_CreateWindow() //by Kobarin
    {//Open に渡された HWND が NULL だったときにデフォルトのウィンドウを作成
        static wchar_t base_classname[] = L"GuruGuruSMFPortHidden";
        if(hwnd)return;//既に作成済み(通常はない筈)
        wchar_t classname[128];
        //クラス名をインスタンスごとに異なるものにする
        _snwprintf_s(classname, _TRUNCATE, L"%s-%08x-%08x", base_classname, GetCurrentThreadId(), GetTickCount());
        //
        WNDCLASSEX wcex;
        wcex.cbSize = sizeof(WNDCLASSEX);
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = DefWindowProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = GetModuleHandle(NULL);
        wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
        wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName = NULL;
        wcex.lpszClassName = classname;
        wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

        RegisterClassEx(&wcex);
        hwnd = CreateWindow(
            classname, classname, WS_OVERLAPPEDWINDOW,
            0, 0, 100, 100,
            NULL, NULL, GetModuleHandle(NULL), NULL
        );
        //ShowWindow(hwnd, SW_SHOWDEFAULT);
        UpdateWindow(hwnd);
    }
    void Port::_DestroyWindow()//by Kobarin
    {//Open に渡された HWND が NULL だったときに作成したウィンドウを破棄
        if(!hwnd)return;
        wchar_t classname[128];
        classname[0] = 0;
        GetClassName(hwnd, classname, _countof(classname));
        DestroyWindow(hwnd);
        hwnd = NULL;
        UnregisterClass(classname, GetModuleHandle(NULL));
    }
    // DirectMusic管理スレッド
    unsigned int WINAPI Port::threadPort(void* p_userdata)
    {
        CoInitializeEx(NULL, COINIT_MULTITHREADED);
        //CoInitialize(NULL);
        ((Port*)p_userdata)->OnWorker();
        CoUninitialize();
        return 0;
    }

    // 全オブジェクトの操作覚悟で作りましたが
    // OpenとCloseだけでも動いてるので様子見
    // ↓by Kobarin
    // Open/Close 以外もこのスレッド内で処理するようにした
    // 戻り値を必須としないコマンドは非同期での処理にも対応
    void Port::OnWorker()
    {
        bool exit = false;
        while(!exit)
        {
            #if 0
            WaitForSingleObject(sem_exec, INFINITE);//セマフォカウンタが 0 である間は戻って来ない、戻ったらカウンタが 1 減る
            #else
            //メッセージループを回さないと64bit版の本体から使用したときに DDE がまともに機能しなくなる
            //いまいち納得行かず
            HANDLE hHandles[] = {sem_exec};
            DWORD dwWait = MsgWaitForMultipleObjects(1, hHandles, FALSE, INFINITE, QS_ALLINPUT);
            if(dwWait == WAIT_OBJECT_0+1){
                MSG msg;
                while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
                continue;
            }
            #endif
            #ifdef _DEBUG
            if (commands.empty()) {//セマフォから戻るのは PostCommand が呼ばれた後なのでコマンドが空であることはない
                OutputDebugString(L"OnWorker::error! commands.empty() == true\n");
                DebugBreak();//ここが実行されることがあってはならない
                continue;
            }
            #endif
            EnterCriticalSection(&lockObject);
            PortCommand cmd = commands.front();
            LeaveCriticalSection(&lockObject);
            switch(cmd.type)
            {
            case CommandType_Open:
                //resultOpen = _Open(cmd.Open.handle);
                *cmd.Open.presult = _Open(cmd.Open.handle);
                break;
            case CommandType_Close:
                _Close();
                exit = true;//デバイスを閉じたらスレッドを終了させる
                break;
            case CommandType_GetRefTime:{
                *cmd.GetRefTime.presult = _GetReferenceTime(cmd.GetRefTime.time);
                break;
            }
            case CommandType_SendShortMsg:{
                _SendShortMessage(cmd.ShortMsg.port, cmd.ShortMsg.time, cmd.ShortMsg.midiMessage);
                break;
            }
            case CommandType_SendLongMsg:{
                _SendLongMessage(cmd.LongMsg.port, cmd.LongMsg.time, cmd.LongMsg.midiMessage, cmd.LongMsg.length);
                break;
            }
            case CommandType_SetVoiceList:{
                _SetVoiceList(cmd.SetVoiceList.list, cmd.SetVoiceList.count);
                break;
            }
            case CommandType_AddDls: {
                *cmd.AddDls.presult = _AddDls(cmd.AddDls.fileName);
                break;
            }
            default:
                break;
            }
            EnterCriticalSection(&lockObject);
            commands.pop();
            ReleaseSemaphore(sem_post, 1, NULL);
            if (cmd.event)SetEvent(cmd.event);//処理が終わったらシグナル
            LeaveCriticalSection(&lockObject);
        }
    }

    // コマンドをキューに入れる
    void Port::PostCommand(PortCommand cmd)
    {
        if(!handle_thread)return;
        #ifdef _DEBUG
        if (commands.size() > command_max) {//ポスト出来る上限を超えている
            //ここが実行されたらどこかロジックが間違っている
            OutputDebugString(L"PostCommand::error! commands.size() > command_max\n");
            DebugBreak();
        }
        #endif
        WaitForSingleObject(sem_post, INFINITE);//コマンドをポスト出来るようになるまで待つ
        //この時点でコマンドの空きが1つ(commands.size()==command_max-1)以上なければならない
        #ifdef _DEBUG
        if (commands.size() >= command_max) {
            //ここが実行されたらどこかロジックが間違っている
            OutputDebugString(L"PostCommand::error! commands.size() >= command_max\n");
            DebugBreak();
        }
        else if(commands.size() == command_max-1){//空きが残り1つの状態
        //command_max の値が
        //200 以上だと開始直後でもまず通らない
        //100 だと曲データによっては開始直後は通る
        // 30 だと開始直後以外はほぼ通らない
        // 20 だと開始直後以外はたまに通る
        // 10 だと普段から通る
            OutputDebugString(L"PostCommand::command.size() == command_max-1\n");
            //DebugBreak();
        }
        #endif
        //非同期対応のコマンドだけ cmd.event を NULL に設定する
        //非同期対応なのに終了まで待っても動作上は問題ない
        switch(cmd.type){
            case CommandType_SendShortMsg:
            case CommandType_SendLongMsg:
            case CommandType_SetVoiceList:
                cmd.event = NULL;
                break;
            default:
                cmd.event = event_wait;
                break;
        }
        EnterCriticalSection(&lockObject);
        commands.push(cmd);
        ReleaseSemaphore(sem_exec, 1, NULL);//呼び出し直前にカウンタが0だったなら呼び出し後、ワーカースレッドが動き出す
        LeaveCriticalSection(&lockObject);
        if(cmd.event){//コマンドが終了するのを待つ
            WaitForSingleObject(cmd.event, INFINITE);
        }
    }

    // ワーカースレッドを開始
    void Port::StartWorker(void)
    {
        if (handle_thread) return;  // すでにスレッドが立っている時は使い回し(普通はないはず)
        //isWorkerOn = true;
        InitializeCriticalSection(&lockObject);
        sem_exec = CreateSemaphore(NULL, 0, command_max, NULL);
        sem_post = CreateSemaphore(NULL, command_max, command_max, NULL);
        event_wait = CreateEvent(NULL, FALSE, FALSE, NULL);
        handle_thread = (HANDLE)_beginthreadex(NULL, 0, threadPort, this, 0, NULL);
    }

    // ワーカースレッドの終了処理
    void Port::FinalizeWorker()
    {
        WaitForSingleObject(handle_thread, INFINITE);
        CloseHandle(handle_thread);
        handle_thread = 0;

        CloseHandle(sem_exec);
        sem_exec = 0;
        CloseHandle(sem_post);
        sem_post = 0;
        CloseHandle(event_wait);
        event_wait = 0;
        DeleteCriticalSection(&lockObject);
        while (!commands.empty()) commands.pop();//by Kobarin(コマンドを空にする(念の為))
    }

    // 初期化
    GGSERROR Port::Open(HWND handle)
    {
        if (isOpen) return GgsError::NoError;   // すでに開いてる時は帰る　handleが変わることはないはず
        StartWorker();

        GGSERROR resultOpen = GgsError::CannotOpenDevice;   //by Kobarin
        PortCommand cmd;
        cmd.type = CommandType_Open;
        cmd.Open.handle = handle;
        cmd.Open.presult = &resultOpen;
        PostCommand(cmd);

        // 完了待ち
        // ここの待ち時間がかなり長いのでうまいこと並列処理にしたいところ
        //WaitForSingleObject(_hOpened, INFINITE);

        if (resultOpen != GgsError::NoError)
        {
            // 開くのに失敗していた時は中途半端なオブジェクトを始末しておく
            Close();
        }

        return resultOpen;
    }

    // 後始末
    void Port::Close()
    {
        if (!handle_thread) return; // スレッドがない時はそのまま帰る

        PortCommand cmd;
        cmd.type = CommandType_Close;
        PostCommand(cmd);

        FinalizeWorker();
    }
    // コンストラクタ
    Port::Port()
    {
        music = 0;
        port = 0;
        buffer = 0;
        loader = 0;
        defaultDls = 0;
        clock = 0;

        downloadedList = 0;
        downloadedCount = 0;


        isOpen = false;
        //isWorkerOn = false;

        handle_thread = 0;
        //_hOpened = 0;
        //resultOpen = GgsError::ApiNull;   // エラーコードが妥当ではないかもしれません(宿題)
        // 通常は参照されませんが、へっぽこなOpenの仕方をするとこれが返る可能性があります

        //by Kobarin
        port_max = 1;
        sem_exec = NULL;
        sem_post = NULL;
        event_wait = NULL;
        hwnd = NULL;
        ZeroMemory(&lockObject, sizeof(lockObject));//コンパイラ警告(C26495)が気持ち悪いので
        //ここまで
    }

    // デストラクタ
    Port::~Port()
    {
        Close(); // ここではすでに閉じられてないといけません
    }

    // 初期化本体(ワーカースレッド上で実行)
    GGSERROR Port::_Open(HWND handle)
    {
        _Close();
        //by Kobarin
        //IASIO::Init に NULL を渡すと DDE が機能しなくなる経験から、ウィンドウハンドルに
        //NULL を渡されたら念の為内部でウィンドウを作成してそれを渡すようにする
        //handle = FindWindow(L"TFrmMIDI", NULL);//本体のメインウィンドウのハンドルを渡しても特に問題なく動作する模様
        if(!handle){
            _CreateWindow();
            handle = hwnd;
        }
        HRESULT hr;
        hr = CoCreateInstance(CLSID_DirectMusic ,NULL ,
                    CLSCTX_INPROC_SERVER ,IID_IDirectMusic8,
                    (LPVOID*)&music);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;
        hr = music->SetDirectSound(NULL ,handle);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;

        //ポート作成
        DMUS_PORTPARAMS param;
        ZeroMemory(&param, sizeof(DMUS_PORTPARAMS));
        param.dwSize = sizeof(DMUS_PORTPARAMS);
        param.dwValidParams = DMUS_PORTPARAMS_VOICES
            | DMUS_PORTPARAMS_CHANNELGROUPS
            | DMUS_PORTPARAMS_SAMPLERATE
            | DMUS_PORTPARAMS_EFFECTS
            ;
        param.dwVoices = 128;
        //by Kobarin
        //本当は曲データに合わせた数にしたいところだが GuruGuruSMF の仕様上デバイスを開く前に SMF を開くとは限らないので事前にポート数を知ることが出来ない
        param.dwChannelGroups = MAX_PORT;
        param.dwSampleRate = 44100;
        param.dwEffectFlags = DMUS_EFFECT_REVERB | DMUS_EFFECT_CHORUS /*| DMUS_EFFECT_DELAY*/;

        GUID guidPort;
        hr = music->GetDefaultPort(&guidPort);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;
        hr = music->CreatePort(guidPort, &param, &port, NULL);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;
        hr = port->Activate(TRUE);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;

        // バッファ作成
        DMUS_BUFFERDESC bufferDesc;

        bufferDesc.dwSize = sizeof(DMUS_BUFFERDESC);
        bufferDesc.dwFlags = 0;
        bufferDesc.guidBufferFormat = GUID_NULL;
        bufferDesc.cbBuffer = 128;
        hr = music->CreateMusicBuffer(&bufferDesc, &buffer, NULL);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;

        // タイマ取得
        hr = port->GetLatencyClock(&clock);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;

        // ローダー作成
        hr = CoCreateInstance(CLSID_DirectMusicLoader, NULL,
                    CLSCTX_INPROC_SERVER, IID_IDirectMusicLoader8,
                    (LPVOID*)&loader);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;

        wchar_t path[MAX_PATH];
        if(GetCurrentDirectoryW(MAX_PATH, path) == 0){
            DWORD errCode = GetLastError();
            if(errCode == ERROR_CALL_NOT_IMPLEMENTED){  // Windows9x ハードコーディングです
                char pathA[MAX_PATH];
                GetCurrentDirectoryA(MAX_PATH, pathA);
                MultiByteToWideChar(CP_ACP, 0, pathA, -1, path, MAX_PATH);
            }
        }

        loader->SetSearchDirectory(GUID_DirectMusicAllTypes, path, FALSE);


        // デフォルトのDLSをダウンロード
        DMUS_OBJECTDESC objectDesc;
        ZeroMemory(&objectDesc, sizeof(DMUS_OBJECTDESC));
        objectDesc.dwSize = sizeof(DMUS_OBJECTDESC);
        objectDesc.guidClass = CLSID_DirectMusicCollection;
        objectDesc.guidObject = GUID_DefaultGMCollection;
        objectDesc.dwValidData = DMUS_OBJ_CLASS | DMUS_OBJ_OBJECT;
        hr = loader->GetObject(&objectDesc, IID_IDirectMusicCollection8,
                    (LPVOID*)&defaultDls);
        if(FAILED(hr))  return GgsError::CannotOpenDevice;

        isOpen = true;
        port_max = 1;
        return GgsError::NoError;
    }

    // 音色リストをリセット
    void Port::ResetVoiceList()
    {
        if(downloadedCount == 0) return;

        for(int i=0; i<downloadedCount; i++){
            if(downloadedList[i]){
                port->UnloadInstrument(downloadedList[i]);
                downloadedList[i]->Release();
            }
        }
        delete[] downloadedList;
        downloadedList = 0;
        downloadedCount = 0;
    }

    // 音色を取り出し
    int Port::GetInstrument(int voice, IDirectMusicInstrument8** instrument)
    {
        // まずはデフォルトから探す
        defaultDls->GetInstrument(voice, instrument);
        if(*instrument != 0) return 0;

        // 見つからなければリストから探す
        for(int i=0; i<(int)dlsList.size(); i++){
            if(dlsList[i]->GetInstrument(voice, instrument) == 0) return 0;
        }
        return -1;
    }

    // 音色リストをセット
    void Port::SetVoiceList(DWORD* list, int count)
    {
        if(!handle_thread)return;
        PortCommand cmd;
        cmd.type = CommandType_SetVoiceList;
        cmd.SetVoiceList.list = list;//非同期対応するなら動的確保してコピーした方が安全？
        cmd.SetVoiceList.count = count;
        PostCommand(cmd);
    }
    void Port::_SetVoiceList(DWORD * list, int count)
    {//OnWorker のスレッドからのみ呼び出し可
        ResetVoiceList();

        IDirectMusicInstrument8* instrument = 0 ;
        IDirectMusicDownloadedInstrument8* downloaded = 0;

        downloadedList = new IDirectMusicDownloadedInstrument8*[count];
        downloadedCount = count;

        // 有効ノートは全範囲
        DMUS_NOTERANGE noteRange;
        noteRange.dwLowNote = 0;
        noteRange.dwHighNote = 127;

        // 音色を読み出してportに設定
        for(int i=0; i<count; i++){
            downloadedList[i] = 0;

            int voice = list[i];

            if(GetInstrument(voice, &instrument)){  // 音色がなかったらバンク0
                defaultDls->GetInstrument(voice&0xff0000ff, &instrument);
                if(instrument == 0) continue;   // これはお手上げ
                instrument->SetPatch(voice);
            }

            port->DownloadInstrument(instrument, &downloaded, &noteRange, 1);
            downloadedList[i] = downloaded;
            downloaded = 0;
            instrument->Release();
            instrument = 0;
        }
        port->Compact();
    }

    // 後始末本体(ワーカースレッド上で実行)
    void Port::_Close()
    {
        if(isOpen){
            isOpen = false;
        }

        ResetVoiceList();

        for(int i=0; i<(int)dlsList.size(); i++){
            delete dlsList[i];
        }
        dlsList.clear();

        if(clock){
            clock->Release();
            clock = 0;
        }

        if(buffer){
            buffer->Release();
            buffer = 0;
        }

        if(defaultDls){
            defaultDls->Release();
            defaultDls = 0;
        }

        if(port){
            port->Activate(FALSE);
            port->Release();
            port = 0;
        }

        if(loader){
            loader->Release();
            loader = 0;
        }

        if(music){
            music->Release();
            music = 0;
        }
        _DestroyWindow();//by Kobarin デフォルトで作成したウィンドウを破棄
        port_max = 1;
    }

    // 遅延付き時間を取得
    GGSERROR Port::GetReferenceTime(long long* time)
    {
        //*time = (UINT64)timeGetTime()*10000;return GgsError::NoError;
        //↑これでも問題ないような…。
        if(!isOpen) return GgsError::NotReady;
        //これは OnWorker 内で処理するのはかなり無駄な気がするけど
        GGSERROR result = GgsError::NoError;
        PortCommand cmd;
        cmd.type = CommandType_GetRefTime;
        cmd.GetRefTime.time = time;
        cmd.GetRefTime.presult = &result;
        PostCommand(cmd);
        return *cmd.GetRefTime.presult;
    }
    GGSERROR Port::_GetReferenceTime(long long* time)
    {//OnWorker のスレッドからのみ呼び出し可
        HRESULT hr = clock->GetTime(time);
        if (FAILED(hr)) return GgsError::Failed;
        return GgsError::NoError;
    }
    #if 1
    static void CheckTransferRate(DWORD dwBytes)
    {
        return;
/*        static DWORD s_dwPrevTime = 0;
        static DWORD s_dwBytes = 0;
       s_dwBytes += dwBytes;
        const DWORD dwMaxBytes = 32768;
        DWORD elapsed = timeGetTime() - s_dwPrevTime;
        if(elapsed == 0){
            if (s_dwBytes > dwMaxBytes) {
                Sleep(1);
                s_dwPrevTime = timeGetTime();
                s_dwBytes = 0;
                return;
            }
            return;
        }
        if((UINT64)s_dwBytes *1000 / elapsed >= dwMaxBytes){
            Sleep(1);
            s_dwPrevTime = timeGetTime();
            s_dwBytes = 0;
        }
*/
    }
    #endif
    // 短いメッセージを送る
    GGSERROR Port::SendShortMessage(int port_num, long long time, int midiMessage)
    {
        if (!isOpen) return GgsError::NotReady;
        if(port_num >= port_max)return GgsError::Failed;//ポート番号が大き過ぎ
        PortCommand cmd;
        cmd.type = CommandType_SendShortMsg;
        cmd.ShortMsg.time = time;
        cmd.ShortMsg.midiMessage = midiMessage;
        cmd.ShortMsg.port = port_num;
        PostCommand(cmd);
        return GgsError::NoError;
    }
    GGSERROR Port::_SendShortMessage(int port_num, long long time, int midiMessage)//複数ポート対応SendShortMessage
    {//OnWorker のスレッドからのみ呼び出し可
     //PackStructured の第2引数dwChannelGroup に 0 を渡すと全ポート、0 以外だと指定のポートに送信
        HRESULT hr;
        hr = buffer->PackStructured(time, port_num + 1, midiMessage);
        if (FAILED(hr)) return GgsError::Failed;
        hr = port->PlayBuffer(buffer);
        if (FAILED(hr)) return GgsError::Failed;
        buffer->Flush();
        CheckTransferRate(4);
        return GgsError::NoError;
    }

    // 長いメッセージを送る
    GGSERROR Port::SendLongMessage(int port_num, long long time, LPBYTE midiMessage, int length)
    {
        if (!isOpen) return GgsError::NotReady;
        if (port_num >= port_max)return GgsError::Failed;//ポート番号が大き過ぎ
        PortCommand cmd;
        cmd.type = CommandType_SendLongMsg;
        cmd.LongMsg.time = time;
        cmd.LongMsg.midiMessage = midiMessage;//非同期対応するなら動的確保してコピーした方が安全？
        cmd.LongMsg.length = length;
        cmd.LongMsg.port = port_num;
        PostCommand(cmd);
        return GgsError::NoError;
    }
    GGSERROR Port::_SendLongMessage(int port_num, long long time, LPBYTE midiMessage, int length)//複数ポート対応SendLongMessage
    {//OnWorker のスレッドからのみ呼び出し可
     //PackUnstructured の第2引数dwChannelGroup に 0 を渡すと全ポート、0 以外だと指定のポートに送信
        HRESULT hr;
        hr = buffer->PackUnstructured(time, port_num + 1, length, midiMessage);
        if (FAILED(hr)) return GgsError::Failed;
        hr = port->PlayBuffer(buffer);
        if (FAILED(hr)) return GgsError::Failed;
        buffer->Flush();
        CheckTransferRate(length);
        return GgsError::NoError;
    }

    // DLSを追加
    GGSERROR Port::AddDls(wchar_t* fileName)
    {
        if (!handle_thread) return GgsError::NotReady;
        GGSERROR result = GgsError::NoError;
        PortCommand cmd;
        cmd.type = CommandType_AddDls;
        cmd.AddDls.fileName = fileName;//非同期だとスレッド内で処理するときには不正な値になってる可能性がある
        cmd.AddDls.presult = &result;
        PostCommand(cmd);
        return result;//非同期だと不定な値になる可能性がある
    }
    GGSERROR Port::_AddDls(wchar_t* fileName)
    {//OnWorker のスレッドからのみ呼び出し可
        Dls* dls = new Dls(loader);
        GGSERROR err = dls->LoadFromFile(fileName);
        if (err == GgsError::NoError) {
            dlsList.push_back(dls);
            return err;
        }
        else {
            delete dls;
            return err;
        }

    }

}}

