// license:BSD-3-Clause
// copyright-holders:R. Belmont
/***************************************************************************

    portmap.c

    Midi implementation based on portmidi library

*******************************************************************c********/

#ifndef NO_USE_MIDI

#include <portmidi.h>
#include "osdcore.h"
#include "modules/osdmodule.h"
#include "midi_module.h"

class pm_module : public osd_module, public midi_module
{
public:

	pm_module() : osd_module(OSD_MIDI_PROVIDER, "pm"), midi_module()
	{
	}
	virtual ~pm_module() { }

	virtual int init(const osd_options &options)override;
	virtual void exit()override;

	virtual std::unique_ptr<osd_midi_device> create_midi_device() override;
	virtual void list_midi_devices() override;
};


static const int RX_EVENT_BUF_SIZE = 512;

#define MIDI_SYSEX  0xf0
#define MIDI_EOX    0xf7

class osd_midi_device_pm : public osd_midi_device
{
public:
	osd_midi_device_pm(): pmStream(nullptr), xmit_cnt(0), last_status(0), rx_sysex(false) { }
	virtual ~osd_midi_device_pm() { }
	virtual bool open_input(const char *devname) override;
	virtual bool open_output(const char *devname) override;
	virtual void close() override;
	virtual bool poll() override;
	virtual int read(uint8_t *pOut) override;
	virtual void write(uint8_t data) override;

private:
	PortMidiStream *pmStream;
	PmEvent rx_evBuf[RX_EVENT_BUF_SIZE];
	uint8_t xmit_in[4]; // Pm_Messages mean we can at most have 3 residue bytes
	int xmit_cnt;
	uint8_t last_status;
	bool rx_sysex;
};

std::unique_ptr<osd_midi_device> pm_module::create_midi_device()
{
	return std::make_unique<osd_midi_device_pm>();
}


int pm_module::init(const osd_options &options)
{
	Pm_Initialize();
	return 0;
}

void pm_module::exit()
{
	Pm_Terminate();
}

void pm_module::list_midi_devices()
{
	int num_devs = Pm_CountDevices();
	const PmDeviceInfo *pmInfo;

	printf("\n");

	if (num_devs == 0)
	{
		printf("No MIDI ports were found\n");
		return;
	}

	printf("MIDI input ports:\n");
	for (int i = 0; i < num_devs; i++)
	{
		pmInfo = Pm_GetDeviceInfo(i);

		if (pmInfo->input)
		{
			printf("%s %s\n", pmInfo->name, (i == Pm_GetDefaultInputDeviceID()) ? "(default)" : "");
		}
	}

	printf("\nMIDI output ports:\n");
	for (int i = 0; i < num_devs; i++)
	{
		pmInfo = Pm_GetDeviceInfo(i);

		if (pmInfo->output)
		{
			printf("%s %s\n", pmInfo->name, (i == Pm_GetDefaultOutputDeviceID()) ? "(default)" : "");
		}
	}
}

bool osd_midi_device_pm::open_input(const char *devname)
{
	int num_devs = Pm_CountDevices();
	int found_dev = -1;
	const PmDeviceInfo *pmInfo;
	PortMidiStream *stm;

	if (!strcmp("default", devname))
	{
		found_dev = Pm_GetDefaultInputDeviceID();
	}
	else
	{
		for (int i = 0; i < num_devs; i++)
		{
			pmInfo = Pm_GetDeviceInfo(i);

			if (pmInfo->input)
			{
				if (!strcmp(devname, pmInfo->name))
				{
					found_dev = i;
					break;
				}
			}
		}
	}

	if (found_dev >= 0)
	{
		if (Pm_OpenInput(&stm, found_dev, nullptr, RX_EVENT_BUF_SIZE, nullptr, nullptr) == pmNoError)
		{
			pmStream = stm;
			return true;
		}
		else
		{
			printf("Couldn't open PM device\n");
			return false;
		}
	}
	else
	{
		return false;
	}
}

bool osd_midi_device_pm::open_output(const char *devname)
{
	int num_devs = Pm_CountDevices();
	int found_dev = -1;
	const PmDeviceInfo *pmInfo;
	PortMidiStream *stm;

	if (!strcmp("default", devname))
	{
		found_dev = Pm_GetDefaultOutputDeviceID();
	}
	else
	{
		for (int i = 0; i < num_devs; i++)
		{
			pmInfo = Pm_GetDeviceInfo(i);

			if (pmInfo->output)
			{
				if (!strcmp(devname, pmInfo->name))
				{
					found_dev = i;
					break;
				}
			}
		}
	}

	if (found_dev >= 0)
	{
		if (Pm_OpenOutput(&stm, found_dev, nullptr, 100, nullptr, nullptr, 0) == pmNoError)
		{
			pmStream = stm;
			return true;
		}
		else
		{
			printf("Couldn't open PM device\n");
			return false;
		}
	}
	else
	{
		return false;
	}
}

void osd_midi_device_pm::close()
{
	Pm_Close(pmStream);
}

bool osd_midi_device_pm::poll()
{
	PmError chk = Pm_Poll(pmStream);

	return (chk == pmGotData) ? true : false;
}

int osd_midi_device_pm::read(uint8_t *pOut)
{
	int msgsRead = Pm_Read(pmStream, rx_evBuf, RX_EVENT_BUF_SIZE);
	int bytesOut = 0;

	if (msgsRead <= 0)
	{
		return 0;
	}

	for (int msg = 0; msg < msgsRead; msg++)
	{
		uint8_t status = Pm_MessageStatus(rx_evBuf[msg].message);

		if (rx_sysex)
		{
			if (status & 0x80)  // sys real-time imposing on us?
			{
				if (status == 0xf2)
				{
					*pOut++ = status;
					*pOut++ = Pm_MessageData1(rx_evBuf[msg].message);
					*pOut++ = Pm_MessageData2(rx_evBuf[msg].message);
					bytesOut += 3;
				}
				else if (status == 0xf3)
				{
					*pOut++ = status;
					*pOut++ = Pm_MessageData1(rx_evBuf[msg].message);
					bytesOut += 2;
				}
				else
				{
					*pOut++ = status;
					bytesOut++;
					if (status == MIDI_EOX)
					{
						rx_sysex = false;
					}
				}
			}
			else    // shift out the sysex bytes
			{
				for (int i = 0; i < 4; i++)
				{
					uint8_t byte = rx_evBuf[msg].message & 0xff;
					*pOut++ = byte;
					bytesOut++;
					if (byte == MIDI_EOX)
					{
						rx_sysex = false;
						break;
					}
					rx_evBuf[msg].message >>= 8;
				}
			}
		}
		else
		{
			switch ((status>>4) & 0xf)
			{
				case 0xc:   // 2-byte messages
				case 0xd:
					*pOut++ = status;
					*pOut++ = Pm_MessageData1(rx_evBuf[msg].message);
					bytesOut += 2;
					break;

				case 0xf:   // system common
					switch (status & 0xf)
					{
						case 0: // System Exclusive
						{
							*pOut++ = status;   // this should be OK: the shortest legal sysex is F0 tt dd F7, I believe
							*pOut++ = (rx_evBuf[msg].message>>8) & 0xff;
							*pOut++ = (rx_evBuf[msg].message>>16) & 0xff;
							uint8_t last = *pOut++ = (rx_evBuf[msg].message>>24) & 0xff;
							bytesOut += 4;
							rx_sysex = (last != MIDI_EOX);
							break;
						}

						case 7: // End of System Exclusive
							*pOut++ = status;
							bytesOut += 1;
							rx_sysex = false;
							break;

						case 2: // song pos
							*pOut++ = status;
							*pOut++ = Pm_MessageData1(rx_evBuf[msg].message);
							*pOut++ = Pm_MessageData2(rx_evBuf[msg].message);
							bytesOut += 3;
							break;

						case 3: // song select
							*pOut++ = status;
							*pOut++ = Pm_MessageData1(rx_evBuf[msg].message);
							bytesOut += 2;
							break;

						default:    // all other defined Fx messages are 1 byte
							*pOut++ = status;
							bytesOut += 1;
							break;
					}
					break;

				default:
					*pOut++ = status;
					*pOut++ = Pm_MessageData1(rx_evBuf[msg].message);
					*pOut++ = Pm_MessageData2(rx_evBuf[msg].message);
					bytesOut += 3;
					break;
			}
		}
	}

	return bytesOut;
}

void osd_midi_device_pm::write(uint8_t data)
{
	int bytes_needed = 0;
	PmEvent ev;
	ev.timestamp = 0;   // use the current time

//  printf("write: %02x (%d)\n", data, xmit_cnt);

	// reject data bytes when no valid status exists
	if ((last_status == 0) && !(data & 0x80))
	{
		xmit_cnt = 0;
		return;
	}

	if (xmit_cnt >= 4)
	{
		printf("MIDI out: packet assembly overflow, contact MAMEdev!\n");
		return;
	}

	// handle sysex
	if (last_status == MIDI_SYSEX)
	{
//      printf("sysex: %02x (%d)\n", data, xmit_cnt);

		// if we get a status that isn't sysex, assume it's system common
		if ((data & 0x80) && (data != MIDI_EOX))
		{
//          printf("common during sysex!\n");
			ev.message = Pm_Message(data, 0, 0);
			Pm_Write(pmStream, &ev, 1);
			return;
		}

		xmit_in[xmit_cnt++] = data;

		// if EOX or 4 bytes filled, transmit 4 bytes
		if ((xmit_cnt == 4) || (data == MIDI_EOX))
		{
			ev.message = xmit_in[0] | (xmit_in[1]<<8) | (xmit_in[2]<<16) | (xmit_in[3]<<24);
			Pm_Write(pmStream, &ev, 1);
			xmit_in[0] = xmit_in[1] = xmit_in[2] = xmit_in[3] = 0;
			xmit_cnt = 0;

//          printf("SysEx packet: %08x\n", ev.message);

			// if this is EOX, kill the running status
			if (data == MIDI_EOX)
			{
				last_status = 0;
			}
		}

		return;
	}

	// handle running status.  don't allow system real-time messages to be considered as running status.
	if ((xmit_cnt == 0) && (data & 0x80) && (data < 0xf8))
	{
		last_status = data;
	}

	if ((xmit_cnt == 0) && !(data & 0x80))
	{
		xmit_in[xmit_cnt++] = last_status;
		xmit_in[xmit_cnt++] = data;
//      printf("\trunning status: [%d] = %02x, [%d] = %02x, last_status = %02x\n", xmit_cnt-2, last_status, xmit_cnt-1, data, last_status);
	}
	else
	{
		xmit_in[xmit_cnt++] = data;
//      printf("\tNRS: [%d] = %02x\n", xmit_cnt-1, data);
	}

	if ((xmit_cnt == 1) && (xmit_in[0] == MIDI_SYSEX))
	{
//      printf("Start SysEx!\n");
		last_status = MIDI_SYSEX;
		return;
	}

	// are we there yet?
//  printf("status check: %02x\n", xmit_in[0]);
	switch ((xmit_in[0]>>4) & 0xf)
	{
		case 0xc:   // 2-byte messages
		case 0xd:
			bytes_needed = 2;
			break;

		case 0xf:   // system common
			switch (xmit_in[0] & 0xf)
			{
				case 0: // System Exclusive is handled above
					break;

				case 7: // End of System Exclusive
					bytes_needed = 1;
					break;

				case 2: // song pos
				case 3: // song select
					bytes_needed = 3;
					break;

				default:    // all other defined Fx messages are 1 byte
					bytes_needed = 1;
					break;
			}
			break;

		default:
			bytes_needed = 3;
			break;
	}

	if (xmit_cnt == bytes_needed)
	{
		ev.message = Pm_Message(xmit_in[0], xmit_in[1], xmit_in[2]);
		Pm_Write(pmStream, &ev, 1);
		xmit_cnt = 0;
	}

}
#else
	#include "modules/osdmodule.h"
	#include "midi_module.h"

	MODULE_NOT_SUPPORTED(pm_module, OSD_MIDI_PROVIDER, "pm")
#endif


MODULE_DEFINITION(MIDI_PM, pm_module)
