/*
** music_opl_mididevice.cpp
** Provides an emulated OPL implementation of a MIDI output device.
**
**---------------------------------------------------------------------------
** Copyright 2008 Randy Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
**
*/

// HEADER FILES ------------------------------------------------------------

#include <stdexcept>
#include "zmusic/zmusic_internal.h"
#include "mididevice.h"
#include "zmusic/mus2midi.h"

#ifdef HAVE_OPL
#include "oplsynth/opl.h"
#include "oplsynth/opl_mus_player.h"

// MACROS ------------------------------------------------------------------

// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------

// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------

// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------

// EXTERNAL DATA DECLARATIONS ----------------------------------------------

// PRIVATE DATA DEFINITIONS ------------------------------------------------

OPLConfig oplConfig;

// OPL implementation of a MIDI output device -------------------------------

class OPLMIDIDevice : public SoftSynthMIDIDevice, protected OPLmusicBlock
{
	float OutputGainFactor;
public:
	OPLMIDIDevice(int core);
	int OpenRenderer() override;
	void Close() override;
	int GetTechnology() const override;
	std::string GetStats() override;

protected:
	void CalcTickRate() override;
	int PlayTick() override;
	void HandleEvent(int status, int parm1, int parm2) override;
	void HandleLongEvent(const uint8_t *data, int len) override;
	void ComputeOutput(float *buffer, int len) override;
	bool ServiceStream(void *buff, int numbytes) override;
	int GetDeviceType() const override { return MDEV_OPL; }
	void ChangeSettingNum(const char *setting, double value) override;
};


// PUBLIC DATA DEFINITIONS -------------------------------------------------

// CODE --------------------------------------------------------------------

//==========================================================================
//
// OPLMIDIDevice Contructor
//
//==========================================================================

OPLMIDIDevice::OPLMIDIDevice(int core)
	: SoftSynthMIDIDevice((int)OPL_SAMPLE_RATE), OPLmusicBlock(core, oplConfig.numchips)
{
	FullPan = oplConfig.fullpan;
	memcpy(OPLinstruments, oplConfig.OPLinstruments, sizeof(OPLinstruments));
	OutputGainFactor = oplConfig.gain;
	StreamBlockSize = 14;
}

//==========================================================================
//
// OPLMIDIDevice :: Open
//
// Returns 0 on success.
//
//==========================================================================

int OPLMIDIDevice::OpenRenderer()
{
	if (io == NULL || 0 == (NumChips = io->Init(currentCore, NumChips, FullPan, true)))
	{
		return 1;
	}
	isMono = !FullPan && !io->IsOPL3;
	stopAllVoices();
	resetAllControllers(100);
	return 0;
}

//==========================================================================
//
// OPLMIDIDevice :: Close
//
//==========================================================================

void OPLMIDIDevice::Close()
{
	SoftSynthMIDIDevice::Close();
	io->Reset();
}

//==========================================================================
//
// OPLMIDIDevice :: GetTechnology
//
//==========================================================================

int OPLMIDIDevice::GetTechnology() const
{
	return MIDIDEV_FMSYNTH;
}

//==========================================================================
//
// OPLMIDIDevice :: CalcTickRate
//
// Tempo is the number of microseconds per quarter note.
// Division is the number of ticks per quarter note.
//
//==========================================================================

void OPLMIDIDevice::CalcTickRate()
{
	SoftSynthMIDIDevice::CalcTickRate();
	io->SetClockRate(OPLmusicBlock::SamplesPerTick = SoftSynthMIDIDevice::SamplesPerTick);
}

//==========================================================================
//
// OPLMIDIDevice :: PlayTick
//
// We derive from two base classes that both define PlayTick(), so we need
// to be unambiguous about which one to use.
//
//==========================================================================

int OPLMIDIDevice::PlayTick()
{
	return SoftSynthMIDIDevice::PlayTick();
}

//==========================================================================
//
// OPLMIDIDevice :: HandleEvent
//
// Processes a normal MIDI event.
//
//==========================================================================

void OPLMIDIDevice::HandleEvent(int status, int parm1, int parm2)
{
	int command = status & 0xF0;
	int channel = status & 0x0F;

	// Swap voices 9 and 15, because their roles are reversed
	// in MUS and MIDI formats.
	if (channel == 9)
	{
		channel = 15;
	}
	else if (channel == 15)
	{
		channel = 9;
	}

	switch (command)
	{
	case MIDI_NOTEOFF:
		playingcount--;
		noteOff(channel, parm1);
		break;

	case MIDI_NOTEON:
		playingcount++;
		noteOn(channel, parm1, parm2);
		break;

	case MIDI_POLYPRESS:
		//DEBUGOUT("Unhandled note aftertouch: Channel %d, note %d, value %d\n", channel, parm1, parm2);
		break;

	case MIDI_CTRLCHANGE:
		switch (parm1)
		{
		// some controllers here get passed on but are not handled by the player.
		//case 0:	changeBank(channel, parm2);							break;
		case 1:		changeModulation(channel, parm2);					break;
		case 6:		changeExtended(channel, ctrlDataEntryHi, parm2);	break;
		case 7:		changeVolume(channel, parm2, false);				break;
		case 10:	changePanning(channel, parm2);						break;
		case 11:	changeVolume(channel, parm2, true);					break;
		case 38:	changeExtended(channel, ctrlDataEntryLo, parm2);	break;
		case 64:	changeSustain(channel, parm2);						break;
		//case 67:	changeSoftPedal(channel, parm2);					break;
		//case 91:	changeReverb(channel, parm2);						break;
		//case 93:	changeChorus(channel, parm2);						break;
		case 98:	changeExtended(channel, ctrlNRPNLo, parm2);			break;
		case 99:	changeExtended(channel, ctrlNRPNHi, parm2);			break;
		case 100:	changeExtended(channel, ctrlRPNLo, parm2);			break;
		case 101:	changeExtended(channel, ctrlRPNHi, parm2);			break;
		case 120:	allNotesOff(channel, parm2);						break;
		case 121:	resetControllers(channel, 100);						break;
		case 123:	notesOff(channel, parm2);							break;
		//case 126:	changeMono(channel, parm2);							break;
		//case 127:	changePoly(channel, parm2);							break;
		default:
			//DEBUGOUT("Unhandled controller: Channel %d, controller %d, value %d\n", channel, parm1, parm2);
			break;
		}
		break;

	case MIDI_PRGMCHANGE:
		programChange(channel, parm1);
		break;

	case MIDI_CHANPRESS:
		//DEBUGOUT("Unhandled channel aftertouch: Channel %d, value %d\n", channel, parm1, 0);
		break;

	case MIDI_PITCHBEND:
		changePitch(channel, parm1, parm2);
		break;
	}
}

//==========================================================================
//
// OPLMIDIDevice :: HandleLongEvent
//
//==========================================================================

void OPLMIDIDevice::HandleLongEvent(const uint8_t *data, int len)
{
}

//==========================================================================
//
// OPLMIDIDevice :: ComputeOutput
//
// We override ServiceStream, so this function is never actually called.
//
//==========================================================================

void OPLMIDIDevice::ComputeOutput(float *buffer, int len)
{
}

//==========================================================================
//
// OPLMIDIDevice :: ServiceStream
//
//==========================================================================

bool OPLMIDIDevice::ServiceStream(void *buff, int numbytes)
{
	float *samples = (float *)buff;
	bool ret = OPLmusicBlock::ServiceStream(buff, numbytes);
	int numsamples = numbytes / sizeof(float);
	for(int i=0; i < numsamples; i++)
	{
		samples[i] *= OutputGainFactor;
	}
	return ret;
}

//==========================================================================
//
// OPLMIDIDevice :: ChangeSettingNum
//
// Changes a numeric setting.
//
//==========================================================================

void OPLMIDIDevice::ChangeSettingNum(const char *setting, double value)
{
	if (strncmp(setting, "oplemu.", 7))
	{
		return;
	}
	setting += 7;

	if (strcmp(setting, "gain") == 0)
	{
		OutputGainFactor = value;
	}
}

//==========================================================================
//
// OPLMIDIDevice :: GetStats
//
//==========================================================================

std::string OPLMIDIDevice::GetStats()
{
	std::string out;
	for (uint32_t i = 0; i < io->NumChannels; ++i)
	{
		char star = '*';
		if (voices[i].index == ~0u)
		{
			star = '/';
		}
		else if (voices[i].sustained)
		{
			star = '-';
		}
		else if (voices[i].current_instr_voice == &voices[i].current_instr->voices[1])
		{
			star = '\\';
		}
		else
		{
			star = '+';
		}
		out += star;
	}
	return out;
}


MIDIDevice* CreateOplMIDIDevice(const char *Args)
{
	if (!oplConfig.genmidiset) throw std::runtime_error("Cannot play OPL without GENMIDI data");
	int core = oplConfig.core;
	if (Args != NULL && *Args >= '0' && *Args < '4') core = *Args - '0';
	return new OPLMIDIDevice(core);
}

#else
MIDIDevice* CreateOplMIDIDevice(const char* Args)
{
	throw std::runtime_error("OPL device not supported in this configuration");
}
#endif
