/*
 * MidiInOut.cpp
 * -------------
 * Purpose: A plugin for sending and receiving MIDI data.
 * Notes  : (currently none)
 * Authors: Johannes Schultz (OpenMPT Devs)
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */

#include "stdafx.h"
#include "MidiInOut.h"
#include "MidiInOutEditor.h"
#include "../../common/FileReader.h"
#include "../../soundlib/Sndfile.h"
#include "../Reporting.h"
#include <algorithm>
#include <sstream>
#ifdef MODPLUG_TRACKER
#include "../Mptrack.h"
#endif
#include "mpt/io/io.hpp"
#include "mpt/io/io_stdstream.hpp"


OPENMPT_NAMESPACE_BEGIN


IMixPlugin* MidiInOut::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct)
{
	try
	{
		return new (std::nothrow) MidiInOut(factory, sndFile, mixStruct);
	} catch(RtMidiError &)
	{
		return nullptr;
	}
}


MidiInOut::MidiInOut(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct)
	: IMidiPlugin(factory, sndFile, mixStruct)
	, m_inputDevice(m_midiIn)
	, m_outputDevice(m_midiOut)
#ifdef MODPLUG_TRACKER
	, m_programName(_T("Default"))
#endif // MODPLUG_TRACKER
{
	m_mixBuffer.Initialize(2, 2);
	InsertIntoFactoryList();
}


MidiInOut::~MidiInOut()
{
	MidiInOut::Suspend();
}


uint32 MidiInOut::GetLatency() const
{
	// There is only a latency if the user-provided latency value is greater than the negative output latency.
	return mpt::saturate_round<uint32>(std::min(0.0, m_latency + GetOutputLatency()) * m_SndFile.GetSampleRate());
}


void MidiInOut::SaveAllParameters()
{
	auto chunk = GetChunk(false);
	if(chunk.empty())
		return;

	m_pMixStruct->defaultProgram = -1;
	m_pMixStruct->pluginData.assign(chunk.begin(), chunk.end());
}


void MidiInOut::RestoreAllParameters(int32 program)
{
	IMixPlugin::RestoreAllParameters(program);	// First plugin version didn't use chunks.
	SetChunk(mpt::as_span(m_pMixStruct->pluginData), false);
}


enum ChunkFlags
{
	kLatencyCompensation	= 0x01,	// Implicit in current plugin version
	kLatencyPresent			= 0x02,	// Latency value is present as double-precision float
	kIgnoreTiming			= 0x04,	// Do not send timing and sequencing information
	kFriendlyInputName		= 0x08,	// Preset also stores friendly name of input device
	kFriendlyOutputName		= 0x10,	// Preset also stores friendly name of output device
};

IMixPlugin::ChunkData MidiInOut::GetChunk(bool /*isBank*/)
{
	const std::string programName8 = mpt::ToCharset(mpt::Charset::UTF8, m_programName);
	uint32 flags = kLatencyCompensation | kLatencyPresent | (m_sendTimingInfo ? 0 : kIgnoreTiming);
#ifdef MODPLUG_TRACKER
	const std::string inFriendlyName = (m_inputDevice.index == MidiDevice::NO_MIDI_DEVICE) ? m_inputDevice.name : mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, m_inputDevice.name), true, false));
	const std::string outFriendlyName = (m_outputDevice.index == MidiDevice::NO_MIDI_DEVICE) ? m_outputDevice.name : mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, m_outputDevice.name), false, false));
	if(inFriendlyName != m_inputDevice.name)
	{
		flags |= kFriendlyInputName;
	}
	if(outFriendlyName != m_outputDevice.name)
	{
		flags |= kFriendlyOutputName;
	}
#endif

	std::ostringstream s;
	mpt::IO::WriteRaw(s, "fEvN", 4);	// VST program chunk magic
	mpt::IO::WriteIntLE< int32>(s, GetVersion());
	mpt::IO::WriteIntLE<uint32>(s, 1);	// Number of programs
	mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(programName8.size()));
	mpt::IO::WriteIntLE<uint32>(s, m_inputDevice.index);
	mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(m_inputDevice.name.size()));
	mpt::IO::WriteIntLE<uint32>(s, m_outputDevice.index);
	mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(m_outputDevice.name.size()));
	mpt::IO::WriteIntLE<uint32>(s, flags);
	mpt::IO::WriteRaw(s, programName8.c_str(), programName8.size());
	mpt::IO::WriteRaw(s, m_inputDevice.name.c_str(), m_inputDevice.name.size());
	mpt::IO::WriteRaw(s, m_outputDevice.name.c_str(), m_outputDevice.name.size());
	mpt::IO::WriteIntLE<uint64>(s, IEEE754binary64LE(m_latency).GetInt64());
#ifdef MODPLUG_TRACKER
	if(flags & kFriendlyInputName)
	{
		mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(inFriendlyName.size()));
		mpt::IO::WriteRaw(s, inFriendlyName.c_str(), inFriendlyName.size());
	}
	if(flags & kFriendlyOutputName)
	{
		mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(outFriendlyName.size()));
		mpt::IO::WriteRaw(s, outFriendlyName.c_str(), outFriendlyName.size());
	}
#endif
	m_chunkData = s.str();
	return mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(m_chunkData));
}


// Try to match a port name against stored name or friendly name (preferred)
static void FindPort(MidiDevice::ID &id, unsigned int numPorts, const std::string &name, const std::string &friendlyName, MidiDevice &midiDevice, bool isInput)
{
	bool foundFriendly = false;
	for(unsigned int i = 0; i < numPorts; i++)
	{
		try
		{
			auto portName = midiDevice.GetPortName(i);
			bool deviceNameMatches = (portName == name);
#ifdef MODPLUG_TRACKER
			if(!friendlyName.empty() && friendlyName == mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, portName), isInput, false)))
			{
				// Preferred match
				id = i;
				foundFriendly = true;
				if(deviceNameMatches)
				{
					return;
				}
			}
#else
			MPT_UNREFERENCED_PARAMETER(friendlyName)
#endif
			if(deviceNameMatches && !foundFriendly)
			{
				id = i;
			}
		} catch(const RtMidiError &)
		{
		}
	}
}


void MidiInOut::SetChunk(const ChunkData &chunk, bool /*isBank*/)
{
	FileReader file(chunk);
	if(!file.CanRead(9 * sizeof(uint32))
		|| !file.ReadMagic("fEvN")				// VST program chunk magic
		|| file.ReadInt32LE() > GetVersion()	// Plugin version
		|| file.ReadUint32LE() < 1)				// Number of programs
		return;

	uint32 nameStrSize = file.ReadUint32LE();
	MidiDevice::ID inID = file.ReadUint32LE();
	uint32 inStrSize = file.ReadUint32LE();
	MidiDevice::ID outID = file.ReadUint32LE();
	uint32 outStrSize = file.ReadUint32LE();
	uint32 flags = file.ReadUint32LE();

	std::string progName, inName, outName, inFriendlyName, outFriendlyName;
	file.ReadString<mpt::String::maybeNullTerminated>(progName, nameStrSize);
	m_programName = mpt::ToCString(mpt::Charset::UTF8, progName);

	file.ReadString<mpt::String::maybeNullTerminated>(inName, inStrSize);
	file.ReadString<mpt::String::maybeNullTerminated>(outName, outStrSize);

	if(flags & kLatencyPresent)
		m_latency = file.ReadDoubleLE();
	else
		m_latency = 0.0f;
	m_sendTimingInfo = !(flags & kIgnoreTiming);

	if(flags & kFriendlyInputName)
		file.ReadString<mpt::String::maybeNullTerminated>(inFriendlyName, file.ReadUint32LE());
	if(flags & kFriendlyOutputName)
		file.ReadString<mpt::String::maybeNullTerminated>(outFriendlyName, file.ReadUint32LE());

	// Try to match an input port name against stored name or friendly name (preferred)
	FindPort(inID, m_midiIn.getPortCount(), inName, inFriendlyName, m_inputDevice, true);
	FindPort(outID, m_midiOut.getPortCount(), outName, outFriendlyName, m_outputDevice, false);

	SetParameter(MidiInOut::kInputParameter, DeviceIDToParameter(inID));
	SetParameter(MidiInOut::kOutputParameter, DeviceIDToParameter(outID));
}


void MidiInOut::SetParameter(PlugParamIndex index, PlugParamValue value)
{
	value = mpt::safe_clamp(value, 0.0f, 1.0f);
	MidiDevice::ID newDevice = ParameterToDeviceID(value);
	OpenDevice(newDevice, (index == kInputParameter));

	// Update selection in editor
	MidiInOutEditor *editor = dynamic_cast<MidiInOutEditor *>(GetEditor());
	if(editor != nullptr)
		editor->SetCurrentDevice((index == kInputParameter), newDevice);
}


float MidiInOut::GetParameter(PlugParamIndex index)
{
	const MidiDevice &device = (index == kInputParameter) ? m_inputDevice : m_outputDevice;
	return DeviceIDToParameter(device.index);
}


#ifdef MODPLUG_TRACKER

CString MidiInOut::GetParamName(PlugParamIndex param)
{
	if(param == kInputParameter)
		return _T("MIDI In");
	else
		return _T("MIDI Out");
}


// Parameter value as text
CString MidiInOut::GetParamDisplay(PlugParamIndex param)
{
	const MidiDevice &device = (param == kInputParameter) ? m_inputDevice : m_outputDevice;
	return mpt::ToCString(mpt::Charset::UTF8, device.name);
}


CAbstractVstEditor *MidiInOut::OpenEditor()
{
	try
	{
		return new MidiInOutEditor(*this);
	} catch(mpt::out_of_memory e)
	{
		mpt::delete_out_of_memory(e);
		return nullptr;
	}
}

#endif // MODPLUG_TRACKER


// Processing (we don't process any audio, only MIDI messages)
void MidiInOut::Process(float *, float *, uint32 numFrames)
{
	if(m_midiOut.isPortOpen())
	{
		mpt::lock_guard<mpt::mutex> lock(m_mutex);

		// Send MIDI clock
		if(m_nextClock < 1)
		{
			if(m_sendTimingInfo)
			{
				m_outQueue.push_back(Message(GetOutputTimestamp(), 0xF8));
			}

			double bpm = m_SndFile.GetCurrentBPM();
			if(bpm > 0.0)
			{
				m_nextClock += 2.5 * m_SndFile.GetSampleRate() / bpm;
			}
		}
		m_nextClock -= numFrames;

		double now = m_clock.Now() * (1.0 / 1000.0);
		auto message = m_outQueue.begin();
		while(message != m_outQueue.end() && message->m_time <= now)
		{
			try
			{
				m_midiOut.sendMessage(message->m_message, message->m_size);
			} catch(const RtMidiError &)
			{
			}
			message++;
		}
		m_outQueue.erase(m_outQueue.begin(), message);
	}
}


void MidiInOut::InputCallback(double /*deltatime*/, std::vector<unsigned char> &message)
{
	// We will check the bypass status before passing on the message, and not before entering the function,
	// because otherwise we might read garbage if we toggle bypass status in the middle of a SysEx message.
	bool isBypassed = IsBypassed();
	if(message.empty())
	{
		return;
	} else if(!m_bufferedInput.empty())
	{
		// SysEx message (continued)
		m_bufferedInput.insert(m_bufferedInput.end(), message.begin(), message.end());
		if(message.back() == 0xF7)
		{
			// End of message found!
			if(!isBypassed)
				ReceiveSysex(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(m_bufferedInput)));
			m_bufferedInput.clear();
		}
	} else if(message.front() == 0xF0)
	{
		// Start of SysEx message...
		if(message.back() != 0xF7)
			m_bufferedInput.insert(m_bufferedInput.end(), message.begin(), message.end());	// ...but not the end!
		else if(!isBypassed)
			ReceiveSysex(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(message)));
	} else if(!isBypassed)
	{
		// Regular message
		uint32 msg = 0;
		memcpy(&msg, message.data(), std::min(message.size(), sizeof(msg)));
		ReceiveMidi(msg);
	}
}


// Resume playback
void MidiInOut::Resume()
{
	// Resume MIDI I/O
	m_isResumed = true;
	m_nextClock = 0;
	m_outQueue.clear();
	m_clock.SetResolution(1);
	OpenDevice(m_inputDevice.index, true);
	OpenDevice(m_outputDevice.index, false);
	if(m_midiOut.isPortOpen() && m_sendTimingInfo)
	{
		MidiSend(0xFA);	// Start
	}
}


// Stop playback
void MidiInOut::Suspend()
{
	// Suspend MIDI I/O
	if(m_midiOut.isPortOpen() && m_sendTimingInfo)
	{
		try
		{
			unsigned char message[1] = { 0xFC };	// Stop
			m_midiOut.sendMessage(message, 1);
		} catch(const RtMidiError &)
		{
		}
	}
	//CloseDevice(inputDevice);
	CloseDevice(m_outputDevice);
	m_clock.SetResolution(0);
	m_isResumed = false;
}


// Playback discontinuity
void MidiInOut::PositionChanged()
{
	if(m_sendTimingInfo)
	{
		MidiSend(0xFC);	// Stop
		MidiSend(0xFA);	// Start
	}
}


void MidiInOut::Bypass(bool bypass)
{
	if(bypass)
	{
		mpt::lock_guard<mpt::mutex> lock(m_mutex);
		m_outQueue.clear();
	}
	IMidiPlugin::Bypass(bypass);
}


bool MidiInOut::MidiSend(uint32 midiCode)
{
	if(!m_midiOut.isPortOpen() || IsBypassed())
	{
		// We need an output device to send MIDI messages to.
		return true;
	}

	mpt::lock_guard<mpt::mutex> lock(m_mutex);
	m_outQueue.push_back(Message(GetOutputTimestamp(), &midiCode, 3));
	return true;
}


bool MidiInOut::MidiSysexSend(mpt::const_byte_span sysex)
{
	if(!m_midiOut.isPortOpen() || IsBypassed())
	{
		// We need an output device to send MIDI messages to.
		return true;
	}

	mpt::lock_guard<mpt::mutex> lock(m_mutex);
	m_outQueue.push_back(Message(GetOutputTimestamp(), sysex.data(), sysex.size()));
	return true;
}


void MidiInOut::HardAllNotesOff()
{
	const bool wasSuspended = !IsResumed();
	if(wasSuspended)
	{
		Resume();
	}

	for(uint8 mc = 0; mc < std::size(m_MidiCh); mc++)		//all midi chans
	{
		PlugInstrChannel &channel = m_MidiCh[mc];
		channel.ResetProgram();

		SendMidiPitchBend(mc, EncodePitchBendParam(MIDIEvents::pitchBendCentre));  // centre pitch bend
		MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllSoundOff, mc, 0));           // all sounds off

		for(size_t i = 0; i < std::size(channel.noteOnMap); i++)
		{
			for(auto &c : channel.noteOnMap[i])
			{
				while(c != 0)
				{
					MidiSend(MIDIEvents::NoteOff(mc, static_cast<uint8>(i), 0));
					c--;
				}
			}
		}
	}

	if(wasSuspended)
	{
		Suspend();
	}
}


// Open a device for input or output.
void MidiInOut::OpenDevice(MidiDevice::ID newDevice, bool asInputDevice)
{
	MidiDevice &device = asInputDevice ? m_inputDevice : m_outputDevice;

	if(device.index == newDevice && device.stream.isPortOpen())
	{
		// No need to re-open this device.
		return;
	}

	CloseDevice(device);

	device.index = newDevice;
	device.stream.closePort();

	if(device.index == kNoDevice)
	{
		// Dummy device
		device.name = "<none>";
		return;
	}

	device.name = device.GetPortName(newDevice);
	//if(m_isResumed)
	{
		mpt::lock_guard<mpt::mutex> lock(m_mutex);
		
		try
		{
			device.stream.openPort(newDevice);
			if(asInputDevice)
			{
				m_midiIn.setCallback(InputCallback, this);
				m_midiIn.ignoreTypes(false, true, true);
			}
		} catch(RtMidiError &error)
		{
			device.name = "Unavailable";
			MidiInOutEditor *editor = dynamic_cast<MidiInOutEditor *>(GetEditor());
			if(editor != nullptr)
			{
				Reporting::Error("MIDI device cannot be opened. Is it open in another application?\n\n" + error.getMessage(), "MIDI Input / Output", editor);
			}
		}
	}
}


// Close an active device.
void MidiInOut::CloseDevice(MidiDevice &device)
{
	if(device.stream.isPortOpen())
	{
		mpt::lock_guard<mpt::mutex> lock(m_mutex);
		device.stream.closePort();
	}
}


// Calculate the current output timestamp
double MidiInOut::GetOutputTimestamp() const
{
	return m_clock.Now() * (1.0 / 1000.0) + GetOutputLatency() + m_latency;
}


// Get a device name
std::string MidiDevice::GetPortName(MidiDevice::ID port)
{
	return stream.getPortName(port);
}


OPENMPT_NAMESPACE_END