/*
 *  Copyright (C) 2002-2021  The DOSBox Team
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */


#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <mmsystem.h>
#include <string>
#include <sstream>

#define WIN32_MIDI_PORT_PROTECT 1

class MidiHandler_win32: public MidiHandler {
private:
	HMIDIOUT m_out = NULL;
    MIDIHDR m_hdr = {};
	HANDLE m_event = NULL;
    bool useMidiVolume = false;
    bool isMicrosoftSoftSynth = false;
    bool resetVolume = true;
	bool isOpen;

#if WIN32_MIDI_PORT_PROTECT
	HINSTANCE hMidiHelper = NULL;
	bool midi_dll = false;
	bool midi_dll_active = false;

	void MidiHelper_Reset()
	{
		midi_dll = false;
		midi_dll_active = false;


		// force unloading of old app - releases VSC lock
		hMidiHelper = LoadLibrary( "midi_helper.dll" );
		if( hMidiHelper ) {
			midi_dll = true;
			while( FreeLibrary( hMidiHelper ) != 0 ) Sleep(1);
		}
	}

	int MidiHelper_Start( DWORD devID )
	{
		HMIDIOUT (*func_ptr)(DWORD);

		hMidiHelper = LoadLibrary( "midi_helper.dll" );
		if( !hMidiHelper ) return -1;

    func_ptr = (HMIDIOUT(*)(DWORD))GetProcAddress( hMidiHelper,"MIDIHelper_OpenMidiOut" );
		if (!func_ptr ) return -1;

		m_out = func_ptr( devID );
		if( m_out == 0 ) return -1;


		midi_dll_active = true;
		return 0;
	}

	void MidiHelper_End()
	{
		void (*func_ptr)(void);
		func_ptr = (void(*)(void))GetProcAddress( hMidiHelper,"MIDIHelper_CloseMidiOut" );
		if (!func_ptr) return;

		func_ptr();
	}
#endif

public:
	MidiHandler_win32() : MidiHandler(),isOpen(false) {};
	const char * GetName(void) { return "win32";};

	bool Open(const char * conf) {
		MIDIOUTCAPS mididev;
		if (isOpen) return false;


#if WIN32_MIDI_PORT_PROTECT
		// VSC crash protection
		MidiHelper_Reset();
#endif


		m_event = CreateEvent (NULL, true, true, NULL);
		MMRESULT res = MMSYSERR_NOERROR;
		if(conf && *conf) {
			std::string strconf(conf);
			std::istringstream configmidi(strconf);
			unsigned int total = midiOutGetNumDevs();
			unsigned int nummer = total;
			configmidi >> nummer;
			if (configmidi.fail() && total) {
				lowcase(strconf);
				for(unsigned int i = 0; i< total;i++) {
					midiOutGetDevCaps(i, &mididev, sizeof(MIDIOUTCAPS));
					std::string devname(mididev.szPname);
					lowcase(devname);
					if (devname.find(strconf) != std::string::npos) {
						nummer = i;
						break;
					}
				}
			}

			if (nummer < total) {
				midiOutGetDevCaps(nummer, &mididev, sizeof(MIDIOUTCAPS));
				LOG_MSG("MIDI:win32 selected %s",mididev.szPname);
#if !defined(HX_DOS)
                /* If we're not talking to MIDI hardware, but the default Microsoft software synthesizer,
                   and it supports volume control, we want to use it, because Microsoft's MIDI synth also
                   does not recognize the Roland GS master volume SysEx either. */
                if(mididev.dwSupport & MIDICAPS_VOLUME) {
                    if(mididev.wTechnology == MOD_SWSYNTH) {
                        isMicrosoftSoftSynth = true;
                    }
                }
#else
                isMicrosoftSoftSynth = false;
#endif
                resetVolume = true;

#if WIN32_MIDI_PORT_PROTECT
				if( midi_dll == false || strcmp( mididev.szPname, "Roland VSC" ) != 0 )
					res = midiOutOpen(&m_out, nummer, (DWORD_PTR)m_event, 0, CALLBACK_EVENT);
				else {
					// Roland VSC - crash protection
					res = MidiHelper_Start(nummer);
				}
#else
				res = midiOutOpen(&m_out, nummer, (DWORD_PTR)m_event, 0, CALLBACK_EVENT);
#endif


				if( res != MMSYSERR_NOERROR ) {
					if( strcmp( mididev.szPname, "Roland VSC" ) == 0 ) MessageBox( 0, "Roland VSC failed", "MIDI", MB_OK | MB_TOPMOST );

					if( nummer != 0 ) {
						LOG_MSG("MIDI:win32 selected %s","default");
						res = midiOutOpen(&m_out, MIDI_MAPPER, (DWORD_PTR)m_event, 0, CALLBACK_EVENT);
					}
				}
			}
		} else {
			res = midiOutOpen(&m_out, MIDI_MAPPER, (DWORD_PTR)m_event, 0, CALLBACK_EVENT);
		}
		if (res != MMSYSERR_NOERROR) return false;

		Reset();

		isOpen=true;
		return true;
	};

	void Close(void) {
		if (!isOpen) return;
		isOpen=false;


#if WIN32_MIDI_PORT_PROTECT
		if( midi_dll ) MidiHelper_End();
#endif

		// flush buffers, then shutdown
		midiOutReset(m_out);
		midiOutClose(m_out);

		CloseHandle (m_event);
	};

	void PlayMsg(uint8_t * msg) {
		midiOutShortMsg(m_out, *(uint32_t*)msg);

        /* This must be done AFTER to prevent the notes left hanging from ever becoming audible before reset */
        if(*msg == 0xFF/*MIDI RESET*/) {
            midiOutReset(m_out);
            resetVolume = true;
        }
        else {
            if(resetVolume) {
                if(isMicrosoftSoftSynth) {
                    midiOutSetVolume(m_out, 0xFFFF);
                }
                resetVolume = false;
            }
        }
	};

	void PlaySysex(uint8_t * sysex,Bitu len) {
#if WIN32_MIDI_PORT_PROTECT
		if( midi_dll_active == false ) {
#endif
			if (WaitForSingleObject (m_event, 2000) == WAIT_TIMEOUT) {
				LOG(LOG_MISC,LOG_ERROR)("Can't send midi message");
				return;
			}
#if WIN32_MIDI_PORT_PROTECT
		}
#endif

        if(roland_gs_sysex) {
            if(sysex[1] == 0x41/*Roland*/ && sysex[3] == 0x42/*GS*/ && sysex[4] == 0x12/*Send*/ && len >= 9) {
                const uint32_t addr =
                    ((uint32_t)sysex[5] << 16) +
                    ((uint32_t)sysex[6] << 8) +
                    (uint32_t)sysex[7];

                if(addr == 0x400004) { /* MASTER VOLUME */
                    if(isMicrosoftSoftSynth) {
                        /* input: MIDI volume 0-127
                           output: MIDI volume 0x0000-0xFFFF */
                        unsigned int mvol = sysex[8];
                        if(mvol > 127) mvol = 127;
                        mvol = (mvol * 0xFFFFu) / 127u;
                        midiOutSetVolume(m_out, mvol);
                        resetVolume = false;
                        return;
                    }
                }
            }
        }

		midiOutUnprepareHeader (m_out, &m_hdr, sizeof (m_hdr));

		m_hdr.lpData = (char *) sysex;
		m_hdr.dwBufferLength = (DWORD)len;
		m_hdr.dwBytesRecorded = (DWORD)len;
		m_hdr.dwUser = 0;

		MMRESULT result = midiOutPrepareHeader (m_out, &m_hdr, sizeof (m_hdr));
		if (result != MMSYSERR_NOERROR) return;
		ResetEvent (m_event);
		result = midiOutLongMsg (m_out,&m_hdr,sizeof(m_hdr));
		if (result != MMSYSERR_NOERROR) {
			SetEvent (m_event);
			return;
		}

#if WIN32_MIDI_PORT_PROTECT
		if( midi_dll_active == true ) {
			while( midiOutUnprepareHeader (m_out, &m_hdr, sizeof (m_hdr)) != 0 )
				Sleep(1);
		}
#endif
	}
	
	void ListAll(Program* base) {
#if defined (WIN32)
		unsigned int total = midiOutGetNumDevs();	
		for(unsigned int i = 0;i < total;i++) {
			MIDIOUTCAPS mididev;
			midiOutGetDevCaps(i, &mididev, sizeof(MIDIOUTCAPS));
			base->WriteOut("  %2d - \"%s\"\n",i,mididev.szPname);
		}
#endif
	}

	void Reset()
	{
		uint8_t buf[64];

		// flush buffers
		midiOutReset(m_out);


		// GM1 reset
		buf[0] = 0xf0;
		buf[1] = 0x7e;
		buf[2] = 0x7f;
		buf[3] = 0x09;
		buf[4] = 0x01;
		buf[5] = 0xf7;
		PlaySysex( (uint8_t *) buf, 6 );


		// GS1 reset
		buf[0] = 0xf0;
		buf[1] = 0x41;
		buf[2] = 0x10;
		buf[3] = 0x42;
		buf[4] = 0x12;
		buf[5] = 0x40;
		buf[6] = 0x00;
		buf[7] = 0x7f;
		buf[8] = 0x00;
		buf[9] = 0x41;
		buf[10] = 0xf7;
		PlaySysex( (uint8_t *) buf, 11 );
	}
};

MidiHandler_win32 Midi_win32; 
