// SPDX-License-Identifier: Unlicense

#include "midi.h"

#ifdef SHR3D_MIDI

#include "global.h"
#include "string_.h"

#ifdef PLATFORM_ANDROID_SDL
#include "amidi/AMidi.h"
#include <unistd.h>
#include <jni.h>
#endif // PLATFORM_ANDROID_SDL

#ifdef PLATFORM_WINDOWS
#include <conio.h>     /* include for kbhit() and getch() functions */
#include <stdio.h>     /* for printf() function */

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>   /* required before including mmsystem.h */
#include <mmsystem.h>  /* multimedia functions (such as MIDI) for Windows */
#endif // PLATFORM_WINDOWS

#include <set>


#ifdef PLATFORM_ANDROID_SDL
static AMidiDevice* sNativeReceiveDevice = NULL;
static AMidiOutputPort* sMidiOutputPort = NULL;
#endif // PLATFORM_ANDROID_SDL

void midiAutoConnectDevices(const std::u8string& deviceNames)
{
  std::vector<std::u8string> v = String::split(deviceNames, ',');
  for (const std::u8string& name : v)
  {
    for (i32 i = 0; i < Const::midiMaxDeviceCount; ++i)
    {
      if (name == Global::midiDeviceNames[i])
      {
        Midi::openDevice(i);
        continue;
      }
    }
  }
}

enum struct MidiBinding {
  audioEffectVolumeCoarse,
  audioEffectVolumeFine,
  audioEffectVolumeCoopCoarse,
  audioEffectVolumeCoopFine,
  AudioMusicVolumeCoarse,
  AudioMusicVolumeFine,
  EnvironmentClearColorR,
  EnvironmentClearColorG,
  EnvironmentClearColorB,
  HighwaySpeedMultiplierCoarse,
  HighwaySpeedMultiplierFine,
  SfxTone0,
  SfxTone1,
  SfxTone2,
  SfxTone3,
  SfxTone4,
  SfxTone5,
  SfxTone6,
  SfxTone7,
  SfxTone8,
  SfxTone9,
  SfxToneDec,
  SfxToneInc,
  SfxToneBankDec,
  SfxToneBankInc,
  Metronome,
  MetronomeVolumeCoarse,
  MetronomeVolumeFine,
  Tuner,
#ifdef SHR3D_ENVIRONMENT_MILK
  Milk,
#endif // SHR3D_ENVIRONMENT_MILK
  OverallLevel,
  Mute,
  QuickRepeater,
  QuickRepeaterJumpBegin,
  MusicPlaybackPosition,
  //MusicPlaybackPositionCoarse,
  //MusicPlaybackPositionFine,
#ifdef SHR3D_MUSIC_STRETCHER
  MusicStretchRatio,
#endif // SHR3D_MUSIC_STRETCHER
  SIZE
};

static f32 combineCoarseAndFineValue(const f32 coarseValue, const f32 fineValue)
{
  return clamp(coarseValue + Settings::midiFineValueFactor * (fineValue - 0.5f), 0.0f, 1.0f);
}

static void interpretMidiNote(const u8 noteNumber, u8 velocity)
{
  switch (Global::midiNoteMode[noteNumber])
  {
  case MidiNoteMode::press:
    if (velocity == 0)
      return;
    velocity = 127;
    break;
  case MidiNoteMode::toggle:
    if (velocity == 0)
      return;
    {
      static bool toggles[128] = {};
      toggles[noteNumber] = !toggles[noteNumber];
      velocity = toggles[noteNumber] ? 127 : 0;
    }
    break;
  case MidiNoteMode::analog:
    break;
  }

  const MidiBinding binding = MidiBinding(Global::midiNoteBinding[noteNumber]);
  if (binding == MidiBinding(0xFF))
    return;


  switch (binding)
  {
  case MidiBinding::AudioMusicVolumeCoarse:
    Global::midiAudioMusicVolumeCoarse = f32(velocity) / 127.0f;
    Settings::audioMusicVolume = combineCoarseAndFineValue(Global::midiAudioMusicVolumeCoarse, Global::midiAudioMusicVolumeFine);
    break;
  case MidiBinding::AudioMusicVolumeFine:
    Global::midiAudioMusicVolumeFine = f32(velocity) / 127.0f;
    Settings::audioMusicVolume = combineCoarseAndFineValue(Global::midiAudioMusicVolumeCoarse, Global::midiAudioMusicVolumeFine);
    break;
  case MidiBinding::audioEffectVolumeCoarse:
    Global::midiAudioEffectVolumeCoarse = f32(velocity) / 127.0f;
    Settings::audioEffectVolume = combineCoarseAndFineValue(Global::midiAudioEffectVolumeCoarse, Global::midiAudioEffectVolumeFine);
    break;
  case MidiBinding::audioEffectVolumeFine:
    Global::midiAudioEffectVolumeFine = f32(velocity) / 127.0f;
    Settings::audioEffectVolume = combineCoarseAndFineValue(Global::midiAudioEffectVolumeCoarse, Global::midiAudioEffectVolumeFine);
    break;
  case MidiBinding::audioEffectVolumeCoopCoarse:
    Global::midiAudioEffectVolumeCoopCoarse = f32(velocity) / 127.0f;
    Settings::audioEffectVolumeCoop = combineCoarseAndFineValue(Global::midiAudioEffectVolumeCoopCoarse, Global::midiAudioEffectVolumeCoopFine);
    break;
  case MidiBinding::audioEffectVolumeCoopFine:
    Global::midiAudioEffectVolumeCoopFine = f32(velocity) / 127.0f;
    Settings::audioEffectVolumeCoop = combineCoarseAndFineValue(Global::midiAudioEffectVolumeCoopCoarse, Global::midiAudioEffectVolumeCoopFine);
    break;
  case MidiBinding::EnvironmentClearColorR:
    Settings::environmentClearColor.r = f32(velocity) / 127.0f; // missing mutex
    break;
  case MidiBinding::EnvironmentClearColorG:
    Settings::environmentClearColor.g = f32(velocity) / 127.0f;
    break;
  case MidiBinding::EnvironmentClearColorB:
    Settings::environmentClearColor.b = f32(velocity) / 127.0f;
    break;
  case MidiBinding::HighwaySpeedMultiplierCoarse:
    Global::midiHighwayScrollSpeedCoarse = f32(velocity);
    Settings::highwayScrollSpeed = combineCoarseAndFineValue(Global::midiHighwayScrollSpeedCoarse, Global::midiHighwayScrollSpeedFine);
    break;
  case MidiBinding::HighwaySpeedMultiplierFine:
    Global::midiHighwayScrollSpeedFine = f32(velocity);
    Settings::highwayScrollSpeed = combineCoarseAndFineValue(Global::midiHighwayScrollSpeedCoarse, Global::midiHighwayScrollSpeedFine);
    break;
  case MidiBinding::SfxTone0:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank;
    break;
  case MidiBinding::SfxTone1:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 1;
    break;
  case MidiBinding::SfxTone2:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 2;
    break;
  case MidiBinding::SfxTone3:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 3;
    break;
  case MidiBinding::SfxTone4:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 4;
    break;
  case MidiBinding::SfxTone5:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 5;
    break;
  case MidiBinding::SfxTone6:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 6;
    break;
  case MidiBinding::SfxTone7:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 7;
    break;
  case MidiBinding::SfxTone8:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 8;
    break;
  case MidiBinding::SfxTone9:
    Global::activeSfxToneIndex = (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank) * Const::sfxToneTonesPerBank + 9;
    break;
  case MidiBinding::SfxToneDec:
    if (Global::activeSfxToneIndex > 0)
      --Global::activeSfxToneIndex;
    break;
  case MidiBinding::SfxToneInc:
    if (Global::activeSfxToneIndex < Const::sfxToneTonesPerBank)
      ++Global::activeSfxToneIndex;
    break;
  case MidiBinding::SfxToneBankDec:
    if (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank > 0)
      Global::activeSfxToneIndex -= Const::sfxToneTonesPerBank;
    break;
  case MidiBinding::SfxToneBankInc:
    if (Global::activeSfxToneIndex / Const::sfxToneTonesPerBank < I32::max / Const::sfxToneTonesPerBank - 1)
      Global::activeSfxToneIndex += Const::sfxToneTonesPerBank;
    break;
  case MidiBinding::Metronome:
    Global::inputMetronome.clickedInUiOrMidi = true;
    break;
  case MidiBinding::MetronomeVolumeCoarse:
    Global::midiMetronomeVolumeCoarse = f32(velocity / 127.0f);
    Settings::metronomeVolume = combineCoarseAndFineValue(Global::midiMetronomeVolumeCoarse, Global::midiMetronomeVolumeFine);
    break;
  case MidiBinding::MetronomeVolumeFine:
    Global::midiMetronomeVolumeFine = f32(velocity / 127.0f);
    Settings::metronomeVolume = combineCoarseAndFineValue(Global::midiMetronomeVolumeCoarse, Global::midiMetronomeVolumeFine);
    break;
  case MidiBinding::Tuner:
    Global::inputTuner.toggled = !Global::inputTuner.toggled;
    break;
#ifdef SHR3D_ENVIRONMENT_MILK
  case MidiBinding::Milk:
    Settings::environmentMilk = !Settings::environmentMilk;
    break;
#endif // SHR3D_ENVIRONMENT_MILK
  case MidiBinding::OverallLevel:
    if (Global::selectedSongIndex >= 0)
    {
      const f32 overallMaxLevel = f32(velocity) / 127.0f;
      std::vector<i32>& sectionLevels = Global::songLevels[Global::selectedArrangementIndex].sectionLevels;
      for (i32 i = 0; i < sectionLevels.size(); ++i)
      {
        const Song::LeveledSection& leveledSection = Global::songTracks[Global::selectedArrangementIndex].leveledSections[i];
        sectionLevels[i] = i32(overallMaxLevel * f32(leveledSection.maxLevel));
      }
      Global::songTrackLevelAdjusted[Global::selectedArrangementIndex] = Song::loadTrackLevelAdjusted(Global::songTracks[Global::selectedArrangementIndex], sectionLevels);
    }
    break;
  case MidiBinding::Mute:
    Global::inputMute.toggled = !Global::inputMute.toggled;
    break;
  case MidiBinding::QuickRepeater:
    Global::inputQuickRepeater.clickedInUiOrMidi = true;
    break;
  case MidiBinding::QuickRepeaterJumpBegin:
    Global::inputQuickRepeaterJumpBegin.clickedInUiOrMidi = true;
    break;
  case MidiBinding::MusicPlaybackPosition:
    if (Global::selectedSongIndex >= 0)
    {
      Global::musicTimeElapsedNS = TimeNS(f32(velocity) / 127.0f * f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd)) - Global::songInfos[Global::selectedSongIndex].shredDelayBegin;
#ifdef SHR3D_MUSIC_STRETCHER
      Global::musicPlaybackPosition = i64(musicTimeElapsedToPlaybackPositionNonStretched(Global::musicTimeElapsedNS, f32(sampleRate())) * Global::musicStretchRatio);
#else // SHR3D_MUSIC_STRETCHER
      Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(Global::musicTimeElapsedNS) * f32(sampleRate()));
#endif // SHR3D_MUSIC_STRETCHER
    }
    break;
    //  case MidiBinding::MusicPlaybackPositionCoarse:
    //    if (Global::selectedSongIndex >= 0)
    //    {
    //      Global::midiMusicPlaybackPositionCoarse = f32(velocity) / 127.0f;
    //      const f32 combinedValue = combineCoarseAndFineValue(Global::midiMusicPlaybackPositionCoarse, Global::midiMusicPlaybackPositionFine);
    //      Global::musicTimeElapsedNS = TimeNS(combinedValue * f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd)) - Global::songInfos[Global::selectedSongIndex].shredDelayBegin;
    //#ifdef SHR3D_MUSIC_STRETCHER
    //      Global::musicPlaybackPosition = musicTimeElapsedToPlaybackPositionNonStretched(Global::musicTimeElapsedNS, f32(sampleRate())) * Global::musicStretchRatio;
    //#else // SHR3D_MUSIC_STRETCHER
    //      Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(Global::musicTimeElapsedNS) * f32(sampleRate()));
    //#endif // SHR3D_MUSIC_STRETCHER
    //    }
    //    break;
    //  case MidiBinding::MusicPlaybackPositionFine:
    //    if (Global::selectedSongIndex >= 0)
    //    {
    //      Global::midiMusicPlaybackPositionFine = f32(velocity) / 127.0f;
    //      const f32 combinedValue = combineCoarseAndFineValue(Global::midiMusicPlaybackPositionCoarse, Global::midiMusicPlaybackPositionFine);
    //      Global::musicTimeElapsedNS = TimeNS(combinedValue * f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd)) - Global::songInfos[Global::selectedSongIndex].shredDelayBegin;
    //#ifdef SHR3D_MUSIC_STRETCHER
    //      Global::musicPlaybackPosition = musicTimeElapsedToPlaybackPositionNonStretched(Global::musicTimeElapsedNS, f32(sampleRate())) * Global::musicStretchRatio;
    //#else // SHR3D_MUSIC_STRETCHER
    //      Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(Global::musicTimeElapsedNS) * f32(sampleRate()));
    //#endif // SHR3D_MUSIC_STRETCHER
    //    }
    //    break;
#ifdef SHR3D_MUSIC_STRETCHER
  case MidiBinding::MusicStretchRatio:
    Global::musicStretchRatio = Const::musicStretcherMaxStretchRatio / (Const::musicStretcherMaxStretchRatio + (1.0f - Const::musicStretcherMaxStretchRatio) * f32(127_u8 - velocity) / 127.0f);
    break;
#endif // SHR3D_MUSIC_STRETCHER
  default:
    unreachable();
  }
}

static void handleMidiDevice(const MidiStatus midiStatus, const u8 noteNumber, const u8 velocity)
{
  std::unique_lock lock(Global::playedNotesFromAudioMutex);
  Global::playedNotesFromAudio[Global::playedNotesFromAudioIndex].status = midiStatus;
  Global::playedNotesFromAudio[Global::playedNotesFromAudioIndex].note = MidiNote(noteNumber);
  Global::playedNotesFromAudio[Global::playedNotesFromAudioIndex].velocity = velocity;
  ++Global::playedNotesFromAudioIndex;
}

static void processMidi(u8 status, u8 data1, u8 data2)
{
  MidiStatus midiStatus{};

  switch (status)
  {
  case 128: // Note Off Chan 1
  case 129: // Note Off Chan 2
  case 130: // Note Off Chan 3
  case 131: // Note Off Chan 4
  case 132: // Note Off Chan 5
  case 133: // Note Off Chan 6
  case 134: // Note Off Chan 7
  case 135: // Note Off Chan 8
  case 136: // Note Off Chan 9
  case 137: // Note Off Chan 10
    midiStatus = MidiStatus::chan1NoteOff;
    break;
  case 144: // Note On Chan 1
  case 145: // Note On Chan 2
  case 146: // Note On Chan 3
  case 147: // Note On Chan 4
  case 148: // Note On Chan 5
  case 149: // Note On Chan 6
  case 150: // Note On Chan 7
  case 151: // Note On Chan 8
  case 152: // Note On Chan 9
  case 153: // Note On Chan 10
    midiStatus = MidiStatus::chan1NoteOn;
    break;
  case 176: // Mode Change Chan 1
  case 177: // Mode Change Chan 2
  case 178: // Mode Change Chan 3
  case 179: // Mode Change Chan 4
  case 180: // Mode Change Chan 5
  case 181: // Mode Change Chan 6
  case 182: // Mode Change Chan 7
  case 183: // Mode Change Chan 8
  case 184: // Mode Change Chan 9
  case 185: // Mode Change Chan 10
    break;
  case 192: // Chan 1 Program Change
    break;
  case 248: // Timing clock
    break;
  default:
    unreachable();
  }

  Global::midiLearnNote = data1;

  if (Settings::tunerNoteDetectionSource == NoteDetectionSource::midiDevice && Global::tunerMidiDevice != 0)
  {
    if (data1 != 0 && data2 != 0)
      handleMidiDevice(midiStatus, data1, data2);
  }
  else
    interpretMidiNote(data1, data2);
}

#ifdef PLATFORM_ANDROID_SDL
static void* readThreadRoutine(void* /*context*/)
{
  //AMidiOutputPort* outputPort = sMidiOutputPort;

  const size_t MAX_BYTES_TO_RECEIVE = 128;
  uint8_t incomingMessage[MAX_BYTES_TO_RECEIVE];

  for (;;)
  {
    int32_t opcode;
    size_t numBytesReceived;
    int64_t timestamp;
    ssize_t numMessagesReceived = AMidiOutputPort_receive(sMidiOutputPort, &opcode, incomingMessage, MAX_BYTES_TO_RECEIVE, &numBytesReceived, &timestamp);

    if (numMessagesReceived > 0 && numBytesReceived >= 0)
    {
      if (opcode == AMIDI_OPCODE_DATA && (incomingMessage[0] & 0xF0) != 0xF0) // 0xF0=kMIDISysCmdChan
      {
        processMidi(incomingMessage[0], incomingMessage[1], incomingMessage[2]);
  }
}
    usleep(2000);
  }

  return NULL;
}

extern "C" JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_startReadingMidi(JNIEnv* env, jobject, jobject midiDeviceObj, jint portNumber)
{
  AMidiDevice_fromJava(env, midiDeviceObj, &sNativeReceiveDevice);
  AMidiOutputPort_open(sNativeReceiveDevice, portNumber, &sMidiOutputPort);
}
#endif // PLATFORM_ANDROID_SDL

#ifdef PLATFORM_WINDOWS
static void CALLBACK MidiInProc(HMIDIIN /*hMidiIn*/, UINT wMsg, DWORD /*dwInstance*/, DWORD dwParam1, DWORD /*dwParam2*/)
{
  switch (wMsg)
  {
  case MIM_OPEN:
    break;
  case MIM_CLOSE:
    break;
  case MIM_DATA:
  {
    const u8 status = dwParam1 & 0xFF;
    const u8 data1 = (dwParam1 >> 8) & 0xFF; // noteNumber
    const u8 data2 = (dwParam1 >> 16) & 0xFF; // velocity

    processMidi(status, data1, data2);
  }
  break;
  case MIM_LONGDATA:
    //ASSERT(false);
  case MIM_ERROR:
    //ASSERT(false);
    break;
  case MIM_LONGERROR:
    //ASSERT(false); // there is an error because midi device is not closed on exit
    break;
  case MIM_MOREDATA:
    ASSERT(false);
    break;
  default:
    unreachable();
  }
  return;
}
#endif // PLATFORM_WINDOWS

void Midi::openDevice(const i32 index)
{
#ifdef PLATFORM_ANDROID_SDL
  JNIEnv* env = nullptr;
  Global::g_JVM->GetEnv((void**)&env, JNI_VERSION_1_4);

  jclass mainActivity = env->FindClass("org/libsdl/app/SDLActivity");

  jmethodID connectMidiDevice_method = env->GetMethodID(mainActivity, "connectMidiDevice", "(I)V");
  jobject activity = (jobject)Global::androidActivity;

  env->CallVoidMethod(activity, connectMidiDevice_method, index);

  Global::midiConnectedDevices[index] = 1;
#endif // PLATFORM_ANDROID_SDL

#ifdef PLATFORM_WINDOWS
  MMRESULT flag = midiInOpen(&Global::midiConnectedDevices[index], index, (DWORD_PTR)(void*)MidiInProc, 0, CALLBACK_FUNCTION);
  ASSERT(flag == MMSYSERR_NOERROR);

  MIDIHDR midiHeader{};
  static char SysXBuffer[256] = {};
  midiHeader.lpData = &SysXBuffer[0];
  midiHeader.dwBufferLength = sizeof(SysXBuffer);

  flag = midiInPrepareHeader(Global::midiConnectedDevices[index], &midiHeader, sizeof(midiHeader));
  ASSERT(flag == MMSYSERR_NOERROR);

  flag = midiInAddBuffer(Global::midiConnectedDevices[index], &midiHeader, sizeof(midiHeader));
  ASSERT(flag == MMSYSERR_NOERROR);

  flag = midiInStart(Global::midiConnectedDevices[index]);
  ASSERT(flag == MMSYSERR_NOERROR);
#endif // PLATFORM_WINDOWS
}

void Midi::closeDevice(const i32 index)
{
#ifdef PLATFORM_ANDROID_SDL
  Global::midiConnectedDevices[index] = 0;
#endif // PLATFORM_ANDROID_SDL

#ifdef PLATFORM_WINDOWS
  MMRESULT flag = midiInStop(Global::midiConnectedDevices[index]);
  ASSERT(flag == MMSYSERR_NOERROR);

  MIDIHDR midiHeader{};
  flag = midiInUnprepareHeader(Global::midiConnectedDevices[index], &midiHeader, sizeof(midiHeader));
  ASSERT(flag == MMSYSERR_NOERROR);

  flag = midiInReset(Global::midiConnectedDevices[index]);
  ASSERT(flag == MMSYSERR_NOERROR);

  flag = midiInClose(Global::midiConnectedDevices[index]);
  ASSERT(flag == MMSYSERR_NOERROR);

  Global::midiConnectedDevices[index] = nullptr;
#endif // PLATFORM_WINDOWS
}

void Midi::init()
{
#ifdef PLATFORM_ANDROID_SDL
  JNIEnv* env = nullptr;
  Global::g_JVM->GetEnv((void**)&env, JNI_VERSION_1_4);

  jclass mainActivity = env->FindClass("org/libsdl/app/SDLActivity");

  jmethodID getMidiDeviceNames_method = env->GetMethodID(mainActivity, "getMidiDeviceNames", "()Ljava/util/ArrayList;");
  jobject activity = (jobject)Global::androidActivity;
  jobject midiDeviceList = env->CallObjectMethod(activity, getMidiDeviceNames_method);

  jclass arrayListClass = env->FindClass("java/util/ArrayList");
  jmethodID toArrayMethod = env->GetMethodID(arrayListClass, "toArray", "()[Ljava/lang/Object;");
  jobjectArray midiDeviceArray = (jobjectArray)env->CallObjectMethod(midiDeviceList, toArrayMethod);

  jsize arrayLength = env->GetArrayLength(midiDeviceArray);
  Global::midiDeviceCount = arrayLength;
  jclass charSequenceClass = env->FindClass("java/lang/CharSequence");
  jmethodID toStringId = env->GetMethodID(charSequenceClass, "toString", "()Ljava/lang/String;");
  for (jsize i = 0; i < arrayLength; ++i)
  {
    jstring deviceName = (jstring)env->GetObjectArrayElement(midiDeviceArray, i);
    const char* deviceNameUTF = env->GetStringUTFChars(deviceName, 0);
    Global::midiDeviceNames[i] = deviceNameUTF;
    env->ReleaseStringUTFChars(deviceName, deviceNameUTF);
  }
#endif // PLATFORM_ANDROID_SDL

#ifdef PLATFORM_WINDOWS
  Global::midiDeviceCount = midiInGetNumDevs();
  ASSERT(Global::midiDeviceCount <= Const::midiMaxDeviceCount);

  for (i32 i = 0; i < Global::midiDeviceCount; ++i)
  {
    MIDIINCAPS inputCapabilities;
    midiInGetDevCaps(i, &inputCapabilities, sizeof(inputCapabilities));
#ifdef UNICODE
    char ch[MAXPNAMELEN];
    const char DefChar = ' ';
    WideCharToMultiByte(CP_ACP, 0, inputCapabilities.szPname, -1, ch, MAXPNAMELEN, &DefChar, NULL);
    Global::midiDeviceNames[i] = reinterpret_cast<const char8_t*>(ch);
#else // UNICODE
    Global::midiDeviceNames[i] = inputCapabilities.szPname;
#endif // UNICODE

    if (Settings::tunerMidiDevice == Global::midiDeviceNames[i])
      Global::tunerMidiDevice = i;
  }
#endif // PLATFORM_WINDOWS

  midiAutoConnectDevices(Settings::midiAutoConnectDevices);
}

#ifdef PLATFORM_ANDROID_SDL
void Midi::tick()
{
  const size_t MAX_BYTES_TO_RECEIVE = 128;
  uint8_t incomingMessage[MAX_BYTES_TO_RECEIVE];

  int32_t opcode;
  size_t numBytesReceived;
  int64_t timestamp;
  ssize_t numMessagesReceived = AMidiOutputPort_receive(sMidiOutputPort, &opcode, incomingMessage, MAX_BYTES_TO_RECEIVE, &numBytesReceived, &timestamp);

  if (numMessagesReceived > 0 && numBytesReceived >= 0)
  {
    if (opcode == AMIDI_OPCODE_DATA && (incomingMessage[0] & 0xF0) != 0xF0) // 0xF0=kMIDISysCmdChan
    {
      processMidi(incomingMessage[0], incomingMessage[1], incomingMessage[2]);
    }
  }
  }
#endif // PLATFORM_ANDROID_SDL

void Midi::fini()
{
  std::set<std::u8string> deviceNames;

  for (i32 i = 0; i < Const::midiMaxDeviceCount; ++i)
  {
    if (Global::midiConnectedDevices[i] != 0)
    {
      //Midi::closeDevice(i);
      deviceNames.insert(Global::midiDeviceNames[i]);
    }
  }

  Settings::midiAutoConnectDevices.clear();
  for (const std::u8string& name : deviceNames)
    Settings::midiAutoConnectDevices += name + u8',';
  if (Settings::midiAutoConnectDevices.size() >= 1)
    Settings::midiAutoConnectDevices.pop_back();
}

#endif // SHR3D_MIDI
