#include <assert.h>
#include "gnu_javax_sound_midi_wce_WCEMidiOutputPort.h"
#include "gspmidi\gspmidi.h"
#include "wcesound.h"

#define MIDI_WINDOW_CLASS_NAME _T("gnu.javax.sound.midi.wce.MidiWindow")
#define WAVEHDRS_COUNT  2

/**
 * Application window messages
 */
#define APP_START_NATIVE_MIDI_FILE  (WM_APP+1)

/**
 * static variables.
 */
static MAP_DEC_PLUGIN* g_plugin;
static HWND g_hwndMidi;
static HWAVEOUT g_hWaveOut;
static WAVEHDR g_wavehdrs[WAVEHDRS_COUNT];
static WAVEHDR* g_currentWaveHdr;
static size_t g_BufferSize;

extern _TCHAR g_szConfigFile[];
extern BOOL g_fUpdateConfig;

extern void ResetConfig();

DWORD MidiThreadProc(LPVOID lpParameter);
LRESULT CALLBACK MidiWndProc(HWND, UINT, WPARAM, LPARAM); 

/*
 * Class:     gnu_javax_sound_midi_wce_WCEMidiOutputPort
 * Method:    openNative
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_gnu_javax_sound_midi_wce_WCEMidiOutputPort_openNative
  (JNIEnv *env, jobject obj)
{
  HANDLE hThread = CreateThread(NULL, 0, MidiThreadProc, NULL, 0, NULL);
  if (! hThread)
  {
    env->ThrowNew(env->FindClass("java/lang/Error"), "Cannot create a native thread");
    return;
  }
  CloseHandle(hThread);
}

/*
 * Class:     gnu_javax_sound_midi_wce_WCEMidiOutputPort
 * Method:    closeNative
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_gnu_javax_sound_midi_wce_WCEMidiOutputPort_closeNative
(JNIEnv *env, jobject obj)
{
  
}

/*
 * Class:     gnu_javax_sound_midi_wce_WCEMidiOutputPort
 * Method:    isNativePlaying
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_gnu_javax_sound_midi_wce_WCEMidiOutputPort_isNativePlaying
  (JNIEnv *, jobject)
{
  return g_currentWaveHdr ? JNI_TRUE : JNI_FALSE;
}

/*
 * Class:     gnu_javax_sound_midi_wce_WCEMidiOutputPort
 * Method:    startNativeMidiFile
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_gnu_javax_sound_midi_wce_WCEMidiOutputPort_startNativeMidiFile
(JNIEnv *env, jobject obj, jstring fileName)
{
  _TCHAR path[MAX_PATH+1];
  jsize len = env->GetStringLength(fileName);
  const jchar* tmp = env->GetStringChars(fileName, NULL);
  if (len >= MAX_PATH)
  {
    env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "filename too long");
    return;
  }
  _tcsncpy(path, (const _TCHAR*) tmp, len);
  path[len] = _T('\0');
  env->ReleaseStringChars(fileName, tmp);

  SendMessage(g_hwndMidi, APP_START_NATIVE_MIDI_FILE, (WPARAM) path, 0);
}

/*
 * Class:     gnu_javax_sound_midi_wce_WCEMidiOutputPort
 * Method:    stopNativeMidiFile
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_gnu_javax_sound_midi_wce_WCEMidiOutputPort_stopNativeMidiFile
(JNIEnv *env, jobject obj)
{
}

/*
 * Class:     gnu_javax_sound_midi_wce_WCEMidiOutputPort
 * Method:    closeNativeMidiFile
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_gnu_javax_sound_midi_wce_WCEMidiOutputPort_closeNativeMidiFile
(JNIEnv *env, jobject obj)
{
}



DWORD MidiThreadProc(LPVOID lpParameter)
{
  // Initialize gspmidi
  g_plugin = mapGetDecoder();
  g_plugin->Init();
  _tcscpy(g_szConfigFile, _T("\\gm.cfg")); // ToDo: read from system properties.
  g_fUpdateConfig = TRUE;
  ResetConfig();

  // Create a Window to handle WaveOut events.
  WNDCLASS wndclass = {0};
  wndclass.lpfnWndProc = MidiWndProc;
  wndclass.hInstance = GetModuleHandle(NULL);
  wndclass.lpszClassName = MIDI_WINDOW_CLASS_NAME;

  if (! RegisterClass(&wndclass))
  {
    return FALSE;
  }

  g_hwndMidi = CreateWindow(MIDI_WINDOW_CLASS_NAME,
                            NULL,
                            WS_DISABLED,
                            0,
                            0,
                            0,
                            0,
                            NULL,
                            NULL,
                            wndclass.hInstance,
                            NULL);
  if (! g_hwndMidi)
  {
    return FALSE;
  }

  MSG msg;
  while (GetMessage(&msg, g_hwndMidi, 0, 0) > 0)
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return TRUE;
}

/**
 * 
 */
LRESULT CALLBACK MidiWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
  case APP_START_NATIVE_MIDI_FILE:
    {
      // open MIDI file.
      MAP_PLUGIN_FILE_INFO fileInfo;
      if (! g_plugin->OpenFile((LPCTSTR) wParam, &fileInfo))
      {
        return 0;
      }
      if (! g_plugin->StartDecodeFile())
      {
        return 0;
      }
      
      // open Wave out device.
      WAVEFORMATEX wf = {0};
      WORD nChannels = fileInfo.nChannels;
      DWORD nSamplingRate = fileInfo.nSampleRate;
      DWORD nBitsPerSample = fileInfo.nBitsPerSample;

      wf.wFormatTag = WAVE_FORMAT_PCM;
      wf.nChannels = nChannels;
      wf.nSamplesPerSec = nSamplingRate;
      wf.nAvgBytesPerSec = nBitsPerSample * nSamplingRate * nChannels / 8;
      wf.nBlockAlign = (WORD) (nBitsPerSample * nChannels / 8);
      wf.wBitsPerSample = (WORD) nBitsPerSample;
      MMRESULT mmr = waveOutOpen(&g_hWaveOut, WAVE_MAPPER, &wf, (DWORD) hwnd, 0, CALLBACK_WINDOW);
      if (mmr != MMSYSERR_NOERROR)
      {
        return 0;
      }

      // Initialize WAVEHDRs
      g_BufferSize = wf.nAvgBytesPerSec * 4;
      for (int i = 0; i < WAVEHDRS_COUNT; ++i)
      {
        memset(&g_wavehdrs[i], 0, sizeof(WAVEHDR));
        LPSTR lpWave = (LPSTR) malloc(g_BufferSize); 
        if (! lpWave)
        {
          return 0;
        }
        g_wavehdrs[i].lpData = (LPSTR) lpWave;
        g_wavehdrs[i].dwBufferLength = g_BufferSize / 2;
        g_wavehdrs[i].dwFlags=WHDR_BEGINLOOP | WHDR_ENDLOOP;
        g_wavehdrs[i].dwLoops=1;
        mmr = waveOutPrepareHeader(g_hWaveOut, &g_wavehdrs[i], sizeof(WAVEHDR));
        if (mmr != MMSYSERR_NOERROR)
        {
          return 0;
        }
      }
      g_currentWaveHdr = &g_wavehdrs[0];
    }
    break;

  case MM_WOM_OPEN:
    {
      HWAVEOUT hWaveOut = (HWAVEOUT) wParam;
      g_wavehdrs[0].dwBytesRecorded = 0;
      int nRet;
      while (! g_wavehdrs[0].dwBytesRecorded)
      {
       nRet = g_plugin->DecodeFile(&g_wavehdrs[0]);
      }
      if (nRet == PLUGIN_RET_SUCCESS)
      {
        if (g_wavehdrs[0].dwBufferLength < g_wavehdrs[0].dwBytesRecorded)
        {
            // ???
        }
        MMRESULT mmr = waveOutWrite(hWaveOut, &g_wavehdrs[0], sizeof(WAVEHDR));
        g_wavehdrs[0].dwBufferLength = g_BufferSize / 2;

        g_wavehdrs[1].dwBytesRecorded = 0;
        nRet = g_plugin->DecodeFile(&g_wavehdrs[1]);
        if (nRet == PLUGIN_RET_SUCCESS)
        {
          if (g_wavehdrs[1].dwBufferLength < g_wavehdrs[1].dwBytesRecorded)
          {
            // ???
          }
          mmr = waveOutWrite(hWaveOut, &g_wavehdrs[1], sizeof(WAVEHDR));
          g_wavehdrs[1].dwBufferLength = g_BufferSize / 2;
        }
      }
    }
    break;

  case MM_WOM_DONE:
    {
      HWAVEOUT hWaveOut = (HWAVEOUT) wParam;
      WAVEHDR* whdr = g_currentWaveHdr;
      if (! whdr)
      {
        // stop playing file.
        g_plugin->StopDecodeFile();
        g_plugin->CloseFile();
        for (int i = 0; i < WAVEHDRS_COUNT; ++i)
        {
          waveOutUnprepareHeader(hWaveOut, &g_wavehdrs[i], sizeof(WAVEHDR));
          free(g_wavehdrs[i].lpData);
        }
        waveOutClose(hWaveOut);
        break;
      }
      whdr->dwBytesRecorded = 0;
      int nRet;
      while (! whdr->dwBytesRecorded)
      {
        nRet = g_plugin->DecodeFile(whdr);
        if (nRet != PLUGIN_RET_SUCCESS)
        {
          break;
        }
      }
      if (nRet == PLUGIN_RET_SUCCESS || nRet == PLUGIN_RET_EOF)
      {
        if (whdr->dwBufferLength < whdr->dwBytesRecorded)
        {
            // ???
        }
        MMRESULT mmr = waveOutWrite(hWaveOut, whdr, sizeof(WAVEHDR));
        whdr->dwBufferLength = g_BufferSize / 2;
        g_currentWaveHdr = (g_currentWaveHdr == &g_wavehdrs[0]) ? &g_wavehdrs[1] : &g_wavehdrs[0];

        if (nRet == PLUGIN_RET_EOF)
        {
          g_currentWaveHdr = NULL;
        }
      }
      else
      {
        g_currentWaveHdr = NULL;
      }
    }
    break;

  default:
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
  }
  return 0;
}
