/*
 * Copyright (C) 2016-2018 Paul Davis <paul@linuxaudiosystems.com>
 * Copyright (C) 2017-2018 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 <algorithm>
#include <bitset>
#include <cmath>
#include <limits>
#include <regex>

#include <stdlib.h>
#include <pthread.h>

#include "pbd/compose.h"
#include "pbd/convert.h"
#include "pbd/debug.h"
#include "pbd/failed_constructor.h"
#include "pbd/file_utils.h"
#include "pbd/search_path.h"
#include "pbd/enumwriter.h"

#include "midi++/parser.h"

#include "temporal/time.h"
#include "temporal/bbt_time.h"

#include "ardour/amp.h"
#include "ardour/async_midi_port.h"
#include "ardour/audioengine.h"
#include "ardour/dB.h"
#include "ardour/debug.h"
#include "ardour/internal_send.h"
#include "ardour/midiport_manager.h"
#include "ardour/midi_track.h"
#include "ardour/midi_port.h"
#include "ardour/plugin.h"
#include "ardour/plugin_insert.h"
#include "ardour/selection.h"
#include "ardour/session.h"
#include "ardour/tempo.h"
#include "ardour/triggerbox.h"
#include "ardour/types_convert.h"
#include "ardour/utils.h"

#include "gtkmm2ext/gui_thread.h"
#include "gtkmm2ext/rgb_macros.h"

#include "gtkmm2ext/colors.h"

#include "gui.h"
#include "launchkey_4.h"

#include "pbd/i18n.h"

#ifdef PLATFORM_WINDOWS
#define random() rand()
#endif

using namespace ARDOUR;
using namespace PBD;
using namespace Glib;
using namespace ArdourSurface;
using namespace ArdourSurface::LAUNCHPAD_NAMESPACE;
using namespace Gtkmm2ext;

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

/* USB IDs */

#define NOVATION             0x1235

#define LAUNCHKEY4_MINI_25   0x0141
#define LAUNCHKEY4_MINI_37   0x0142
#define LAUNCHKEY4_25        0x0143
#define LAUNCHKEY4_37        0x0144
#define LAUNCHKEY4_49        0x0145
#define LAUNCHKEY4_61        0x0146

static int first_fader = 0x9;
static const int PAD_COLUMNS = 8;
static const int PAD_ROWS = 2;
static const int NFADERS = 9;
static int last_detected = 0x0;

bool
LaunchKey4::available ()
{
	/* no preconditions other than the device being present */
	return true;
}

bool
LaunchKey4::match_usb (uint16_t vendor, uint16_t device)
{
	if (vendor != NOVATION) {
		return false;
	}

	switch (device) {
	case LAUNCHKEY4_MINI_25:
	case LAUNCHKEY4_MINI_37:
	case LAUNCHKEY4_25:
	case LAUNCHKEY4_37:
	case LAUNCHKEY4_49:
	case LAUNCHKEY4_61:
		last_detected = device;
		return true;
	}


	return false;
}

bool
LaunchKey4::probe (std::string& i, std::string& o)
{
	vector<string> midi_inputs;
	vector<string> midi_outputs;

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

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

	std::regex rx (X_("Launchkey (Mini MK4|MK4).*MI"), std::regex::extended);

	auto has_lppro = [&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(), has_lppro);
	auto po = std::find_if (midi_outputs.begin (), midi_outputs.end (), has_lppro);

	if (pi == midi_inputs.end () || po == midi_outputs.end ()) {
		return false;
	}

	i = *pi;
	o = *po;
	return true;
}

LaunchKey4::LaunchKey4 (ARDOUR::Session& s)
#ifdef LAUNCHPAD_MINI
	: MIDISurface (s, X_("Novation Launchkey Mini"), X_("Launchkey Mini"), true)
#else
	: MIDISurface (s, X_("Novation Launchkey 4"), X_("Launchkey MK4"), true)
#endif
	, _daw_out_port (nullptr)
	, _gui (nullptr)
	, scroll_x_offset (0)
	, scroll_y_offset (0)
	, device_pid (0x0)
	, mode_channel (0xf)
	, pad_function (MuteSolo)
	, shift_pressed (false)
	, layer_pressed (false)
	, bank_start (0)
	, button_mode (ButtonsRecEnable) // reset via toggle later
	, encoder_mode (EncoderMixer)
	, num_plugin_controls (0)
{
	run_event_loop ();
	port_setup ();

	std::string  pn_in, pn_out;
	if (probe (pn_in, pn_out)) {
		_async_in->connect (pn_in);
		_async_out->connect (pn_out);
	}

	build_color_map ();
	build_pad_map ();

	Trigger::TriggerPropertyChange.connect (trigger_connections, invalidator (*this), std::bind (&LaunchKey4::trigger_property_change, this, _1, _2), this);
	ControlProtocol::PluginSelected.connect (session_connections, invalidator (*this), std::bind (&LaunchKey4::plugin_selected, this, _1), this);

	session->RecordStateChanged.connect (session_connections, invalidator(*this), std::bind (&LaunchKey4::record_state_changed, this), this);
	session->TransportStateChange.connect (session_connections, invalidator(*this), std::bind (&LaunchKey4::transport_state_changed, this), this);
	session->RouteAdded.connect (session_connections, invalidator(*this), std::bind (&LaunchKey4::stripables_added, this), this);
	session->SoloChanged.connect (session_connections, invalidator(*this), std::bind (&LaunchKey4::solo_changed, this), this);
}

LaunchKey4::~LaunchKey4 ()
{
	DEBUG_TRACE (DEBUG::Launchkey, "launchkey control surface object being destroyed\n");

	trigger_connections.drop_connections ();
	route_connections.drop_connections ();
	session_connections.drop_connections ();

	for (size_t n = 0; n < sizeof (pads) / sizeof (pads[0]); ++n) {
		pads[n].timeout_connection.disconnect ();
	}

	stop_event_loop ();
	tear_down_gui ();

	MIDISurface::drop ();

}

void
LaunchKey4::transport_state_changed ()
{
	MIDI::byte msg[9];

	msg[0] = 0xb0 | mode_channel;
	msg[1] = 0x73;

	msg[3] = 0xb0 | mode_channel;
	msg[4] = Play;

	msg[6] = 0xb0 | mode_channel;
	msg[7] = Stop;

	if (session->transport_rolling()) {
		msg[2] = 0x7f;
		msg[5] = 0x0;
	} else {
		msg[2] = 0x0;
		msg[5] = 0x7f;
	}

	if (session->get_play_loop()) {
		msg[8] = 0x7f;
	} else {
		msg[8] = 0x0;
	}

	daw_write (msg, 9);

	map_rec_enable ();
}

void
LaunchKey4::record_state_changed ()
{
	map_rec_enable();
}

void
LaunchKey4::map_rec_enable ()
{
	if (button_mode != ButtonsRecEnable) {
		return;
	}

	MIDI::byte msg[3];
	int channel = session->actively_recording() ? 0x0 : 0x2;
	const int rec_color_index = 0x5; /* bright red */
	const int norec_color_index = 0x0;

	/* The global rec-enable button */

	msg[0] = 0xb0 | channel;
	msg[1] = 0x75;
	msg[2] = session->get_record_enabled() ? rec_color_index : norec_color_index;

	daw_write (msg, 3);

	/* Now all the tracks */

	for (int i = 0; i < NFADERS-1; ++i) {
		show_rec_enable (i);
	}
}

void
LaunchKey4::show_rec_enable (int n)
{
	LightingMode mode = session->actively_recording() ? Solid : Pulse;
	const int rec_color_index = 0x5; /* bright red */
	const int norec_color_index = 0x0;

	if (stripable[n]) {
		std::shared_ptr<AutomationControl> ac = stripable[n]->rec_enable_control();
		if (ac) {
			light_button (Button1 + n, mode, ac->get_value() ? rec_color_index : norec_color_index);
		} else {
			light_button (Button1 + n, Solid, 0x0);
		}
	} else {
		light_button (Button1 + n, Solid, 0x0);
	}
}

int
LaunchKey4::set_active (bool yn)
{
	DEBUG_TRACE (DEBUG::Launchkey, string_compose("Launchpad X::set_active init with yn: %1\n", yn));

	if (yn == active()) {
		return 0;
	}

	if (yn) {

		if (device_acquire ()) {
			return -1;
		}

	} else {
		/* Control Protocol Manager never calls us with false, but
		 * insteads destroys us.
		 */
	}

	ControlProtocol::set_active (yn);

	DEBUG_TRACE (DEBUG::Launchkey, string_compose("Launchpad X::set_active done with yn: '%1'\n", yn));

	return 0;
}

void
LaunchKey4::run_event_loop ()
{
	DEBUG_TRACE (DEBUG::Launchkey, "start event loop\n");
	BaseUI::run ();
}

void
LaunchKey4::stop_event_loop ()
{
	DEBUG_TRACE (DEBUG::Launchkey, "stop event loop\n");
	BaseUI::quit ();
}

int
LaunchKey4::begin_using_device ()
{
	DEBUG_TRACE (DEBUG::Launchkey, "begin using device\n");

	/* get device model */

	_data_required = true;
	MidiByteArray device_inquiry (6, 0xf0, 0x7e, 0x7f, 0x06, 0x01, 0xf7);
	write (device_inquiry);

	return 0;
}

void
LaunchKey4::finish_begin_using_device ()
{
	DEBUG_TRACE (DEBUG::Launchkey, "finish begin using device\n");

	_data_required = false;

	if (MIDISurface::begin_using_device ()) {
		return;
	}

	connect_daw_ports ();

	/* enter DAW mode */

	set_daw_mode (true);
	set_pad_function (MuteSolo);

	/* catch current selection, if any so that we can wire up the pads if appropriate */
	stripable_selection_changed ();
	switch_bank (0);
	toggle_button_mode ();
	use_encoders (true);
	set_encoder_bank (0);

	/* Set configuration for fader displays, which is never altered */

	MIDI::byte display_config[10];

	display_config[0] = 0xf0;
	display_config[1] = 0x0;
	display_config[2] = 0x20;
	display_config[3] = 0x29;
	display_config[4] = (device_pid>>8) & 0x7f;
	display_config[5] = device_pid & 0x7f;
	display_config[6] = 0x4;

	display_config[8] = 0x61;
	display_config[9] = 0xf7;

	for (int fader = 0; fader < 9; ++fader) {
		/* 2 line display for all faders */
		display_config[7] = 0x5 + fader;
		daw_write (display_config, 10);
	}
	std::cerr << "Configuring displays now\n";
	configure_display (StationaryDisplay, 0x1);
	set_display_target (StationaryDisplay, 0, "ardour", true);
	set_display_target (StationaryDisplay, 1, string(), true);

	configure_display (DAWPadFunctionDisplay, 0x1);

	/* In this DAW, mixer mode controls pan */
	set_display_target (MixerPotMode, 1, "Level", false);
}

void
LaunchKey4::set_daw_mode (bool yn)
{
	MidiByteArray msg;

	msg.push_back (0x9f);
	msg.push_back (0xc);
	msg.push_back (yn ? 0x7f : 0x0);
	daw_write (msg);

	if (yn) {
		mode_channel = 0x0;
	} else {
		mode_channel = 0xf;
	}

	if (yn) {
		all_pads_out ();
	}
}

void
LaunchKey4::all_pads (int color_index)
{
	MIDI::byte msg[3];

	msg[0] = 0x90;
	msg[2] = color_index;

	/* top row */
	for (int i = 0; i < 8; ++i) {
		msg[1] = 0x60 + i;
		daw_write (msg, 3);
	}
	for (int i = 0; i < 8; ++i) {
		msg[1] = 0x70 + i;
		daw_write (msg, 3);
	}
}

void
LaunchKey4::all_pads_out ()
{
	all_pads (0x0);
}

int
LaunchKey4::stop_using_device ()
{
	DEBUG_TRACE (DEBUG::Launchkey, "stop using device\n");

	if (!_in_use) {
		DEBUG_TRACE (DEBUG::Launchkey, "nothing to do, device not in use\n");
		return 0;
	}

	set_daw_mode (false);

	return MIDISurface::stop_using_device ();
}

XMLNode&
LaunchKey4::get_state() const
{
	XMLNode& node (MIDISurface::get_state());

	XMLNode* child = new XMLNode (X_("DAWInput"));
	child->add_child_nocopy (_daw_in->get_state());
	node.add_child_nocopy (*child);
	child = new XMLNode (X_("DAWOutput"));
	child->add_child_nocopy (_daw_out->get_state());
	node.add_child_nocopy (*child);

	return node;
}

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

	int retval = 0;

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

	return retval;
}

std::string
LaunchKey4::input_port_name () const
{
	switch (last_detected) {
	case LAUNCHKEY4_MINI_25:
	case LAUNCHKEY4_MINI_37:
		return X_(":Launchpad Mini MK3.*MIDI (In|2)");
	default:
		break;
	}
	return X_(":Launchpad X MK3.*MIDI (In|2)");
}

std::string
LaunchKey4::output_port_name () const
{
	switch (last_detected) {
	case LAUNCHKEY4_MINI_25:
	case LAUNCHKEY4_MINI_37:
		return X_(":Launchpad Mini MK3.*MIDI (Out|2)");
	default:
		break;
	}

	return X_(":Launchpad X MK3.*MIDI (Out|2)");
}

void
LaunchKey4::relax (Pad & pad)
{
}

void
LaunchKey4::relax (Pad & pad, int)
{
}

void
LaunchKey4::build_pad_map ()
{
	for (int n = 0; n < 8; ++n) {
		int pid = 0x60 + n;
		pads[n] = Pad (pid, n, 0);
	}
	for (int n = 0; n < 8; ++n) {
		int pid = 0x70 + n;
		pads[8+n] = Pad (pid, n, 1);
	}
}

void
LaunchKey4::use_encoders (bool onoff)
{
	MIDI::byte msg[3];
	msg[0] = 0xb6;
	msg[1] = 0x45;
	msg[2] = (onoff ? 0x7f : 0x0);
	daw_write (msg, 3);

	if (!onoff) {
		return;
	}

	MIDI::byte display_config[10];

	display_config[0] = 0xf0;
	display_config[1] = 0x0;
	display_config[2] = 0x20;
	display_config[3] = 0x29;
	display_config[4] = (device_pid>>8) & 0x7f;
	display_config[5] = device_pid & 0x7f;
	display_config[6] = 0x4;

	display_config[8] = 0x62;
	display_config[9] = 0xf7;

	for (int encoder = 0; encoder < 8; ++encoder) {
		/* 2 line display for all encoders */
		display_config[7] = 0x15 + encoder;
		daw_write (display_config, 10);
	}
}

void
LaunchKey4::handle_midi_sysex (MIDI::Parser& parser, MIDI::byte* raw_bytes, size_t sz)
{
#ifndef NDEBUG
	if (DEBUG_ENABLED(DEBUG::Launchkey)) {
		std::stringstream str;
		str << "Sysex received, size " << sz << std::endl;
		str << hex;
		for (size_t n = 0; n < sz; ++n) {
			str << "0x" << (int) raw_bytes[n] << ' ';
		}
		str << std::endl;
		std::cerr << str.str();
	}
#endif

	if (sz != 17) {
		return;
	}

	MIDI::byte dp_lsb;
	MIDI::byte dp_msb;

	if (raw_bytes[1] == 0x7e &&
	    raw_bytes[2] == 0x0 &&
	    raw_bytes[3] == 0x6 &&
	    raw_bytes[4] == 0x2 &&
	    raw_bytes[5] == 0x0 &&
	    raw_bytes[6] == 0x20 &&
	    raw_bytes[7] == 0x29) {
		dp_lsb = raw_bytes[8];
		dp_msb = raw_bytes[9];

		const int family = (dp_msb<<8)|dp_lsb;
		switch (family) {
		case LAUNCHKEY4_MINI_25:
		case LAUNCHKEY4_MINI_37:
			device_pid = 0x0213;
			break;
		case LAUNCHKEY4_25:
		case LAUNCHKEY4_37:
		case LAUNCHKEY4_49:
		case LAUNCHKEY4_61:
			device_pid = 0x0214;
			break;
		default:
			return;
		}

		finish_begin_using_device ();
		return;
	}
}

void
LaunchKey4::handle_midi_controller_message_chnF (MIDI::Parser& parser, MIDI::EventTwoBytes* ev)
{
	if (ev->controller_number < 0x05 || ev->controller_number > 0xd) {
		return;
	}

	int fader_number = ev->controller_number - 0x5;
	fader_move (fader_number, ev->value);
}

void
LaunchKey4::handle_midi_controller_message (MIDI::Parser& parser, MIDI::EventTwoBytes* ev)
{
	/* Remember: fader controller events are delivered via if (ev->controller_::handle_midi_controller_message_chnF() */
	if (&parser != _daw_in_port->parser()) {
		if (ev->controller_number == 0x69 && ev->value == 0x7f) {
			DEBUG_TRACE (DEBUG::Launchkey, string_compose ("function button press on non-DAW port, CC %1 (value %2)\n", (int) ev->controller_number, (int) ev->value));
			function_press ();
			return;
		}
		/* we don't process CC messages from the regular port */
		DEBUG_TRACE (DEBUG::Launchkey, string_compose ("skip non-DAW CC %1 (value %2)\n", (int) ev->controller_number, (int) ev->value));
		return;
	}

#ifndef NDEBUG
	std::stringstream ss;
	ss << "CC 0x" << std::hex << (int) ev->controller_number << std::dec << " value (" << (int) ev->value << ")\n";
	DEBUG_TRACE (DEBUG::Launchkey, ss.str());
#endif

	/* Shift being pressed can change everything */

	if (ev->controller_number == 0x48) {
		if (ev->value) {
			shift_pressed = true;
		} else {
			shift_pressed = false;
		}
		return;
	}

	/* Scene launch */
	if (ev->controller_number == 0x68) {
		if (ev->value) {
			scene_press ();
		}
		return;
	}

	/* Button 9 (below fader 9 */

	if (ev->controller_number == Button9) {
		/* toggle on press only */

		if (ev->value) {
			toggle_button_mode ();
		}
		return;
	}

	/* Encoder Mode button */

	if (ev->controller_number == 0x41) {
		switch (ev->value) {
		case 2:
			set_encoder_mode (EncoderPlugins);
			break;
		case 1:
			set_encoder_mode (EncoderMixer);
			break;
		case 4:
			set_encoder_mode (EncoderSendA);
			break;
		case 5:
			set_encoder_mode (EncoderTransport);
			break;
		default:
			break;
		}
		return;
	}

	/* Encoder Bank Buttons */

	if (ev->controller_number == 0x33) {
		/* up'; use press only  */
		if (ev->value && encoder_bank >  0) {
			set_encoder_bank (encoder_bank - 1);
		}
		return;
	}

	if (ev->controller_number == 0x34) {
		/* down; use press only */
		if (ev->value && encoder_bank < 2) {
			set_encoder_bank (encoder_bank + 1);
		}
		return;
	}

	switch (ev->controller_number) {
	case 0x6a:
		if (ev->value) button_up ();
		return;
	case 0x6b:
		if (ev->value) button_down ();
		return;
	case 0x67:
		if (ev->value) button_left ();
		return;
	case 0x66:
		if (ev->value) button_right ();
		return;
	}

	/* Buttons below faders */

	if (ev->controller_number >= Button1 && ev->controller_number <= Button8) {

		if (ev->value == 0x7f) {
			button_press (ev->controller_number - Button1);
		} else {
			button_release (ev->controller_number - Button1);
		}

		return;

	} else  if (ev->controller_number >= Knob1 && ev->controller_number <= Knob8) {

		encoder (ev->controller_number - Knob1, ev->value - 64);
		return;

	} else  if (ev->controller_number >= 0x55 && ev->controller_number <= 0x5c) {

		encoder (ev->controller_number - Knob1, ev->value - 64);
		return;
	}

	if (ev->value == 0x7f) {
		switch (ev->controller_number) {
		case Function:
			function_press ();
			break;
		case Undo:
			undo_press ();
			break;
		case Play:
			if (device_pid == 0x213) {
				/* Mini version only play button, so toggle */
				if (session->transport_rolling()) {
					transport_stop();
				} else {
					transport_play();
				}
			} else {
				transport_play ();
			}
			break;
		case Stop:
			transport_stop ();
			break;
		case RecEnable:
			set_record_enable (!get_record_enabled());
			break;
		case Loop:
			loop_toggle ();
			break;
		default:
			break;
		}
	}
}

void
LaunchKey4::handle_midi_note_on_message (MIDI::Parser& parser, MIDI::EventTwoBytes* ev)
{
	if (ev->velocity == 0) {
		handle_midi_note_off_message (parser, ev);
		return;
	}

	if (&parser != _daw_in_port->parser()) {
		/* we don't process note messages from the regular port */
		DEBUG_TRACE (DEBUG::Launchkey, string_compose ("skip non-DAW Note On %1/0x%3%4%5 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec));
		return;
	}

	int pad_number;

	switch (ev->note_number) {
	case 0x60:
		pad_number = 0;
		break;
	case 0x61:
		pad_number = 1;
		break;
	case 0x62:
		pad_number = 2;
		break;
	case 0x63:
		pad_number = 3;
		break;
	case 0x64:
		pad_number = 4;
		break;
	case 0x65:
		pad_number = 5;
		break;
	case 0x66:
		pad_number = 6;
		break;
	case 0x67:
		pad_number = 7;
		break;

	case 0x70:
		pad_number = 8;
		break;
	case 0x71:
		pad_number = 9;
		break;
	case 0x72:
		pad_number = 10;
		break;
	case 0x73:
		pad_number = 11;
		break;
	case 0x74:
		pad_number = 12;
		break;
	case 0x75:
		pad_number = 13;
		break;
	case 0x76:
		pad_number = 14;
		break;
	case 0x77:
		pad_number = 15;
		break;
	default:
		return;
	}

	DEBUG_TRACE (DEBUG::Launchkey, string_compose ("Note On %1/0x%3%4%5 (velocity %2) => pad %6\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec, pad_number));

	Pad& pad = pads[pad_number];

	switch (pad_function) {
	case MuteSolo:
		pad_mute_solo (pad);
		break;
	case Triggers:
		pad_trigger (pad, ev->velocity);
		break;
	default:
		break;
	}
}

void
LaunchKey4::handle_midi_note_off_message (MIDI::Parser&, MIDI::EventTwoBytes* ev)
{
	int pad_number;

	switch (ev->note_number) {
	case 0x60:
		pad_number = 0;
		break;
	case 0x61:
		pad_number = 1;
		break;
	case 0x62:
		pad_number = 2;
		break;
	case 0x63:
		pad_number = 3;
		break;
	case 0x64:
		pad_number = 4;
		break;
	case 0x65:
		pad_number = 5;
		break;
	case 0x66:
		pad_number = 6;
		break;
	case 0x67:
		pad_number = 7;
		break;

	case 0x70:
		pad_number = 8;
		break;
	case 0x71:
		pad_number = 9;
		break;
	case 0x72:
		pad_number = 10;
		break;
	case 0x73:
		pad_number = 11;
		break;
	case 0x74:
		pad_number = 12;
		break;
	case 0x75:
		pad_number = 13;
		break;
	case 0x76:
		pad_number = 14;
		break;
	case 0x77:
		pad_number = 15;
		break;
	default:
		return;
	}

	DEBUG_TRACE (DEBUG::Launchkey, string_compose ("Note Off %1/0x%3%4%5 (velocity %2)\n", (int) ev->note_number, (int) ev->velocity, std::hex, (int) ev->note_number, std::dec));
	pad_release (pads[pad_number]);
}

void
LaunchKey4::pad_trigger (Pad& pad, int velocity)
{
	if (shift_pressed) {
		trigger_stop_col (pad.x, true); /* immediate */
	} else {
		TriggerPtr trigger = session->trigger_at (pad.x, pad.y + scroll_y_offset);
		switch (trigger->state()) {
		case Trigger::Stopped:
			trigger->bang (velocity / 127.0f);
			break;
		default:
			break;
		}
		start_press_timeout (pad);
	}
}

void
LaunchKey4::pad_release (Pad& pad)
{
	pad.timeout_connection.disconnect ();
}

void
LaunchKey4::start_press_timeout (Pad& pad)
{
	Glib::RefPtr<Glib::TimeoutSource> timeout = Glib::TimeoutSource::create (250); // milliseconds
	pad.timeout_connection = timeout->connect (sigc::bind (sigc::mem_fun (*this, &LaunchKey4::long_press_timeout), pad.x));
	timeout->attach (main_loop()->get_context());
}

bool
LaunchKey4::long_press_timeout (int col)
{
	std::cerr << "timeout!\n";
	trigger_stop_col (col, false); /* non-immediate */
	return false; /* don't get called again */
}

void
LaunchKey4::trigger_property_change (PropertyChange pc, Trigger* t)
{
	if (pad_function != Triggers) {
		return;
	}

	int x = t->box().order();
	int y = t->index();

	DEBUG_TRACE (DEBUG::Launchpad, string_compose ("prop change %1 for trigger at %2, %3\n", pc, x, y));

	if (y < scroll_y_offset || y > scroll_y_offset + 1) {
		/* not visible at present */
		return;
	}

	if (x < scroll_x_offset || x > scroll_x_offset + 7) {
		/* not visible at present */
		return;
	}

	y -= scroll_y_offset;
	x -= scroll_x_offset;

	/* name property change is sent when slots are loaded or unloaded */

	PropertyChange our_interests;
	our_interests.add (Properties::running);
	our_interests.add (Properties::name);;

	if (pc.contains (our_interests)) {

		Pad& pad (pads[(y*8) + x]);
		std::shared_ptr<Route> r = session->get_remote_nth_route (scroll_x_offset + x);

		trigger_pad_light (pad, r, t);
	}
}

void
LaunchKey4::trigger_pad_light (Pad& pad, std::shared_ptr<Route> r, Trigger* t)
{
	if (!r || !t || !t->playable()) {
		unlight_pad (pad.id);
		return;
	}

	MIDI::byte msg[3];

	msg[0] = 0x90;
	msg[1] = pad.id;

	switch (t->state()) {
	case Trigger::Stopped:
		msg[2] = find_closest_palette_color (r->presentation_info().color());
		break;

	case Trigger::WaitingToStart:
		msg[0] |= 0x2; /* channel 2=> pulsing */
		msg[2] = 0x17; // find_closest_palette_color (r->presentation_info().color()));
		break;

	case Trigger::Running:
		/* choose contrasting color from the base one */
		msg[2] = find_closest_palette_color (HSV(r->presentation_info().color()).opposite());
		break;

	case Trigger::WaitingForRetrigger:
	case Trigger::WaitingToStop:
	case Trigger::WaitingToSwitch:
	case Trigger::Stopping:
		msg[0] |= 0x2; /* pulse */
		msg[2] = find_closest_palette_color (HSV(r->presentation_info().color()).opposite());
	}

	daw_write (msg, 3);
}

void
LaunchKey4::map_triggers ()
{
	for (int x = 0; x < PAD_COLUMNS; ++x) {
		map_triggerbox (x);
	}
}

void
LaunchKey4::map_triggerbox (int x)
{
	std::shared_ptr<Route> r = session->get_remote_nth_route (x + scroll_x_offset);

	for (int y = 0; y < PAD_ROWS; ++y) {
		Pad& pad (pads[(y*8) + x]);
		TriggerPtr t = session->trigger_at (x + scroll_x_offset, y + scroll_y_offset);
		trigger_pad_light (pad, r, t.get());
	}
}

void
LaunchKey4::pad_mute_solo (Pad& pad)
{
	if (!stripable[pad.x]) {
		return;
	}

	if (pad.y == 0) {
		session->set_control (stripable[pad.x]->mute_control(), !stripable[pad.x]->mute_control()->get_value(), PBD::Controllable::UseGroup);
	} else {
		session->set_control (stripable[pad.x]->solo_control(), !stripable[pad.x]->solo_control()->get_value(), PBD::Controllable::UseGroup);
	}
}

void
LaunchKey4::port_registration_handler ()
{
	MIDISurface::port_registration_handler ();
	connect_daw_ports ();
}

void
LaunchKey4::connect_daw_ports ()
{
	if (!_daw_in || !_daw_out) {
		/* ports not registered yet */
		return;
	}

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

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

	/* get all MIDI Ports */

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

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

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

        std::string regex_str;

        if (device_pid == 0x213) {
	        regex_str =  X_("Launchkey Mini MK4.*(DAW|MIDI 2|DA$)");
        } else {
	        regex_str =  X_("Launchkey MK4.*(DAW|MIDI 2|DA$)");
        }

        std::regex rx (regex_str, std::regex::extended);

        auto is_dawport = [&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_dawport);
        auto po = std::find_if (midi_outputs.begin (), midi_outputs.end (), is_dawport);

        if (pi == midi_inputs.end() || po == midi_inputs.end()) {
	        std::cerr << "daw port not found\n";
	        return;
        }

        if (!_daw_in->connected()) {
	        AudioEngine::instance()->connect (_daw_in->name(), *pi);
        }

        if (!_daw_out->connected()) {
	        AudioEngine::instance()->connect (_daw_out->name(), *po);
        }


	connect_to_port_parser (*_daw_in_port);

	MIDI::Parser* p = _daw_in_port->parser();
	/* fader messages are controllers but always on channel 0xf */
	p->channel_controller[15].connect_same_thread (*this, std::bind (&LaunchKey4::handle_midi_controller_message_chnF, this, _1, _2));

	/* Connect DAW input port to event loop */

	AsyncMIDIPort* asp;

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

int
LaunchKey4::ports_acquire ()
{
	int ret = MIDISurface::ports_acquire ();

	if (!ret) {
		_daw_in  = AudioEngine::instance()->register_input_port (DataType::MIDI, string_compose (X_("%1 daw in"), port_name_prefix), true);
		if (_daw_in) {
			_daw_in_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_daw_in).get();
			_daw_out = AudioEngine::instance()->register_output_port (DataType::MIDI, string_compose (X_("%1 daw out"), port_name_prefix), true);
		}
		if (_daw_out) {
			_daw_out_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_daw_out).get();
			return 0;
		}

		ret = -1;
	}

	return ret;
}

void
LaunchKey4::ports_release ()
{
	/* wait for button data to be flushed */
	MIDI::Port* daw_port = std::dynamic_pointer_cast<AsyncMIDIPort>(_daw_out).get();
	AsyncMIDIPort* asp;
	asp = dynamic_cast<AsyncMIDIPort*> (daw_port);
	asp->drain (10000, 500000);

	{
		PBD::Mutex::Lock em (AudioEngine::instance()->process_lock());
		AudioEngine::instance()->unregister_port (_daw_in);
		AudioEngine::instance()->unregister_port (_daw_out);
	}

	_daw_in.reset ((ARDOUR::Port*) 0);
	_daw_out.reset ((ARDOUR::Port*) 0);

	MIDISurface::ports_release ();
}

void
LaunchKey4::daw_write (const MidiByteArray& data)
{
	DEBUG_TRACE (DEBUG::Launchkey, string_compose ("daw write %1 %2\n", data.size(), data));
	_daw_out_port->write (&data[0], data.size(), 0);
}

void
LaunchKey4::daw_write (MIDI::byte const * data, size_t size)
{

#ifndef NDEBUG
	std::stringstream str;

	if (DEBUG_ENABLED(DEBUG::Launchkey)) {
		str << hex;
		for (size_t n = 0; n < size; ++n) {
			str << (int) data[n] << ' ';
		}
	}
#endif

	DEBUG_TRACE (DEBUG::Launchkey, string_compose ("daw write %1 [%2]\n", size, str.str()));
	_daw_out_port->write (data, size, 0);
}

void
LaunchKey4::stripable_selection_changed ()
{
	map_selection ();

	if (session->selection().first_selected_stripable()) {
		set_display_target (GlobalTemporaryDisplay, 0, session->selection().first_selected_stripable()->name(), true);
	}
}

void
LaunchKey4::show_scene_ids ()
{
	set_display_target (DAWPadFunctionDisplay, 0, string_compose ("Scenes %1 + %2", scroll_y_offset + 1, scroll_y_offset + 2), true);
}

void
LaunchKey4::button_up ()
{
	if (pad_function != Triggers) {
		return;
	}

	if (scroll_y_offset >= 1) {
		scroll_y_offset -= 1;
		show_scene_ids ();
	}
}

void
LaunchKey4::button_down()
{
	if (pad_function != Triggers) {
		return;
	}

	scroll_y_offset += 1;
	show_scene_ids ();
}

void
LaunchKey4::build_color_map ()
{
	/* RGB values taken from using color picker on PDF of LP manual, page
	 * 10, but without zero (off)
	 */

	static uint32_t novation_color_chart_left_side[] = {
		0xb3b3b3ff,
		0xddddddff,
		0xffffffff,
		0xffb3b3ff,
		0xff6161ff,
		0xdd6161ff,
		0xb36161ff,
		0xfff3d5ff,
		0xffb361ff,
		0xdd8c61ff,
		0xb37661ff,
		0xffeea1ff,
		0xffff61ff,
		0xdddd61ff,
		0xb3b361ff,
		0xddffa1ff,
		0xc2ff61ff,
		0xa1dd61ff,
		0x81b361ff,
		0xc2ffb3ff,
		0x61ff61ff,
		0x61dd61ff,
		0x61b361ff,
		0xc2ffc2ff,
		0x61ff8cff,
		0x61dd76ff,
		0x61b36bff,
		0xc2ffccff,
		0x61ffccff,
		0x61dda1ff,
		0x61b381ff,
		0xc2fff3ff,
		0x61ffe9ff,
		0x61ddc2ff,
		0x61b396ff,
		0xc2f3ffff,
		0x61eeffff,
		0x61c7ddff,
		0x61a1b3ff,
		0xc2ddffff,
		0x61c7ffff,
		0x61a1ddff,
		0x6181b3ff,
		0xa18cffff,
		0x6161ffff,
		0x6161ddff,
		0x6161b3ff,
		0xccb3ffff,
		0xa161ffff,
		0x8161ddff,
		0x7661b3ff,
		0xffb3ffff,
		0xff61ffff,
		0xdd61ddff,
		0xb361b3ff,
		0xffb3d5ff,
		0xff61c2ff,
		0xdd61a1ff,
		0xb3618cff,
		0xff7661ff,
		0xe9b361ff,
		0xddc261ff,
		0xa1a161ff,
	};

	static uint32_t novation_color_chart_right_side[] = {
		0x61b361ff,
		0x61b38cff,
		0x618cd5ff,
		0x6161ffff,
		0x61b3b3ff,
		0x8c61f3ff,
		0xccb3c2ff,
		0x8c7681ff,
		/**/
		0xff6161ff,
		0xf3ffa1ff,
		0xeefc61ff,
		0xccff61ff,
		0x76dd61ff,
		0x61ffccff,
		0x61e9ffff,
		0x61a1ffff,
		/**/
		0x8c61ffff,
		0xcc61fcff,
		0xcc61fcff,
		0xa17661ff,
		0xffa161ff,
		0xddf961ff,
		0xd5ff8cff,
		0x61ff61ff,
		/**/
		0xb3ffa1ff,
		0xccfcd5ff,
		0xb3fff6ff,
		0xcce4ffff,
		0xa1c2f6ff,
		0xd5c2f9ff,
		0xf98cffff,
		0xff61ccff,
		/**/
		0xff61ccff,
		0xf3ee61ff,
		0xe4ff61ff,
		0xddcc61ff,
		0xb3a161ff,
		0x61ba76ff,
		0x76c28cff,
		0x8181a1ff,
		/**/
		0x818cccff,
		0xccaa81ff,
		0xdd6161ff,
		0xf9b3a1ff,
		0xf9ba76ff,
		0xfff38cff,
		0xe9f9a1ff,
		0xd5ee76ff,
		/**/
		0x8181a1ff,
		0xf9f9d5ff,
		0xddfce4ff,
		0xe9e9ffff,
		0xe4d5ffff,
		0xb3b3b3ff,
		0xd5d5d5ff,
		0xf9ffffff,
		/**/
		0xe96161ff,
		0xe96161ff,
		0x81f661ff,
		0x61b361ff,
		0xf3ee61ff,
		0xb3a161ff,
		0xeec261ff,
		0xc27661ff
	};

	for (size_t n = 0; n < sizeof (novation_color_chart_left_side) / sizeof (novation_color_chart_left_side[0]); ++n) {
		uint32_t color = novation_color_chart_left_side[n];
		/* Add 1 to account for missing zero at zero in the table */
		std::pair<int,uint32_t> p (static_cast<int>(1 + n), color);
		color_map.insert (p);
	}

	for (size_t n = 0; n < sizeof (novation_color_chart_right_side) / sizeof (novation_color_chart_right_side[0]); ++n) {
		uint32_t color = novation_color_chart_right_side[n];
		/* Add 40 to account for start offset number shown in page 10 of the LP manual */
		std::pair<int,uint32_t> p (static_cast<int>(40 + n), color);
		color_map.insert (p);
	}
}

int
LaunchKey4::find_closest_palette_color (uint32_t color)
{
	auto distance = std::numeric_limits<double>::max();
	int index = -1;

	NearestMap::iterator n = nearest_map.find (color);
	if (n != nearest_map.end()) {
		return n->second;
	}

	HSV hsv_c (color);

	for (auto const & c : color_map) {

		HSV hsv_p (c.second);

		double chr = M_PI * (hsv_c.h / 180.0);
		double phr = M_PI * (hsv_p.h /180.0);
		double t1 = (sin (chr) * hsv_c.s * hsv_c.v) - (sin (phr) * hsv_p.s* hsv_p.v);
		double t2 = (cos (chr) * hsv_c.s * hsv_c.v) - (cos (phr) * hsv_p.s * hsv_p.v);
		double t3 = hsv_c.v - hsv_p.v;
		double d = (t1 * t1) + (t2 * t2) + (0.5 * (t3 * t3));


		if (d < distance) {
			index = c.first;
			distance = d;
		}
	}

	nearest_map.insert (std::pair<uint32_t,int> (color, index));

	return index;
}

void
LaunchKey4::route_property_change (PropertyChange const & pc, int col)
{
	if (pc.contains (Properties::color)) {
		map_triggerbox (col);
	}


	if (pc.contains (Properties::selected)) {
	}
}

void
LaunchKey4::fader_move (int which, int val)
{
	std::shared_ptr<AutomationControl> ac;

	if (which == 8) {
		std::shared_ptr<Route> monitor = session->monitor_out();

		if (monitor) {
			ac = monitor->gain_control();
		} else {
			std::shared_ptr<Route> master = session->master_out();
			if (!master) {
				return;
			}
			ac = master->gain_control();
		}

	} else {
		if (!stripable[which]) {
			return;
		}

		ac = stripable[which]->gain_control();
	}

	if (ac) {
		gain_t gain = ARDOUR::slider_position_to_gain_with_max (val/127.0, ARDOUR::Config->get_max_gain());
		session->set_control (ac, gain, PBD::Controllable::NoGroup);

		char buf[16];
		snprintf (buf, sizeof (buf), "%.1f dB", accurate_coefficient_to_dB (gain));
		set_display_target (DisplayTarget (0x5 + which), 1, buf, true);
	}
}

void
LaunchKey4::automation_control_change (int n, std::weak_ptr<AutomationControl> wac)
{
	std::shared_ptr<AutomationControl> ac = wac.lock();
	if (!ac) {
		return;
	}

	MIDI::byte msg[3];
	msg[0] = 0xb4;
	msg[1] = first_fader + n;

	switch (current_fader_bank) {
	case VolumeFaders:
	case SendAFaders:
	case SendBFaders:
		msg[2] = (MIDI::byte) (ARDOUR::gain_to_slider_position_with_max (ac->get_value(), ARDOUR::Config->get_max_gain()) * 127.0);
		break;
	case PanFaders:
		msg[2] = (MIDI::byte) (ac->get_value() * 127.0);
		break;
	default:
		break;
	}

	daw_write (msg, 3);
}

void
LaunchKey4::encoder (int which, int step)
{
	switch (encoder_mode) {
	case EncoderPlugins:
		encoder_plugin (which, step);
		break;
	case EncoderMixer:
		encoder_mixer (which, step);
		break;
	case EncoderSendA:
		encoder_senda (which, step);
		break;
	case EncoderTransport:
		encoder_transport (which, step);
		break;
	}
}

void
LaunchKey4::plugin_selected (std::weak_ptr<PluginInsert> wpi)
{
	std::shared_ptr<PluginInsert> pi (wpi.lock());
	if (!pi) {
		return;
	}

	current_plugin = pi->plugin();
	uint32_t n = 0;

	while (n < 24) {

		Evoral::ParameterDescriptor pd;
		Evoral::Parameter param (PluginAutomation, 0, n);

		std::shared_ptr<AutomationControl> ac = pi->automation_control (param, false);
		if (ac) {
			controls[n] = ac;
		} else {
			break;
		}

		++n;
	}

	num_plugin_controls = n;

	while (n < 24) {
		controls[n].reset ();
		++n;
	}

	if (encoder_mode == EncoderPlugins) {
		label_encoders ();
		/* light up/down arrows appropriately */
		set_encoder_bank (encoder_bank);
	}
}

void
LaunchKey4::show_encoder_value (int n, std::shared_ptr<Plugin> plugin, int control, std::shared_ptr<AutomationControl> ac, bool display)
{
	bool ok;
	std::string str;
	uint32_t p = plugin->nth_parameter (control, ok);

	if (!ok || !plugin->print_parameter (p, str)) {
		char buf[32];
		double val = ac->get_value ();
		snprintf (buf, sizeof (buf), "%.2f", val);
		set_display_target (DisplayTarget (0x15 + n), 2, buf, display);
		return;
	}

	set_display_target (DisplayTarget (0x15 + n), 2, str, true);
}

void
LaunchKey4::setup_screen_for_encoder_plugins ()
{
	uint32_t n = 0;

	std::shared_ptr<ARDOUR::Plugin> plugin = current_plugin.lock();

	if (plugin) {
		while (n < 8) {
			uint32_t ctrl = (encoder_bank * 8) + n;

			std::shared_ptr<AutomationControl> ac = controls[ctrl].lock();
			bool ok;

			if (!ac) {
				break;
			}
			int p = plugin->nth_parameter (n, ok);
			if (!ok) {
				break;
			}

			std::string label = plugin->parameter_label (p);

			set_display_target (DisplayTarget (0x15+n), 0, plugin->name(), (n == 0));
			set_display_target (DisplayTarget (0x15+n), 1, label,(n == 0));
			show_encoder_value (n, plugin, ctrl, ac, (n == 0));
			++n;
		}
	}

	while (n < 8) {
		set_display_target (DisplayTarget (0x15+n), 0, plugin->name(), (n == 0));
		set_display_target (DisplayTarget (0x15+n), 1, "--", (n == 0));
		set_display_target (DisplayTarget (0x15+n), 2, string(), (n == 0));
		++n;
	}
}

void
LaunchKey4::encoder_plugin (int which, int step)
{
	std::shared_ptr<Plugin> plugin (current_plugin.lock());
	if (!plugin)  {
		return;
	}

	int control = which + (encoder_bank * 8);
	std::shared_ptr<AutomationControl> ac (controls[control].lock());

	if (!ac) {
		return;
	}

	double val = ac->internal_to_interface (ac->get_value());
	val += step/127.0;
	ac->set_value (ac->interface_to_internal (val), PBD::Controllable::NoGroup);

	show_encoder_value (which, plugin, control, ac, true);
}

void
LaunchKey4::encoder_mixer (int which, int step)
{
	switch (encoder_bank) {
	case 0:
		encoder_level (which, step);
		break;
	case 1:
		encoder_pan (which, step);
		break;
	default:
		break;
	}
}

void
LaunchKey4::encoder_pan (int which, int step)
{
	if (!stripable[which]) {
		return;
	}

	std::shared_ptr<AutomationControl> ac (stripable[which]->pan_azimuth_control());

	if (!ac) {
		return;
	}

	double val = ac->internal_to_interface (ac->get_value());
	session->set_control (ac, ac->interface_to_internal (val - (step/127.0)), Controllable::NoGroup);

	char buf[64];
	snprintf (buf, sizeof (buf), _("L:%3d R:%3d"), (int) rint (100.0 * (1.0 - val)), (int) rint (100.0 * val));
	set_display_target (DisplayTarget (0x15 + which), 2, buf, true);
}


void
LaunchKey4::encoder_level (int which, int step)
{
	if (!stripable[which]) {
		return;
	}

	std::shared_ptr<GainControl> gc (stripable[which]->gain_control());

	if (!gc) {
		return;
	}

	gain_t gain;

	if (shift_pressed) {
		gain = gc->get_value();
	} else {
		double pos = ARDOUR::gain_to_slider_position_with_max (gc->get_value(), ARDOUR::Config->get_max_gain());
		pos += (step/127.0);
		gain = ARDOUR::slider_position_to_gain_with_max (pos, ARDOUR::Config->get_max_gain());
		session->set_control (gc, gain, Controllable::NoGroup);
	}

	char buf[16];
	snprintf (buf, sizeof (buf), "%.1f dB", accurate_coefficient_to_dB (gain));
	set_display_target (DisplayTarget (0x15 + which), 2, buf, true);
}

void
LaunchKey4::encoder_senda (int which, int step)
{
	std::shared_ptr<Stripable> s = session->selection().first_selected_stripable();
	if (!s) {
		return;
	}

	std::shared_ptr<Route> target_bus = std::dynamic_pointer_cast<Route> (s);
	if (!target_bus) {
		return;
	}

	if (!stripable[which]) {
		return;
	}

	std::shared_ptr<Route> route = std::dynamic_pointer_cast<Route> (stripable[which]);
	if (!route) {
		return;
	}

	std::shared_ptr<InternalSend> send = std::dynamic_pointer_cast<InternalSend> (route->internal_send_for (target_bus));
	if (!send) {
		return;
	}

	std::shared_ptr<GainControl> gc = send->gain_control();
	if (!gc) {
		return;
	}
	gain_t gain;

	if (shift_pressed) {
		/* Just display current value */
		gain = gc->get_value();
	} else {
		double pos = ARDOUR::gain_to_slider_position_with_max (gc->get_value(), ARDOUR::Config->get_max_gain());
		pos += (step/127.0);
		gain = ARDOUR::slider_position_to_gain_with_max (pos, ARDOUR::Config->get_max_gain());
		session->set_control (gc, gain, Controllable::NoGroup);
	}

	char buf[16];
	snprintf (buf, sizeof (buf), "%.1f dB", accurate_coefficient_to_dB (gain));
	set_display_target (DisplayTarget (0x15 + which), 1, string_compose ("> %1", send->target_route()->name()), true);
	set_display_target (DisplayTarget (0x15 + which), 2, buf, true);
}

void
LaunchKey4::encoder_transport (int which, int step)
{
	switch (which) {
	case 0:
		transport_shuttle (step);
		break;
	case 1:
		zoom (step);
		break;
	case 2:
		loop_start_move (step);
		break;
	case 3:
		loop_end_move (step);
		break;
	case 4:
		jump_to_marker (step);
		break;
	case 5:
		break;
	case 6:
		break;
	case 7:
		break;
	}
}

void
LaunchKey4::transport_shuttle (int step)
{
	using namespace Temporal;

	/* 1 step == 1/10th current page */
	timepos_t pos (session->transport_sample());

	if (pos == 0  && step < 0) {
		return;
	}

	Beats b = pos.beats();

	if (step > 0) {
		b = b.round_up_to_beat ();
		b += Beats (1, 0) * step;
	} else {
		b = b.round_down_to_beat ();
		b += Beats (1, 0) * step; // step is negative, so add
		if (b < Beats()) {
			b = Beats();
		}
	}

	BBT_Time bbt = TempoMap::use()->bbt_at (b);
	std::stringstream str;
	str << bbt;

	set_display_target (DisplayTarget (0x15), 2, str.str(), true);

	session->request_locate (timepos_t (b).samples());
}

void
LaunchKey4::zoom (int step)
{
	if (step > 0) {
		while (step--) {
			temporal_zoom_in ();
		}
	} else {
		while (step++ < 0) {
			temporal_zoom_out ();
		}
	}
	set_display_target (DisplayTarget (0x15 + 1), 2, string(), true);
}

void
LaunchKey4::loop_start_move (int step)
{
	using namespace Temporal;

	Location* l = session->locations()->auto_loop_location ();
	BBT_Offset dur;

	if (!l) {
		/* XXX NEEDS WRAPPING IN REVERSIBLE COMMAND */
		timepos_t ph (session->transport_sample());
		timepos_t beat_later ((ph.beats() + Beats (1,0)).round_to_beat());

		Location* loc = new Location (*session, timepos_t (ph.beats()), beat_later, _("Loop"),  Location::IsAutoLoop);
		session->locations()->add (loc, true);
		session->set_auto_loop_location (loc);

		dur = BBT_Offset (0, 1, 0);

	} else {
		timepos_t start = l->start();
		start = start.beats() + Beats (step, 0);
		if (start.is_zero() || start.is_negative()) {
			return;
		}
		l->set_start (start);

		TempoMap::SharedPtr map (TempoMap::use());
		BBT_Time bbt_start = map->bbt_at (start);
		BBT_Time bbt_end = map->bbt_at (l->end());

		/* XXX this is not correct since start/end may cross meter boundaries */

		dur = map->metric_at (BBT_Argument (bbt_start)).meter().bbt_delta (bbt_end, bbt_start);
	}

	std::stringstream str;
	str << dur;
	set_display_target (DisplayTarget (0x15 + 2), 2, str.str(), true);
}

void
LaunchKey4::loop_end_move (int step)
{
	using namespace Temporal;

	Location* l = session->locations()->auto_loop_location ();
	BBT_Offset dur;

	if (!l) {
		/* XXX NEEDS WRAPPING IN REVERSIBLE COMMAND */
		timepos_t ph (session->transport_sample());
		timepos_t beat_later ((ph.beats() + Beats (1,0)).round_to_beat());

		Location* loc = new Location (*session, timepos_t (ph.beats()), beat_later, _("Loop"),  Location::IsAutoLoop);
		session->locations()->add (loc, true);
		session->set_auto_loop_location (loc);
		dur = BBT_Offset (0, 1, 0);
	} else {
		timepos_t end = l->end();
		end = end.beats() + Beats (step, 0);
		if (end.is_zero() || end.is_negative()) {
			return;
		}
		l->set_end (end);

		TempoMap::SharedPtr map (TempoMap::use());
		BBT_Time bbt_start = map->bbt_at (l->start());
		BBT_Time bbt_end = map->bbt_at (end);

		/* XXX this is not correct since start/end may cross meter boundaries */

		dur = map->metric_at (BBT_Argument (bbt_start)).meter().bbt_delta (bbt_end, bbt_start);
	}

	std::stringstream str;
	str << dur;
	set_display_target (DisplayTarget (0x15 + 3), 2, str.str(), true);
}

void
LaunchKey4::jump_to_marker (int step)
{
	timepos_t pos;
	Location::Flags noflags = Location::Flags (0);
	Location* loc;

	if (step > 0) {
		pos = session->locations()->first_mark_after_flagged (timepos_t (session->audible_sample()+1), true, noflags, noflags, noflags, &loc);

		if (pos == timepos_t::max (Temporal::AudioTime)) {
			return;
		}

	} else {
		pos = session->locations()->first_mark_before_flagged (timepos_t (session->audible_sample()), true, noflags, noflags, noflags, &loc);

		//handle the case where we are rolling, and we're less than one-half second past the mark, we want to go to the prior mark...
		if (session->transport_rolling()) {
			if ((session->audible_sample() - pos.samples()) < session->sample_rate()/2) {
				timepos_t prior = session->locations()->first_mark_before (pos);
				pos = prior;
			}
		}

		if (pos == timepos_t::max (Temporal::AudioTime)) {
			return;
		}
	}

	session->request_locate (pos.samples());

	set_display_target (DisplayTarget (0x15+4), 2, loc->name(), true);
}

void
LaunchKey4::set_pad_function (PadFunction f)
{
	MIDI::byte msg[3];
	std::string str;

	/* make the LK forget about any currently lit pads, because we overload
	   mode 0x2 and it gets confusing when it tries to restore lighting.
	*/
	all_pads (0x5);
	all_pads_out ();

	msg[0] = 0xb6;
	msg[1] = 0x40; /* set pad layout */

	switch (f) {
	case MuteSolo:
		str = "Mute/Solo";
		break;
	case Triggers:
		str = "Cues & Scenes";
		break;
	}

	pad_function = f;

	if (pad_function == Triggers) {
		map_triggers ();
	} else if (pad_function == MuteSolo) {
		map_mute_solo ();
	}

	/* Turn up/down arrows on/off depending on pad mode, also scene mode */

	msg[0] = 0xb0;
	msg[2] = (pad_function == Triggers ? 0x3 : 0x0);

	msg[1] = 0x6a; /* upper */
	daw_write (msg, 3);
	msg[1] = 0x6b; /* lower */
	daw_write (msg, 3);
	msg[1] = 0x68; /* scene */
	daw_write (msg, 3);

	configure_display (DAWPadFunctionDisplay, 0x1);
	set_display_target (DAWPadFunctionDisplay, 0, str, true);
}

void
LaunchKey4::select_display_target (DisplayTarget dt)
{
	MidiByteArray msg;

	msg.push_back (0xf0);
	msg.push_back (0x0);
	msg.push_back (0x20);
	msg.push_back (0x29);
	msg.push_back ((device_pid >> 8) & 0x7f);
	msg.push_back (device_pid & 0x7f);
	msg.push_back (0x4);
	msg.push_back (dt);
	msg.push_back (0x7f);
	msg.push_back (0xf7);

	daw_write (msg);
}

void
LaunchKey4::set_plugin_encoder_name (int encoder, int field, std::string const & str)
{
	set_display_target (PluginPotMode, field, str, true);
}

void
LaunchKey4::set_display_target (DisplayTarget dt, int field, std::string const & str, bool display)
{
	MidiByteArray msg;

	msg.push_back (0xf0);
	msg.push_back (0x0);
	msg.push_back (0x20);
	msg.push_back (0x29);
	msg.push_back ((device_pid >> 8) & 0x7f);
	msg.push_back (device_pid & 0x7f);
	msg.push_back (0x6);
	msg.push_back (dt);
	msg.push_back (display ? ((1<<6) | (field & 0x7f)) : (field & 0x7f));

	for (auto c : str) {
		msg.push_back (c & 0x7f);
	}

	msg.push_back (0xf7);

	daw_write (msg);
	write (msg);
}

void
LaunchKey4::configure_display (DisplayTarget target, int config)
{
	MidiByteArray msg (9, 0xf0, 0x00, 0x29, 0xff, 0xff, 0x04, 0xff, 0xff, 0xf7);

	msg[3] = (device_pid >> 8) & 0x7f;
	msg[4] = device_pid & 0x7f;

	msg[6] = target;
	msg[7] = config & 0x7f;

	daw_write (msg);
}

void
LaunchKey4::function_press ()
{
	switch (pad_function) {
	case MuteSolo:
		set_pad_function (Triggers);
		break;
	case Triggers:
		set_pad_function (MuteSolo);
		break;
	}
}

void
LaunchKey4::undo_press ()
{
	if (shift_pressed) {
		redo ();
	} else {
		undo ();
	}
}

void
LaunchKey4::button_press (int n)
{
	std::shared_ptr<AutomationControl> ac;

	if (!stripable[n]) {
		return;
	}

	switch (button_mode) {
	case ButtonsSelect:
		session->selection().select_stripable_and_maybe_group (stripable[n], SelectionSet);
		break;
	case ButtonsRecEnable:
		ac = stripable[n]->rec_enable_control();
		if (ac) {
			ac->set_value (!ac->get_value(), Controllable::NoGroup);
		}
		break;
	}
}

void
LaunchKey4::button_release (int n)
{
}

void
LaunchKey4::solo_changed ()
{
	map_mute_solo ();
}

void
LaunchKey4::mute_changed (uint32_t n)
{
	show_mute (n);
}

void
LaunchKey4::rec_enable_changed (uint32_t n)
{
	show_rec_enable (n);
}

void
LaunchKey4::switch_bank (uint32_t base)
{
	stripable_connections.drop_connections ();

	/* work backwards so we can tell if we should actually switch banks */

	std::shared_ptr<Stripable> s[8];

	for (int n = 0; n < 8; ++n) {
		s[n] = session->get_remote_nth_stripable (base+n, PresentationInfo::Flag (PresentationInfo::Route|PresentationInfo::VCA));
	}

	if (!s[0]) {
		/* not even the first stripable exists, do nothing */
		return;
	}

	for (int n = 0; n < 8; ++n) {
		stripable[n] = s[n];
	}

	/* at least one stripable in this bank */

	bank_start = base;

	for (int n = 0; n < 8; ++n) {

		if (stripable[n]) {

			/* stripable goes away? refill the bank, starting at the same point */

			stripable[n]->DropReferences.connect (stripable_connections, invalidator (*this), std::bind (&LaunchKey4::switch_bank, this, bank_start), this);
			stripable[n]->presentation_info().PropertyChanged.connect (stripable_connections, invalidator (*this), std::bind (&LaunchKey4::stripable_property_change, this, _1, n), this);
			stripable[n]->mute_control()->Changed.connect (stripable_connections, invalidator (*this), std::bind (&LaunchKey4::mute_changed, this, n), this);
			std::shared_ptr<AutomationControl> ac = stripable[n]->rec_enable_control();
			if (ac) {
				ac->Changed.connect (stripable_connections, invalidator (*this), std::bind (&LaunchKey4::rec_enable_changed, this, n), this);
			}
		}

		/* Set fader "title" fields to show current bank */

		for (int n = 0; n < 8; ++n) {
			if (stripable[n]) {
				set_display_target (DisplayTarget (0x5 + n), 0, stripable[n]->name(), true);
			} else {
				set_display_target (DisplayTarget (0x5 + n), 0, string(), true);
			}
		}

		if (session->monitor_out()) {
			set_display_target (DisplayTarget (0x5 + 8), 0, session->monitor_out()->name(), true);
		} else if (session->master_out()) {
			set_display_target (DisplayTarget (0x5 + 8), 0, session->master_out()->name(), true);
		}
	}

	if (button_mode == ButtonsSelect) {
		map_selection ();
	} else {
		map_rec_enable ();
	}

	switch (pad_function) {
	case Triggers:
		map_triggers ();
		break;
	case MuteSolo:
		map_mute_solo ();
		break;
	default:
		break;
	}

	if (encoder_mode != EncoderTransport) {
		set_encoder_titles_to_route_names ();
	}
}

void
LaunchKey4::stripable_property_change (PropertyChange const& what_changed, uint32_t which)
{
	if (what_changed.contains (Properties::color)) {
		show_selection (which);
	}

	if (what_changed.contains (Properties::hidden)) {
		switch_bank (bank_start);
	}

	if (what_changed.contains (Properties::selected)) {

		if (!stripable[which]) {
			return;
		}
	}

}

void
LaunchKey4::stripables_added ()
{
	/* reload current bank */
	switch_bank (bank_start);
}

void
LaunchKey4::button_right ()
{
	if (pad_function == Triggers) {
		switch_bank (bank_start + 1);
		scroll_x_offset = bank_start;
	} else {
		switch_bank (bank_start + 8);
	}
	std::cerr << "rright to " << bank_start << std::endl;

	if (stripable[0]) {
		set_display_target (GlobalTemporaryDisplay, 0, stripable[0]->name(), true);
	}
}

void
LaunchKey4::button_left ()
{
	if (pad_function == Triggers) {
		if (bank_start > 0) {
			switch_bank (bank_start - 1);
			scroll_x_offset = bank_start;
		}
	} else {
		if (bank_start > 7) {
			switch_bank (bank_start - 8);
		}
	}

	std::cerr << "left to " << bank_start << std::endl;

	if (stripable[0]) {
		set_display_target (GlobalTemporaryDisplay, 0, stripable[0]->name(), true);
	}
}

void
LaunchKey4::toggle_button_mode ()
{
	switch (button_mode) {
	case ButtonsSelect:
		button_mode = ButtonsRecEnable;
		map_rec_enable ();
		break;
	case ButtonsRecEnable:
		button_mode = ButtonsSelect;
		map_selection ();
		break;
	}

	MIDI::byte msg[3];
	msg[0] = 0xb0;
	msg[1] = Button9;

	if (button_mode == ButtonsSelect) {
		msg[2] = 0x3; /* brght white */
	} else {
		msg[2] = 0x5; /* red */
	}

	daw_write (msg, 3);
}

void
LaunchKey4::map_selection ()
{
	for (int n = 0; n < 8; ++n) {
		show_selection (n);
	}
}

void
LaunchKey4::show_selection (int n)
{
	const int first_button = 0x25;
	const int selection_color = 0xd; /* bright yellow */

	if (!stripable[n]) {
		light_button (first_button + n, Off, 0);
	} else if (stripable[n]->is_selected()) {
		light_button (first_button + n, Solid, selection_color);
	} else {
		light_button (first_button + n, Solid, find_closest_palette_color (stripable[n]->presentation_info().color ()));
	}
}

void
LaunchKey4::map_mute_solo ()
{
	for (int n = 0; n < 8; ++n)  {
		show_mute (n);
		show_solo (n);
	}
}

void
LaunchKey4::show_mute (int n)
{
	if (!stripable[n]) {
		return;
	}

	std::shared_ptr<MuteControl> mc (stripable[n]->mute_control());
	if (!mc) {
		return;
	}
	MIDI::byte msg[3];
	msg[0] = 0x90;
	msg[1] = 0x60 + n;
	if (mc->muted_by_self()) {
		// std::cerr << stripable[n]->name() << " muted by self\n";
		msg[2] = 0xd; /* bright yellow */
	} else if (mc->muted_by_others_soloing() || mc->muted_by_masters()) {
		// std::cerr << stripable[n]->name() << " muted by others\n";
		msg[2] = 0x49; /* soft yellow */
	} else {
		// std::cerr << stripable[n]->name() << " not muted\n";
		msg[2] = 0x0;;
	}

	daw_write (msg, 3);
}

void
LaunchKey4::show_solo (int n)
{
	if (!stripable[n]) {
		return;
	}

	std::shared_ptr<SoloControl> sc (stripable[n]->solo_control());
	if (!sc) {
		return;
	}
	MIDI::byte msg[3];
	msg[0] = 0x90;
	msg[1] = 0x70 + n;
	if (sc->soloed_by_self_or_masters()) {
		msg[2] = 0x15; /* bright green */
	} else if (sc->soloed_by_others()) {
		msg[2] = 0x4b; /* soft green */
	} else {
		msg[2] = 0x0;
	}

	daw_write (msg, 3);
}

void
LaunchKey4::light_button (int which, LightingMode mode, int color_index)
{
	MIDI::byte msg[3];

	msg[1] = which;

	switch (mode) {
	case Off:
		msg[0] = 0xb0;
		msg[2] = 0x0;
		break;

	case Solid:
		msg[0] = 0xb0;
		msg[2] = color_index & 0x7f;
		break;

	case Flash:
		msg[0] = 0xb1;
		msg[2] = color_index & 0x7f;
		break;

	case Pulse:
		msg[0] = 0xb2;
		msg[2] = color_index & 0x7f;
		break;
	}

	daw_write (msg, 3);
}


void
LaunchKey4::light_pad (int pid, LightingMode mode, int color_index)
{
	MIDI::byte msg[3];

	msg[1] = pid;

	switch (mode) {
	case Off:
		msg[0] = 0x90;
		msg[2] = 0x0;
		break;

	case Solid:
		msg[0] = 0x90;
		msg[2] = color_index & 0x7f;
		break;

	case Flash:
		msg[0] = 0x91;
		msg[2] = color_index & 0x7f;
		break;

	case Pulse:
		msg[0] = 0x92;
		msg[2] = color_index & 0x7f;
		break;
	}

	daw_write (msg, 3);
}

void
LaunchKey4::unlight_pad (int pad_id)
{
	light_pad (pad_id, Solid, 0x0);
}

void
LaunchKey4::set_encoder_bank (int n)
{
	bool light_up_arrow = false;
	bool light_down_arrow = false;

	encoder_bank = n;

	/* Ordering:

	   9
	   1
	   2
	*/

	if (encoder_mode == EncoderPlugins) {

		switch (encoder_bank) {
		case 0:
			if (num_plugin_controls > 8) {
				light_down_arrow = true;
			}
			break;
		case 1:
			if (num_plugin_controls > 8) {
				light_up_arrow = true;
			}
			if (num_plugin_controls > 16) {
				light_down_arrow = true;
			}
			break;
		case 2:
			if (num_plugin_controls > 16) {
				light_up_arrow = true;
			}
			break;
		}

	} else if (encoder_mode == EncoderMixer) {

		switch (encoder_bank) {
		case 0:
			light_down_arrow = true;
			break;
		case 1:
			light_down_arrow = true;
			light_up_arrow = true;
			break;
		case 2:
			light_up_arrow = true;
			break;
		default:
			return;
		}
	}

	MIDI::byte msg[6];
	/* Color doesn't really matter, these LEDs are single-color. Just turn
	   it on or off.
	*/
	const int color_index = 0x3;

	msg[0] = 0xb0;
	msg[1] = 0x33; /* top */
	msg[3] = 0xb0;
	msg[4] = 0x34; /* bottom */

	if (light_up_arrow) {
		msg[2] = color_index;
	} else {
		msg[2] = 0x0;
	}

	if (light_down_arrow) {
		msg[5] = color_index;
	} else {
		msg[5] = 0x0;
	}

	/* Stupid device doesn't seem to like both messages "at once" */
	daw_write (msg, 3);
	daw_write (&msg[3], 3);

	label_encoders ();
}

void
LaunchKey4::label_encoders ()
{
	std::shared_ptr<Plugin> plugin (current_plugin.lock());

	switch (encoder_mode) {
	case EncoderMixer:
	case EncoderSendA:
		set_encoder_titles_to_route_names ();
		switch (encoder_bank) {
		case 0:
			for (int n = 0; n < 8; ++n) {
				set_display_target (DisplayTarget (0x15 + n), 1, "Level", false);
			}
			set_display_target (GlobalTemporaryDisplay, 0, "Levels", true);
			break;
		case 1:
			for (int n = 0; n < 8; ++n) {
				set_display_target (DisplayTarget (0x15 + n), 1, "Pan", false);
			}
			set_display_target (GlobalTemporaryDisplay, 0, "Panning", true);
			break;
		default:
			break;
		}
		break;
	case EncoderPlugins:
		setup_screen_for_encoder_plugins ();
		break;
	case EncoderTransport:
		set_display_target (DisplayTarget (0x15), 1, "Shuttle", true);
		set_display_target (DisplayTarget (0x16), 1, "Zoom", true);
		set_display_target (DisplayTarget (0x17), 1, "Loop Start", true);
		set_display_target (DisplayTarget (0x18), 1, "Loop End", true);
		set_display_target (DisplayTarget (0x19), 1, "Jump to Marker", true);
		set_display_target (DisplayTarget (0x1a), 1, string(), true);
		set_display_target (DisplayTarget (0x1b), 1, string(), true);
		set_display_target (DisplayTarget (0x1c), 1, string(), true);
		for (int n = 0; n < 8; ++n) {
			set_display_target (DisplayTarget (0x15 + n), 0, "Transport", true);
		}
		set_display_target (GlobalTemporaryDisplay, 0, "Transport", true);
		break;
	}
}

void
LaunchKey4::set_encoder_mode (EncoderMode m)
{
	encoder_mode = m;
	set_encoder_bank (0);

	/* device firmware reset to continuous controller mode, so switch back
	 * if (ev->controller_to encoders
	 */

	use_encoders (true);
	label_encoders ();
}

void
LaunchKey4::set_encoder_titles_to_route_names ()
{
	/* Set encoder "title" fields to show current bank */
	bool first = true;

	for (int n = 0; n < 8; ++n) {
		if (stripable[n]) {
			set_display_target (DisplayTarget (0x15 + n), 0, stripable[n]->name(), first);
			first = false;
		} else {
			set_display_target (DisplayTarget (0x15 + n), 0, string(), true);
		}
	}
}

void
LaunchKey4::in_msecs (int msecs, std::function<void()> func)
{
	Glib::RefPtr<Glib::TimeoutSource> timeout = Glib::TimeoutSource::create (msecs); // milliseconds
	timeout->connect (sigc::bind_return (func, false));
	timeout->attach (main_loop()->get_context());
}

void
LaunchKey4::scene_press ()
{
	if (shift_pressed) {
		trigger_stop_all (true); /* immediate stop */
	} else {
		trigger_cue_row (scroll_y_offset);
	}
}
