#include "plugin.hpp"

#define MAX_REPETITIONS 32                  /// max number of repetitions
#define TRIGGER_TIME 0.001

// a tempo/clock calculator that responds to pings - this sets the base tempo, multiplication/division of 
// this tempo occurs in the BurstEngine
struct PingableClock {

	dsp::Timer timer;                   // time the gap between pings
	dsp::PulseGenerator clockTimer;     // counts down from tempo length to zero
	dsp::BooleanTrigger clockExpiry;    // checks for when the clock timer runs out

	float pingDuration = 0.5f;          // used for calculating and updating tempo (default 2Hz / 120 bpm)
	float tempo = 0.5f;                 // actual current tempo of clock

	PingableClock() {
		clockTimer.trigger(tempo);
	}

	void process(bool pingRecieved, float sampleTime) {
		timer.process(sampleTime);

		bool clockRestarted = false;

		if (pingRecieved) {

			bool tempoShouldBeUpdated = true;
			float duration = timer.getTime();

			// if the ping was unusually different to last time
			bool outlier = duration > (pingDuration * 2) || duration < (pingDuration / 2);
			// if there is a previous estimate of tempo, but it's an outlier
			if ((pingDuration && outlier)) {
				// don't calculate tempo from this; prime so future pings will update
				tempoShouldBeUpdated = false;
				pingDuration = 0;
			}
			else {
				pingDuration = duration;
			}
			timer.reset();

			if (tempoShouldBeUpdated) {
				// if the tempo should be updated, do so
				tempo = pingDuration;
				clockRestarted = true;
			}
		}

		// we restart the clock if a) a new valid ping arrived OR b) the current clock expired
		clockRestarted = clockExpiry.process(!clockTimer.process(sampleTime)) || clockRestarted;
		if (clockRestarted) {
			clockTimer.reset();
			clockTimer.trigger(tempo);
		}
	}

	bool isTempoOutHigh() {
		// give a 1ms pulse as tempo out
		return clockTimer.remaining > tempo - TRIGGER_TIME;
	}
};

// engine that generates a burst when triggered
struct BurstEngine {

	dsp::PulseGenerator eocOutput;      // for generating EOC trigger
	dsp::PulseGenerator burstOutput;    // for generating triggers for each occurance of the burst
	dsp::Timer burstTimer;              // for timing how far through the current burst we are

	float timings[MAX_REPETITIONS + 1] = {};        // store timings (calculated once on burst trigger)

	int triggersOccurred = 0;       // how many triggers have been
	int triggersRequested = 0;      // how many bursts have been requested (fixed over course of burst)
	bool active = true;             // is there a burst active
	bool wasInhibited = false;      // was this burst inhibited (i.e. just the first trigger sent)

	std::tuple<float, float, bool> process(float sampleTime) {

		if (active) {
			burstTimer.process(sampleTime);
		}

		bool eocTriggered = false;
		if (burstTimer.time > timings[triggersOccurred]) {
			if (triggersOccurred < triggersRequested) {
				burstOutput.reset();
				burstOutput.trigger(TRIGGER_TIME);
			}
			else if (triggersOccurred == triggersRequested) {
				eocOutput.reset();
				eocOutput.trigger(TRIGGER_TIME);
				active = false;
				eocTriggered = true;
			}
			triggersOccurred++;
		}

		const float burstOut = burstOutput.process(sampleTime);
		// NOTE: we don't get EOC if the burst was inhibited
		const float eocOut = eocOutput.process(sampleTime) * !wasInhibited;
		return std::make_tuple(burstOut, eocOut, eocTriggered);
	}

	void trigger(int numBursts, int multDiv, float baseTimeWindow, float distribution, bool inhibitBurst, bool includeOriginalTrigger) {

		active = true;
		wasInhibited = inhibitBurst;

		// the window in which the burst fits is a multiple (or division) of the base tempo
		int divisions = multDiv + (multDiv > 0 ? 1 : multDiv < 0 ? -1 : 0); 	// skip 2/-2
		float actualTimeWindow = baseTimeWindow;
		if (divisions > 0) {
			actualTimeWindow = baseTimeWindow * divisions;
		}
		else if (divisions < 0) {
			actualTimeWindow = baseTimeWindow / (-divisions);
		}

		// calculate the times at which triggers should fire, will be skewed by distribution
		const float power = 1 + std::abs(distribution) * 2;
		for (int i = 0; i <= numBursts; ++i) {
			if (distribution >= 0) {
				timings[i] = actualTimeWindow * std::pow((float)i / numBursts, power);
			}
			else {
				timings[i] = actualTimeWindow * std::pow((float)i / numBursts, 1 / power);
			}
		}

		triggersOccurred = includeOriginalTrigger ? 0 : 1;
		triggersRequested = inhibitBurst ? 1 : numBursts;
		burstTimer.reset();
	}
};

struct Burst : Module {
	enum ParamIds {
		CYCLE_PARAM,
		QUANTITY_PARAM,
		TRIGGER_PARAM,
		QUANTITY_CV_PARAM,
		DISTRIBUTION_PARAM,
		TIME_PARAM,
		PROBABILITY_PARAM,
		NUM_PARAMS
	};
	enum InputIds {
		QUANTITY_INPUT,
		DISTRIBUTION_INPUT,
		PING_INPUT,
		TIME_INPUT,
		PROBABILITY_INPUT,
		TRIGGER_INPUT,
		NUM_INPUTS
	};
	enum OutputIds {
		TEMPO_OUTPUT,
		EOC_OUTPUT,
		OUT_OUTPUT,
		NUM_OUTPUTS
	};
	enum LightIds {
		ENUMS(QUANTITY_LIGHTS, 16),
		TEMPO_LIGHT,
		EOC_LIGHT,
		OUT_LIGHT,
		NUM_LIGHTS
	};


	dsp::SchmittTrigger pingTrigger; 	// for detecting Ping in
	dsp::SchmittTrigger triggTrigger;	// for detecting Trigg in
	dsp::BooleanTrigger buttonTrigger;	// for detecting when the trigger button is pressed
	dsp::ClockDivider ledUpdate; 		// for only updating LEDs every N samples
	const int ledUpdateRate = 16; 		// LEDs updated every N = 16 samples

	PingableClock pingableClock;
	BurstEngine burstEngine;
	bool includeOriginalTrigger = true;

	Burst() {
		config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
		configSwitch(Burst::CYCLE_PARAM, 0.0, 1.0, 0.0, "Mode", {"One-shot", "Cycle"});
		auto quantityParam = configParam(Burst::QUANTITY_PARAM, 1, MAX_REPETITIONS, 4, "Number of bursts");
		quantityParam->snapEnabled = true;
		configButton(Burst::TRIGGER_PARAM, "Manual Trigger");
		configParam(Burst::QUANTITY_CV_PARAM, 0.0, 1.0, 1.0, "Quantity CV");
		configParam(Burst::DISTRIBUTION_PARAM, -1.0, 1.0, 0.0, "Distribution");
		auto timeParam = configParam(Burst::TIME_PARAM, -4.0, 4.0, 0.0, "Time Division/Multiplication");
		timeParam->snapEnabled = true;
		configParam(Burst::PROBABILITY_PARAM, 0.0, 1.0, 0.0, "Probability", "%", 0.f, -100, 100.);

		configInput(QUANTITY_INPUT, "Quantity CV");
		configInput(DISTRIBUTION_INPUT, "Distribution");
		configInput(PING_INPUT, "Ping");
		configInput(TIME_INPUT, "Time Division/Multiplication");
		configInput(PROBABILITY_INPUT, "Probability");
		configInput(TRIGGER_INPUT, "Trigger");
		
		ledUpdate.setDivision(ledUpdateRate);
	}

	void process(const ProcessArgs& args) override {

		const bool pingReceived = pingTrigger.process(inputs[PING_INPUT].getVoltage());
		pingableClock.process(pingReceived, args.sampleTime);

		if (ledUpdate.process()) {
			updateLEDRing(args);
		}

		const float quantityCV = params[QUANTITY_CV_PARAM].getValue() * clamp(inputs[QUANTITY_INPUT].getVoltage(), -5.0, +10.f) / 5.f;
		const int quantity = clamp((int)(params[QUANTITY_PARAM].getValue() + std::round(16 * quantityCV)), 1, MAX_REPETITIONS);

		const bool loop = params[CYCLE_PARAM].getValue();

		const float divMultCV = 4.0 * inputs[TIME_INPUT].getVoltage() / 10.f;
		const int divMult = -clamp((int)(divMultCV + params[TIME_PARAM].getValue()), -4, +4);

		const float distributionCV = inputs[DISTRIBUTION_INPUT].getVoltage() / 10.f;
		const float distribution = clamp(distributionCV + params[DISTRIBUTION_PARAM].getValue(), -1.f, +1.f);

		const bool triggerInputTriggered = triggTrigger.process(inputs[TRIGGER_INPUT].getVoltage());
		const bool triggerButtonTriggered = buttonTrigger.process(params[TRIGGER_PARAM].getValue());
		const bool startBurst = triggerInputTriggered || triggerButtonTriggered;

		if (startBurst) {
			const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f);
			const bool inhibitBurst = rack::random::uniform() < prob;

			// remember to do at current tempo
			burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger);
		}

		float burstOut, eocOut;
		bool eoc;
		std::tie(burstOut, eocOut, eoc) = burstEngine.process(args.sampleTime);

		// if the burst has finished, we can also re-trigger
		if (eoc && loop) {
			const float prob = clamp(params[PROBABILITY_PARAM].getValue() + inputs[PROBABILITY_INPUT].getVoltage() / 10.f, 0.f, 1.f);
			const bool inhibitBurst = rack::random::uniform() < prob;

			// remember to do at current tempo
			burstEngine.trigger(quantity, divMult, pingableClock.tempo, distribution, inhibitBurst, includeOriginalTrigger);
		}

		const bool tempoOutHigh = pingableClock.isTempoOutHigh();
		outputs[TEMPO_OUTPUT].setVoltage(10.f * tempoOutHigh);
		lights[TEMPO_LIGHT].setBrightnessSmooth(tempoOutHigh, args.sampleTime);

		outputs[OUT_OUTPUT].setVoltage(10.f * burstOut);
		lights[OUT_LIGHT].setBrightnessSmooth(burstOut, args.sampleTime);

		outputs[EOC_OUTPUT].setVoltage(10.f * eocOut);
		lights[EOC_LIGHT].setBrightnessSmooth(eocOut, args.sampleTime);
	}

	void updateLEDRing(const ProcessArgs& args) {
		int activeLed;
		if (burstEngine.active) {
			activeLed = (burstEngine.triggersOccurred - 1) % 16;
		}
		else {
			activeLed = (((int) params[QUANTITY_PARAM].getValue() - 1) % 16);
		}
		for (int i = 0; i < 16; ++i) {
			lights[QUANTITY_LIGHTS + i].setBrightnessSmooth(i == activeLed, args.sampleTime * ledUpdateRate);
		}
	}

	json_t* dataToJson() override {
		json_t* rootJ = json_object();
		json_object_set_new(rootJ, "includeOriginalTrigger", json_boolean(includeOriginalTrigger));

		return rootJ;
	}

	void dataFromJson(json_t* rootJ) override {
		json_t* includeOriginalTriggerJ = json_object_get(rootJ, "includeOriginalTrigger");
		if (includeOriginalTriggerJ) {
			includeOriginalTrigger = json_boolean_value(includeOriginalTriggerJ);
		}
	}
};


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

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

		addParam(createParam<BefacoSwitch>(mm2px(Vec(28.44228, 10.13642)), module, Burst::CYCLE_PARAM));
		addParam(createParam<Davies1900hWhiteKnobEndless>(mm2px(Vec(9.0322, 16.21467)), module, Burst::QUANTITY_PARAM));
		addParam(createParam<BefacoPush>(mm2px(Vec(28.43253, 29.6592)), module, Burst::TRIGGER_PARAM));
		addParam(createParam<BefacoTinyKnobLightGrey>(mm2px(Vec(17.26197, 41.95461)), module, Burst::QUANTITY_CV_PARAM));
		addParam(createParam<BefacoTinyKnobDarkGrey>(mm2px(Vec(22.85243, 58.45676)), module, Burst::DISTRIBUTION_PARAM));
		addParam(createParam<BefacoTinyKnobBlack>(mm2px(Vec(28.47229, 74.91607)), module, Burst::TIME_PARAM));
		addParam(createParam<BefacoTinyKnobDarkGrey>(mm2px(Vec(22.75115, 91.35201)), module, Burst::PROBABILITY_PARAM));

		addInput(createInput<BananutBlack>(mm2px(Vec(2.02153, 42.27628)), module, Burst::QUANTITY_INPUT));
		addInput(createInput<BananutBlack>(mm2px(Vec(7.90118, 58.74959)), module, Burst::DISTRIBUTION_INPUT));
		addInput(createInput<BananutBlack>(mm2px(Vec(2.05023, 75.25163)), module, Burst::PING_INPUT));
		addInput(createInput<BananutBlack>(mm2px(Vec(13.7751, 75.23049)), module, Burst::TIME_INPUT));
		addInput(createInput<BananutBlack>(mm2px(Vec(7.89545, 91.66642)), module, Burst::PROBABILITY_INPUT));
		addInput(createInput<BananutBlack>(mm2px(Vec(1.11155, 109.30346)), module, Burst::TRIGGER_INPUT));

		addOutput(createOutput<BananutRed>(mm2px(Vec(11.07808, 109.30346)), module, Burst::TEMPO_OUTPUT));
		addOutput(createOutput<BananutRed>(mm2px(Vec(21.08452, 109.32528)), module, Burst::EOC_OUTPUT));
		addOutput(createOutput<BananutRed>(mm2px(Vec(31.01113, 109.30346)), module, Burst::OUT_OUTPUT));

		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(14.03676, 9.98712)), module, Burst::QUANTITY_LIGHTS + 0));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(18.35846, 10.85879)), module, Burst::QUANTITY_LIGHTS + 1));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(22.05722, 13.31827)), module, Burst::QUANTITY_LIGHTS + 2));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(24.48707, 16.96393)), module, Burst::QUANTITY_LIGHTS + 3));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(25.38476, 21.2523)), module, Burst::QUANTITY_LIGHTS + 4));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(24.48707, 25.5354)), module, Burst::QUANTITY_LIGHTS + 5));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(22.05722, 29.16905)), module, Burst::QUANTITY_LIGHTS + 6));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(18.35846, 31.62236)), module, Burst::QUANTITY_LIGHTS + 7));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(14.03676, 32.48786)), module, Burst::QUANTITY_LIGHTS + 8));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(9.74323, 31.62236)), module, Burst::QUANTITY_LIGHTS + 9));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(6.10149, 29.16905)), module, Burst::QUANTITY_LIGHTS + 10));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(3.68523, 25.5354)), module, Burst::QUANTITY_LIGHTS + 11));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(2.85312, 21.2523)), module, Burst::QUANTITY_LIGHTS + 12));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(3.68523, 16.96393)), module, Burst::QUANTITY_LIGHTS + 13));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(6.10149, 13.31827)), module, Burst::QUANTITY_LIGHTS + 14));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(9.74323, 10.85879)), module, Burst::QUANTITY_LIGHTS + 15));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(14.18119, 104.2831)), module, Burst::TEMPO_LIGHT));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(24.14772, 104.2831)), module, Burst::EOC_LIGHT));
		addChild(createLight<SmallLight<RedLight>>(mm2px(Vec(34.11425, 104.2831)), module, Burst::OUT_LIGHT));
	}

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

		menu->addChild(new MenuSeparator());
		menu->addChild(createBoolPtrMenuItem("Include original trigger in output", "", &module->includeOriginalTrigger));
	}
};


Model* modelBurst = createModel<Burst, BurstWidget>("Burst");

