/* -----------------------------------------------------------------------------
 *
 * Giada - Your Hardcore Loopmachine
 *
 * -----------------------------------------------------------------------------
 *
 * Copyright (C) 2010-2021 Giovanni A. Zuliani | Monocasual
 *
 * This file is part of Giada - Your Hardcore Loopmachine.
 *
 * Giada - Your Hardcore Loopmachine 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 3 of the License, or (at your option) any later version.
 *
 * Giada - Your Hardcore Loopmachine 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 Giada - Your Hardcore Loopmachine. If not, see
 * <http://www.gnu.org/licenses/>.
 *
 * -------------------------------------------------------------------------- */

#include "core/midiDispatcher.h"
#include "core/conf.h"
#include "core/mixer.h"
#include "core/mixerHandler.h"
#include "core/model/model.h"
#include "core/plugins/plugin.h"
#include "core/plugins/pluginHost.h"
#include "core/recorder.h"
#include "core/types.h"
#include "glue/events.h"
#include "glue/plugin.h"
#include "utils/log.h"
#include "utils/math.h"
#include <cassert>
#include <vector>

namespace giada::m
{
MidiDispatcher::MidiDispatcher(model::Model& m)
: onDispatch(nullptr)
, m_learnCb(nullptr)
, m_model(m)
{
}

/* -------------------------------------------------------------------------- */

void MidiDispatcher::startChannelLearn(int param, ID channelId, std::function<void()> f)
{
	m_learnCb = [=](m::MidiEvent e) { learnChannel(e, param, channelId, f); };
}

void MidiDispatcher::startMasterLearn(int param, std::function<void()> f)
{
	m_learnCb = [=](m::MidiEvent e) { learnMaster(e, param, f); };
}

#ifdef WITH_VST

void MidiDispatcher::startPluginLearn(std::size_t paramIndex, ID pluginId, std::function<void()> f)
{
	m_learnCb = [=](m::MidiEvent e) { learnPlugin(e, paramIndex, pluginId, f); };
}

#endif

void MidiDispatcher::stopLearn()
{
	m_learnCb = nullptr;
}

/* -------------------------------------------------------------------------- */

void MidiDispatcher::clearMasterLearn(int param, std::function<void()> f)
{
	learnMaster(MidiEvent(), param, f); // Empty event (0x0)
}

void MidiDispatcher::clearChannelLearn(int param, ID channelId, std::function<void()> f)
{
	learnChannel(MidiEvent(), param, channelId, f); // Empty event (0x0)
}

#ifdef WITH_VST

void MidiDispatcher::clearPluginLearn(std::size_t paramIndex, ID pluginId, std::function<void()> f)
{
	learnPlugin(MidiEvent(), paramIndex, pluginId, f); // Empty event (0x0)
}

#endif

/* -------------------------------------------------------------------------- */

void MidiDispatcher::dispatch(uint32_t msg)
{
	assert(onDispatch != nullptr);

	/* Here we want to catch two things: a) note on/note off from a MIDI keyboard 
	and b) knob/wheel/slider movements from a MIDI controller. 
	We must also fix the velocity zero issue for those devices that sends NOTE
	OFF events as NOTE ON + velocity zero. Let's make it a real NOTE OFF event. */

	MidiEvent midiEvent(msg);
	midiEvent.fixVelocityZero();

	u::log::print("[midiDispatcher] MIDI received - 0x%X (chan %d)\n", midiEvent.getRaw(),
	    midiEvent.getChannel());

	/* Start dispatcher. Don't parse channels if MIDI learn is ON, just learn 
	the incoming MIDI signal. The action is not invoked directly, but scheduled 
	to be perfomed by the Event Dispatcher. */

	Action action = {0, 0, 0, midiEvent};
	auto   event  = m_learnCb != nullptr ? EventDispatcher::EventType::MIDI_DISPATCHER_LEARN : EventDispatcher::EventType::MIDI_DISPATCHER_PROCESS;

	onDispatch(event, action);
}

/* -------------------------------------------------------------------------- */

void MidiDispatcher::learn(const MidiEvent& e)
{
	assert(m_learnCb != nullptr);
	m_learnCb(e);
}

/* -------------------------------------------------------------------------- */

void MidiDispatcher::process(const MidiEvent& e)
{
	assert(onEventReceived != nullptr);

	processMaster(e);
	processChannels(e);
	onEventReceived();
}

/* -------------------------------------------------------------------------- */

bool MidiDispatcher::isMasterMidiInAllowed(int c)
{
	int  filter  = m_model.get().midiIn.filter;
	bool enabled = m_model.get().midiIn.enabled;
	return enabled && (filter == -1 || filter == c);
}

/* -------------------------------------------------------------------------- */

bool MidiDispatcher::isChannelMidiInAllowed(ID channelId, int c)
{
	return m_model.get().getChannel(channelId).midiLearner.isAllowed(c);
}

/* -------------------------------------------------------------------------- */

#ifdef WITH_VST

void MidiDispatcher::processPlugins(const std::vector<Plugin*>& plugins, const MidiEvent& midiEvent)
{
	uint32_t pure = midiEvent.getRawNoVelocity();
	float    vf   = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, 1.0f);

	/* Plugins' parameters layout reflects the structure of the matrix
	Channel::midiInPlugins. It is safe to assume then that Plugin 'p' and 
	parameter indexes match both the structure of Channel::midiInPlugins and the 
	vector of plugins. */

	for (Plugin* p : plugins)
	{
		for (const MidiLearnParam& param : p->midiInParams)
		{
			if (pure != param.getValue())
				continue;
			c::events::setPluginParameter(p->id, param.getIndex(), vf, /*gui=*/false);
			u::log::print("  >>> [pluginId=%d paramIndex=%d] (pure=0x%X, value=%d, float=%f)\n",
			    p->id, param.getIndex(), pure, midiEvent.getVelocity(), vf);
		}
	}
}

#endif

/* -------------------------------------------------------------------------- */

void MidiDispatcher::processChannels(const MidiEvent& midiEvent)
{
	uint32_t pure = midiEvent.getRawNoVelocity();

	for (const Channel& c : m_model.get().channels)
	{

		/* Do nothing on this channel if MIDI in is disabled or filtered out for
		the current MIDI channel. */
		if (!c.midiLearner.isAllowed(midiEvent.getChannel()))
			continue;

		if (pure == c.midiLearner.keyPress.getValue())
		{
			u::log::print("  >>> keyPress, ch=%d (pure=0x%X)\n", c.id, pure);
			c::events::pressChannel(c.id, midiEvent.getVelocity(), Thread::MIDI);
		}
		else if (pure == c.midiLearner.keyRelease.getValue())
		{
			u::log::print("  >>> keyRel ch=%d (pure=0x%X)\n", c.id, pure);
			c::events::releaseChannel(c.id, Thread::MIDI);
		}
		else if (pure == c.midiLearner.mute.getValue())
		{
			u::log::print("  >>> mute ch=%d (pure=0x%X)\n", c.id, pure);
			c::events::toggleMuteChannel(c.id, Thread::MIDI);
		}
		else if (pure == c.midiLearner.kill.getValue())
		{
			u::log::print("  >>> kill ch=%d (pure=0x%X)\n", c.id, pure);
			c::events::killChannel(c.id, Thread::MIDI);
		}
		else if (pure == c.midiLearner.arm.getValue())
		{
			u::log::print("  >>> arm ch=%d (pure=0x%X)\n", c.id, pure);
			c::events::toggleArmChannel(c.id, Thread::MIDI);
		}
		else if (pure == c.midiLearner.solo.getValue())
		{
			u::log::print("  >>> solo ch=%d (pure=0x%X)\n", c.id, pure);
			c::events::toggleSoloChannel(c.id, Thread::MIDI);
		}
		else if (pure == c.midiLearner.volume.getValue())
		{
			float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_VOLUME);
			u::log::print("  >>> volume ch=%d (pure=0x%X, value=%d, float=%f)\n",
			    c.id, pure, midiEvent.getVelocity(), vf);
			c::events::setChannelVolume(c.id, vf, Thread::MIDI);
		}
		else if (pure == c.midiLearner.pitch.getValue())
		{
			float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_PITCH);
			u::log::print("  >>> pitch ch=%d (pure=0x%X, value=%d, float=%f)\n",
			    c.id, pure, midiEvent.getVelocity(), vf);
			c::events::setChannelPitch(c.id, vf, Thread::MIDI);
		}
		else if (pure == c.midiLearner.readActions.getValue())
		{
			u::log::print("  >>> toggle read actions ch=%d (pure=0x%X)\n", c.id, pure);
			c::events::toggleReadActionsChannel(c.id, Thread::MIDI);
		}

#ifdef WITH_VST
		/* Process learned plugins parameters. */
		processPlugins(c.plugins, midiEvent);
#endif

		/* Redirect raw MIDI message (pure + velocity) to plug-ins in armed
		channels. */
		if (c.armed)
			c::events::sendMidiToChannel(c.id, midiEvent, Thread::MIDI);
	}
}

/* -------------------------------------------------------------------------- */

void MidiDispatcher::processMaster(const MidiEvent& midiEvent)
{
	const uint32_t       pure   = midiEvent.getRawNoVelocity();
	const model::MidiIn& midiIn = m_model.get().midiIn;

	if (pure == midiIn.rewind)
	{
		c::events::rewindSequencer(Thread::MIDI);
		u::log::print("  >>> rewind (master) (pure=0x%X)\n", pure);
	}
	else if (pure == midiIn.startStop)
	{
		c::events::toggleSequencer(Thread::MIDI);
		u::log::print("  >>> startStop (master) (pure=0x%X)\n", pure);
	}
	else if (pure == midiIn.actionRec)
	{
		c::events::toggleActionRecording();
		u::log::print("  >>> actionRec (master) (pure=0x%X)\n", pure);
	}
	else if (pure == midiIn.inputRec)
	{
		c::events::toggleInputRecording();
		u::log::print("  >>> inputRec (master) (pure=0x%X)\n", pure);
	}
	else if (pure == midiIn.metronome)
	{
		c::events::toggleMetronome();
		u::log::print("  >>> metronome (master) (pure=0x%X)\n", pure);
	}
	else if (pure == midiIn.volumeIn)
	{
		float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_VOLUME);
		c::events::setMasterInVolume(vf, Thread::MIDI);
		u::log::print("  >>> input volume (master) (pure=0x%X, value=%d, float=%f)\n",
		    pure, midiEvent.getVelocity(), vf);
	}
	else if (pure == midiIn.volumeOut)
	{
		float vf = u::math::map(midiEvent.getVelocity(), G_MAX_VELOCITY, G_MAX_VOLUME);
		c::events::setMasterOutVolume(vf, Thread::MIDI);
		u::log::print("  >>> output volume (master) (pure=0x%X, value=%d, float=%f)\n",
		    pure, midiEvent.getVelocity(), vf);
	}
	else if (pure == midiIn.beatDouble)
	{
		c::events::multiplyBeats();
		u::log::print("  >>> sequencer x2 (master) (pure=0x%X)\n", pure);
	}
	else if (pure == midiIn.beatHalf)
	{
		c::events::divideBeats();
		u::log::print("  >>> sequencer /2 (master) (pure=0x%X)\n", pure);
	}
}

/* -------------------------------------------------------------------------- */

void MidiDispatcher::learnChannel(MidiEvent e, int param, ID channelId, std::function<void()> doneCb)
{
	if (!isChannelMidiInAllowed(channelId, e.getChannel()))
		return;

	uint32_t raw = e.getRawNoVelocity();

	Channel& ch = m_model.get().getChannel(channelId);

	switch (param)
	{
	case G_MIDI_IN_KEYPRESS:
		ch.midiLearner.keyPress.setValue(raw);
		break;
	case G_MIDI_IN_KEYREL:
		ch.midiLearner.keyRelease.setValue(raw);
		break;
	case G_MIDI_IN_KILL:
		ch.midiLearner.kill.setValue(raw);
		break;
	case G_MIDI_IN_ARM:
		ch.midiLearner.arm.setValue(raw);
		break;
	case G_MIDI_IN_MUTE:
		ch.midiLearner.mute.setValue(raw);
		break;
	case G_MIDI_IN_SOLO:
		ch.midiLearner.solo.setValue(raw);
		break;
	case G_MIDI_IN_VOLUME:
		ch.midiLearner.volume.setValue(raw);
		break;
	case G_MIDI_IN_PITCH:
		ch.midiLearner.pitch.setValue(raw);
		break;
	case G_MIDI_IN_READ_ACTIONS:
		ch.midiLearner.readActions.setValue(raw);
		break;
	case G_MIDI_OUT_L_PLAYING:
		ch.midiLighter.playing.setValue(raw);
		break;
	case G_MIDI_OUT_L_MUTE:
		ch.midiLighter.mute.setValue(raw);
		break;
	case G_MIDI_OUT_L_SOLO:
		ch.midiLighter.solo.setValue(raw);
		break;
	}

	m_model.swap(model::SwapType::SOFT);

	stopLearn();
	doneCb();
}

/* -------------------------------------------------------------------------- */

void MidiDispatcher::learnMaster(MidiEvent e, int param, std::function<void()> doneCb)
{
	if (!isMasterMidiInAllowed(e.getChannel()))
		return;

	uint32_t raw = e.getRawNoVelocity();

	switch (param)
	{
	case G_MIDI_IN_REWIND:
		m_model.get().midiIn.rewind = raw;
		break;
	case G_MIDI_IN_START_STOP:
		m_model.get().midiIn.startStop = raw;
		break;
	case G_MIDI_IN_ACTION_REC:
		m_model.get().midiIn.actionRec = raw;
		break;
	case G_MIDI_IN_INPUT_REC:
		m_model.get().midiIn.inputRec = raw;
		break;
	case G_MIDI_IN_METRONOME:
		m_model.get().midiIn.metronome = raw;
		break;
	case G_MIDI_IN_VOLUME_IN:
		m_model.get().midiIn.volumeIn = raw;
		break;
	case G_MIDI_IN_VOLUME_OUT:
		m_model.get().midiIn.volumeOut = raw;
		break;
	case G_MIDI_IN_BEAT_DOUBLE:
		m_model.get().midiIn.beatDouble = raw;
		break;
	case G_MIDI_IN_BEAT_HALF:
		m_model.get().midiIn.beatHalf = raw;
		break;
	}

	m_model.swap(model::SwapType::SOFT);

	stopLearn();
	doneCb();
}

/* -------------------------------------------------------------------------- */

#ifdef WITH_VST

void MidiDispatcher::learnPlugin(MidiEvent e, std::size_t paramIndex, ID pluginId, std::function<void()> doneCb)
{
	model::DataLock lock   = m_model.lockData(model::SwapType::NONE);
	Plugin*         plugin = m_model.findShared<Plugin>(pluginId);

	assert(plugin != nullptr);
	assert(paramIndex < plugin->midiInParams.size());

	plugin->midiInParams[paramIndex].setValue(e.getRawNoVelocity());

	stopLearn();
	doneCb();
}

#endif
} // namespace giada::m
