#include <rtmidi.hpp>
#include <midi.hpp>
#include <map>

#pragma GCC diagnostic push
#ifndef __clang__
	#pragma GCC diagnostic ignored "-Wsuggest-override"
#endif
#include <rtmidi/RtMidi.h>
#pragma GCC diagnostic pop


namespace rack {


struct RtMidiInputDevice : midi::InputDevice {
	RtMidiIn* rtMidiIn;

	RtMidiInputDevice(int driverId, int deviceId) {
		rtMidiIn = new RtMidiIn((RtMidi::Api) driverId, "VCV Rack");
		assert(rtMidiIn);
		rtMidiIn->ignoreTypes(false, false, false);
		rtMidiIn->setCallback(midiInputCallback, this);
		rtMidiIn->openPort(deviceId, "VCV Rack input");
	}

	~RtMidiInputDevice() {
		rtMidiIn->closePort();
		delete rtMidiIn;
	}

	static void midiInputCallback(double timeStamp, std::vector<unsigned char>* message, void* userData) {
		if (!message)
			return;
		if (!userData)
			return;

		RtMidiInputDevice* midiInputDevice = (RtMidiInputDevice*) userData;
		if (!midiInputDevice)
			return;

		// Users have reported that some MIDI devices can send messages >3 bytes. I don't know how this is possible, so just reject the message.
		if (message->size() > 3)
			return;

		midi::Message msg;
		msg.size = message->size();
		for (int i = 0; i < msg.size; i++) {
			msg.bytes[i] = (*message)[i];
		}
		midiInputDevice->onMessage(msg);
	}
};


struct RtMidiOutputDevice : midi::OutputDevice {
	RtMidiOut* rtMidiOut;

	RtMidiOutputDevice(int driverId, int deviceId) {
		rtMidiOut = new RtMidiOut((RtMidi::Api) driverId, "VCV Rack");
		assert(rtMidiOut);
		rtMidiOut->openPort(deviceId, "VCV Rack output");
	}

	~RtMidiOutputDevice() {
		rtMidiOut->closePort();
		delete rtMidiOut;
	}

	void sendMessage(midi::Message message) override {
		rtMidiOut->sendMessage(message.bytes, message.size);
	}
};


struct RtMidiDriver : midi::Driver {
	int driverId;
	/** Just for querying MIDI driver information */
	RtMidiIn* rtMidiIn;
	RtMidiOut* rtMidiOut;
	std::map<int, RtMidiInputDevice*> inputDevices;
	std::map<int, RtMidiOutputDevice*> outputDevices;

	RtMidiDriver(int driverId) {
		this->driverId = driverId;
		rtMidiIn = new RtMidiIn((RtMidi::Api) driverId);
		assert(rtMidiIn);
		rtMidiOut = new RtMidiOut((RtMidi::Api) driverId);
		assert(rtMidiOut);
	}

	~RtMidiDriver() {
		delete rtMidiIn;
		delete rtMidiOut;
	}

	std::string getName() override {
		switch (driverId) {
			case RtMidi::UNSPECIFIED: return "Unspecified";
			case RtMidi::MACOSX_CORE: return "Core MIDI";
			case RtMidi::LINUX_ALSA: return "ALSA";
			case RtMidi::UNIX_JACK: return "JACK";
			case RtMidi::WINDOWS_MM: return "Windows MIDI";
			case RtMidi::RTMIDI_DUMMY: return "Dummy MIDI";
			default: return "";
		}
	}
	std::vector<int> getInputDeviceIds() override {
		// TODO The IDs unfortunately jump around in RtMidi. Is there a way to keep them constant when a MIDI device is added/removed?
		int count = rtMidiIn->getPortCount();
		std::vector<int> deviceIds;
		for (int i = 0; i < count; i++)
			deviceIds.push_back(i);
		return deviceIds;
	}

	std::string getInputDeviceName(int deviceId) override {
		if (deviceId >= 0) {
			return rtMidiIn->getPortName(deviceId);
		}
		return "";
	}

	midi::InputDevice* subscribeInput(int deviceId, midi::Input* input) override {
		if (!(0 <= deviceId && deviceId < (int) rtMidiIn->getPortCount()))
			return NULL;
		RtMidiInputDevice* device = inputDevices[deviceId];
		if (!device) {
			inputDevices[deviceId] = device = new RtMidiInputDevice(driverId, deviceId);
		}

		device->subscribe(input);
		return device;
	}

	void unsubscribeInput(int deviceId, midi::Input* input) override {
		auto it = inputDevices.find(deviceId);
		if (it == inputDevices.end())
			return;
		RtMidiInputDevice* device = it->second;
		device->unsubscribe(input);

		// Destroy device if nothing is subscribed anymore
		if (device->subscribed.empty()) {
			inputDevices.erase(it);
			delete device;
		}
	}

	std::vector<int> getOutputDeviceIds() override {
		int count = rtMidiOut->getPortCount();
		std::vector<int> deviceIds;
		for (int i = 0; i < count; i++)
			deviceIds.push_back(i);
		return deviceIds;
	}

	std::string getOutputDeviceName(int deviceId) override {
		if (deviceId >= 0) {
			return rtMidiOut->getPortName(deviceId);
		}
		return "";
	}

	midi::OutputDevice* subscribeOutput(int deviceId, midi::Output* output) override {
		if (!(0 <= deviceId && deviceId < (int) rtMidiOut->getPortCount()))
			return NULL;
		RtMidiOutputDevice* device = outputDevices[deviceId];
		if (!device) {
			outputDevices[deviceId] = device = new RtMidiOutputDevice(driverId, deviceId);
		}

		device->subscribe(output);
		return device;
	}

	void unsubscribeOutput(int deviceId, midi::Output* output) override {
		auto it = outputDevices.find(deviceId);
		if (it == outputDevices.end())
			return;
		RtMidiOutputDevice* device = it->second;
		device->unsubscribe(output);

		// Destroy device if nothing is subscribed anymore
		if (device->subscribed.empty()) {
			outputDevices.erase(it);
			delete device;
		}
	}
};


void rtmidiInit() {
	std::vector<RtMidi::Api> rtApis;
	RtMidi::getCompiledApi(rtApis);
	for (RtMidi::Api api : rtApis) {
		int driverId = (int) api;
		midi::Driver* driver = new RtMidiDriver(driverId);
		midi::addDriver(driverId, driver);
	}
}


} // namespace rack
