/*
 * Copyright (C) 2022 Robin Gareus <robin@gareus.org>
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <regex>

#include "pbd/debug.h"
#include "pbd/i18n.h"

#include "ardour/async_midi_port.h"
#include "ardour/audioengine.h"
#include "ardour/bundle.h"
#include "ardour/debug.h"
#include "ardour/midiport_manager.h"
#include "ardour/midi_port.h"
#include "ardour/session.h"

#include "midi_surface.h"

using namespace ARDOUR;
using namespace Glib;
using namespace PBD;

#include "pbd/abstract_ui.cc" // instantiate template

MIDISurface::MIDISurface (ARDOUR::Session& s, std::string const & namestr, std::string const & port_prefix, bool use_pad_filter)
	: ControlProtocol (s, namestr)
	, AbstractUI<MidiSurfaceRequest> (namestr)
	, with_pad_filter (use_pad_filter)
	, _in_use (false)
	, port_name_prefix (port_prefix)
	, _connection_state (ConnectionState (0))
{
}

MIDISurface::~MIDISurface ()
{
	/* leave it all up to derived classes, because ordering it hard.  */
}

void
MIDISurface::port_setup ()
{
	ports_acquire ();

	if (!input_port_name().empty() || !output_port_name().empty()) {
		ARDOUR::AudioEngine::instance()->PortRegisteredOrUnregistered.connect (port_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::port_registration_handler, this), this);
	}
	ARDOUR::AudioEngine::instance()->PortConnectedOrDisconnected.connect (port_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::connection_handler, this, _1, _2, _3, _4, _5), this);

	port_registration_handler ();
}

void
MIDISurface::drop ()
{
	/* do this before stopping the event loop, so that we don't get any notifications */
	port_connections.drop_connections ();
	stop_using_device ();
	device_release ();
	ports_release ();
}

int
MIDISurface::ports_acquire ()
{
	DEBUG_TRACE (DEBUG::MIDISurface, "acquiring ports\n");

	/* setup ports */

	_async_in  = AudioEngine::instance()->register_input_port (DataType::MIDI, string_compose (X_("%1 in"), port_name_prefix), true);
	_async_out = AudioEngine::instance()->register_output_port (DataType::MIDI, string_compose (X_("%1 out"), port_name_prefix), true);

	if (_async_in == 0 || _async_out == 0) {
		DEBUG_TRACE (DEBUG::MIDISurface, "cannot register ports\n");
		return -1;
	}

	/* We do not add our ports to the input/output bundles because we don't
	 * want users wiring them by hand. They could use JACK tools if they
	 * really insist on that (and use JACK)
	 */

	_input_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_async_in).get();
	_output_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_async_out).get();

	/* Create a shadow port where, depending on the state of the surface,
	 * we will make pad note on/off events appear. The surface code will
	 * automatically this port to the first selected MIDI track.
	 */

	if (with_pad_filter) {
		std::dynamic_pointer_cast<AsyncMIDIPort>(_async_in)->add_shadow_port (string_compose (_("%1 Pads"), port_name_prefix), boost::bind (&MIDISurface::pad_filter, this, _1, _2));
		std::shared_ptr<MidiPort> shadow_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_async_in)->shadow_port();

		if (shadow_port) {

			_output_bundle.reset (new ARDOUR::Bundle (port_name_prefix, false));

			_output_bundle->add_channel (
				shadow_port->name(),
				ARDOUR::DataType::MIDI,
				session->engine().make_port_name_non_relative (shadow_port->name())
				);
		}
	}

	session->BundleAddedOrRemoved ();

	connect_to_parser ();

	/* Connect input port to event loop */

	AsyncMIDIPort* asp;

	asp = dynamic_cast<AsyncMIDIPort*> (_input_port);
	asp->xthread().set_receive_handler (sigc::bind (sigc::mem_fun (this, &MIDISurface::midi_input_handler), _input_port));
	asp->xthread().attach (main_loop()->get_context());

	return 0;
}

void
MIDISurface::ports_release ()
{
	DEBUG_TRACE (DEBUG::MIDISurface, "releasing ports\n");

	/* wait for button data to be flushed */
	AsyncMIDIPort* asp;
	asp = dynamic_cast<AsyncMIDIPort*> (_output_port);
	asp->drain (10000, 500000);

	{
		Glib::Threads::Mutex::Lock em (AudioEngine::instance()->process_lock());
		AudioEngine::instance()->unregister_port (_async_in);
		AudioEngine::instance()->unregister_port (_async_out);
	}

	_async_in.reset ((ARDOUR::Port*) 0);
	_async_out.reset ((ARDOUR::Port*) 0);
	_input_port = 0;
	_output_port = 0;
}

void
MIDISurface::port_registration_handler ()
{
	if (!_async_in || !_async_out) {
		/* ports not registered yet */
		return;
	}

	if (_async_in->connected() && _async_out->connected()) {
		/* don't waste cycles here */
		return;
	}

	std::vector<std::string> midi_inputs;
	std::vector<std::string> midi_outputs;

	AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsPhysical|IsOutput), midi_inputs);
	AudioEngine::instance()->get_ports ("", DataType::MIDI, PortFlags (IsPhysical|IsInput), midi_outputs);

	if (midi_inputs.empty() || midi_outputs.empty()) {
		return;
	}

        /* Try to find the input & output ports, whose pretty name varies on
         * Linux depending on the version of ALSA, but is fairly consistent
         * across newer ALSA and other platforms.
         */

	/* See if the input port is available, and maybe connect that */

	string ip = input_port_name ();

	if (ip[0] == ':') {
		std::regex rx (ip.substr (1), std::regex::extended);

		auto is_the_input = [&rx](string const &s) {
			std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s);
			return std::regex_search (pn, rx);
		};

		auto pi = std::find_if (midi_inputs.begin(), midi_inputs.end(), is_the_input);
		if (pi != midi_inputs.end()) {
			AudioEngine::instance()->connect (_async_in->name(), *pi);
		}
	} else {
		/* regular partial string search */
		auto is_the_input = [&ip](string const &s) {
			std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s);
			return pn.find (ip) != string::npos;
		};

		auto pi = std::find_if (midi_inputs.begin(), midi_inputs.end(), is_the_input);
		if (pi != midi_inputs.end()) {
			AudioEngine::instance()->connect (_async_in->name(), *pi);
		}
	}

	/* Now see if the output port is available, and maybe connect that */

	string op = output_port_name ();

	if (op[0] == ':') {
		std::regex rx (op.substr (1), std::regex::extended);

		auto is_the_output = [&rx](string const &s) {
			std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s);
			return std::regex_search (pn, rx);
		};

		auto po = std::find_if (midi_outputs.begin(), midi_outputs.end(), is_the_output);
		if (po != midi_outputs.end()) {
				AudioEngine::instance()->connect (_async_in->name(), *po);
		}
	} else {
		/* regular partial string search */
		auto is_the_output = [&op](string const &s) {
			std::string pn = AudioEngine::instance()->get_hardware_port_name_by_name(s);
			return pn.find (op) != string::npos;
		};

		auto po = std::find_if (midi_outputs.begin(), midi_outputs.end(), is_the_output);
		if (po != midi_outputs.end()) {
			AudioEngine::instance()->connect (_async_in->name(), *po);
		}

	}
}

bool
MIDISurface::connection_handler (std::weak_ptr<ARDOUR::Port>, std::string name1, std::weak_ptr<ARDOUR::Port>, std::string name2, bool yn)
{
	DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("MIDISurface::connection_handler start, %1 %2 %3\n", name1, (yn ? "connected" : "disconnected"), name2));

	if (!_input_port || !_output_port) {
		return false;
	}

	std::string ni = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (std::shared_ptr<ARDOUR::Port>(_async_in)->name());
	std::string no = ARDOUR::AudioEngine::instance()->make_port_name_non_relative (std::shared_ptr<ARDOUR::Port>(_async_out)->name());

	if (ni == name1 || ni == name2) {
		if (yn) {
			_connection_state |= InputConnected;
		} else {
			_connection_state &= ~InputConnected;
		}
	} else if (no == name1 || no == name2) {
		if (yn) {
			_connection_state |= OutputConnected;
		} else {
			_connection_state &= ~OutputConnected;
		}
	} else {
		DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("Connections between %1 and %2 changed, but I ignored it\n", name1, name2));
		/* not our ports */
		return false;
	}

	DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("our ports changed connection state: %1 -> %2 connected ? %3, connection state now %4\n",
	                                                 name1, name2, yn, _connection_state));

	if ((_connection_state & (InputConnected|OutputConnected)) == (InputConnected|OutputConnected)) {

		/* XXX this is a horrible hack. Without a short sleep here,
		   something prevents the device wakeup messages from being
		   sent and/or the responses from being received.
		*/

                DEBUG_TRACE (DEBUG::MIDISurface, "device now connected for both input and output\n");
		g_usleep (100000);

                /* may not have the device open if it was just plugged
                   in. Really need USB device detection rather than MIDI port
                   detection for this to work well.
                */

                device_acquire ();
                begin_using_device ();

	} else {
		DEBUG_TRACE (DEBUG::MIDISurface, "Device disconnected (input or output or both) or not yet fully connected\n");
		stop_using_device ();
	}

	ConnectionChange (); /* emit signal for our GUI */

	DEBUG_TRACE (DEBUG::MIDISurface, "connection_handler  end\n");

	return true; /* connection status changed */
}

std::shared_ptr<Port>
MIDISurface::output_port()
{
	return _async_out;
}

std::shared_ptr<Port>
MIDISurface::input_port()
{
	return _async_in;
}

void
MIDISurface::write (const MidiByteArray& data)
{
	/* immediate delivery */
	_output_port->write (&data[0], data.size(), 0);
}

void
MIDISurface::write (MIDI::byte const * data, size_t size)
{
	_output_port->write (data, size, 0);
}

bool
MIDISurface::midi_input_handler (IOCondition ioc, MIDI::Port* port)
{
	if (ioc & ~IO_IN) {
		DEBUG_TRACE (DEBUG::MIDISurface, "MIDI port closed\n");
		return false;
	}

	if (ioc & IO_IN) {

		DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("something happened on  %1\n", port->name()));

		AsyncMIDIPort* asp = dynamic_cast<AsyncMIDIPort*>(port);
		if (asp) {
			asp->clear ();
		}

		DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("data available on %1\n", port->name()));
		if (_in_use) {
			samplepos_t now = AudioEngine::instance()->sample_time();
			port->parse (now);
		}
	}

	return true;
}

void
MIDISurface::connect_to_parser ()
{
	connect_to_port_parser (*_input_port);
}

void
MIDISurface::connect_to_port_parser (MIDI::Port& port)
{
	MIDI::Parser* p = port.parser();

	DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("Connecting to signals on port %1 using parser %2\n", port.name(), p));

	/* Incoming sysex */
	p->sysex.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_sysex, this, _1, _2, _3));
	/* V-Pot messages are Controller */
	p->controller.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_controller_message, this, _1, _2));
	/* Button messages are NoteOn */
	p->note_on.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_note_on_message, this, _1, _2));
	/* Button messages are NoteOn but libmidi++ sends note-on w/velocity = 0 as note-off so catch them too */
	p->note_off.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_note_on_message, this, _1, _2));
	/* Fader messages are Pitchbend */
	p->channel_pitchbend[0].connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_pitchbend_message, this, _1, _2));

	p->poly_pressure.connect_same_thread (*this, boost::bind (&MIDISurface::handle_midi_polypressure_message, this, _1, _2));
}

void
MIDISurface::thread_init ()
{
	pthread_set_name (event_loop_name().c_str());

	PBD::notify_event_loops_about_thread_creation (pthread_self(), event_loop_name(), 2048);
	ARDOUR::SessionEvent::create_per_thread_pool (event_loop_name(), 128);

	set_thread_priority ();
}

void
MIDISurface::connect_session_signals()
{
	// receive routes added
	//session->RouteAdded.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MackieControlProtocol::notify_routes_added, this, _1), this);
	// receive VCAs added
	//session->vca_manager().VCAAdded.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_vca_added, this, _1), this);

	// receive record state toggled
	session->RecordStateChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_record_state_changed, this), this);
	// receive transport state changed
	session->TransportStateChange.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_transport_state_changed, this), this);
	session->TransportLooped.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_loop_state_changed, this), this);
	// receive punch-in and punch-out
	Config->ParameterChanged.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_parameter_changed, this, _1), this);
	session->config.ParameterChanged.connect (session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_parameter_changed, this, _1), this);
	// receive rude solo changed
	session->SoloActive.connect(session_connections, MISSING_INVALIDATOR, boost::bind (&MIDISurface::notify_solo_active_changed, this, _1), this);
}

XMLNode&
MIDISurface::get_state() const
{
	XMLNode& node (ControlProtocol::get_state());
	XMLNode* child;

	child = new XMLNode (X_("Input"));
	child->add_child_nocopy (_async_in->get_state());
	node.add_child_nocopy (*child);
	child = new XMLNode (X_("Output"));
	child->add_child_nocopy (_async_out->get_state());
	node.add_child_nocopy (*child);

	return node;
}

int
MIDISurface::set_state (const XMLNode & node, int version)
{
	DEBUG_TRACE (DEBUG::MIDISurface, string_compose ("MIDISurface::set_state: active %1\n", active()));

	if (ControlProtocol::set_state (node, version)) {
		return -1;
	}

	XMLNode* child;

	if ((child = node.child (X_("Input"))) != 0) {
		XMLNode* portnode = child->child (Port::state_node_name.c_str());
		if (portnode) {
			portnode->remove_property ("name");
			_async_in->set_state (*portnode, version);
		}
	}

	if ((child = node.child (X_("Output"))) != 0) {
		XMLNode* portnode = child->child (Port::state_node_name.c_str());
		if (portnode) {
			portnode->remove_property ("name");
			_async_out->set_state (*portnode, version);
		}
	}

	return 0;
}

void
MIDISurface::do_request (MidiSurfaceRequest * req)
{
	if (req->type == CallSlot) {

		call_slot (PBD::EventLoop::__invalidator (*this, __FILE__, __LINE__), req->the_slot);

	} else if (req->type == Quit) {

		stop_using_device ();
	}
}

int
MIDISurface::begin_using_device ()
{
	_in_use = true;
	connect_session_signals ();
	return 0;
}

int
MIDISurface::stop_using_device ()
{
	session_connections.drop_connections ();
	_in_use = false;
	return 0;
}

std::list<std::shared_ptr<ARDOUR::Bundle> >
MIDISurface::bundles ()
{
	std::list<std::shared_ptr<ARDOUR::Bundle> > b;

	if (_output_bundle) {
		b.push_back (_output_bundle);
	}

	return b;
}
