#include "plugin.hpp"
#include "stmlib/dsp/hysteresis_quantizer.h"
#include "stmlib/dsp/units.h"
#include "tides2/poly_slope_generator.h"
#include "tides2/ramp_extractor.h"
#include "tides2/io_buffer.h"


static const float kRootScaled[3] = {
	0.125f,
	2.0f,
	130.81f
};

static const tides2::Ratio kRatios[20] = {
	{ 0.0625f, 16 },
	{ 0.125f, 8 },
	{ 0.1666666f, 6 },
	{ 0.25f, 4 },
	{ 0.3333333f, 3 },
	{ 0.5f, 2 },
	{ 0.6666666f, 3 },
	{ 0.75f, 4 },
	{ 0.8f, 5 },
	{ 1, 1 },
	{ 1, 1 },
	{ 1.25f, 4 },
	{ 1.3333333f, 3 },
	{ 1.5f, 2 },
	{ 2.0f, 1 },
	{ 3.0f, 1 },
	{ 4.0f, 1 },
	{ 6.0f, 1 },
	{ 8.0f, 1 },
	{ 16.0f, 1 },
};


struct Tides2 : Module {
	enum ParamIds {
		RANGE_PARAM,
		MODE_PARAM,
		RAMP_PARAM,
		FREQUENCY_PARAM,
		SHAPE_PARAM,
		SMOOTHNESS_PARAM,
		SLOPE_PARAM,
		SHIFT_PARAM,
		SLOPE_CV_PARAM,
		FREQUENCY_CV_PARAM,
		SMOOTHNESS_CV_PARAM,
		SHAPE_CV_PARAM,
		SHIFT_CV_PARAM,
		NUM_PARAMS
	};
	enum InputIds {
		SLOPE_INPUT,
		FREQUENCY_INPUT,
		V_OCT_INPUT,
		SMOOTHNESS_INPUT,
		SHAPE_INPUT,
		SHIFT_INPUT,
		TRIG_INPUT,
		CLOCK_INPUT,
		NUM_INPUTS
	};
	enum OutputIds {
		ENUMS(OUT_OUTPUTS, 4),
		NUM_OUTPUTS
	};
	enum LightIds {
		ENUMS(RANGE_LIGHT, 2),
		ENUMS(OUTPUT_MODE_LIGHT, 2),
		ENUMS(RAMP_MODE_LIGHT, 2),
		ENUMS(OUTPUT_LIGHTS, 4),
		NUM_LIGHTS
	};

	tides2::PolySlopeGenerator poly_slope_generator;
	tides2::RampExtractor ramp_extractor;
	stmlib::HysteresisQuantizer ratio_index_quantizer;

	// State
	int range;
	tides2::OutputMode output_mode;
	tides2::RampMode ramp_mode;
	dsp::BooleanTrigger rangeTrigger;
	dsp::BooleanTrigger modeTrigger;
	dsp::BooleanTrigger rampTrigger;

	// Buffers
	tides2::PolySlopeGenerator::OutputSample out[tides2::kBlockSize] = {};
	stmlib::GateFlags trig_flags[tides2::kBlockSize] = {};
	stmlib::GateFlags clock_flags[tides2::kBlockSize] = {};
	stmlib::GateFlags previous_trig_flag = stmlib::GATE_FLAG_LOW;
	stmlib::GateFlags previous_clock_flag = stmlib::GATE_FLAG_LOW;

	bool must_reset_ramp_extractor = true;
	tides2::OutputMode previous_output_mode = tides2::OUTPUT_MODE_GATES;
	uint8_t frame = 0;

	Tides2() {
		config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
		configParam(RANGE_PARAM, 0.0, 1.0, 0.0, "Frequency range");
		configParam(MODE_PARAM, 0.0, 1.0, 0.0, "Output mode");
		configParam(FREQUENCY_PARAM, -48, 48, 0.0, "Ramp mode");
		configParam(SHAPE_PARAM, 0.0, 1.0, 0.5, "Frequency");
		configParam(RAMP_PARAM, 0.0, 1.0, 0.0, "Shape");
		configParam(SMOOTHNESS_PARAM, 0.0, 1.0, 0.5, "Waveshape transformation");
		configParam(SLOPE_PARAM, 0.0, 1.0, 0.5, "Ascending/descending ratio");
		configParam(SHIFT_PARAM, 0.0, 1.0, 0.5, "Output polarization and shifting");
		configParam(SLOPE_CV_PARAM, -1.0, 1.0, 0.0, "Slope CV");
		configParam(FREQUENCY_CV_PARAM, -1.0, 1.0, 0.0, "Frequency CV");
		configParam(SMOOTHNESS_CV_PARAM, -1.0, 1.0, 0.0, "Smoothness CV");
		configParam(SHAPE_CV_PARAM, -1.0, 1.0, 0.0, "Shape CV");
		configParam(SHIFT_CV_PARAM, -1.0, 1.0, 0.0, "Shift CV");

		poly_slope_generator.Init();
		ratio_index_quantizer.Init();
		onReset();
		onSampleRateChange();
	}

	void onReset() override {
		range = 1;
		output_mode = tides2::OUTPUT_MODE_GATES;
		ramp_mode = tides2::RAMP_MODE_LOOPING;
	}

	void onRandomize() override {
		range = random::u32() % 3;
		output_mode = (tides2::OutputMode)(random::u32() % 4);
		ramp_mode = (tides2::RampMode)(random::u32() % 3);
	}

	void onSampleRateChange() override {
		ramp_extractor.Init(APP->engine->getSampleRate(), 40.f / APP->engine->getSampleRate());
	}

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

		json_object_set_new(rootJ, "range", json_integer(range));
		json_object_set_new(rootJ, "output", json_integer(output_mode));
		json_object_set_new(rootJ, "ramp", json_integer(ramp_mode));

		return rootJ;
	}

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

		json_t* outputJ = json_object_get(rootJ, "output");
		if (outputJ)
			output_mode = (tides2::OutputMode) json_integer_value(outputJ);

		json_t* rampJ = json_object_get(rootJ, "ramp");
		if (rampJ)
			ramp_mode = (tides2::RampMode) json_integer_value(rampJ);
	}

	void process(const ProcessArgs& args) override {
		// Switches
		if (rangeTrigger.process(params[RANGE_PARAM].getValue() > 0.f)) {
			range = (range + 1) % 3;
		}
		if (modeTrigger.process(params[MODE_PARAM].getValue() > 0.f)) {
			output_mode = (tides2::OutputMode)((output_mode + 1) % 4);
		}
		if (rampTrigger.process(params[RAMP_PARAM].getValue() > 0.f)) {
			ramp_mode = (tides2::RampMode)((ramp_mode + 1) % 3);
		}

		// Input gates
		trig_flags[frame] = stmlib::ExtractGateFlags(previous_trig_flag, inputs[TRIG_INPUT].getVoltage() >= 1.7f);
		previous_trig_flag = trig_flags[frame];

		clock_flags[frame] = stmlib::ExtractGateFlags(previous_clock_flag, inputs[CLOCK_INPUT].getVoltage() >= 1.7f);
		previous_clock_flag = clock_flags[frame];

		// Process block
		if (++frame >= tides2::kBlockSize) {
			frame = 0;

			tides2::Range range_mode = (range < 2) ? tides2::RANGE_CONTROL : tides2::RANGE_AUDIO;
			float note = clamp(params[FREQUENCY_PARAM].getValue() + 12.f * inputs[V_OCT_INPUT].getVoltage(), -96.f, 96.f);
			float fm = clamp(params[FREQUENCY_CV_PARAM].getValue() * inputs[FREQUENCY_INPUT].getVoltage() * 12.f, -96.f, 96.f);
			float transposition = note + fm;

			float ramp[tides2::kBlockSize];
			float frequency;

			if (inputs[CLOCK_INPUT].isConnected()) {
				if (must_reset_ramp_extractor) {
					ramp_extractor.Reset();
				}

				tides2::Ratio r = ratio_index_quantizer.Lookup(kRatios, 0.5f + transposition * 0.0105f, 20);
				frequency = ramp_extractor.Process(
				              range_mode == tides2::RANGE_AUDIO,
				              range_mode == tides2::RANGE_AUDIO && ramp_mode == tides2::RAMP_MODE_AR,
				              r,
				              clock_flags,
				              ramp,
				              tides2::kBlockSize);
				must_reset_ramp_extractor = false;
			}
			else {
				frequency = kRootScaled[range] / args.sampleRate * stmlib::SemitonesToRatio(transposition);
				must_reset_ramp_extractor = true;
			}

			// Get parameters
			float slope = clamp(params[SLOPE_PARAM].getValue() + dsp::cubic(params[SLOPE_CV_PARAM].getValue()) * inputs[SLOPE_INPUT].getVoltage() / 10.f, 0.f, 1.f);
			float shape = clamp(params[SHAPE_PARAM].getValue() + dsp::cubic(params[SHAPE_CV_PARAM].getValue()) * inputs[SHAPE_INPUT].getVoltage() / 10.f, 0.f, 1.f);
			float smoothness = clamp(params[SMOOTHNESS_PARAM].getValue() + dsp::cubic(params[SMOOTHNESS_CV_PARAM].getValue()) * inputs[SMOOTHNESS_INPUT].getVoltage() / 10.f, 0.f, 1.f);
			float shift = clamp(params[SHIFT_PARAM].getValue() + dsp::cubic(params[SHIFT_CV_PARAM].getValue()) * inputs[SHIFT_INPUT].getVoltage() / 10.f, 0.f, 1.f);

			if (output_mode != previous_output_mode) {
				poly_slope_generator.Reset();
				previous_output_mode = output_mode;
			}

			// Render generator
			poly_slope_generator.Render(
			  ramp_mode,
			  output_mode,
			  range_mode,
			  frequency,
			  slope,
			  shape,
			  smoothness,
			  shift,
			  trig_flags,
			  !inputs[TRIG_INPUT].isConnected() && inputs[CLOCK_INPUT].isConnected() ? ramp : NULL,
			  out,
			  tides2::kBlockSize);

			// Set lights
			lights[RANGE_LIGHT + 0].value = (range == 0 || range == 1);
			lights[RANGE_LIGHT + 1].value = (range == 1 || range == 2);
			lights[OUTPUT_MODE_LIGHT + 0].value = (output_mode == tides2::OUTPUT_MODE_AMPLITUDE || output_mode == tides2::OUTPUT_MODE_SLOPE_PHASE);
			lights[OUTPUT_MODE_LIGHT + 1].value = (output_mode == tides2::OUTPUT_MODE_FREQUENCY || output_mode == tides2::OUTPUT_MODE_SLOPE_PHASE);
			lights[RAMP_MODE_LIGHT + 0].value = (ramp_mode == tides2::RAMP_MODE_AD || ramp_mode == tides2::RAMP_MODE_LOOPING);
			lights[RAMP_MODE_LIGHT + 1].value = (ramp_mode == tides2::RAMP_MODE_AR || ramp_mode == tides2::RAMP_MODE_LOOPING);
		}

		// Outputs
		for (int i = 0; i < 4; i++) {
			float value = out[frame].channel[i];
			outputs[OUT_OUTPUTS + i].setVoltage(value);
			lights[OUTPUT_LIGHTS + i].setSmoothBrightness(value, args.sampleTime);
		}
	}
};


struct Tides2Widget : ModuleWidget {
	Tides2Widget(Tides2* module) {
		setModule(module);
		setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Tides2.svg")));

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

		addParam(createParamCentered<TL1105>(mm2px(Vec(7.425, 16.15)), module, Tides2::RANGE_PARAM));
		addParam(createParamCentered<TL1105>(mm2px(Vec(63.325, 16.15)), module, Tides2::MODE_PARAM));
		addParam(createParamCentered<Rogan3PSWhite>(mm2px(Vec(16.325, 33.449)), module, Tides2::FREQUENCY_PARAM));
		addParam(createParamCentered<Rogan3PSWhite>(mm2px(Vec(54.425, 33.449)), module, Tides2::SHAPE_PARAM));
		addParam(createParamCentered<TL1105>(mm2px(Vec(35.375, 38.699)), module, Tides2::RAMP_PARAM));
		addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(35.375, 55.549)), module, Tides2::SMOOTHNESS_PARAM));
		addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(11.575, 60.599)), module, Tides2::SLOPE_PARAM));
		addParam(createParamCentered<Rogan1PSWhite>(mm2px(Vec(59.175, 60.599)), module, Tides2::SHIFT_PARAM));
		addParam(createParamCentered<Trimpot>(mm2px(Vec(9.276, 80.599)), module, Tides2::SLOPE_CV_PARAM));
		addParam(createParamCentered<Trimpot>(mm2px(Vec(22.324, 80.599)), module, Tides2::FREQUENCY_CV_PARAM));
		addParam(createParamCentered<Trimpot>(mm2px(Vec(35.375, 80.599)), module, Tides2::SMOOTHNESS_CV_PARAM));
		addParam(createParamCentered<Trimpot>(mm2px(Vec(48.425, 80.599)), module, Tides2::SHAPE_CV_PARAM));
		addParam(createParamCentered<Trimpot>(mm2px(Vec(61.475, 80.599)), module, Tides2::SHIFT_CV_PARAM));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.775, 96.499)), module, Tides2::SLOPE_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(18.225, 96.499)), module, Tides2::FREQUENCY_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(29.675, 96.499)), module, Tides2::V_OCT_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(41.125, 96.499)), module, Tides2::SMOOTHNESS_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(52.575, 96.499)), module, Tides2::SHAPE_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(64.025, 96.499)), module, Tides2::SHIFT_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.775, 111.099)), module, Tides2::TRIG_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(18.225, 111.099)), module, Tides2::CLOCK_INPUT));

		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(29.675, 111.099)), module, Tides2::OUT_OUTPUTS + 0));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(41.125, 111.099)), module, Tides2::OUT_OUTPUTS + 1));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(52.575, 111.099)), module, Tides2::OUT_OUTPUTS + 2));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(64.025, 111.099)), module, Tides2::OUT_OUTPUTS + 3));

		addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(13.776, 16.149)), module, Tides2::RANGE_LIGHT));
		addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(56.975, 16.149)), module, Tides2::OUTPUT_MODE_LIGHT));
		addChild(createLightCentered<MediumLight<GreenRedLight>>(mm2px(Vec(35.375, 33.449)), module, Tides2::RAMP_MODE_LIGHT));
		addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(26.174, 104.749)), module, Tides2::OUTPUT_LIGHTS + 0));
		addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(37.625, 104.749)), module, Tides2::OUTPUT_LIGHTS + 1));
		addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(49.075, 104.749)), module, Tides2::OUTPUT_LIGHTS + 2));
		addChild(createLightCentered<MediumLight<GreenLight>>(mm2px(Vec(60.525, 104.749)), module, Tides2::OUTPUT_LIGHTS + 3));
	}
};


Model* modelTides2 = createModel<Tides2, Tides2Widget>("Tides2");
