#include "../../plugin.hpp"
#include "../../utils/StripIdFixModule.hpp"
#include "../../utils/ScaledMapParam.hpp"
#include "../../utils/TaskProcessor.hpp"
#include "../../components/MenuLabelEx.hpp"
#include "../../components/CurveMenuItem.hpp"
#include "../../components/SubMenuSlider.hpp"
#include "../../components/MidiWidget.hpp"
#include "../../components/MenuColorField.hpp"
#include "../../components/MenuColorLabel.hpp"
#include "../../components/MenuColorPicker.hpp"
#include "../../ui/ParamWidgetContextExtender.hpp"
#include "../../ui/OverlayMessageWidget.hpp"
#include "../cvmap/MapModuleBase.hpp"
#include "MidiCat.hpp"
#include <osdialog.h>

namespace StoermelderPackOne {
namespace MidiCat {

static const char PRESET_FILTERS[] = "VCV Rack module preset (.vcvm):vcvm";
static const NVGcolor MAPPING_INDICATOR_COLOR_DEFAULT = nvgRGB(0xff, 0xff, 0x40);

struct MidiCatOutput : midi::Output {
	int lastValues[128];
	bool lastGates[128];

	MidiCatOutput() {
		reset();
	}

	void reset() {
		for (int n = 0; n < 128; n++) {
			lastValues[n] = -1;
			lastGates[n] = false;
		}
	}

	void setValue(int value, int cc, bool force = false) {
		if (value == lastValues[cc] && !force)
			return;
		lastValues[cc] = value;
		// CC
		midi::Message m;
		m.setStatus(0xb);
		m.setNote(cc);
		m.setValue(value);
		sendMessage(m);
	}

	void setGate(int vel, int note, bool noteOffVelocityZero, bool force = false) {
		if (vel > 0) {
			// Note on
			if (!lastGates[note] || force) {
				midi::Message m;
				m.setStatus(0x9);
				m.setNote(note);
				m.setValue(vel);
				sendMessage(m);
			}
		}
		else if (vel == 0) {
			// Note off
			if (lastGates[note] || force) {
				midi::Message m;
				m.setStatus(noteOffVelocityZero ? 0x9 : 0x8);
				m.setNote(note);
				m.setValue(0);
				sendMessage(m);
			}
		}
		lastGates[note] = vel > 0;
	}
};


enum MIDIMODE {
	MIDIMODE_DEFAULT = 0,
	MIDIMODE_LOCATE = 1
};

struct MidiCatPrecisionProcessor {
	bool pickedUp;
	int midiRefPoint;
	float paramValue;
	float precision;

	void init(int midiMin, int midiMax) {
		pickedUp = false;
		this->midiRefPoint = (midiMax - midiMin) / 2;
	}

	void setPrecision(float precision, float paramValue, int midiRefPoint = -1) {
		this->precision = precision;
		this->paramValue = paramValue;
		if (midiRefPoint != -1) this->midiRefPoint = midiRefPoint;
	}

	float process(int midiValue) {
		if (!pickedUp) {
			if (midiValue == midiRefPoint) {
				pickedUp = true;
			}
			return paramValue;
		}
		else {
			float diff = float(midiValue - midiRefPoint) * precision;
			return paramValue + diff;
		}
	}
};

struct MidiCatParam : ScaledMapParam<int> {
	enum class CLOCKMODE {
		OFF = 0,
		ARM = 1,
		ARM_DEFERRED_FEEDBACK = 2
	};

	CLOCKMODE clockMode = CLOCKMODE::OFF;
	int clockSource = 0;

	int setValueDeffered;
	int getValueLast;

	int lightFirstId = -1;
	int lightNumColors = 0;

	MidiCatPrecisionProcessor precProcessor;

	void reset(bool resetSettings = true) override {
		if (resetSettings) {
			clockMode = CLOCKMODE::OFF;
			clockSource = 0;
		}
		lightFirstId = -1;
		lightNumColors = 0;
		ScaledMapParam<int>::reset(resetSettings);
	}

	void setValue(float i) override {
		switch (clockMode) {
			case CLOCKMODE::OFF:
				ScaledMapParam<int>::setValue(i);
				break;
			case CLOCKMODE::ARM:
			case CLOCKMODE::ARM_DEFERRED_FEEDBACK:
				setValueDeffered = i;
				break;
		}
	}

	int getValue() override {
		if (!hasLight()) {
			switch (clockMode) {
				case CLOCKMODE::OFF:
					return ScaledMapParam<int>::getValue();
				case CLOCKMODE::ARM:
					return setValueDeffered;
				case CLOCKMODE::ARM_DEFERRED_FEEDBACK:
					return getValueLast;
			}
		}
		else {
			if (paramQuantity->module->lights.size() >= size_t(lightFirstId + lightNumColors)) {
				int f = 0;
				for (int i = 0; i < lightNumColors; i++) {
					int b = int(std::ceil(paramQuantity->module->lights[lightFirstId + i].getBrightness() * 4.f));
					f += b << (i * 2);
				}
				return std::min(f << ((3 - lightNumColors) * 2 + 1), 127);
			}
		}
		return 0;
	}

	void tick(int clock) {
		if (clockMode != CLOCKMODE::OFF && clockSource == clock) {
			ScaledMapParam<int>::setValue(setValueDeffered);
		}
		if (clockMode == CLOCKMODE::ARM_DEFERRED_FEEDBACK) {
			getValueLast = ScaledMapParam<int>::getValue();
		}
	}

	bool isNear(int value, int jump = -1) {
		if (value == -1) return false;
		int p = getValue();
		int delta3p = (limitMaxT - limitMinT + 1) * 3 / 100;
		bool r = p - delta3p <= value && value <= p + delta3p;

		if (jump >= 0) {
			int delta7p = (limitMaxT - limitMinT + 1) * 7 / 100;
			r = r && p - delta7p <= jump && jump <= p + delta7p;
		}

		return r;
	}

	void setLight(int lightFirstId = -1, int lightNumColors = 0) {
		this->lightFirstId = lightFirstId;
		this->lightNumColors = lightNumColors;
		if (lightFirstId >= 0 && clockMode == CLOCKMODE::ARM_DEFERRED_FEEDBACK) {
			clockMode = CLOCKMODE::ARM;
		}
	}

	inline bool hasLight() {
		return lightFirstId >= 0;
	}
};


struct MidiCatModule : Module, StripIdFixModule, ExpanderChangeListener {
	/** [Stored to Json] */
	midi::InputQueue midiInput;
	/** [Stored to Json] */
	MidiCatOutput midiOutput;

	/** [Stored to JSON] */
	int panelTheme = 0;

	struct MidiCcAdapter {
		MidiCatModule* module;
		int id;
		int current = -1;
		uint64_t lastTs = 0;
		uint64_t diffTs;

		/** [Stored to Json] */
		int cc;
		/** [Stored to Json] */
		CCMODE ccMode;
		/** [Stored to Json] */
		bool cc14bit = false;

		bool process() {
			int previous = current;
			if (cc14bit) {
				if (module->valuesCcTs[cc] > lastTs && module->valuesCcTs[cc + 32] > lastTs) {
					current = module->valuesCc[cc] * 128 + module->valuesCc[cc + 32];
					diffTs = module->ts - lastTs;
					lastTs = module->ts;
				}
			}
			else {
				if (module->valuesCcTs[cc] > lastTs) {
					current = module->valuesCc[cc];
					diffTs = module->ts - lastTs;
					lastTs = module->ts;
				}
			}
			return current >= 0 && current != previous;
		}

		int getValue() {
			return current;
		}

		void setValue(int value, bool sendOnly) {
			if (cc == -1) return;
			if (cc14bit) {
				module->midiOutput.setValue(value / 128, cc, true);
				module->midiOutput.setValue(value % 128, cc + 32, true);
			}
			else {
				module->midiOutput.setValue(value, cc, current == -1 || sendOnly);
			}
			if (!sendOnly) current = value;
		}

		void reset() {
			cc = -1;
			current = -1;
		}

		void resetValue() {
			current = -1;
		}

		int getCc() {
			return cc;
		}

		void setCc(int cc) {
			this->cc = cc;
			if (cc == -1 || cc > 32) set14bit(false);
			current = -1;
		}

		bool get14bit() {
			return cc14bit;
		}

		void set14bit(bool value) {
			cc14bit = value;
			current = -1;
			if (cc14bit) {
				module->midiParam[id].setLimits(0, 128 * 128 - 1, -1);
			}
			else {
				module->midiParam[id].setLimits(0, 127, -1);
			}
		}
	};

	struct MidiNoteAdapter {
		MidiCatModule* module;
		int id;
		int current = -1;
		uint64_t lastTs = 0;
		uint64_t diffTs;

		/** [Stored to Json] */
		int note;
		/** [Stored to Json] Use the velocity value of each channel when notes are used */
		NOTEMODE noteMode;

		bool process() {
			int previous = current;
			if (module->valuesNoteTs[note] > lastTs) {
				current = module->valuesNote[note];
				diffTs = module->ts - lastTs;
				lastTs = module->ts;
			}
			return current >= 0 && current != previous;
		}

		int getValue() {
			return current;
		}

		void setValue(int value, bool sendOnly) {
			if (note == -1) return;
			module->midiOutput.setGate(value, note, (module->midiOptions[id] >> MIDIOPTION_VELZERO_BIT) & 1U, current == -1 || sendOnly);
			if (!sendOnly) current = value;
		}

		void reset() {
			note = -1;
			current = -1;
		}

		void resetValue() {
			current = -1;
		}

		int getNote() {
			return note;
		}

		void setNote(int note) {
			this->note = note;
			current = -1;
		}
	};

	/** Number of maps */
	int mapLen = 0;
	/** [Stored to Json] The mapped CC number of each channel */
	MidiCcAdapter ccs[MAX_CHANNELS];
	/** [Stored to Json] The mapped note number of each channel */
	MidiNoteAdapter notes[MAX_CHANNELS];
	/** [Stored to JSON] */
	int midiOptions[MAX_CHANNELS];
	/** [Stored to JSON] */
	bool midiIgnoreDevices;
	/** [Stored to JSON] */
	bool clearMapsOnLoad;

	/** [Stored to Json] The mapped param handle of each channel */
	ParamHandleIndicator paramHandles[MAX_CHANNELS];

	/** Channel ID of the learning session for MIDI/param mapping */
	int learningId;
	/** Wether multiple slots or just one slot should be learned */
	bool learnSingleSlot = false;
	/** Whether the CC has been set during the learning session */
	bool learnedCc;
	int learnedCcLast = -1;
	/** Whether the note has been set during the learning session */
	bool learnedNote;
	int learnedNoteLast = -1;
	/** Whether the param has been set during the learning session */
	bool learnedParam;

	/** Channel ID of the learning session forLED binding */
	int learningLightId = -1;

	/** [Stored to Json] */
	bool textScrolling = true;
	/** [Stored to Json] */
	std::string textLabel[MAX_CHANNELS];
	/** [Stored to Json] */
	bool locked;

	/** [Stored to Json] */
	NVGcolor mappingIndicatorColor = MAPPING_INDICATOR_COLOR_DEFAULT;
	/** [Stored to Json] */
	bool mappingIndicatorHidden = false;

	uint64_t ts = 0;

	/** The value of each CC number */
	int valuesCc[128];
	uint64_t valuesCcTs[128];
	/** The value of each note number */
	int valuesNote[128];
	uint64_t valuesNoteTs[128];

	MIDIMODE midiMode = MIDIMODE::MIDIMODE_DEFAULT;
	bool ccFineMode = false;

	/** Track last values */
	int lastValueIn[MAX_CHANNELS];
	int lastValueInIndicate[MAX_CHANNELS];
	int lastValueOut[MAX_CHANNELS];

	dsp::RingBuffer<int, 8> overlayQueue;
	/** [Stored to Json] */
	bool overlayEnabled;

	/** [Stored to Json] */
	MidiCatParam midiParam[MAX_CHANNELS];
	/** [Stored to Json] */
	bool midiResendPeriodically;
	ClockDividerEx midiResendDivider;

	ClockDividerEx processDivider;
	/** [Stored to Json] */
	int processDivision;
	ClockDividerEx indicatorDivider;
	/** [Stored to JSON] */
	bool parameterChangesDirect = false;

	/** Holds the time needed for long presses */
	uint64_t longPressDuration;

	TaskProcessor<> taskProcessorUi;

	// MEM-expander
	std::atomic<MidiCatMemBase*> expMem{NULL};
	int64_t expMemModuleId = -1;

	// CTX-expander
	std::atomic<MidiCatCtxBase*> expCtx{NULL};

	// CLK-expander
	std::atomic<Module*> expClk{NULL};
	dsp::SchmittTrigger expClkTrigger[4];

	// FINE-expander
	std::atomic<MidiCatFineBase*> expFine{NULL};
	dsp::SchmittTrigger expFineLowTrigger;
	dsp::SchmittTrigger expFineHighTrigger;

	MidiCatModule() {
		panelTheme = pluginSettings.panelThemeDefault;
		registerExpanderListener("MidiCat", this);
		config(0, 0, 0, 0);
		for (int id = 0; id < MAX_CHANNELS; id++) {
			paramHandles[id].color = mappingIndicatorColor;
			APP->engine->addParamHandle(&paramHandles[id]);
			midiParam[id].setLimits(0, 127, -1);
			ccs[id].module = notes[id].module = this;
			ccs[id].id = notes[id].id = id;
		}
		indicatorDivider.setDivision(2048);
		midiResendDivider.setDivision(APP->engine->getSampleRate() / 2);
		onReset();
	}

	~MidiCatModule() {
		unregisterExpanderListener("MidiCat", this);
		for (int id = 0; id < MAX_CHANNELS; id++) {
			APP->engine->removeParamHandle(&paramHandles[id]);
		}
	}

	void onExpanderChange(const Module::ExpanderChangeEvent& e) override {
		notifyExpanderListeners("MidiCat");
	}

	void onReset() override {
		expandersChanged = true;

		learningId = -1;
		learnedCc = false;
		learnedNote = false;
		learnedParam = false;
		// Use NoLock because Engine::resetModule() already holds the engine write lock.
		// We may also be called from the MIDIMap() constructor; that could be problematic, 
		// but during construction all ParamHandles are unbound (they do not point to any Module),
		// so this should be safe.
		clearMaps_NoLock();
		mapLen = 1;
		for (int i = 0; i < 128; i++) {
			valuesCc[i] = -1;
			valuesCcTs[i] = 0;
			valuesNote[i] = -1;
			valuesNoteTs[i] = 0;
		}
		for (int i = 0; i < MAX_CHANNELS; i++) {
			lastValueIn[i] = -1;
			lastValueOut[i] = -1;
			ccs[i].ccMode = CCMODE::DIRECT;
			notes[i].noteMode = NOTEMODE::MOMENTARY;
			textLabel[i] = "";
			midiOptions[i] = 0;
			midiParam[i].reset();
		}
		mappingIndicatorHidden = false;
		mappingIndicatorColor = MAPPING_INDICATOR_COLOR_DEFAULT;
		locked = false;
		midiInput.reset();
		midiOutput.reset();
		midiOutput.midi::Output::reset();
		midiIgnoreDevices = false;
		midiResendPeriodically = false;
		midiResendDivider.reset();
		processDivision = 64;
		processDivider.setDivision(processDivision);
		processDivider.reset();
		overlayEnabled = true;
		clearMapsOnLoad = false;

		parameterChangesDirect = false;
	}

	void onSampleRateChange() override {
		midiResendDivider.setDivision(APP->engine->getSampleRate() / 2);
		longPressDuration = (uint64_t)(APP->engine->getSampleRate() / 2);
	}

	void process(const ProcessArgs &args) override {
		ts++;

		// Aquire new MIDI messages from the queue
		midi::Message msg;
		bool midiReceived = false;
		while (midiInput.tryPop(&msg, args.frame)) {
			bool r = midiProcessMessage(msg);
			midiReceived = midiReceived || r;
		}

		// Only step channels when some midi event has been received. Additionally
		// step channels for parameter changes made manually every 128th loop. Notice
		// that midi allows about 1000 messages per second, so checking for changes more often
		// won't lead to higher precision on midi output.
		if (processDivider.process() || midiReceived) {
			processMappings(args.sampleTime);
		}

		// Handle indicators - blinking
		if (indicatorDivider.process()) {
			float t = indicatorDivider.getDivision() * args.sampleTime;
			for (int i = 0; i < mapLen; i++) {
				paramHandles[i].color = mappingIndicatorHidden ? color::BLACK_TRANSPARENT : mappingIndicatorColor;
				if (paramHandles[i].moduleId >= 0) {
					paramHandles[i].process(t, learningId == i);
				}
			}
		}

		if (midiResendPeriodically && midiResendDivider.process()) {
			midiResendFeedback();
		}

		// Expanders
		if (expandersChanged) {
			bool expMemFound = false;
			bool expCtxFound = false;
			bool expClkFound = false;
			bool expFineFound = false;

			Module* exp = rightExpander.module;
			for (int i = 0; i < 4; i++) {
				if (!exp) break;	
				if (exp->model == modelMidiCatMem && !expMemFound) {
					expMem.store(dynamic_cast<MidiCatMemBase*>(exp));
					expMemFound = true;
					exp = exp->rightExpander.module;
					continue;
				}
				if (exp->model == modelMidiCatCtx && !expCtxFound) {
					expCtx.store(dynamic_cast<MidiCatCtxBase*>(exp));
					expCtxFound = true;
					exp = exp->rightExpander.module;
					continue;
				}
				if (exp->model == modelMidiCatClk && !expClkFound) {
					expClk.store(exp);
					expClkFound = true;
					exp = exp->rightExpander.module;
					continue;
				}
				if (exp->model == modelMidiCatFine && !expFineFound) {
					expFine.store(dynamic_cast<MidiCatFineBase*>(exp));
					expFineFound = true;
					exp = exp->rightExpander.module;
					continue;
				}
				break;
			}

			if (!expMemFound) {
				expMem.store(NULL);
			}
			if (!expCtxFound) {
				expCtx.store(NULL);
			}
			if (!expClkFound) {
				if (expClk) {
					for (int i = 0; i < MAX_CHANNELS; i++) {
						midiParam[i].clockMode = MidiCatParam::CLOCKMODE::OFF;
						midiParam[i].clockSource = 0;
					}
				}
				expClk.store(NULL);
			}
			if (!expFineFound) {
				expFine.store(NULL);
				ccFineMode = false;
			}

			expandersChanged = false;
		}

		if (expClk.load()) expClkProcess();
		if (expFine.load()) expFineProcess();
	}

	void processMappings(float sampleTime) {
		float st = sampleTime * float(processDivision);

		for (int id = 0; id < mapLen; id++) {
			int cc = ccs[id].getCc();
			int note = notes[id].getNote();
			if (cc < 0 && note < 0)
				continue;

			// Get Module
			Module* module = paramHandles[id].module;
			if (!module)
				continue;

			// Get ParamQuantity
			int paramId = paramHandles[id].paramId;
			ParamQuantity* paramQuantity = module->paramQuantities[paramId];
			if (!paramQuantity)
				continue;

			if (!paramQuantity->isBounded())
				continue;

			switch (midiMode) {
				case MIDIMODE::MIDIMODE_DEFAULT: {
					midiParam[id].setParamQuantity(paramQuantity);
					int t = -1;
					float t_p = 0.f;

					// Check if CC value has been set and changed
					if (cc >= 0 && ccs[id].process()) {
						switch (ccs[id].ccMode) {
							case CCMODE::DIRECT:
								if (lastValueIn[id] != ccs[id].getValue()) {
									lastValueIn[id] = ccs[id].getValue();
									t = ccs[id].getValue();
								}
								break;
							case CCMODE::PICKUP1:
								if (lastValueIn[id] != ccs[id].getValue()) {
									if (!ccFineMode) {
										if (midiParam[id].isNear(lastValueIn[id])) {
											midiParam[id].resetFilter();
											t = ccs[id].getValue();
										}
									}
									else {
										t = 0;
										t_p = midiParam[id].precProcessor.process(ccs[id].getValue());
									}
									lastValueIn[id] = ccs[id].getValue();
								}
								break;
							case CCMODE::PICKUP2:
								if (lastValueIn[id] != ccs[id].getValue()) {
									if (!ccFineMode) {
										if (midiParam[id].isNear(lastValueIn[id], ccs[id].getValue())) {
											midiParam[id].resetFilter();
											t = ccs[id].getValue();
										}
									}
									else {
										t = 0;
										t_p = midiParam[id].precProcessor.process(ccs[id].getValue());
									}
									lastValueIn[id] = ccs[id].getValue();
								}
								break;
							case CCMODE::TOGGLE:
								if (ccs[id].getValue() > 0 && (lastValueIn[id] == -1 || lastValueIn[id] >= 0)) {
									t = midiParam[id].getLimitMax();
									lastValueIn[id] = -2;
								} 
								else if (ccs[id].getValue() == 0 && lastValueIn[id] == -2) {
									t = midiParam[id].getLimitMax();
									lastValueIn[id] = -3;
								}
								else if (ccs[id].getValue() > 0 && lastValueIn[id] == -3) {
									t = midiParam[id].getLimitMin();
									lastValueIn[id] = -4;
								}
								else if (ccs[id].getValue() == 0 && lastValueIn[id] == -4) {
									t = midiParam[id].getLimitMin();
									lastValueIn[id] = -1;
								}
								break;
							case CCMODE::TOGGLE_VALUE:
								if (ccs[id].getValue() > 0 && (lastValueIn[id] == -1 || lastValueIn[id] >= 0)) {
									t = ccs[id].getValue();
									lastValueIn[id] = -2;
								} 
								else if (ccs[id].getValue() == 0 && lastValueIn[id] == -2) {
									t = midiParam[id].getValue();
									lastValueIn[id] = -3;
								}
								else if (ccs[id].getValue() > 0 && lastValueIn[id] == -3) {
									t = midiParam[id].getLimitMin();
									lastValueIn[id] = -4;
								}
								else if (ccs[id].getValue() == 0 && lastValueIn[id] == -4) {
									t = midiParam[id].getLimitMin();
									lastValueIn[id] = -1;
								}
								break;
							case CCMODE::SNAPPED:
								if (ccs[id].getValue() > 0)
									t = midiParam[id].getNextSnappedValue();
								else
									t = midiParam[id].getValue();
								lastValueIn[id] = -1;
								break;
							case CCMODE::SNAPPED_SL:
								if (ccs[id].getValue() == 0)
									if (ccs[id].diffTs < longPressDuration)
										t = midiParam[id].getNextSnappedValue();
									else
										t = midiParam[id].getPrevSnappedValue();
								else
									t = midiParam[id].getValue();
								lastValueIn[id] = -1;
								break;
						}
					}

					// Check if note value has been set and changed
					if (note >= 0 && notes[id].process()) {
						switch (notes[id].noteMode) {
							case NOTEMODE::MOMENTARY:
								if (lastValueIn[id] != notes[id].getValue()) {
									t = notes[id].getValue();
									if (t > 0) t = 127;
									lastValueIn[id] = notes[id].getValue();
								} 
								break;
							case NOTEMODE::MOMENTARY_VEL:
								if (lastValueIn[id] != notes[id].getValue()) {
									t = notes[id].getValue();
									lastValueIn[id] = notes[id].getValue();
								}
								break;
							case NOTEMODE::TOGGLE:
								if (notes[id].getValue() > 0 && (lastValueIn[id] == -1 || lastValueIn[id] >= 0)) {
									t = 127;
									lastValueIn[id] = -2;
								} 
								else if (notes[id].getValue() == 0 && lastValueIn[id] == -2) {
									t = 127;
									lastValueIn[id] = -3;
								}
								else if (notes[id].getValue() > 0 && lastValueIn[id] == -3) {
									t = 0;
									lastValueIn[id] = -4;
								}
								else if (notes[id].getValue() == 0 && lastValueIn[id] == -4) {
									t = 0;
									lastValueIn[id] = -1;
								}
								break;
							case NOTEMODE::TOGGLE_VEL:
								if (notes[id].getValue() > 0 && (lastValueIn[id] == -1 || lastValueIn[id] >= 0)) {
									t = notes[id].getValue();
									lastValueIn[id] = -2;
								} 
								else if (notes[id].getValue() == 0 && lastValueIn[id] == -2) {
									t = midiParam[id].getValue();
									lastValueIn[id] = -3;
								}
								else if (notes[id].getValue() > 0 && lastValueIn[id] == -3) {
									t = 0;
									lastValueIn[id] = -4;
								}
								else if (notes[id].getValue() == 0 && lastValueIn[id] == -4) {
									t = 0;
									lastValueIn[id] = -1;
								}
								break;
							case NOTEMODE::SNAPPED:
								if (notes[id].getValue() > 0)
									t = midiParam[id].getNextSnappedValue();
								else
									t = midiParam[id].getValue();
								lastValueIn[id] = -1;
								break;
							case NOTEMODE::SNAPPED_SL:
								if (notes[id].getValue() == 0) {
									if (notes[id].diffTs < longPressDuration)
										t = midiParam[id].getNextSnappedValue();
									else
										t = midiParam[id].getPrevSnappedValue();
								}
								else {
									t = midiParam[id].getValue();
								}
								lastValueIn[id] = -1;
								break;
						}
					}

					// Set a new value for the mapped parameter
					if (t >= 0) {
						midiParam[id].setValue(t + t_p);
						if (overlayEnabled && overlayQueue.capacity() > 0) overlayQueue.push(id);
					}

					// Apply value on the mapped parameter (respecting slew and scale)
					midiParam[id].process(st);

					// Retrieve the current value of the parameter (ignoring slew and scale)
					int v = midiParam[id].getValue();

					// In some cases the MIDI feedback is detached from the actual parameter value
					// (toggle mode, attached to a light)
					// 2026-01-05: Also, do not update the parameter value here when in snap mode, because the tracked
					// value might be different to the actual value due to snapping
					bool sendOnlyFeedback = false;
					if (lastValueIn[id] < 0 || midiParam[id].hasLight() || paramQuantity->snapEnabled) {
						sendOnlyFeedback = true;
					}

					// Midi feedback
					if (lastValueOut[id] != v) {
						if (cc >= 0 && ccs[id].ccMode == CCMODE::DIRECT)
							lastValueIn[id] = v;
						// 2026-01-02: Fixes feedback in note/momentary mode
						// Update the internal state... does it break something else?
						// -- Fixed wrong internal state after manual parameter adjustment
						// -- 2026-01-05: Breaks snapped params, but fixed by "sendOnlyFeedback"
						if (!sendOnlyFeedback) midiParam[id].setValue(v);
						// --
						ccs[id].setValue(v, sendOnlyFeedback);
						notes[id].setValue(v, sendOnlyFeedback);
						lastValueOut[id] = v;
					}
				} 
				break;

				case MIDIMODE::MIDIMODE_LOCATE: {
					bool indicate = false;
					if ((cc >= 0 && valuesCc[cc] >= 0) && lastValueInIndicate[id] != valuesCc[cc]) {
						lastValueInIndicate[id] = valuesCc[cc];
						indicate = true;
					}
					if ((note >= 0 && valuesNote[note] >= 0) && lastValueInIndicate[id] != valuesNote[note]) {
						lastValueInIndicate[id] = valuesNote[note];
						indicate = true;
					}
					if (indicate) {
						int64_t moduleId = paramQuantity->module->getId();
						taskProcessorUi.enqueue([this, id, moduleId]() {
							ModuleWidget* mw = APP->scene->rack->getModule(moduleId);
							paramHandles[id].indicate(mw);
						});
					}
				} 
				break;
			}
		}
	}

	bool midiProcessMessage(const midi::Message& msg) {
		switch (msg.getStatus()) {
			case 0xb: { // cc
				return midiCc(msg);
			}
			case 0x8: {	// note off
				return midiNoteRelease(msg);
			}
			case 0x9: {	// note on
				if (msg.getValue() > 0) {
					return midiNotePress(msg);
				}
				else {
					// Many keyboards send a "note on" command with 0 velocity to mean "note release"
					return midiNoteRelease(msg);
				}
			}
			case 0xf: { // system
				if (msg.getChannel() == 0xf) { // reset
					midiReset();
				}
				return false;
			}
			default: {
				return false;
			}
		}
	}

	bool midiCc(const midi::Message& msg) {
		uint8_t cc = msg.getNote();
		uint8_t value = msg.getValue();
		// Learn
		if (learningId >= 0 && learnedCcLast != cc && (learnedCcLast == -1 || learnedCcLast != cc - 32) && valuesCc[cc] != value) {
			ccs[learningId].setCc(cc);
			ccs[learningId].ccMode = CCMODE::DIRECT;
			notes[learningId].setNote(-1);
			learnedCc = true;
			learnedCcLast = cc;
			commitLearn();
			updateMapLen();
			refreshParamHandleText(learningId);
		}
		bool midiReceived = valuesCc[cc] != value;
		valuesCc[cc] = value;
		valuesCcTs[cc] = ts;
		return midiReceived;
	}

	bool midiNotePress(const midi::Message& msg) {
		uint8_t note = msg.getNote();
		uint8_t vel = msg.getValue();
		// Learn
		if (learningId >= 0 && learnedNoteLast != note) {
			ccs[learningId].setCc(-1);
			notes[learningId].setNote(note);
			notes[learningId].noteMode = NOTEMODE::MOMENTARY;
			learnedNote = true;
			learnedNoteLast = note;
			commitLearn();
			updateMapLen();
			refreshParamHandleText(learningId);
		}
		bool midiReceived = valuesNote[note] != vel;
		valuesNote[note] = vel;
		valuesNoteTs[note] = ts;
		return midiReceived;
	}

	bool midiNoteRelease(const midi::Message& msg) {
		uint8_t note = msg.getNote();
		bool midiReceived = valuesNote[note] != 0;
		valuesNote[note] = 0;
		valuesNoteTs[note] = ts;
		return midiReceived;
	}

	void midiResendFeedback() {
		for (int i = 0; i < MAX_CHANNELS; i++) {
			lastValueOut[i] = -1;
			ccs[i].resetValue();
			notes[i].resetValue();
		}
	}

	void midiReset() {
		for (size_t i = 0; i < MAX_CHANNELS; i++) {
			lastValueIn[i] = -1;
		}
	}

	MidiCatParam& getMap(int id) {
		return midiParam[id];
	}

	void clearMap(int id, bool midiOnly = false) {
		learningId = -1;
		ccs[id].reset();
		notes[id].reset();
		midiOptions[id] = 0;
		midiParam[id].reset();
		if (!midiOnly) {
			textLabel[id] = "";
			APP->engine->updateParamHandle(&paramHandles[id], -1, 0, true);
			updateMapLen();
			refreshParamHandleText(id);
		}
	}

	void clearMaps_WithLock() {
		learningId = -1;
		for (int id = 0; id < MAX_CHANNELS; id++) {
			ccs[id].reset();
			notes[id].reset();
			textLabel[id] = "";
			midiOptions[id] = 0;
			midiParam[id].reset();
			APP->engine->updateParamHandle(&paramHandles[id], -1, 0, true);
			refreshParamHandleText(id);
		}
		mapLen = 1;
		expMemModuleId = -1;
	}

	void clearMaps_NoLock() {
		learningId = -1;
		for (int id = 0; id < MAX_CHANNELS; id++) {
			ccs[id].reset();
			notes[id].reset();
			textLabel[id] = "";
			midiOptions[id] = 0;
			midiParam[id].reset();
			APP->engine->updateParamHandle_NoLock(&paramHandles[id], -1, 0, true);
			refreshParamHandleText(id);
		}
		mapLen = 1;
		expMemModuleId = -1;
	}

	void updateMapLen() {
		// Find last nonempty map
		int id;
		for (id = MAX_CHANNELS - 1; id >= 0; id--) {
			if (ccs[id].getCc() >= 0 || notes[id].getNote() >= 0 || paramHandles[id].moduleId >= 0)
				break;
		}
		mapLen = id + 1;
		// Add an empty "Mapping..." slot
		if (mapLen < MAX_CHANNELS) {
			mapLen++;
		}
	}

	void commitLearn() {
		if (learningId < 0)
			return;
		if (!learnedCc && !learnedNote)
			return;
		if (!learnedParam && paramHandles[learningId].moduleId < 0)
			return;

		// Copy settings from the previous slot
		if (learningId > 0) {
			ccs[learningId].ccMode = ccs[learningId - 1].ccMode;
			ccs[learningId].set14bit(ccs[learningId - 1].get14bit() && learnedCc && learnedCcLast < 32);
			notes[learningId].noteMode = notes[learningId - 1].noteMode;
			midiOptions[learningId] = midiOptions[learningId - 1];
			midiParam[learningId].setSlew(midiParam[learningId - 1].getSlew());
			midiParam[learningId].setMin(midiParam[learningId - 1].getMin());
			midiParam[learningId].setMax(midiParam[learningId - 1].getMax());
			midiParam[learningId].setCurve(midiParam[learningId - 1].getCurve());
			midiParam[learningId].clockMode = midiParam[learningId - 1].clockMode;
			midiParam[learningId].clockSource = midiParam[learningId - 1].clockSource;
		}
		textLabel[learningId] = "";

		// Reset learned state
		learnedCc = false;
		learnedNote = false;
		learnedParam = false;

		// Find next incomplete map
		while (!learnSingleSlot && ++learningId < MAX_CHANNELS) {
			if ((ccs[learningId].getCc() < 0 && notes[learningId].getNote() < 0) || paramHandles[learningId].moduleId < 0)
				return;
		}
		learningId = -1;
	}

	int enableLearn(int id, bool learnSingle = false) {
		if (id == -1) {
			// Find next incomplete map
			while (++id < MAX_CHANNELS) {
				if (ccs[id].getCc() < 0 && notes[id].getNote() < 0 && paramHandles[id].moduleId < 0)
					break;
			}
			if (id == MAX_CHANNELS) {
				return -1;
			}
		}

		if (id == mapLen) {
			disableLearn();
			return -1;
		}
		if (learningId != id) {
			learningId = id;
			learnedCc = false;
			learnedCcLast = -1;
			learnedNote = false;
			learnedNoteLast = -1;
			learnedParam = false;
			learnSingleSlot = learnSingle;
		}
		return id;
	}

	void disableLearn() {
		learningId = -1;
	}

	void disableLearn(int id) {
		if (learningId == id) {
			learningId = -1;
		}
	}

	void learnParam(int id, int64_t moduleId, int paramId, bool resetMidiSettings = true) {
		APP->engine->updateParamHandle(&paramHandles[id], moduleId, paramId, true);
		midiParam[id].reset(resetMidiSettings);
		// Reset binding to light
		midiParam[learningId].setLight();
		learnedParam = true;
		commitLearn();
		updateMapLen();
	}

	/** Bind a module to the MIDI-CAT mappings.
	 *  Called from the UI thread.
	 */
	void moduleBind(Module* m, bool keepCcAndNote) {
		if (!m) return;
		if (!keepCcAndNote) {
			clearMaps_WithLock();
		}
		else {
			// Clean up some additional mappings on the end
			for (int i = int(m->params.size()); i < mapLen; i++) {
				APP->engine->updateParamHandle(&paramHandles[i], -1, -1, true);
			}
		}
		for (size_t i = 0; i < m->params.size() && i < MAX_CHANNELS; i++) {
			learnParam(int(i), m->id, int(i));
		}

		updateMapLen();
	}

	/** Bind the module connected to the expander to the MIDI-CAT mappings.
	 *  Called from the UI thread.
	 */
	void moduleBindExpander(bool keepCcAndNote) {
		Module::Expander* exp = &leftExpander;
		if (exp->moduleId < 0) return;
		Module* m = exp->module;
		if (!m) return;
		moduleBind(m, keepCcAndNote);
	}

	void refreshParamHandleText(int id) {
		std::string text = "MIDI-CAT";
		if (ccs[id].getCc() >= 0) {
			text += string::f(" cc%02d", ccs[id].getCc());
		}
		if (notes[id].getNote() >= 0) {
			static const char* noteNames[] = {
				"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
			};
			int oct = notes[id].getNote() / 12 - 1;
			int semi = notes[id].getNote() % 12;
			text += string::f(" note %s%d", noteNames[semi], oct);
		}
		paramHandles[id].text = text;
	}

	/** Save mapping to the expander for the given model.
	 *  Called from the UI thread.
	 */
	void expMemSave(std::string pluginSlug, std::string moduleSlug) {
		MemModule* m = new MemModule;
		Module* module = NULL;
		for (size_t i = 0; i < MAX_CHANNELS; i++) {
			if (paramHandles[i].moduleId < 0) continue;
			if (paramHandles[i].module->model->plugin->slug != pluginSlug && paramHandles[i].module->model->slug == moduleSlug) continue;
			module = paramHandles[i].module;

			MemParam* p = new MemParam;
			p->paramId = paramHandles[i].paramId;
			p->cc = ccs[i].getCc();
			p->ccMode = ccs[i].ccMode;
			p->cc14bit = ccs[i].get14bit();
			p->note = notes[i].getNote();
			p->noteMode = notes[i].noteMode;
			p->label = textLabel[i];
			p->midiOptions = midiOptions[i];
			p->slew = midiParam[i].getSlew();
			p->min = midiParam[i].getMin();
			p->max = midiParam[i].getMax();
			p->curve = midiParam[i].getCurve();
			p->lightFirstId = midiParam[i].lightFirstId;
			p->lightNumColors = midiParam[i].lightNumColors;
			m->paramMap.push_back(p);
		}
		m->pluginName = module->model->plugin->name;
		m->moduleName = module->model->name;

		auto expMem = this->expMem.load();
		auto p = std::pair<std::string, std::string>(pluginSlug, moduleSlug);
		auto it = expMem->getMemStorage()->find(p);
		if (it != expMem->getMemStorage()->end()) {
			delete it->second;
		}

		(*expMem->getMemStorage())[p] = m;
	}

	/** Delete mapping from the expander for the given model.
	 *  Called from the UI thread.
	 */
	void expMemDelete(std::string pluginSlug, std::string moduleSlug) {
		auto expMem = this->expMem.load();
		auto p = std::pair<std::string, std::string>(pluginSlug, moduleSlug);
		auto it = expMem->getMemStorage()->find(p);
		delete it->second;
		expMem->getMemStorage()->erase(p);
	}

	/** Apply mapping from the expander to the given module.
	 *  Called from the UI thread.
	 */
	void expMemApply(Module* m) {
		if (!m) return;
		auto expMem = this->expMem.load();
		auto p = std::pair<std::string, std::string>(m->model->plugin->slug, m->model->slug);
		auto it = expMem->getMemStorage()->find(p);
		if (it == expMem->getMemStorage()->end()) return;
		MemModule* map = it->second;

		clearMaps_WithLock();
		expMemModuleId = m->id;
		int i = 0;
		for (MemParam* it : map->paramMap) {
			learnParam(i, m->id, it->paramId);
			ccs[i].setCc(it->cc);
			ccs[i].ccMode = it->ccMode;
			ccs[i].set14bit(it->cc14bit);
			notes[i].setNote(it->note);
			notes[i].noteMode = it->noteMode;
			textLabel[i] = it->label;
			midiOptions[i] = it->midiOptions;
			midiParam[i].setSlew(it->slew);
			midiParam[i].setMin(it->min);
			midiParam[i].setMax(it->max);
			midiParam[i].setCurve(it->curve);
			midiParam[i].lightFirstId = it->lightFirstId;
			midiParam[i].lightNumColors = it->lightNumColors;
			i++;
		}
		updateMapLen();
	}

	/** Test if there is a mapping for the given module in the expander.
	 *  Called from the UI thread.
	 */
	bool expMemTest(Module* m) {
		if (!m) return false;
		auto expMem = this->expMem.load();
		auto p = std::pair<std::string, std::string>(m->model->plugin->slug, m->model->slug);
		auto it = expMem->getMemStorage()->find(p);
		if (it == expMem->getMemStorage()->end()) return false;
		if (expMem->getMemModuleRestriction()->size() > 0) {
			auto it2 = expMem->getMemModuleRestriction()->find(m->getId());
			if (it2 == expMem->getMemModuleRestriction()->end()) return false;
		}
		return true;
	}

	/** process-function for the CLK-expander - handles the four clock inputs.
	 *  Called from the dsp thread.
	 */
	void expClkProcess() {
		auto expClk = this->expClk.load();
		for (int i = 0; i < 4; i++) {
			if (expClkTrigger[i].process(expClk->inputs[i].getVoltage())) {
				for (int j = 0; j < mapLen; j++) {
					midiParam[j].tick(i);
				}
			}
		}
	}

	/** process-function for the FINE-expander.
	 *  Called from the dsp thread.
	 */
	void expFineProcess() {
		auto expFine = this->expFine.load();
		auto e1 = expFineLowTrigger.processEvent(expFine->getLowRangeVoltage());
		auto e2 = expFineHighTrigger.processEvent(expFine->getHighRangeVoltage());
		if (e1 == dsp::SchmittTrigger::TRIGGERED && !expFineHighTrigger.isHigh()) {
			setFineMode(true, expFine->getLowRange());
		}
		if (e1 == dsp::SchmittTrigger::UNTRIGGERED && !expFineHighTrigger.isHigh()) {
			setFineMode(false, 0.f);
		}
		if (e2 == dsp::SchmittTrigger::TRIGGERED) {
			setFineMode(true, expFine->getHighRange(), expFineLowTrigger.isHigh());
		}
		if (e2 == dsp::SchmittTrigger::UNTRIGGERED) {
			if (expFineLowTrigger.isHigh())
				setFineMode(true, expFine->getLowRange(), true);
			else
				setFineMode(false, 0.f);
		}
	}

	json_t* dataToJson() override {
		json_t* rootJ = json_object();
		json_object_set_new(rootJ, "panelTheme", json_integer(panelTheme));

		json_object_set_new(rootJ, "textScrolling", json_boolean(textScrolling));
		json_object_set_new(rootJ, "mappingIndicatorHidden", json_boolean(mappingIndicatorHidden));
		json_object_set_new(rootJ, "mappingIndicatorColor", json_string(color::toHexString(mappingIndicatorColor).c_str()));
		json_object_set_new(rootJ, "locked", json_boolean(locked));
		json_object_set_new(rootJ, "processDivision", json_integer(processDivision));
		json_object_set_new(rootJ, "overlayEnabled", json_boolean(overlayEnabled));
		json_object_set_new(rootJ, "clearMapsOnLoad", json_boolean(clearMapsOnLoad));

		json_object_set_new(rootJ, "parameterChangesDirect", json_boolean(parameterChangesDirect));

		json_t* mapsJ = json_array();
		for (int id = 0; id < mapLen; id++) {
			json_t* mapJ = json_object();
			json_object_set_new(mapJ, "cc", json_integer(ccs[id].getCc()));
			json_object_set_new(mapJ, "ccMode", json_integer((int)ccs[id].ccMode));
			json_object_set_new(mapJ, "cc14bit", json_boolean(ccs[id].get14bit()));
			json_object_set_new(mapJ, "note", json_integer(notes[id].getNote()));
			json_object_set_new(mapJ, "noteMode", json_integer((int)notes[id].noteMode));
			json_object_set_new(mapJ, "moduleId", json_integer(paramHandles[id].moduleId));
			json_object_set_new(mapJ, "paramId", json_integer(paramHandles[id].paramId));
			json_object_set_new(mapJ, "label", json_string(textLabel[id].c_str()));
			json_object_set_new(mapJ, "midiOptions", json_integer(midiOptions[id]));
			json_object_set_new(mapJ, "slew", json_real(midiParam[id].getSlew()));
			json_object_set_new(mapJ, "min", json_real(midiParam[id].getMin()));
			json_object_set_new(mapJ, "max", json_real(midiParam[id].getMax()));
			json_object_set_new(mapJ, "curve", json_real(midiParam[id].getCurve()));
			json_object_set_new(mapJ, "clockMode", json_integer((int)midiParam[id].clockMode));
			json_object_set_new(mapJ, "clockSource", json_integer(midiParam[id].clockSource));
			json_object_set_new(mapJ, "lightFirstId", json_integer(midiParam[id].lightFirstId));
			json_object_set_new(mapJ, "lightNumColors", json_integer(midiParam[id].lightNumColors));
			json_array_append_new(mapsJ, mapJ);
		}
		json_object_set_new(rootJ, "maps", mapsJ);

		json_object_set_new(rootJ, "midiResendPeriodically", json_boolean(midiResendPeriodically));
		json_object_set_new(rootJ, "midiIgnoreDevices", json_boolean(midiIgnoreDevices));
		json_object_set_new(rootJ, "midiInput", midiInput.toJson());
		json_object_set_new(rootJ, "midiOutput", midiOutput.toJson());
		return rootJ;
	}

	void dataFromJson(json_t* rootJ) override {
		json_t* panelThemeJ = json_object_get(rootJ, "panelTheme");
		if (panelThemeJ) panelTheme = json_integer_value(panelThemeJ);

		json_t* textScrollingJ = json_object_get(rootJ, "textScrolling");
		if (textScrollingJ) textScrolling = json_boolean_value(textScrollingJ);
		json_t* mappingIndicatorHiddenJ = json_object_get(rootJ, "mappingIndicatorHidden");
		if (mappingIndicatorHiddenJ) mappingIndicatorHidden = json_boolean_value(mappingIndicatorHiddenJ);
		json_t* mappingIndicatorColorJ = json_object_get(rootJ, "mappingIndicatorColor");
		if (mappingIndicatorColorJ) mappingIndicatorColor = color::fromHexString(json_string_value(mappingIndicatorColorJ));
		json_t* lockedJ = json_object_get(rootJ, "locked");
		if (lockedJ) locked = json_boolean_value(lockedJ);
		json_t* processDivisionJ = json_object_get(rootJ, "processDivision");
		if (processDivisionJ) setProcessDivision(json_integer_value(processDivisionJ));
		json_t* overlayEnabledJ = json_object_get(rootJ, "overlayEnabled");
		if (overlayEnabledJ) overlayEnabled = json_boolean_value(overlayEnabledJ);
		json_t* clearMapsOnLoadJ = json_object_get(rootJ, "clearMapsOnLoad");
		if (clearMapsOnLoadJ) clearMapsOnLoad = json_boolean_value(clearMapsOnLoadJ);

		if (clearMapsOnLoad) {
			// Use NoLock because we're already in an Engine write-lock.
			clearMaps_NoLock();
		}

		json_t* parameterChangesDirectJ = json_object_get(rootJ, "parameterChangesDirect");
		if (parameterChangesDirectJ) setParameterChangesDirect(json_boolean_value(parameterChangesDirectJ));

		json_t* mapsJ = json_object_get(rootJ, "maps");
		if (mapsJ) {
			json_t* mapJ;
			size_t mapIndex;
			json_array_foreach(mapsJ, mapIndex, mapJ) {
				if (mapIndex >= MAX_CHANNELS) {
					continue;
				}

				json_t* ccJ = json_object_get(mapJ, "cc");
				json_t* ccModeJ = json_object_get(mapJ, "ccMode");
				json_t* cc14bitJ = json_object_get(mapJ, "cc14bit");
				json_t* noteJ = json_object_get(mapJ, "note");
				json_t* noteModeJ = json_object_get(mapJ, "noteMode");
				json_t* moduleIdJ = json_object_get(mapJ, "moduleId");
				json_t* paramIdJ = json_object_get(mapJ, "paramId");
				json_t* labelJ = json_object_get(mapJ, "label");
				json_t* midiOptionsJ = json_object_get(mapJ, "midiOptions");
				json_t* slewJ = json_object_get(mapJ, "slew");
				json_t* minJ = json_object_get(mapJ, "min");
				json_t* maxJ = json_object_get(mapJ, "max");
				json_t* curveJ = json_object_get(mapJ, "curve");
				json_t* clockModeJ = json_object_get(mapJ, "clockMode");
				json_t* clockSourceJ = json_object_get(mapJ, "clockSource");
				json_t* lightFirstIdJ = json_object_get(mapJ, "lightFirstId");
				json_t* lightNumColorsJ = json_object_get(mapJ, "lightNumColors");

				if (!(ccJ || noteJ)) {
					ccs[mapIndex].setCc(-1);
					notes[mapIndex].setNote(-1);
					APP->engine->updateParamHandle_NoLock(&paramHandles[mapIndex], -1, 0, true);
					continue;
				}
				if (!(moduleIdJ || paramIdJ)) {
					APP->engine->updateParamHandle_NoLock(&paramHandles[mapIndex], -1, 0, true);
				}

				ccs[mapIndex].setCc(ccJ ? json_integer_value(ccJ) : -1);
				ccs[mapIndex].ccMode = (CCMODE)json_integer_value(ccModeJ);
				if (cc14bitJ) ccs[mapIndex].set14bit(json_boolean_value(cc14bitJ));
				notes[mapIndex].setNote(noteJ ? json_integer_value(noteJ) : -1);
				notes[mapIndex].noteMode = (NOTEMODE)json_integer_value(noteModeJ);
				midiOptions[mapIndex] = json_integer_value(midiOptionsJ);
				int64_t moduleId = moduleIdJ ? json_integer_value(moduleIdJ) : -1;
				int paramId = paramIdJ ? json_integer_value(paramIdJ) : 0;
				if (moduleId >= 0) {
					moduleId = idFix(moduleId);
					if (moduleId != paramHandles[mapIndex].moduleId || paramId != paramHandles[mapIndex].paramId) {
						APP->engine->updateParamHandle_NoLock(&paramHandles[mapIndex], moduleId, paramId, false);
						refreshParamHandleText(mapIndex);
					}
				}
				if (labelJ) textLabel[mapIndex] = json_string_value(labelJ);
				if (slewJ) midiParam[mapIndex].setSlew(json_real_value(slewJ));
				if (minJ) midiParam[mapIndex].setMin(json_real_value(minJ));
				if (maxJ) midiParam[mapIndex].setMax(json_real_value(maxJ));
				if (curveJ) midiParam[mapIndex].setCurve(json_real_value(curveJ));
				if (clockModeJ) midiParam[mapIndex].clockMode = (MidiCatParam::CLOCKMODE)json_integer_value(clockModeJ);
				if (clockSourceJ) midiParam[mapIndex].clockSource = json_integer_value(clockSourceJ);
				if (lightFirstIdJ) midiParam[mapIndex].lightFirstId = json_integer_value(lightFirstIdJ);
				if (lightNumColorsJ) midiParam[mapIndex].lightNumColors = json_integer_value(lightNumColorsJ);
			}
		}

		updateMapLen();
		idFixClearMap();
		
		json_t* midiResendPeriodicallyJ = json_object_get(rootJ, "midiResendPeriodically");
		if (midiResendPeriodicallyJ) midiResendPeriodically = json_boolean_value(midiResendPeriodicallyJ);

		if (!midiIgnoreDevices) {
			json_t* midiIgnoreDevicesJ = json_object_get(rootJ, "midiIgnoreDevices");
			if (midiIgnoreDevicesJ)	midiIgnoreDevices = json_boolean_value(midiIgnoreDevicesJ);
			json_t* midiInputJ = json_object_get(rootJ, "midiInput");
			if (midiInputJ) midiInput.fromJson(midiInputJ);
			json_t* midiOutputJ = json_object_get(rootJ, "midiOutput");
			if (midiOutputJ) midiOutput.fromJson(midiOutputJ);
		}
	}

	void setProcessDivision(int d) {
		processDivision = d;
		processDivider.setDivision(d);
		processDivider.reset();
	}

	void setMode(MIDIMODE midiMode) {
		if (this->midiMode == midiMode)
			return;
		this->midiMode = midiMode;
		switch (midiMode) {
			case MIDIMODE::MIDIMODE_LOCATE:
				for (int i = 0; i < MAX_CHANNELS; i++)
					lastValueInIndicate[i] = std::max(0, lastValueIn[i]);
				break;
			default:
				break;
		}
	}

	void setParameterChangesDirect(bool b) {
		parameterChangesDirect = b;
		for (int id = 0; id < MAX_CHANNELS; id++) {
			midiParam[id].parameterChangesDirect = parameterChangesDirect;
		}		
	}

	void setFineMode(bool enabled, float precision, bool updateRefPoint = false) {
		if (enabled) {
			if (!ccFineMode) {
				for (int id = 0; id < MAX_CHANNELS; id++) {
					if (midiParam[id].paramQuantity) {
						midiParam[id].precProcessor.init(midiParam[id].getLimitMin(), midiParam[id].getLimitMax());
					}
				}
			}
			for (int id = 0; id < MAX_CHANNELS; id++) {
				if (midiParam[id].paramQuantity) {
					midiParam[id].precProcessor.setPrecision(precision, midiParam[id].getRawValue(), updateRefPoint ? ccs[id].getValue() : -1);
				}
			}
			ccFineMode = true;
		}
		else {
			ccFineMode = false;
		}
	}

	MidiCatParam::CLOCKMODE getClockMode(int id) {
		return midiParam[id].clockMode;
	}

	void setClockMode(int id, MidiCatParam::CLOCKMODE mode) {
		if (mode != MidiCatParam::CLOCKMODE::OFF) {
			if (ccs[id].getCc() >= 0 && (ccs[id].ccMode == CCMODE::PICKUP1 || ccs[id].ccMode == CCMODE::PICKUP2)) {
				ccs[id].ccMode = CCMODE::DIRECT;
			}
			if (notes[id].getNote() >= 0 && (notes[id].noteMode == NOTEMODE::MOMENTARY || notes[id].noteMode == NOTEMODE::MOMENTARY_VEL)) {
				notes[id].noteMode = NOTEMODE::TOGGLE;
			}
			midiParam[id].setSlew(0.f);
		}
		midiParam[id].clockMode = mode;
	}
};


struct MidiCatCurveMenuItem : CurveMenuItem {
	MidiCatParam* p;
	MidiCatCurveMenuItem(MidiCatParam* p) {
		this->p = p;
	}
	float getCurveValue() override {
		return p->getCurve();
	}
	void setCurveValue(float v) override {
		p->setCurve(v);
	}
};

struct ScalingInputLabel : MenuLabelEx {
	MidiCatParam* p;
	void step() override {
		float min = std::min(p->getMin(), p->getMax());
		float max = std::max(p->getMin(), p->getMax());

		float g1 = rescale(0.f, min, max, p->limitMin, p->limitMax);
		g1 = clamp(g1, p->limitMin, p->limitMax);
		int g1a = std::round(g1);
		float g2 = rescale(1.f, min, max, p->limitMin, p->limitMax);
		g2 = clamp(g2, p->limitMin, p->limitMax);
		int g2a = std::round(g2);

		rightText = string::f("[%i, %i]", g1a, g2a);
	}
}; // struct ScalingInputLabel

struct ScalingOutputLabel : MenuLabelEx {
	MidiCatParam* p;
	void step() override {
		float min = p->getMin();
		float max = p->getMax();

		float f1 = rescale(p->limitMin, p->limitMin, p->limitMax, min, max);
		f1 = clamp(f1, 0.f, 1.f) * 100.f;
		float f2 = rescale(p->limitMax, p->limitMin, p->limitMax, min, max);
		f2 = clamp(f2, 0.f, 1.f) * 100.f;

		rightText = string::f("[%.1f%%, %.1f%%]", f1, f2);
	}
}; // struct ScalingOutputLabel


struct MidiCatSelectionWidget : Widget {
	enum class LEARN_MODE {
		OFF = 0,
		CLEAR = 1,
		APPEND = 2
	};

	MidiCatModule* module;

	LEARN_MODE learnMode = LEARN_MODE::OFF;
	bool bindLights = false;

	bool selecting = false;
	math::Vec selectionStart;
	math::Vec selectionEnd;
	math::Vec mousePos;

	void enableLearn(LEARN_MODE mode, bool bindLights = false) {
		learnMode = learnMode == LEARN_MODE::OFF ? mode : LEARN_MODE::OFF;
		this->bindLights = bindLights;
		GLFWcursor* cursor = glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR);
		if (APP->window) glfwSetCursor(APP->window->win, cursor);
	}

	void onHover(const HoverEvent& e) override {
		mousePos = e.pos;
		Widget::onHover(e);
	}

	void onButton(const ButtonEvent& e) override {
		if (learnMode != LEARN_MODE::OFF && e.button == GLFW_MOUSE_BUTTON_LEFT) {
			e.consume(this);
		}
		Widget::onButton(e);
	}

	void onDragStart(const DragStartEvent& e) override {
		if (learnMode != LEARN_MODE::OFF && e.button == GLFW_MOUSE_BUTTON_LEFT) {
			selecting = true;
			selectionStart = mousePos;
			selectionEnd = mousePos;
			e.consume(this);
		}
		Widget::onDragStart(e);
	}

	void onDragEnd(const DragEndEvent& e) override {
		if (selecting && e.button == GLFW_MOUSE_BUTTON_LEFT) {
			mapParamsFromRect();
			selecting = false;
			learnMode = LEARN_MODE::OFF;
			if (APP->window) glfwSetCursor(APP->window->win, NULL);
			e.consume(this);
		}
		Widget::onDragEnd(e);
	}

	void onDragHover(const DragHoverEvent& e) override {
		mousePos = e.pos;
		if (selecting) {
			selectionEnd = mousePos;
		}
		Widget::onDragHover(e);
	}

	void mapParamsFromRect() {
		math::Rect selectionBox = math::Rect::fromCorners(selectionStart, selectionEnd);
		std::list<ModuleWidget*> selected;
		for (ModuleWidget* mw : APP->scene->rack->getModules()) {
			if (selectionBox.intersects(mw->box)) {
				selected.push_back(mw);
			}
		}
		if (selected.size() != 1) {
			return;
		}

		ModuleWidget* mw = selected.front();
		std::list<ParamWidget*> selectedParams;
		math::Rect selectionBox1(selectionBox.pos.minus(mw->box.pos), selectionBox.size);
		getAllDescendentsByTypeAndBox<ParamWidget*>(mw, selectionBox1, selectedParams);

		if (learnMode == LEARN_MODE::CLEAR) {
			module->clearMaps_WithLock();
		}

		selectedParams.reverse();
		for (ParamWidget* pw : selectedParams) {
			int id = module->enableLearn(-1, true);
			module->learnParam(id, pw->module->getId(), pw->paramId);

			if (bindLights) {
				std::list<ModuleLightWidget*> lw;
				getAllDescendentsByTypeAndBox(mw, pw->box, lw);
				if (lw.size() == 1) {
					module->getMap(id).setLight(lw.front()->firstLightId, lw.front()->getNumColors());
				}
			}
		}
		module->disableLearn();
	}

	void draw(const DrawArgs& args) override {
		// Draw selection rectangle
		if (selecting) {
			nvgBeginPath(args.vg);
			math::Rect selectionBox = math::Rect::fromCorners(selectionStart, selectionEnd);
			nvgRect(args.vg, RECT_ARGS(selectionBox));
			nvgFillColor(args.vg, nvgRGBAf(0, 0, 1, 0.25));
			nvgFill(args.vg);
			nvgStrokeWidth(args.vg, 2.0);
			nvgStrokeColor(args.vg, nvgRGBAf(0, 0, 1, 0.5));
			nvgStroke(args.vg);
		}
	}

	template <class T>
	void getAllDescendentsByTypeAndBox(Widget* w, math::Rect selectionBox, std::list<T>& selected) {
		for (auto it = w->children.rbegin(); it != w->children.rend(); it++) {
			Widget* child = *it;
			// Filter child by visibility and position
			if (!child->visible)
				continue;
			if (!selectionBox.intersects(child->box))
				continue;

			math::Rect selectionBox1(selectionBox.pos.minus(child->box.pos), selectionBox.size);
			getAllDescendentsByTypeAndBox<T>(child, selectionBox1, selected);
			T t = dynamic_cast<T>(child);
			if (t) {
				selected.push_back(t);
			}
		}
	}
}; // struct MidiCatSelectionWidget

struct MidiCatChoice : MapModuleChoice<MAX_CHANNELS, MidiCatModule> {
	MidiCatChoice() {
		textOffset = Vec(6.f, 14.7f);
		color = nvgRGB(0xf0, 0xf0, 0xf0);
	}

	template <class T>
	T getFirstDescendentByPos(Widget* w, Vec pos) {
		for (auto it = w->children.rbegin(); it != w->children.rend(); it++) {
			Widget* child = *it;
			// Filter child by visibility and position
			if (!child->visible)
				continue;
			if (!child->box.contains(pos))
				continue;

			T t = dynamic_cast<T>(child);
			if (t) return t;

			Vec pos1 = pos.minus(child->box.pos);
			t = getFirstDescendentByPos<T>(child, pos1);
			if (t) return t;
		}
		return NULL;
	}

	void onDeselect(const event::Deselect& e) override {
		if (!module) return;
		if (!processEvents) return;

		// Light-binding was not triggerd by context menu, proceed with regular mapping
		if (module->learningLightId != id) {
			MapModuleChoice<MAX_CHANNELS, MidiCatModule>::onDeselect(e);
		}

		if (module->learningLightId == id || (module->learnedParam && id > 0 && module->getMap(id - 1).hasLight())) {
			ModuleLightWidget* lw = NULL;
			Widget* w = APP->event->getDraggedWidget();
			if (w) {
				ModuleWidget* mw = w->getAncestorOfType<ModuleWidget>();
				if (mw) {
					Vec pos = APP->scene->rack->getMousePos().minus(mw->box.pos);
					lw = getFirstDescendentByPos<ModuleLightWidget*>(mw, pos);
				}
			}
			commitLearnLight(lw, module->learningLightId == id);
			e.consume(this);
		}
	}

	void step() override {
		MapModuleChoice<MAX_CHANNELS, MidiCatModule>::step();
		if (module && module->learningLightId == id) {
			text = getSlotPrefix() + "Binding LED...";
		}
	}

	void commitLearnLight(ModuleLightWidget* lw, bool keepBinding = true) {
		if (lw && lw->module == module->paramHandles[id].module) {
			module->getMap(id).setLight(lw->firstLightId, lw->getNumColors());
		}

		if (keepBinding) {
			// Find next slot for binding an LED
			int learningId = id;
			while (++learningId < MAX_CHANNELS) {
				if (module->paramHandles[learningId].moduleId >= 0 && !module->getMap(learningId).hasLight()) {
					module->learningLightId = learningId;
					module->enableLearn(learningId);
					return;
				}
			}

			disableLearnLight();
		}
	}

	void enableLearnLight() {
		module->learningLightId = id;
		module->enableLearn(id);
		APP->event->setSelectedWidget(this);
		GLFWcursor* cursor = glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR);
		if (APP->window) glfwSetCursor(APP->window->win, cursor);
	}

	void disableLearnLight() {
		module->learningLightId = -1;
		module->disableLearn();
		if (APP->window) glfwSetCursor(APP->window->win, NULL);
	}

	std::string getSlotPrefix() override {
		static const char* noteNames[] = {
			" C", "C#", " D", "D#", " E", " F", "F#", " G", "G#", " A", "A#", " B"
		};
		if (module) {
			char light = ' ';
			if (module->getMap(id).hasLight()) {
				light = '*';
			}
			if (module->ccs[id].getCc() >= 0) {
				return string::f("cc%02d%c", module->ccs[id].getCc(), light);
			}
			else if (module->notes[id].getNote() >= 0) {

				int oct = module->notes[id].getNote() / 12 - 1;
				int semi = module->notes[id].getNote() % 12;
				return string::f(" %s%d%c", noteNames[semi], oct, light);
			}
			else if (module->paramHandles[id].moduleId >= 0) {
				return string::f("....%c", light);
			}
			else {
				return "";
			}
		}
		else {
			// fake data for module browser
			return id % 2 == 0 ? string::f("cc%02d ", id) : string::f(" %s2 ", noteNames[id % 12]);
		}
	}

	std::string getSlotLabel() override {
		return module->textLabel[id];
	}

	void prependContextMenu(Menu* menu) override {
		menu->addChild(createSubmenuItem("MIDI-CAT Menu", "", [=](Menu* menu) {
			ModuleWidget* moduleWidget = APP->scene->rack->getModule(module->getId());
			moduleWidget->appendContextMenu(menu);
		}));
	}

	void appendContextMenu(Menu* menu) override {
		struct CcModeMenuItem : MenuItem {
			MidiCatModule* module;
			int id;

			CcModeMenuItem() {
				rightText = RIGHT_ARROW;
			}

			struct CcModeItem : MenuItem {
				MidiCatModule* module;
				int id;
				CCMODE ccMode;

				void onAction(const event::Action& e) override {
					module->ccs[id].ccMode = ccMode;
				}
				void step() override {
					rightText = module->ccs[id].ccMode == ccMode ? "✔" : "";
					MenuItem::step();
				}
			};

			Menu* createChildMenu() override {
				Menu* menu = new Menu;
				menu->addChild(construct<CcModeItem>(&MenuItem::text, "Direct", &CcModeItem::module, module, &CcModeItem::id, id, &CcModeItem::ccMode, CCMODE::DIRECT));
				menu->addChild(construct<CcModeItem>(&MenuItem::text, "Pickup (snap)", &CcModeItem::module, module, &CcModeItem::id, id, &CcModeItem::ccMode, CCMODE::PICKUP1));
				dynamic_cast<MenuItem*>(menu->children.back())->disabled = module->midiParam[id].clockMode != MidiCatParam::CLOCKMODE::OFF;
				menu->addChild(construct<CcModeItem>(&MenuItem::text, "Pickup (jump)", &CcModeItem::module, module, &CcModeItem::id, id, &CcModeItem::ccMode, CCMODE::PICKUP2));
				dynamic_cast<MenuItem*>(menu->children.back())->disabled = module->midiParam[id].clockMode != MidiCatParam::CLOCKMODE::OFF;
				menu->addChild(construct<CcModeItem>(&MenuItem::text, "Toggle", &CcModeItem::module, module, &CcModeItem::id, id, &CcModeItem::ccMode, CCMODE::TOGGLE));
				menu->addChild(construct<CcModeItem>(&MenuItem::text, "Toggle + Value", &CcModeItem::module, module, &CcModeItem::id, id, &CcModeItem::ccMode, CCMODE::TOGGLE_VALUE));
				menu->addChild(construct<CcModeItem>(&MenuItem::text, "Snapped", &CcModeItem::module, module, &CcModeItem::id, id, &CcModeItem::ccMode, CCMODE::SNAPPED));
				menu->addChild(construct<CcModeItem>(&MenuItem::text, "Snapped (short/long)", &CcModeItem::module, module, &CcModeItem::id, id, &CcModeItem::ccMode, CCMODE::SNAPPED_SL));
				return menu;
			}
		}; // struct CcModeMenuItem
		
		struct Cc14bitItem : MenuItem {
			MidiCatModule* module;
			int id;
			void onAction(const event::Action& e) override {
				module->ccs[id].set14bit(!module->ccs[id].get14bit());
			}
			void step() override {
				rightText = CHECKMARK(module->ccs[id].get14bit());
				MenuItem::step();
			}
		};

		struct NoteModeMenuItem : MenuItem {
			MidiCatModule* module;
			int id;

			NoteModeMenuItem() {
				rightText = RIGHT_ARROW;
			}

			struct NoteModeItem : MenuItem {
				MidiCatModule* module;
				int id;
				NOTEMODE noteMode;

				void onAction(const event::Action& e) override {
					module->notes[id].noteMode = noteMode;
				}
				void step() override {
					rightText = module->notes[id].noteMode == noteMode ? "✔" : "";
					MenuItem::step();
				}
			};

			Menu* createChildMenu() override {
				Menu* menu = new Menu;
				menu->addChild(construct<NoteModeItem>(&MenuItem::text, "Momentary", &NoteModeItem::module, module, &NoteModeItem::id, id, &NoteModeItem::noteMode, NOTEMODE::MOMENTARY));
				dynamic_cast<MenuItem*>(menu->children.back())->disabled = module->midiParam[id].clockMode != MidiCatParam::CLOCKMODE::OFF;
				menu->addChild(construct<NoteModeItem>(&MenuItem::text, "Momentary + Velocity", &NoteModeItem::module, module, &NoteModeItem::id, id, &NoteModeItem::noteMode, NOTEMODE::MOMENTARY_VEL));
				dynamic_cast<MenuItem*>(menu->children.back())->disabled = module->midiParam[id].clockMode != MidiCatParam::CLOCKMODE::OFF;
				menu->addChild(construct<NoteModeItem>(&MenuItem::text, "Toggle", &NoteModeItem::module, module, &NoteModeItem::id, id, &NoteModeItem::noteMode, NOTEMODE::TOGGLE));
				menu->addChild(construct<NoteModeItem>(&MenuItem::text, "Toggle + Velocity", &NoteModeItem::module, module, &NoteModeItem::id, id, &NoteModeItem::noteMode, NOTEMODE::TOGGLE_VEL));
				menu->addChild(construct<NoteModeItem>(&MenuItem::text, "Snapped", &NoteModeItem::module, module, &NoteModeItem::id, id, &NoteModeItem::noteMode, NOTEMODE::SNAPPED));
				menu->addChild(construct<NoteModeItem>(&MenuItem::text, "Snapped (short/long)", &NoteModeItem::module, module, &NoteModeItem::id, id, &NoteModeItem::noteMode, NOTEMODE::SNAPPED_SL));
				return menu;
			}
		}; // struct NoteModeMenuItem

		struct NoteVelZeroMenuItem : MenuItem {
			MidiCatModule* module;
			int id;

			void onAction(const event::Action& e) override {
				module->midiOptions[id] ^= 1UL << MIDIOPTION_VELZERO_BIT;
			}
			void step() override {
				rightText = CHECKMARK((module->midiOptions[id] >> MIDIOPTION_VELZERO_BIT) & 1U);
				MenuItem::step();
			}
		}; // struct NoteVelZeroMenuItem

		if (module->ccs[id].getCc() >= 0 || module->notes[id].getNote() >= 0) {
			menu->addChild(createMenuItem("Clear MIDI assignment", "", [=]() { module->clearMap(id, true); }));
		}

		menu->addChild(createMenuItem("Bind feedback to LED (experimental)", CHECKMARK(module->getMap(id).hasLight()), [this] { enableLearnLight(); }));
		if (module->midiParam[id].hasLight()) {
			menu->addChild(createMenuItem("Remove LED binding", "", [this] { module->getMap(id).setLight(); }));
		}

		if (module->ccs[id].getCc() >= 0) {
			menu->addChild(new MenuSeparator());
			menu->addChild(construct<CcModeMenuItem>(&MenuItem::text, "Input mode for CC", &CcModeMenuItem::module, module, &CcModeMenuItem::id, id));
			menu->addChild(construct<Cc14bitItem>(&MenuItem::text, "14-bit", &MenuItem::disabled, module->ccs[id].getCc() > 32, &Cc14bitItem::module, module, &Cc14bitItem::id, id));
		}
		if (module->notes[id].getNote() >= 0) {
			menu->addChild(new MenuSeparator());
			menu->addChild(construct<NoteModeMenuItem>(&MenuItem::text, "Input mode for notes", &NoteModeMenuItem::module, module, &NoteModeMenuItem::id, id));
			menu->addChild(construct<NoteVelZeroMenuItem>(&MenuItem::text, "Send \"note on, velocity 0\"", &NoteVelZeroMenuItem::module, module, &NoteVelZeroMenuItem::id, id));
		}

		struct PresetMenuItem : MenuItem {
			MidiCatModule* module;
			int id;
			PresetMenuItem() {
				rightText = RIGHT_ARROW;
			}

			Menu* createChildMenu() override {
				struct PresetItem : MenuItem {
					MidiCatParam* p;
					float min, max;
					void onAction(const event::Action& e) override {
						p->setMin(min);
						p->setMax(max);
					}
				};

				Menu* menu = new Menu;
				menu->addChild(construct<PresetItem>(&MenuItem::text, "Default", &PresetItem::p, &module->midiParam[id], &PresetItem::min, 0.f, &PresetItem::max, 1.f));
				menu->addChild(construct<PresetItem>(&MenuItem::text, "Inverted", &PresetItem::p, &module->midiParam[id], &PresetItem::min, 1.f, &PresetItem::max, 0.f));
				menu->addChild(construct<PresetItem>(&MenuItem::text, "Lower 50%", &PresetItem::p, &module->midiParam[id], &PresetItem::min, 0.f, &PresetItem::max, 0.5f));
				menu->addChild(construct<PresetItem>(&MenuItem::text, "Upper 50%", &PresetItem::p, &module->midiParam[id], &PresetItem::min, 0.5f, &PresetItem::max, 1.f));
				return menu;
			}
		}; // struct PresetMenuItem

		struct LabelMenuItem : MenuItem {
			MidiCatModule* module;
			int id;

			LabelMenuItem() {
				rightText = RIGHT_ARROW;
			}

			struct LabelField : ui::TextField {
				MidiCatModule* module;
				int id;
				void onSelectKey(const event::SelectKey& e) override {
					if (e.action == GLFW_PRESS && e.key == GLFW_KEY_ENTER) {
						module->textLabel[id] = text;

						ui::MenuOverlay* overlay = getAncestorOfType<ui::MenuOverlay>();
						overlay->requestDelete();
						e.consume(this);
					}

					if (!e.getTarget()) {
						ui::TextField::onSelectKey(e);
					}
				}
			};

			struct ResetItem : ui::MenuItem {
				MidiCatModule* module;
				int id;
				void onAction(const event::Action& e) override {
					module->textLabel[id] = "";
				}
			};

			Menu* createChildMenu() override {
				Menu* menu = new Menu;

				LabelField* labelField = new LabelField;
				labelField->placeholder = "Label";
				labelField->text = module->textLabel[id];
				labelField->box.size.x = 180;
				labelField->module = module;
				labelField->id = id;
				menu->addChild(labelField);

				ResetItem* resetItem = new ResetItem;
				resetItem->text = "Reset";
				resetItem->module = module;
				resetItem->id = id;
				menu->addChild(resetItem);

				return menu;
			}
		}; // struct LabelMenuItem

		menu->addChild(Rack::createSlider(
			[this]() { return module->midiParam[id].getSlew(); },
			[this](float v) { module->midiParam[id].setSlew(clamp(v, 0.f, 5.f)); },
			0.f, 5.f, 0.f, "Slew-limiting", "", 1.f, 220.0f
		));
		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Scaling"));
		std::string l = string::f("Input %s", module->ccs[id].getCc() >= 0 ? "MIDI CC" : (module->notes[id].getNote() >= 0 ? "MIDI vel" : ""));
		menu->addChild(construct<ScalingInputLabel>(&MenuLabel::text, l, &ScalingInputLabel::p, &module->midiParam[id]));
		menu->addChild(construct<ScalingOutputLabel>(&MenuLabel::text, "Parameter range", &ScalingOutputLabel::p, &module->midiParam[id]));
		menu->addChild(Rack::createSlider(
			[this]() { return module->midiParam[id].getMin(); },
			[this](float v) { module->midiParam[id].setMin(v); },
			-1.f, 2.f, 0.f, "Low", "%", 100.f, 220.0f
		));
		menu->addChild(Rack::createSlider(
			[this]() { return module->midiParam[id].getMax(); },
			[this](float v) { module->midiParam[id].setMax(v); },
			-1.f, 2.f, 1.f, "High", "%", 100.f, 220.0f
		));
		menu->addChild(construct<PresetMenuItem>(&MenuItem::text, "Presets", &PresetMenuItem::module, module, &PresetMenuItem::id, id));
		menu->addChild(new MidiCatCurveMenuItem(&module->midiParam[id]));
		menu->addChild(new MenuSeparator());
		menu->addChild(construct<LabelMenuItem>(&MenuItem::text, "Custom label", &LabelMenuItem::module, module, &LabelMenuItem::id, id));

		if (module->expClk != NULL) {
			menu->addChild(new MenuSeparator());
			menu->addChild(createMenuLabel("CLK-expander"));
			if (!module->getMap(id).hasLight()) {
				menu->addChild(StoermelderPackOne::Rack::createMapSubmenuItem<MidiCatParam::CLOCKMODE>("Quantization",
					{
						{ MidiCatParam::CLOCKMODE::OFF, "Off" },
						{ MidiCatParam::CLOCKMODE::ARM, "On (instant feedback)" },
						{ MidiCatParam::CLOCKMODE::ARM_DEFERRED_FEEDBACK, "On (deferred feedback)" }
					},
					[=]() { return module->getClockMode(id); },
					[=](MidiCatParam::CLOCKMODE v) { module->setClockMode(id, v); }
				));
			}
			else {
				menu->addChild(StoermelderPackOne::Rack::createMapSubmenuItem<MidiCatParam::CLOCKMODE>("Quantization",
					{
						{ MidiCatParam::CLOCKMODE::OFF, "Off" },
						{ MidiCatParam::CLOCKMODE::ARM, "On" },
					},
					[=]() { return module->getClockMode(id); },
					[=](MidiCatParam::CLOCKMODE v) { module->setClockMode(id, v); }
				));
			}
			menu->addChild(StoermelderPackOne::Rack::createMapPtrSubmenuItem("Source",
				{
					{ 0, "Clock 1" },
					{ 1, "Clock 2" },
					{ 2, "Clock 3" },
					{ 3, "Clock 4" }
				},
				&module->midiParam[id].clockSource
			));
		}
	}
};

struct MidiCatDisplay : MapModuleDisplay<MAX_CHANNELS, MidiCatModule, MidiCatChoice>, OverlayMessageProvider {
	void step() override {
		if (module) {
			int mapLen = module->mapLen;
			for (int id = 1; id < MAX_CHANNELS; id++) {
				choices[id]->visible = (id < mapLen);
				separators[id]->visible = (id < mapLen);
			}
		}
		MapModuleDisplay<MAX_CHANNELS, MidiCatModule, MidiCatChoice>::step();
	}

	int nextOverlayMessageId() override {
		if (module->overlayQueue.empty())
			return -1;
		return module->overlayQueue.shift();
	}

	void getOverlayMessage(int id, Message& m) override {
		ParamQuantity* paramQuantity = choices[id]->getParamQuantity();
		if (!paramQuantity) return;

		std::string label = choices[id]->getSlotLabel();
		if (label == "") label = paramQuantity->name;

		m.title = paramQuantity->getDisplayValueString() + paramQuantity->getUnit();
		m.subtitle[0] = paramQuantity->module->model->name;
		m.subtitle[1] = label;
	}
};

struct MidiCatBaseWidget : ThemedModuleWidget<MidiCatModule>, ParamWidgetContextExtender {
	MidiCatModule* module;
	MidiCatDisplay* mapWidget;
	MidiCatSelectionWidget* selectionWidget = NULL;

	MidiCatMemBase* expMem;
	BufferedSwitchQuantity* expMemPrevQuantity;
	BufferedSwitchQuantity* expMemNextQuantity;
	BufferedSwitchQuantity* expMemParamQuantity;

	MidiCatCtxBase* expCtx;
	BufferedSwitchQuantity* expCtxMapQuantity;

	enum class LEARN_MODE {
		OFF = 0,
		BIND_CLEAR = 1,
		BIND_KEEP = 2,
		MEM = 3
	};

	LEARN_MODE learnMode = LEARN_MODE::OFF;

	MidiCatBaseWidget(MidiCatModule* module, std::string baseName)
		: ThemedModuleWidget<MidiCatModule>(module, baseName, "MidiCat") {
		setModule(module);
		this->module = module;

		addChild(createWidget<StoermelderBlackScrew>(Vec(RACK_GRID_WIDTH, 0)));
		addChild(createWidget<StoermelderBlackScrew>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
		addChild(createWidget<StoermelderBlackScrew>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
		addChild(createWidget<StoermelderBlackScrew>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

		if (module) {
			selectionWidget = new MidiCatSelectionWidget;
			selectionWidget->module = module;
			APP->scene->rack->addChild(selectionWidget);
		}
	}

	~MidiCatBaseWidget() {
		if (learnMode != LEARN_MODE::OFF && APP->window) {
			if (APP->window) glfwSetCursor(APP->window->win, NULL);
		}

		if (selectionWidget) {
			APP->scene->rack->removeChild(selectionWidget);
			delete selectionWidget;
		}
	}

	void loadMidiMapPreset_dialog() {
		osdialog_filters* filters = osdialog_filters_parse(PRESET_FILTERS);
		DEFER({
			osdialog_filters_free(filters);
		});

		char* path = osdialog_file(OSDIALOG_OPEN, "", NULL, filters);
		if (!path) {
			// No path selected
			return;
		}
		DEFER({
			free(path);
		});

		loadMidiMapPreset_action(path);
	}

	void loadMidiMapPreset_action(std::string filename) {
		INFO("Loading preset %s", filename.c_str());

		FILE* file = fopen(filename.c_str(), "r");
		if (!file) {
			WARN("Could not load patch file %s", filename.c_str());
			return;
		}
		DEFER({
			fclose(file);
		});

		json_error_t error;
		json_t* moduleJ = json_loadf(file, 0, &error);
		if (!moduleJ) {
			std::string message = string::f("File is not a valid patch file. JSON parsing error at %s %d:%d %s", error.source, error.line, error.column, error.text);
			osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str());
			return;
		}
		DEFER({
			json_decref(moduleJ);
		});

		if (!loadMidiMapPreset_convert(moduleJ))
			return;

		// history::ModuleChange
		history::ModuleChange* h = new history::ModuleChange;
		h->name = "load module preset";
		h->moduleId = module->id;
		h->oldModuleJ = toJson();

		module->fromJson(moduleJ);

		h->newModuleJ = toJson();
		APP->history->push(h);
	}

	bool loadMidiMapPreset_convert(json_t* moduleJ) {
		std::string pluginSlug = json_string_value(json_object_get(moduleJ, "plugin"));
		std::string modelSlug = json_string_value(json_object_get(moduleJ, "model"));

		// Only handle presets for MIDI-Map
		if (!(pluginSlug == "Core" && modelSlug == "MIDI-Map"))
			return false;

		json_object_set_new(moduleJ, "plugin", json_string(module->model->plugin->slug.c_str()));
		json_object_set_new(moduleJ, "model", json_string(module->model->slug.c_str()));
		json_t* dataJ = json_object_get(moduleJ, "data");
		json_object_set(dataJ, "midiInput", json_object_get(dataJ, "midi"));
		return true;
	}

	void step() override {
		ThemedModuleWidget<MidiCatModule>::step();
		if (module) {
			// MEM-expander
			auto expMem_ = module->expMem.load();
			if (expMem_ != expMem) {
				expMem = expMem_;
				if (expMem) {
					expMemPrevQuantity = dynamic_cast<BufferedSwitchQuantity*>(expMem->paramQuantities[1]);
					expMemPrevQuantity->resetBuffer();
					expMemNextQuantity = dynamic_cast<BufferedSwitchQuantity*>(expMem->paramQuantities[2]);
					expMemNextQuantity->resetBuffer();
					expMemParamQuantity = dynamic_cast<BufferedSwitchQuantity*>(expMem->paramQuantities[0]);
					expMemParamQuantity->resetBuffer();
				}
			}
			if (expMem) {
				if (expMemPrevQuantity->getBuffer()) {
					expMemPrevQuantity->resetBuffer();
					expMemPrevModule();
				}
				if (expMemNextQuantity->getBuffer()) {
					expMemNextQuantity->resetBuffer();
					expMemNextModule();
				}
				if (expMemParamQuantity->getBuffer()) {
					expMemParamQuantity->resetBuffer();
					enableLearn(LEARN_MODE::MEM);
				}
				expMem->lights[0].setBrightness(learnMode == LEARN_MODE::MEM);
			}

			// CTX-expander
			auto expCtx_ = module->expCtx.load();
			if (expCtx_ != expCtx) {
				expCtx = expCtx_;
				if (expCtx) {
					expCtxMapQuantity = dynamic_cast<BufferedSwitchQuantity*>(expCtx->paramQuantities[0]);
					expCtxMapQuantity->resetBuffer();
				}
			}
			if (expCtx) {
				if (expCtxMapQuantity->getBuffer()) {
					expCtxMapQuantity->resetBuffer();
					module->enableLearn(-1, true);
				}
			}

			module->taskProcessorUi.process();
		}

		ParamWidgetContextExtender::step();
	}

	void expMemPrevModule() {
		std::vector<ModuleWidget*> modules = APP->scene->rack->getModules();
		auto sort = [&](Widget* w1, Widget* w2) {
			auto t1 = std::make_tuple(w1->box.pos.y, w1->box.pos.x);
			auto t2 = std::make_tuple(w2->box.pos.y, w2->box.pos.x);
			return t1 > t2;
		};
		std::sort(modules.begin(), modules.end(), sort);
		expMemScanModules(modules);
	}

	void expMemNextModule() {
		std::vector<ModuleWidget*> modules = APP->scene->rack->getModules();
		auto sort = [&](Widget* w1, Widget* w2) {
			auto t1 = std::make_tuple(w1->box.pos.y, w1->box.pos.x);
			auto t2 = std::make_tuple(w2->box.pos.y, w2->box.pos.x);
			return t1 < t2;
		};
		std::sort(modules.begin(), modules.end(), sort);
		expMemScanModules(modules);
	}

	void expMemScanModules(std::vector<ModuleWidget*>& modules) {
		f:
		std::vector<ModuleWidget*>::iterator it = modules.begin();
		// Scan for current module in the list
		if (module->expMemModuleId != -1) {
			for (; it != modules.end(); it++) {
				ModuleWidget* mw = *it;
				Module* m = mw->module;
				if (m->id == module->expMemModuleId) {
					it++;
					break;
				}
			}
			// Module not found
			if (it == modules.end()) {
				it = modules.begin();
			}
		}
		// Scan for next module with stored mapping
		for (; it != modules.end(); it++) {
			ModuleWidget* mw = *it;
			Module* m = mw->module;
			if (module->expMemTest(m)) {
				module->expMemApply(m);
				return;
			}
		}
		// No module found yet -> retry from the beginning
		if (module->expMemModuleId != -1) {
			module->expMemModuleId = -1;
			goto f;
		}
	}

	void extendParamWidgetContextMenu(ParamWidget* pw, Menu* menu) override {
		if (!module) return;
		if (module->learningId >= 0) return;
		ParamQuantity* pq = pw->getParamQuantity();
		if (!pq) return;
		
		struct MidiCatBeginItem : MenuLabel {
			MidiCatBeginItem() {
				text = "MIDI-CAT";
			}
		};

		struct MidiCatEndItem : MenuEntry {
			MidiCatEndItem() {
				box.size = Vec();
			}
		};

		struct MapMenuItem : MenuItem {
			MidiCatModule* module;
			ParamQuantity* pq;
			int currentId = -1;

			MapMenuItem() {
				rightText = RIGHT_ARROW;
			}

			Menu* createChildMenu() override {
				struct MapItem : MenuItem {
					MidiCatModule* module;
					int currentId;
					void onAction(const event::Action& e) override {
						module->enableLearn(currentId, true);
					}
				};

				struct MapEmptyItem : MenuItem {
					MidiCatModule* module;
					ParamQuantity* pq;
					void onAction(const event::Action& e) override {
						int id = module->enableLearn(-1, true);
						if (id >= 0) module->learnParam(id, pq->module->id, pq->paramId);
					}
				};

				struct RemapItem : MenuItem {
					MidiCatModule* module;
					ParamQuantity* pq;
					int id;
					int currentId;
					void onAction(const event::Action& e) override {
						module->learnParam(id, pq->module->id, pq->paramId, false);
					}
					void step() override {
						rightText = CHECKMARK(id == currentId);
						MenuItem::step();
					}
				};

				Menu* menu = new Menu;
				if (currentId < 0) {
					menu->addChild(construct<MapEmptyItem>(&MenuItem::text, "Learn MIDI", &MapEmptyItem::module, module, &MapEmptyItem::pq, pq));
				}
				else {
					menu->addChild(construct<MapItem>(&MenuItem::text, "Learn MIDI", &MapItem::module, module, &MapItem::currentId, currentId));
				}

				if (module->mapLen > 0) {
					menu->addChild(new MenuSeparator);
					for (int i = 0; i < module->mapLen; i++) {
						if (module->ccs[i].getCc() >= 0 || module->notes[i].getNote() >= 0) {
							std::string text;
							if (module->textLabel[i] != "") {
								text = module->textLabel[i];
							}
							else if (module->ccs[i].getCc() >= 0) {
								text = string::f("MIDI CC %02d", module->ccs[i].getCc());
							}
							else {
								static const char* noteNames[] = {
									"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
								};
								int oct = module->notes[i].getNote() / 12 - 1;
								int semi = module->notes[i].getNote() % 12;
								text = string::f("MIDI note %s%d", noteNames[semi], oct);
							}
							menu->addChild(construct<RemapItem>(&MenuItem::text, text, &RemapItem::module, module, &RemapItem::pq, pq, &RemapItem::id, i, &RemapItem::currentId, currentId));
						}
					}
				}
				return menu;
			} 
		};

		std::list<Widget*>::iterator beg = menu->children.begin();
		std::list<Widget*>::iterator end = menu->children.end();
		std::list<Widget*>::iterator itCvBegin = end;
		std::list<Widget*>::iterator itCvEnd = end;
		
		for (auto it = beg; it != end; it++) {
			if (itCvBegin == end) {
				MidiCatBeginItem* ml = dynamic_cast<MidiCatBeginItem*>(*it);
				if (ml) { itCvBegin = it; continue; }
			}
			else {
				MidiCatEndItem* ml = dynamic_cast<MidiCatEndItem*>(*it);
				if (ml) { itCvEnd = it; break; }
			}
		}

		for (int id = 0; id < module->mapLen; id++) {
			if (module->paramHandles[id].moduleId == pq->module->id && module->paramHandles[id].paramId == pq->paramId) {
				std::string midiCatId = expCtx ? "on \"" + expCtx->getMidiCatId() + "\"" : "";
				std::list<Widget*> w;
				w.push_back(construct<MapMenuItem>(&MenuItem::text, string::f("Re-map %s", midiCatId.c_str()), &MapMenuItem::module, module, &MapMenuItem::pq, pq, &MapMenuItem::currentId, id));
				w.push_back(Rack::createSlider(
					[this, id]() { return module->midiParam[id].getSlew(); },
					[this, id](float v) { module->midiParam[id].setSlew(clamp(v, 0.f, 5.f)); },
					0.f, 5.f, 0.f, "Slew-limiting", "", 1.f, 220.0f
				));
				w.push_back(construct<MenuLabel>(&MenuLabel::text, "Scaling"));
				std::string l = string::f("Input %s", module->ccs[id].getCc() >= 0 ? "MIDI CC" : (module->notes[id].getNote() >= 0 ? "MIDI vel" : ""));
				w.push_back(construct<ScalingInputLabel>(&MenuLabel::text, l, &ScalingInputLabel::p, &module->midiParam[id]));
				w.push_back(construct<ScalingOutputLabel>(&MenuLabel::text, "Parameter range", &ScalingOutputLabel::p, &module->midiParam[id]));
				w.push_back(Rack::createSlider(
					[this, id]() { return module->midiParam[id].getMin(); },
					[this, id](float v) { module->midiParam[id].setMin(v); },
					-1.f, 2.f, 0.f, "Low", "%", 100.f, 220.0f
				));
				w.push_back(Rack::createSlider(
					[this, id]() { return module->midiParam[id].getMax(); },
					[this, id](float v) { module->midiParam[id].setMax(v); },
					-1.f, 2.f, 1.f, "High", "%", 100.f, 220.0f
				));
				w.push_back(new MidiCatCurveMenuItem(&module->midiParam[id]));
				w.push_back(construct<CenterModuleItem>(&MenuItem::text, "Go to mapping module", &CenterModuleItem::mw, this));
				w.push_back(new MidiCatEndItem);

				if (itCvBegin == end) {
					menu->addChild(new MenuSeparator);
					menu->addChild(construct<MidiCatBeginItem>());
					for (Widget* wm : w) {
						menu->addChild(wm);
					}
				}
				else {
					for (auto i = w.rbegin(); i != w.rend(); ++i) {
						Widget* wm = *i;
						menu->addChild(wm);
						auto it = std::prev(menu->children.end());
						menu->children.splice(std::next(itCvBegin), menu->children, it);
					}
				}
				return;
			}
		}

		if (expCtx) {
			std::string midiCatId = expCtx->getMidiCatId();
			if (midiCatId != "") {
				MenuItem* mapMenuItem = construct<MapMenuItem>(&MenuItem::text, string::f("Map on \"%s\"", midiCatId.c_str()), &MapMenuItem::module, module, &MapMenuItem::pq, pq);
				if (itCvBegin == end) {
					menu->addChild(new MenuSeparator);
					menu->addChild(construct<MidiCatBeginItem>());
					menu->addChild(mapMenuItem);
				}
				else {
					menu->addChild(mapMenuItem);
					auto it = std::find(beg, end, mapMenuItem);
					menu->children.splice(std::next(itCvEnd == end ? itCvBegin : itCvEnd), menu->children, it);
				}
			}
		}
	}

	void onDeselect(const event::Deselect& e) override {
		ModuleWidget::onDeselect(e);
		if (learnMode != LEARN_MODE::OFF) {
			DEFER({
				disableLearn();
			});

			// Learn module
			Widget* w = APP->event->getDraggedWidget();
			if (!w) return;
			ModuleWidget* mw = dynamic_cast<ModuleWidget*>(w);
			if (!mw) mw = w->getAncestorOfType<ModuleWidget>();
			if (!mw || mw == this) return;
			Module* m = mw->module;
			if (!m) return;

			MidiCatModule* module = dynamic_cast<MidiCatModule*>(this->module);
			switch (learnMode) {
				case LEARN_MODE::BIND_CLEAR:
					module->moduleBind(m, false); break;
				case LEARN_MODE::BIND_KEEP:
					module->moduleBind(m, true); break;
				case LEARN_MODE::MEM:
					module->expMemApply(m); break;
				case LEARN_MODE::OFF:
					break;
			}
		}
	}

	void onHoverKey(const event::HoverKey& e) override {
		if (e.action == GLFW_PRESS) {
			switch (e.key) {
				case GLFW_KEY_D: {
					if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) {
						enableLearn(LEARN_MODE::BIND_KEEP);
					}
					if ((e.mods & RACK_MOD_MASK) == (GLFW_MOD_SHIFT | RACK_MOD_CTRL)) {
						enableLearn(LEARN_MODE::BIND_CLEAR);
					}
					break;
				}
				case GLFW_KEY_E: {
					if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) {
						MidiCatModule* module = dynamic_cast<MidiCatModule*>(this->module);
						module->moduleBindExpander(true);
					}
					if ((e.mods & RACK_MOD_MASK) == (GLFW_MOD_SHIFT | RACK_MOD_CTRL)) {
						MidiCatModule* module = dynamic_cast<MidiCatModule*>(this->module);
						module->moduleBindExpander(false);
					}
					break;
				}
				case GLFW_KEY_V: {
					if ((e.mods & RACK_MOD_MASK) == GLFW_MOD_SHIFT) {
						enableLearn(LEARN_MODE::MEM);
					}
					break;
				}
				case GLFW_KEY_ESCAPE: {
					MidiCatModule* module = dynamic_cast<MidiCatModule*>(this->module);
					disableLearn();
					module->disableLearn();
					e.consume(this);
					break;
				}
				case GLFW_KEY_SPACE: {
					if (module->learningId >= 0) {
						MidiCatModule* module = dynamic_cast<MidiCatModule*>(this->module);
						module->enableLearn(module->learningId + 1);
						if (module->learningId == -1) disableLearn();
						e.consume(this);
					}
					break;
				}
			}
		}
		ThemedModuleWidget<MidiCatModule>::onHoverKey(e);
	}

	void enableLearn(LEARN_MODE mode) {
		learnMode = learnMode == LEARN_MODE::OFF ? mode : LEARN_MODE::OFF;
		APP->event->setSelectedWidget(this);
		GLFWcursor* cursor = NULL;
		if (learnMode != LEARN_MODE::OFF) {
			cursor = glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR);
		}
		if (APP->window) glfwSetCursor(APP->window->win, cursor);
	}

	void disableLearn() {
		learnMode = LEARN_MODE::OFF;
		if (APP->window) glfwSetCursor(APP->window->win, NULL);
	}

	void appendContextMenu(Menu* menu) override {
		int menuSize = menu->children.size();
		if (menuSize > 0) {
			menu->addChild(new MenuSeparator());
		}
		menu->addChild(createSubmenuItem("MIDI Input", "", [=](Menu* menu) { appendMidiMenu(menu, &module->midiInput); }));
		menu->addChild(createSubmenuItem("MIDI Output", "", [=](Menu* menu) { appendMidiMenu(menu, &module->midiOutput); }));
		/*
		menu->addChild(createMenuItem("Reset", "", [=]() {
			module->midiReset();
		}));
		*/

		if (menuSize > 0) {
			ThemedModuleWidget<MidiCatModule>::appendContextMenu(menu);
		}

		int sampleRate = int(APP->engine->getSampleRate());
		menu->addChild(new MenuSeparator());
		menu->addChild(createSubmenuItem("Preset load", "",
			[=](Menu* menu) {
				menu->addChild(createBoolPtrMenuItem("Ignore MIDI devices", "", &module->midiIgnoreDevices));
				menu->addChild(createBoolPtrMenuItem("Clear mapping slots", "", &module->clearMapsOnLoad));
			}
		));
		menu->addChild(StoermelderPackOne::Rack::createMapSubmenuItem<int>("Precision", {
				{ 1, string::f("Samplerate (%i Hz)", sampleRate / 1) },
				{ 8, string::f("High (%i Hz)", sampleRate / 8) },
				{ 64, string::f("Moderate (%i Hz)", sampleRate / 64) },
				{ 256, string::f("Lowest (%i Hz)", sampleRate / 256) }
			},
			[=]() {
				return module->processDivision;
			},
			[=](int division) {
				module->setProcessDivision(division);
			}
		));
		if (settings::isPlugin) {
			menu->addChild(createBoolMenuItem("Report parameter changes", "", 
				[=]() {
					return module->parameterChangesDirect;
				},
				[=](bool b) {
					module->setParameterChangesDirect(b);
				}
			));
		}
		menu->addChild(StoermelderPackOne::Rack::createMapSubmenuItem<MIDIMODE>("Mode", {
				{ MIDIMODE::MIDIMODE_DEFAULT, "Operating" },
				{ MIDIMODE::MIDIMODE_LOCATE, "Locate and indicate" }
			},
			[=]() {
				return module->midiMode;
			},
			[=](MIDIMODE midiMode) {
				module->setMode(midiMode);
			}
		));
		menu->addChild(createSubmenuItem("Re-send MIDI feedback", "",
			[=](Menu* menu) {
				menu->addChild(createMenuItem("Now", "", [=]() { module->midiResendFeedback(); }));
				menu->addChild(createBoolPtrMenuItem("Periodically", "", &module->midiResendPeriodically));
			}
		));
		menu->addChild(createMenuItem("Import MIDI-MAP preset", "", [=]() { loadMidiMapPreset_dialog(); }));

		menu->addChild(new MenuSeparator());
		menu->addChild(createSubmenuItem("User interface", "",
			[=](Menu* menu) {
				menu->addChild(createBoolPtrMenuItem("Text scrolling", "", &module->textScrolling));
				menu->addChild(createBoolPtrMenuItem("Lock mapping slots", "", &module->locked));
				menu->addChild(new MenuSeparator);
				menu->addChild(createMenuLabel("Mapping indicators"));
				menu->addChild(createBoolPtrMenuItem("Hide", "", &module->mappingIndicatorHidden));
				menu->addChild(construct<MenuColorLabel>(&MenuColorLabel::fillColor, &module->mappingIndicatorColor));
				menu->addChild(construct<MenuColorPicker>(&MenuColorPicker::color, &module->mappingIndicatorColor));
				menu->addChild(createMenuItem("Reset color", "", [=]() {
					module->mappingIndicatorColor = MAPPING_INDICATOR_COLOR_DEFAULT;
				}));
			}
		));
		menu->addChild(createBoolPtrMenuItem("Status overlay", "", &module->overlayEnabled));

		menu->addChild(new MenuSeparator());
		menu->addChild(createMenuItem("Clear all mappings", "", [=]() { module->clearMaps_WithLock(); }));
		menu->addChild(createSubmenuItem("Map module (left)", "",
			[=](Menu* menu) {
				menu->addChild(createMenuItem("Clear first", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+E", [=]() { module->moduleBindExpander(false); }));
				menu->addChild(createMenuItem("Keep MIDI assignments", RACK_MOD_SHIFT_NAME "+E", [=]() { module->moduleBindExpander(true); }));
			}
		));
		menu->addChild(createSubmenuItem("Map module (select)", "",
			[=](Menu* menu) {
				menu->addChild(createMenuItem("Clear first", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+D", [=]() { enableLearn(LEARN_MODE::BIND_CLEAR); }));
				menu->addChild(createMenuItem("Keep MIDI assignments", RACK_MOD_SHIFT_NAME "+D", [=]() { enableLearn(LEARN_MODE::BIND_KEEP); }));
			}
		));
		menu->addChild(createSubmenuItem("Map parameters by selection", "",
			[=](Menu* menu) {
				menu->addChild(createMenuItem("Clear first", "", [=]() { selectionWidget->enableLearn(MidiCatSelectionWidget::LEARN_MODE::CLEAR); }));
				menu->addChild(createMenuItem("Clear first, bind LEDs (experimental)", "", [=]() { selectionWidget->enableLearn(MidiCatSelectionWidget::LEARN_MODE::CLEAR, true); }));
				menu->addChild(createMenuItem("Append", "", [=]() { selectionWidget->enableLearn(MidiCatSelectionWidget::LEARN_MODE::APPEND); }));
				menu->addChild(createMenuItem("Append, bind LEDs (experimental)", "", [=]() { selectionWidget->enableLearn(MidiCatSelectionWidget::LEARN_MODE::APPEND, true); }));
			}
		));

		if (module->expMem != NULL) {
			appendContextMenuMem(menu);
		}
	}

	void appendContextMenuMem(Menu* menu) {
		MidiCatModule* module = dynamic_cast<MidiCatModule*>(this->module);
		assert(module);

		struct MapMenuItem : MenuItem {
			MidiCatModule* module;
			MapMenuItem() {
				rightText = RIGHT_ARROW;
			}

			Menu* createChildMenu() override {
				struct MidimapModuleItem : MenuItem {
					MidiCatModule* module;
					std::string pluginSlug;
					std::string moduleSlug;
					MemModule* midimapModule;
					MidimapModuleItem() {
						rightText = RIGHT_ARROW;
					}
					Menu* createChildMenu() override {
						struct DeleteItem : MenuItem {
							MidiCatModule* module;
							std::string pluginSlug;
							std::string moduleSlug;
							void onAction(const event::Action& e) override {
								module->expMemDelete(pluginSlug, moduleSlug);
							}
						}; // DeleteItem

						Menu* menu = new Menu;
						menu->addChild(construct<DeleteItem>(&MenuItem::text, "Delete", &DeleteItem::module, module, &DeleteItem::pluginSlug, pluginSlug, &DeleteItem::moduleSlug, moduleSlug));
						return menu;
					}
				}; // MidimapModuleItem

				std::list<std::pair<std::string, MidimapModuleItem*>> l;
				auto expMem = module->expMem.load();
				for (auto it : *expMem->getMemStorage()) {
					MemModule* a = it.second;
					MidimapModuleItem* midimapModuleItem = new MidimapModuleItem;
					midimapModuleItem->text = string::f("%s %s", a->pluginName.c_str(), a->moduleName.c_str());
					midimapModuleItem->module = module;
					midimapModuleItem->midimapModule = a;
					midimapModuleItem->pluginSlug = it.first.first;
					midimapModuleItem->moduleSlug = it.first.second;
					l.push_back(std::pair<std::string, MidimapModuleItem*>(midimapModuleItem->text, midimapModuleItem));
				}

				l.sort();
				Menu* menu = new Menu;
				for (auto it : l) {
					menu->addChild(it.second);
				}
				return menu;
			}
		}; // MapMenuItem

		struct SaveMenuItem : MenuItem {
			MidiCatModule* module;
			SaveMenuItem() {
				rightText = RIGHT_ARROW;
			}

			Menu* createChildMenu() override {
				struct SaveItem : MenuItem {
					MidiCatModule* module;
					std::string pluginSlug;
					std::string moduleSlug;
					void onAction(const event::Action& e) override {
						module->expMemSave(pluginSlug, moduleSlug);
					}
				}; // SaveItem

				typedef std::pair<std::string, std::string> ppair;
				std::list<std::pair<std::string, ppair>> list;
				std::set<ppair> s;
				for (size_t i = 0; i < MAX_CHANNELS; i++) {
					int64_t moduleId = module->paramHandles[i].moduleId;
					if (moduleId < 0) continue;
					Module* m = module->paramHandles[i].module;
					auto q = ppair(m->model->plugin->slug, m->model->slug);
					if (s.find(q) != s.end()) continue;
					s.insert(q);

					if (!m) continue;
					std::string l = string::f("%s %s", m->model->plugin->name.c_str(), m->model->name.c_str());
					auto p = std::pair<std::string, ppair>(l, q);
					list.push_back(p);
				}
				list.sort();

				Menu* menu = new Menu;
				for (auto it : list) {
					menu->addChild(construct<SaveItem>(&MenuItem::text, it.first, &SaveItem::module, module, &SaveItem::pluginSlug, it.second.first, &SaveItem::moduleSlug, it.second.second));
				}
				return menu;
			}
		}; // SaveMenuItem

		menu->addChild(new MenuSeparator());
		menu->addChild(createMenuLabel("MEM-expander"));
		menu->addChild(construct<MapMenuItem>(&MenuItem::text, "Available mappings", &MapMenuItem::module, module));
		menu->addChild(construct<SaveMenuItem>(&MenuItem::text, "Store mapping", &SaveMenuItem::module, module));
		menu->addChild(createMenuItem("Apply mapping", RACK_MOD_SHIFT_NAME "+V", [=]() { enableLearn(LEARN_MODE::MEM); }));
		menu->addChild(createSubmenuItem("Restriction", 
			expMem->getMemModuleRestriction()->size() == 0 ? "Disabled" : string::f("%i modules", expMem->getMemModuleRestriction()->size()),
			[=](Menu* menu) {
				if (APP->scene->rack->hasSelection()) {
					menu->addChild(createMenuItem("Save selection", "", [=]() {
						expMem->getMemModuleRestriction()->clear();
						for (auto it : APP->scene->rack->getSelected()) {
							expMem->getMemModuleRestriction()->insert(it->getModule()->getId());
						}
					}));
				}
				if (expMem->getMemModuleRestriction()->size() > 0) {
					menu->addChild(createMenuItem("Show", "", [=]() {
						APP->scene->rack->deselectAll();
						for (auto it : *expMem->getMemModuleRestriction()) {
							ModuleWidget* mw = APP->scene->rack->getModule(it);
							APP->scene->rack->select(mw);
						}
					}));
					menu->addChild(createMenuItem("Clear", "", [=]() { expMem->getMemModuleRestriction()->clear(); }));
				}
			}
		));
	}
};

struct MidiCatWidget : MidiCatBaseWidget {
	MidiCatWidget(MidiCatModule* module) : MidiCatBaseWidget(module, "MidiCat") {
		MidiWidget<>* midiInputWidget = createWidget<MidiWidget<>>(Vec(0.0f, 36.4f));
		midiInputWidget->box.size = Vec(150.0f, 67.0f);
		midiInputWidget->setMidiPort(module ? &module->midiInput : NULL, "In");
		addChild(midiInputWidget);

		MidiWidget<>* midiOutputWidget = createWidget<MidiWidget<>>(Vec(0.0f, 107.4f));
		midiOutputWidget->box.size = Vec(150.0f, 67.0f);
		midiOutputWidget->setMidiPort(module ? &module->midiOutput : NULL, "Out");
		addChild(midiOutputWidget);

		mapWidget = createWidget<MidiCatDisplay>(Vec(0.0f, 178.5f));
		mapWidget->box.size = Vec(150.0f, 164.7f);
		mapWidget->setModule(module);
		addChild(mapWidget);

		if (module) {
			OverlayMessageWidget::registerProvider(mapWidget);
		}
	}

	~MidiCatWidget() {
		if (module) {
			OverlayMessageWidget::unregisterProvider(mapWidget);
		}
	}
};

struct MidiCatXlWidget : MidiCatBaseWidget {
	MidiCatXlWidget(MidiCatModule* module) : MidiCatBaseWidget(module, "MidiCatXl") {
		mapWidget = createWidget<MidiCatDisplay>(Vec(0.0f, 36.4f));
		mapWidget->box.size = Vec(270.0f, 307.0f);
		mapWidget->setModule(module);
		addChild(mapWidget);

		if (module) {
			OverlayMessageWidget::registerProvider(mapWidget);
		}
	}

	~MidiCatXlWidget() {
		if (module) {
			OverlayMessageWidget::unregisterProvider(mapWidget);
		}
	}
};

} // namespace MidiCat
} // namespace StoermelderPackOne

Model* modelMidiCat = createModel<StoermelderPackOne::MidiCat::MidiCatModule, StoermelderPackOne::MidiCat::MidiCatWidget>("MidiCat");
Model* modelMidiCatXl = createModel<StoermelderPackOne::MidiCat::MidiCatModule, StoermelderPackOne::MidiCat::MidiCatXlWidget>("MidiCatXl");