#include "plugin.hpp"


namespace rack {
namespace core {


struct MIDI_Gate : Module {
	enum ParamIds {
		NUM_PARAMS
	};
	enum InputIds {
		NUM_INPUTS
	};
	enum OutputIds {
		ENUMS(GATE_OUTPUTS, 16),
		NUM_OUTPUTS
	};
	enum LightIds {
		NUM_LIGHTS
	};

	midi::InputQueue midiInput;

	/** True when a cell's gate is held. [cell][channel] */
	bool gates[16][16];
	/** Last velocity value of cell. [cell][channel] */
	uint8_t velocities[16][16];
	/** Triggered when a cell's note is played. [cell][channel] */
	dsp::PulseGenerator trigPulses[16][16];
	/** Cell ID in learn mode, or -1 if none. */
	int learningId;
	/** [cell] */
	int8_t learnedNotes[16];

	// Settings

	enum VelocityMode {
		FIXED_MODE,
		VELOCITY_MODE,
		AFTERTOUCH_MODE,
	};
	VelocityMode velocityMode;
	bool mpeMode;
	bool trigMode;

	MIDI_Gate() {
		config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
		for (int i = 0; i < 16; i++)
			configOutput(GATE_OUTPUTS + i, string::f("Gate %d", i + 1));

		onReset();
	}

	void onReset() override {
		for (int y = 0; y < 4; y++) {
			for (int x = 0; x < 4; x++) {
				learnedNotes[4 * y + x] = 36 + 4 * (3 - y) + x;
			}
		}
		learningId = -1;
		midiInput.reset();
		velocityMode = FIXED_MODE;
		mpeMode = false;
		trigMode = false;
		panic();
	}

	void panic() {
		for (int i = 0; i < 16; i++) {
			for (int c = 0; c < 16; c++) {
				gates[i][c] = false;
				velocities[i][c] = 127;
				trigPulses[i][c].reset();
			}
		}
	}

	void process(const ProcessArgs& args) override {
		midi::Message msg;
		while (midiInput.tryPop(&msg, args.frame)) {
			processMessage(msg);
		}

		int channels = mpeMode ? 16 : 1;

		for (int i = 0; i < 16; i++) {
			for (int c = 0; c < channels; c++) {
				bool pulse = trigPulses[i][c].process(args.sampleTime);
				bool gate = pulse || (!trigMode && gates[i][c]);
				float velocity = gate ? 1.f : 0.f;
				if (velocityMode != FIXED_MODE)
					velocity *= velocities[i][c] / 127.f;
				outputs[GATE_OUTPUTS + i].setVoltage(velocity * 10.f, c);
			}
			outputs[GATE_OUTPUTS + i].setChannels(channels);
		}
	}

	void processMessage(const midi::Message& msg) {
		switch (msg.getStatus()) {
			// note off
			case 0x8: {
				releaseNote(msg.getChannel(), msg.getNote());
			} break;
			// note on
			case 0x9: {
				uint8_t velocity = msg.getValue();
				if (velocity > 0) {
					pressNote(msg.getChannel(), msg.getNote(), velocity);
				}
				else {
					// Note-on event with velocity 0 is an alternative for note-off event.
					releaseNote(msg.getChannel(), msg.getNote());
				}
			} break;
			// polyphonic pressure/aftertouch
			case 0xa: {
				if (velocityMode == AFTERTOUCH_MODE) {
					uint8_t c = mpeMode ? msg.getChannel() : 0;
					uint8_t note = msg.getNote();
					uint8_t velocity = msg.getValue();
					for (int i = 0; i < 16; i++) {
						if (learnedNotes[i] == note) {
							velocities[i][c] = velocity;
						}
					}
				}
			} break;
			// channel pressure/aftertouch
			case 0xd: {
				if (velocityMode == AFTERTOUCH_MODE) {
					uint8_t c = mpeMode ? msg.getChannel() : 0;
					uint8_t velocity = msg.getNote();
					for (int i = 0; i < 16; i++) {
						velocities[i][c] = velocity;
					}
				}
			} break;
			default: break;
		}
	}

	void pressNote(uint8_t channel, uint8_t note, uint8_t vel) {
		int c = mpeMode ? channel : 0;
		// Learn
		if (learningId >= 0) {
			setLearnedNote(learningId, note);
			learningId = -1;
		}
		// Find id
		for (int i = 0; i < 16; i++) {
			if (learnedNotes[i] == note) {
				gates[i][c] = true;
				if (velocityMode == VELOCITY_MODE)
					velocities[i][c] = vel;
				trigPulses[i][c].trigger(1e-3f);
			}
		}
	}

	void releaseNote(uint8_t channel, uint8_t note) {
		int c = mpeMode ? channel : 0;
		// Find id
		for (int i = 0; i < 16; i++) {
			if (learnedNotes[i] == note) {
				gates[i][c] = false;
			}
		}
	}

	void setLearnedNote(int id, int8_t note) {
		// Unset IDs of similar note
		if (note >= 0) {
			for (int id = 0; id < 16; id++) {
				if (learnedNotes[id] == note)
					learnedNotes[id] = -1;
			}
		}
		learnedNotes[id] = note;
	}

	json_t* dataToJson() override {
		json_t* rootJ = json_object();

		json_t* notesJ = json_array();
		for (int i = 0; i < 16; i++) {
			json_array_append_new(notesJ, json_integer(learnedNotes[i]));
		}
		json_object_set_new(rootJ, "notes", notesJ);

		json_object_set_new(rootJ, "velocity", json_integer(velocityMode));

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

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

		json_object_set_new(rootJ, "midi", midiInput.toJson());

		return rootJ;
	}

	void dataFromJson(json_t* rootJ) override {
		json_t* notesJ = json_object_get(rootJ, "notes");
		if (notesJ) {
			for (int i = 0; i < 16; i++) {
				json_t* noteJ = json_array_get(notesJ, i);
				if (noteJ)
					setLearnedNote(i, json_integer_value(noteJ));
			}
		}

		json_t* velocityJ = json_object_get(rootJ, "velocity");
		// Boolean in Rack <=v2.6.4
		if (json_is_true(velocityJ))
			velocityMode = VELOCITY_MODE;
		else if (json_is_integer(velocityJ))
			velocityMode = VelocityMode(json_integer_value(velocityJ));

		json_t* mpeModeJ = json_object_get(rootJ, "mpeMode");
		if (mpeModeJ)
			mpeMode = json_boolean_value(mpeModeJ);

		json_t* trigModeJ = json_object_get(rootJ, "trigMode");
		if (trigModeJ)
			trigMode = json_boolean_value(trigModeJ);

		json_t* midiJ = json_object_get(rootJ, "midi");
		if (midiJ)
			midiInput.fromJson(midiJ);
	}
};


struct MIDI_GateWidget : ModuleWidget {
	MIDI_GateWidget(MIDI_Gate* module) {
		setModule(module);
		setPanel(createPanel(asset::system("res/Core/MIDI_Gate.svg"), asset::system("res/Core/MIDI_Gate-dark.svg")));

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

		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(8.189, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 0));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(19.739, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 1));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(31.289, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 2));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(42.838, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 3));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(8.189, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 4));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(19.739, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 5));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(31.289, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 6));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(42.838, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 7));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(8.189, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 8));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(19.739, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 9));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(31.289, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 10));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(42.838, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 11));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(8.189, 112.998)), module, MIDI_Gate::GATE_OUTPUTS + 12));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(19.739, 112.984)), module, MIDI_Gate::GATE_OUTPUTS + 13));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(31.289, 112.984)), module, MIDI_Gate::GATE_OUTPUTS + 14));
		addOutput(createOutputCentered<ThemedPJ301MPort>(mm2px(Vec(42.838, 112.984)), module, MIDI_Gate::GATE_OUTPUTS + 15));

		typedef Grid16MidiDisplay<NoteChoice<MIDI_Gate>> TMidiDisplay;
		TMidiDisplay* display = createWidget<TMidiDisplay>(mm2px(Vec(0.0, 13.039)));
		display->box.size = mm2px(Vec(50.8, 55.88));
		display->setMidiPort(module ? &module->midiInput : NULL);
		display->setModule(module);
		addChild(display);
	}

	void appendContextMenu(Menu* menu) override {
		MIDI_Gate* module = dynamic_cast<MIDI_Gate*>(this->module);

		menu->addChild(new MenuSeparator);

		menu->addChild(createIndexPtrSubmenuItem("Gate amplitude", {
			"10V",
			"Velocity",
			"Aftertouch",
		}, &module->velocityMode));

		menu->addChild(createBoolPtrMenuItem("MPE mode", "", &module->mpeMode));

		menu->addChild(createBoolPtrMenuItem("Trigger mode", "", &module->trigMode));

		menu->addChild(new MenuSeparator);

		menu->addChild(createMenuItem("Panic", "", [=]() {
			module->panic();
		}));
	}
};


// Use legacy slug for compatibility
Model* modelMIDI_Gate = createModel<MIDI_Gate, MIDI_GateWidget>("MIDITriggerToCVInterface");


} // namespace core
} // namespace rack
