#include "plugin.hpp"
#include "clouds/dsp/granular_processor.h"


struct Clouds : Module {
	enum ParamIds {
		FREEZE_PARAM,
		MODE_PARAM,
		LOAD_PARAM,
		POSITION_PARAM,
		SIZE_PARAM,
		PITCH_PARAM,
		IN_GAIN_PARAM,
		DENSITY_PARAM,
		TEXTURE_PARAM,
		BLEND_PARAM,
		SPREAD_PARAM,
		FEEDBACK_PARAM,
		REVERB_PARAM,
		NUM_PARAMS
	};
	enum InputIds {
		FREEZE_INPUT,
		TRIG_INPUT,
		POSITION_INPUT,
		SIZE_INPUT,
		PITCH_INPUT,
		BLEND_INPUT,
		IN_L_INPUT,
		IN_R_INPUT,
		DENSITY_INPUT,
		TEXTURE_INPUT,
		NUM_INPUTS
	};
	enum OutputIds {
		OUT_L_OUTPUT,
		OUT_R_OUTPUT,
		NUM_OUTPUTS
	};
	enum LightIds {
		FREEZE_LIGHT,
		MIX_GREEN_LIGHT, MIX_RED_LIGHT,
		PAN_GREEN_LIGHT, PAN_RED_LIGHT,
		FEEDBACK_GREEN_LIGHT, FEEDBACK_RED_LIGHT,
		REVERB_GREEN_LIGHT, REVERB_RED_LIGHT,
		NUM_LIGHTS
	};

	dsp::SampleRateConverter<2> inputSrc;
	dsp::SampleRateConverter<2> outputSrc;
	dsp::DoubleRingBuffer<dsp::Frame<2>, 256> inputBuffer;
	dsp::DoubleRingBuffer<dsp::Frame<2>, 256> outputBuffer;

	uint8_t* block_mem;
	uint8_t* block_ccm;
	clouds::GranularProcessor* processor;

	bool triggered = false;

	dsp::SchmittTrigger freezeTrigger;
	bool freeze = false;
	dsp::SchmittTrigger blendTrigger;
	int blendMode = 0;

	clouds::PlaybackMode playback;
	int quality = 0;

	Clouds() {
		config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
		configParam(POSITION_PARAM, 0.0, 1.0, 0.5, "Grain position");
		configParam(SIZE_PARAM, 0.0, 1.0, 0.5, "Grain size");
		configParam(PITCH_PARAM, -2.0, 2.0, 0.0, "Grain pitch");
		configParam(IN_GAIN_PARAM, 0.0, 1.0, 0.5, "Audio input gain");
		configParam(DENSITY_PARAM, 0.0, 1.0, 0.5, "Grain density");
		configParam(TEXTURE_PARAM, 0.0, 1.0, 0.5, "Grain texture");
		configParam(BLEND_PARAM, 0.0, 1.0, 0.5, "Dry/wet");
		configParam(SPREAD_PARAM, 0.0, 1.0, 0.0, "Stereo spread");
		configParam(FEEDBACK_PARAM, 0.0, 1.0, 0.0, "Feedback amount");
		configParam(REVERB_PARAM, 0.0, 1.0, 0.0, "Reverb amount");
		configParam(FREEZE_PARAM, 0.0, 1.0, 0.0, "Freeze");
		configParam(MODE_PARAM, 0.0, 1.0, 0.0, "Mode");
		configParam(LOAD_PARAM, 0.0, 1.0, 0.0, "Load/save");

		const int memLen = 118784;
		const int ccmLen = 65536 - 128;
		block_mem = new uint8_t[memLen]();
		block_ccm = new uint8_t[ccmLen]();
		processor = new clouds::GranularProcessor();
		memset(processor, 0, sizeof(*processor));

		processor->Init(block_mem, memLen, block_ccm, ccmLen);
		onReset();
	}

	~Clouds() {
		delete processor;
		delete[] block_mem;
		delete[] block_ccm;
	}

	void process(const ProcessArgs& args) override {
		// Get input
		dsp::Frame<2> inputFrame = {};
		if (!inputBuffer.full()) {
			inputFrame.samples[0] = inputs[IN_L_INPUT].getVoltage() * params[IN_GAIN_PARAM].getValue() / 5.0;
			inputFrame.samples[1] = inputs[IN_R_INPUT].isConnected() ? inputs[IN_R_INPUT].getVoltage() * params[IN_GAIN_PARAM].getValue() / 5.0 : inputFrame.samples[0];
			inputBuffer.push(inputFrame);
		}

		if (freezeTrigger.process(params[FREEZE_PARAM].getValue())) {
			freeze ^= true;
		}
		if (blendTrigger.process(params[MODE_PARAM].getValue())) {
			blendMode = (blendMode + 1) % 4;
		}

		// Trigger
		if (inputs[TRIG_INPUT].getVoltage() >= 1.0) {
			triggered = true;
		}

		// Render frames
		if (outputBuffer.empty()) {
			clouds::ShortFrame input[32] = {};
			// Convert input buffer
			{
				inputSrc.setRates(args.sampleRate, 32000);
				dsp::Frame<2> inputFrames[32];
				int inLen = inputBuffer.size();
				int outLen = 32;
				inputSrc.process(inputBuffer.startData(), &inLen, inputFrames, &outLen);
				inputBuffer.startIncr(inLen);

				// We might not fill all of the input buffer if there is a deficiency, but this cannot be avoided due to imprecisions between the input and output SRC.
				for (int i = 0; i < outLen; i++) {
					input[i].l = clamp(inputFrames[i].samples[0] * 32767.0f, -32768.0f, 32767.0f);
					input[i].r = clamp(inputFrames[i].samples[1] * 32767.0f, -32768.0f, 32767.0f);
				}
			}

			// Set up processor
			processor->set_playback_mode(playback);
			processor->set_quality(quality);
			processor->Prepare();

			clouds::Parameters* p = processor->mutable_parameters();
			p->trigger = triggered;
			p->gate = triggered;
			p->freeze = freeze || (inputs[FREEZE_INPUT].getVoltage() >= 1.0);
			p->position = clamp(params[POSITION_PARAM].getValue() + inputs[POSITION_INPUT].getVoltage() / 5.0f, 0.0f, 1.0f);
			p->size = clamp(params[SIZE_PARAM].getValue() + inputs[SIZE_INPUT].getVoltage() / 5.0f, 0.0f, 1.0f);
			p->pitch = clamp((params[PITCH_PARAM].getValue() + inputs[PITCH_INPUT].getVoltage()) * 12.0f, -48.0f, 48.0f);
			p->density = clamp(params[DENSITY_PARAM].getValue() + inputs[DENSITY_INPUT].getVoltage() / 5.0f, 0.0f, 1.0f);
			p->texture = clamp(params[TEXTURE_PARAM].getValue() + inputs[TEXTURE_INPUT].getVoltage() / 5.0f, 0.0f, 1.0f);
			p->dry_wet = params[BLEND_PARAM].getValue();
			p->stereo_spread = params[SPREAD_PARAM].getValue();
			p->feedback = params[FEEDBACK_PARAM].getValue();
			// TODO
			// Why doesn't dry audio get reverbed?
			p->reverb = params[REVERB_PARAM].getValue();
			float blend = inputs[BLEND_INPUT].getVoltage() / 5.0f;
			switch (blendMode) {
				case 0:
					p->dry_wet += blend;
					p->dry_wet = clamp(p->dry_wet, 0.0f, 1.0f);
					break;
				case 1:
					p->stereo_spread += blend;
					p->stereo_spread = clamp(p->stereo_spread, 0.0f, 1.0f);
					break;
				case 2:
					p->feedback += blend;
					p->feedback = clamp(p->feedback, 0.0f, 1.0f);
					break;
				case 3:
					p->reverb += blend;
					p->reverb = clamp(p->reverb, 0.0f, 1.0f);
					break;
			}

			clouds::ShortFrame output[32];
			processor->Process(input, output, 32);

			// Convert output buffer
			{
				dsp::Frame<2> outputFrames[32];
				for (int i = 0; i < 32; i++) {
					outputFrames[i].samples[0] = output[i].l / 32768.0;
					outputFrames[i].samples[1] = output[i].r / 32768.0;
				}

				outputSrc.setRates(32000, args.sampleRate);
				int inLen = 32;
				int outLen = outputBuffer.capacity();
				outputSrc.process(outputFrames, &inLen, outputBuffer.endData(), &outLen);
				outputBuffer.endIncr(outLen);
			}

			triggered = false;
		}

		// Set output
		dsp::Frame<2> outputFrame = {};
		if (!outputBuffer.empty()) {
			outputFrame = outputBuffer.shift();
			outputs[OUT_L_OUTPUT].setVoltage(5.0 * outputFrame.samples[0]);
			outputs[OUT_R_OUTPUT].setVoltage(5.0 * outputFrame.samples[1]);
		}

		// Lights
		clouds::Parameters* p = processor->mutable_parameters();
		dsp::VuMeter vuMeter;
		vuMeter.dBInterval = 6.0;
		dsp::Frame<2> lightFrame = p->freeze ? outputFrame : inputFrame;
		vuMeter.setValue(fmaxf(fabsf(lightFrame.samples[0]), fabsf(lightFrame.samples[1])));
		lights[FREEZE_LIGHT].setBrightness(p->freeze ? 0.75 : 0.0);
		lights[MIX_GREEN_LIGHT].setSmoothBrightness(vuMeter.getBrightness(3), args.sampleTime);
		lights[PAN_GREEN_LIGHT].setSmoothBrightness(vuMeter.getBrightness(2), args.sampleTime);
		lights[FEEDBACK_GREEN_LIGHT].setSmoothBrightness(vuMeter.getBrightness(1), args.sampleTime);
		lights[REVERB_GREEN_LIGHT].setBrightness(0.0);
		lights[MIX_RED_LIGHT].setBrightness(0.0);
		lights[PAN_RED_LIGHT].setBrightness(0.0);
		lights[FEEDBACK_RED_LIGHT].setSmoothBrightness(vuMeter.getBrightness(1), args.sampleTime);
		lights[REVERB_RED_LIGHT].setSmoothBrightness(vuMeter.getBrightness(0), args.sampleTime);
	}

	void onReset() override {
		freeze = false;
		blendMode = 0;
		playback = clouds::PLAYBACK_MODE_GRANULAR;
		quality = 0;
	}

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

		json_object_set_new(rootJ, "playback", json_integer((int) playback));
		json_object_set_new(rootJ, "quality", json_integer(quality));
		json_object_set_new(rootJ, "blendMode", json_integer(blendMode));

		return rootJ;
	}

	void dataFromJson(json_t* rootJ) override {
		json_t* playbackJ = json_object_get(rootJ, "playback");
		if (playbackJ) {
			playback = (clouds::PlaybackMode) json_integer_value(playbackJ);
		}

		json_t* qualityJ = json_object_get(rootJ, "quality");
		if (qualityJ) {
			quality = json_integer_value(qualityJ);
		}

		json_t* blendModeJ = json_object_get(rootJ, "blendMode");
		if (blendModeJ) {
			blendMode = json_integer_value(blendModeJ);
		}
	}
};


struct FreezeLight : YellowLight {
	FreezeLight() {
		box.size = Vec(28 - 6, 28 - 6);
		bgColor = color::BLACK_TRANSPARENT;
	}
};


struct CloudsBlendItem : MenuItem {
	Clouds* module;
	int blendMode;
	void onAction(const event::Action& e) override {
		module->blendMode = blendMode;
	}
	void step() override {
		rightText = (module->blendMode == blendMode) ? "✔" : "";
		MenuItem::step();
	}
};


struct CloudsPlaybackItem : MenuItem {
	Clouds* module;
	clouds::PlaybackMode playback;
	void onAction(const event::Action& e) override {
		module->playback = playback;
	}
	void step() override {
		rightText = (module->playback == playback) ? "✔" : "";
		MenuItem::step();
	}
};


struct CloudsQualityItem : MenuItem {
	Clouds* module;
	int quality;
	void onAction(const event::Action& e) override {
		module->quality = quality;
	}
	void step() override {
		rightText = (module->quality == quality) ? "✔" : "";
		MenuItem::step();
	}
};


struct CloudsWidget : ModuleWidget {
	ParamWidget* blendParam;
	ParamWidget* spreadParam;
	ParamWidget* feedbackParam;
	ParamWidget* reverbParam;

	CloudsWidget(Clouds* module) {
		setModule(module);
		setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Clouds.svg")));

		addChild(createWidget<ScrewSilver>(Vec(15, 0)));
		addChild(createWidget<ScrewSilver>(Vec(240, 0)));
		addChild(createWidget<ScrewSilver>(Vec(15, 365)));
		addChild(createWidget<ScrewSilver>(Vec(240, 365)));

		addParam(createParam<Rogan3PSRed>(Vec(27, 93), module, Clouds::POSITION_PARAM));
		addParam(createParam<Rogan3PSGreen>(Vec(108, 93), module, Clouds::SIZE_PARAM));
		addParam(createParam<Rogan3PSWhite>(Vec(190, 93), module, Clouds::PITCH_PARAM));

		addParam(createParam<Rogan1PSRed>(Vec(14, 180), module, Clouds::IN_GAIN_PARAM));
		addParam(createParam<Rogan1PSRed>(Vec(81, 180), module, Clouds::DENSITY_PARAM));
		addParam(createParam<Rogan1PSGreen>(Vec(146, 180), module, Clouds::TEXTURE_PARAM));
		blendParam = createParam<Rogan1PSWhite>(Vec(213, 180), module, Clouds::BLEND_PARAM);
		addParam(blendParam);
		spreadParam = createParam<Rogan1PSRed>(Vec(213, 180), module, Clouds::SPREAD_PARAM);
		addParam(spreadParam);
		feedbackParam = createParam<Rogan1PSGreen>(Vec(213, 180), module, Clouds::FEEDBACK_PARAM);
		addParam(feedbackParam);
		reverbParam = createParam<Rogan1PSBlue>(Vec(213, 180), module, Clouds::REVERB_PARAM);
		addParam(reverbParam);

		addParam(createParam<CKD6>(Vec(12, 43), module, Clouds::FREEZE_PARAM));
		addParam(createParam<TL1105>(Vec(211, 50), module, Clouds::MODE_PARAM));
		addParam(createParam<TL1105>(Vec(239, 50), module, Clouds::LOAD_PARAM));

		addInput(createInput<PJ301MPort>(Vec(15, 274), module, Clouds::FREEZE_INPUT));
		addInput(createInput<PJ301MPort>(Vec(58, 274), module, Clouds::TRIG_INPUT));
		addInput(createInput<PJ301MPort>(Vec(101, 274), module, Clouds::POSITION_INPUT));
		addInput(createInput<PJ301MPort>(Vec(144, 274), module, Clouds::SIZE_INPUT));
		addInput(createInput<PJ301MPort>(Vec(188, 274), module, Clouds::PITCH_INPUT));
		addInput(createInput<PJ301MPort>(Vec(230, 274), module, Clouds::BLEND_INPUT));

		addInput(createInput<PJ301MPort>(Vec(15, 317), module, Clouds::IN_L_INPUT));
		addInput(createInput<PJ301MPort>(Vec(58, 317), module, Clouds::IN_R_INPUT));
		addInput(createInput<PJ301MPort>(Vec(101, 317), module, Clouds::DENSITY_INPUT));
		addInput(createInput<PJ301MPort>(Vec(144, 317), module, Clouds::TEXTURE_INPUT));
		addOutput(createOutput<PJ301MPort>(Vec(188, 317), module, Clouds::OUT_L_OUTPUT));
		addOutput(createOutput<PJ301MPort>(Vec(230, 317), module, Clouds::OUT_R_OUTPUT));

		addChild(createLight<FreezeLight>(Vec(12 + 3, 43 + 3), module, Clouds::FREEZE_LIGHT));
		addChild(createLight<MediumLight<GreenRedLight>>(Vec(82.5, 53), module, Clouds::MIX_GREEN_LIGHT));
		addChild(createLight<MediumLight<GreenRedLight>>(Vec(114.5, 53), module, Clouds::PAN_GREEN_LIGHT));
		addChild(createLight<MediumLight<GreenRedLight>>(Vec(145.5, 53), module, Clouds::FEEDBACK_GREEN_LIGHT));
		addChild(createLight<MediumLight<GreenRedLight>>(Vec(177.5, 53), module, Clouds::REVERB_GREEN_LIGHT));
	}

	void step() override {
		Clouds* module = dynamic_cast<Clouds*>(this->module);

		if (module) {
			blendParam->visible = (module->blendMode == 0);
			spreadParam->visible = (module->blendMode == 1);
			feedbackParam->visible = (module->blendMode == 2);
			reverbParam->visible = (module->blendMode == 3);
		}

		ModuleWidget::step();
	}

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

		menu->addChild(new MenuSeparator);
		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Blend knob"));
		menu->addChild(construct<CloudsBlendItem>(&MenuItem::text, "Wet/dry", &CloudsBlendItem::module, module, &CloudsBlendItem::blendMode, 0));
		menu->addChild(construct<CloudsBlendItem>(&MenuItem::text, "Spread", &CloudsBlendItem::module, module, &CloudsBlendItem::blendMode, 1));
		menu->addChild(construct<CloudsBlendItem>(&MenuItem::text, "Feedback", &CloudsBlendItem::module, module, &CloudsBlendItem::blendMode, 2));
		menu->addChild(construct<CloudsBlendItem>(&MenuItem::text, "Reverb", &CloudsBlendItem::module, module, &CloudsBlendItem::blendMode, 3));

		menu->addChild(new MenuSeparator);
		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Alternative mode"));
		menu->addChild(construct<CloudsPlaybackItem>(&MenuItem::text, "Granular", &CloudsPlaybackItem::module, module, &CloudsPlaybackItem::playback, clouds::PLAYBACK_MODE_GRANULAR));
		menu->addChild(construct<CloudsPlaybackItem>(&MenuItem::text, "Pitch-shifter/time-stretcher", &CloudsPlaybackItem::module, module, &CloudsPlaybackItem::playback, clouds::PLAYBACK_MODE_STRETCH));
		menu->addChild(construct<CloudsPlaybackItem>(&MenuItem::text, "Looping delay", &CloudsPlaybackItem::module, module, &CloudsPlaybackItem::playback, clouds::PLAYBACK_MODE_LOOPING_DELAY));
		menu->addChild(construct<CloudsPlaybackItem>(&MenuItem::text, "Spectral madness", &CloudsPlaybackItem::module, module, &CloudsPlaybackItem::playback, clouds::PLAYBACK_MODE_SPECTRAL));

		menu->addChild(new MenuSeparator);
		menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Quality"));
		menu->addChild(construct<CloudsQualityItem>(&MenuItem::text, "1s 32kHz 16-bit stereo", &CloudsQualityItem::module, module, &CloudsQualityItem::quality, 0));
		menu->addChild(construct<CloudsQualityItem>(&MenuItem::text, "2s 32kHz 16-bit mono", &CloudsQualityItem::module, module, &CloudsQualityItem::quality, 1));
		menu->addChild(construct<CloudsQualityItem>(&MenuItem::text, "4s 16kHz 8-bit µ-law stereo", &CloudsQualityItem::module, module, &CloudsQualityItem::quality, 2));
		menu->addChild(construct<CloudsQualityItem>(&MenuItem::text, "8s 16kHz 8-bit µ-law mono", &CloudsQualityItem::module, module, &CloudsQualityItem::quality, 3));
	}
};


Model* modelClouds = createModel<Clouds, CloudsWidget>("Clouds");
