text: #include "global_pre.hpp"
  #include "Bidoo.hpp"
  #include "BidooComponents.hpp"
  #include "dsp/samplerate.hpp"
  #include "dsp/decimator.hpp"
  #include "dsp/filter.hpp"
  #include "global_ui.hpp"
  
  using namespace std;
  
  namespace rack_plugin_Bidoo {
  
  extern float sawTable[2048];
  extern float triTable[2048];
  
  struct CLACOS : Module {
  	enum ParamIds {
  		PITCH_PARAM,
      FINE_PARAM,
  		DIST_X_PARAM,
      DIST_Y_PARAM = DIST_X_PARAM + 4,
  		WAVEFORM_PARAM = DIST_Y_PARAM + 4,
  		MODE_PARAM = WAVEFORM_PARAM + 4,
  		SYNC_PARAM,
  		FM_PARAM,
  		NUM_PARAMS
  	};
  	enum InputIds {
  		PITCH_INPUT,
      SYNC_INPUT,
  		DIST_X_INPUT,
      DIST_Y_INPUT = DIST_X_INPUT + 4,
  		WAVEFORM_INPUT = DIST_Y_PARAM + 4,
  		FM_INPUT = WAVEFORM_INPUT + 4,
  		NUM_INPUTS
  	};
  	enum OutputIds {
  		MAIN_OUTPUT,
  		NUM_OUTPUTS
  	};
  	enum LightIds {
  		NUM_LIGHTS
  	};
  	bool analog = false;
  	bool soft = false;
  	float lastSyncValue = 0.0f;
  	float phase = 0.0f;
  	float phaseDist = 0.0f;
  	float phaseDistX[4] = {0.5f};
  	float phaseDistY[4] = {0.5f};
  	int waveFormIndex[4] = {0};
  	float freq;
  	float pitch;
  	bool syncEnabled = false;
  	bool syncDirection = false;
  	int index = 0, prevIndex = 3;
  
  	Decimator<16, 16> mainDecimator;
  	RCFilter sqrFilter;
  	RCFilter mainFilter;
  
  	// For analog detuning effect
  	float pitchSlew = 0.0f;
  	int pitchSlewIndex = 0;
  
  	float sinBuffer[16] = {0.0f};
  	float triBuffer[16] = {0.0f};
  	float sawBuffer[16] = {0.0f};
  	float sqrBuffer[16] = {0.0f};
  	float mainBuffer[16] = {0.0f};
  
  	void setPitch(float pitchKnob, float pitchCv) {
  		// Compute frequency
  		pitch = pitchKnob;
  		if (analog) {
  			// Apply pitch slew
  			const float pitchSlewAmount = 3.0f;
  			pitch += pitchSlew * pitchSlewAmount;
  		}
  		else {
  			// Quantize coarse knob if digital mode
  			pitch = roundf(pitch);
  		}
  		pitch += pitchCv;
  		// Note C3
  		freq = 261.626f * powf(2.0f, pitch / 12.0f);
  	}
  
  	void process(float deltaTime, float syncValue) {
  		if (analog) {
  			// Adjust pitch slew
  			if (++pitchSlewIndex > 32) {
  				const float pitchSlewTau = 100.0f; // Time constant for leaky integrator in seconds
  				pitchSlew += (randomNormal() - pitchSlew / pitchSlewTau) / engineGetSampleRate();
  				pitchSlewIndex = 0.0f;
  			}
  		}
  
  		// Advance phase
  		float deltaPhase = clamp(freq * deltaTime, 1e-6f, 0.5f);
  
  		// Detect sync
  		int syncIndex = -1; // Index in the oversample loop where sync occurs [0, OVERSAMPLE)
  		float syncCrossing = 0.0f; // Offset that sync occurs [0.0, 1.0)
  		if (syncEnabled) {
  			syncValue -= 0.01f;
  			if (syncValue > 0.0f && lastSyncValue <= 0.0f) {
  				float deltaSync = syncValue - lastSyncValue;
  				syncCrossing = 1.0f - syncValue / deltaSync;
  				syncCrossing *= 16;
  				syncIndex = (int)syncCrossing;
  				syncCrossing -= syncIndex;
  			}
  			lastSyncValue = syncValue;
  		}
  
  		if (syncDirection)
  			deltaPhase *= -1.0f;
  
  		sqrFilter.setCutoff(40.0f * deltaTime);
  		mainFilter.setCutoff(22000.0f * deltaTime);
  
  		for (int i = 0; i < 16; i++) {
  			if (syncIndex == i) {
  				if (soft) {
  					syncDirection = !syncDirection;
  					deltaPhase *= -1.0f;
  				}
  				else {
  					phase = 0.0f;
  					phaseDist = 0.0f;
  				}
  			}
  
  			if (phase<0.25f)
  				index = 0;
  			else if ((phase>=0.25f) && (phase<0.50f))
  				index = 1;
  			else if ((phase>=0.50f) && (phase<0.75f))
  				index = 2;
  			else
  				index = 3;
  
  			if (analog) {
  				// Quadratic approximation of sine, slightly richer harmonics
  				if (phaseDist < 0.5f)
  					sinBuffer[i] = 1.0f - 16.0f * powf(phaseDist - 0.25f, 2.0f);
  				else
  					sinBuffer[i] = -1.0f + 16.0f * powf(phaseDist - 0.75f, 2.0f);
  				sinBuffer[i] *= 1.08f;
  			}
  			else {
  				sinBuffer[i] = sinf(2.0f * M_PI * phaseDist);
  			}
  			if (analog) {
  				triBuffer[i] = 1.25f * interpolateLinear(triTable, phaseDist * 2047.f);
  			}
  			else {
  				if (phaseDist < 0.25f)
  					triBuffer[i] = 4.0f * phaseDist;
  				else if (phaseDist < 0.75f)
  					triBuffer[i] = 2.0f - 4.0f * phaseDist;
  				else
  					triBuffer[i] = -4.0f + 4.0f * phaseDist;
  			}
  			if (analog) {
  				sawBuffer[i] = 1.66f * interpolateLinear(sawTable, phaseDist * 2047.f);
  			}
  			else {
  				if (phaseDist < 0.5f)
  					sawBuffer[i] = 2.0f * phaseDist;
  				else
  					sawBuffer[i] = -2.0f + 2.0f * phaseDist;
  			}
  			sqrBuffer[i] = (phaseDist < 0.5f) ? 1.0f : -1.0f;
  			if (analog) {
  				// Simply filter here
  				sqrFilter.process(sqrBuffer[i]);
  				sqrBuffer[i] = 0.71f * sqrFilter.highpass();
  			}
  
  			waveFormIndex[index] = inputs[WAVEFORM_INPUT+index].active ? clamp((int)(rescale(inputs[WAVEFORM_INPUT+index].value,0.0f,10.0f,0.0f,3.0f)),0,3) : clamp((int)(params[WAVEFORM_PARAM+index].value),0,3);
  			if (waveFormIndex[index] == 0)
  				mainBuffer[i]=sinBuffer[i];
  			else if (waveFormIndex[index] == 1)
  				mainBuffer[i]=triBuffer[i];
  			else if (waveFormIndex[index] == 2)
  				mainBuffer[i]=sawBuffer[i];
  			else if (waveFormIndex[index] == 3)
  				mainBuffer[i]=sqrBuffer[i];
  
  			mainFilter.process(mainBuffer[i]);
  			mainBuffer[i]=mainFilter.lowpass();
  
  			// Advance phase
  			phase += deltaPhase / 16.0f;
  			phase = eucmod(phase, 1.0f);
  			if (phase<=0.25f)
  				index = 0;
  			else if ((phase>0.25f) && (phase<=0.5f))
  				index = 1;
  			else if ((phase>0.5f) && (phase<=0.75f))
  				index = 2;
  			else
  				index = 3;
  
  			if (prevIndex!=index){
  				phaseDist = phase;
  				prevIndex = index;
  			}
  			else {
  				if (rescale(phase,index*0.25f,(index+1)*0.25f,0.0f,1.0f)<=(phaseDistX[index]))
  					phaseDist = min(phaseDist + (deltaPhase / 16.0f) * phaseDistY[index]/phaseDistX[index], (index+1)*0.25f);
  				else
  					phaseDist = min(phaseDist + (deltaPhase / 16.0f) * (1-phaseDistY[index])/(1-phaseDistX[index]), (index+1)*0.25f);
  				phaseDist = eucmod(phaseDist, 1.0f);
  			}
  		}
  	}
  
  	float main() {
  		return mainDecimator.process(mainBuffer);
  	}
  	float light() {
  		return sinf(2.0f*M_PI * phase);
  	}
  
  	CLACOS() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
  	}
  
  	void step() override;
  
  	json_t *toJson() override {
  		json_t *rootJ = json_object();
  		for (int i=0; i<4; i++) {
  			json_object_set_new(rootJ, ("phaseDistX" + to_string(i)).c_str(), json_real(phaseDistX[i]));
  			json_object_set_new(rootJ, ("phaseDistY" + to_string(i)).c_str(), json_real(phaseDistY[i]));
  		}
  		return rootJ;
  	}
  
  	void fromJson(json_t *rootJ) override {
  		for (int i=0; i<4; i++) {
  			json_t *phaseDistXJ = json_object_get(rootJ, ("phaseDistX" + to_string(i)).c_str());
  			if (phaseDistXJ) {
  				phaseDistX[i] = json_real_value(phaseDistXJ);
  			}
  			json_t *phaseDistYJ = json_object_get(rootJ, ("phaseDistY" + to_string(i)).c_str());
  			if (phaseDistYJ) {
  				phaseDistY[i] = json_real_value(phaseDistYJ);
  			}
  		}
  	}
  
  	void randomize() override {
  		for (int i=0; i<4; i++) {
  			if ((!inputs[CLACOS::DIST_X_INPUT+i].active) && (!inputs[CLACOS::DIST_X_INPUT+i].active)) {
  				phaseDistX[i] = randomUniform();
  				phaseDistY[i] = randomUniform();
  			}
  		}
  	}
  
  	void reset() override {
  		for (int i=0; i<4; i++) {
  			if ((!inputs[CLACOS::DIST_X_INPUT+i].active) && (!inputs[CLACOS::DIST_X_INPUT+i].active)) {
  				phaseDistX[i] = 0.5;
  				phaseDistY[i] = 0.5;
  			}
  		}
  	}
  };
  
  void CLACOS::step() {
  	analog = params[MODE_PARAM].value > 0.0f;
  	soft = params[SYNC_PARAM].value <= 0.0f;
  
  	for (int i=0; i<4; i++) {
  	if (inputs[DIST_X_INPUT+i].active)
  		phaseDistX[i] = rescale(clamp(inputs[DIST_X_INPUT+i].value,0.0f,10.0f),0.0f,10.0f,0.01f,0.99f);
  
  	if (inputs[DIST_Y_INPUT+i].active)
  		phaseDistY[i] = rescale(clamp(inputs[DIST_Y_INPUT+i].value,0.0f,10.0f),0.0f,10.0f,0.01f,0.99f);
  	}
  
  	float pitchFine = 3.0f * quadraticBipolar(params[FINE_PARAM].value);
  	float pitchCv = 12.0f * inputs[PITCH_INPUT].value;
  	if (inputs[FM_INPUT].active) {
  		pitchCv += quadraticBipolar(params[FM_PARAM].value) * 12.0f * inputs[FM_INPUT].value;
  	}
  	setPitch(params[PITCH_PARAM].value, pitchFine + pitchCv);
  	syncEnabled = inputs[SYNC_INPUT].active;
  
  	process(1.0f / engineGetSampleRate(), inputs[SYNC_INPUT].value);
  
  	// Set output
  	outputs[MAIN_OUTPUT].value = 5.0f * main();
  }
  
  struct CLACOSDisplay : TransparentWidget {
  	CLACOS *module;
  	int frame = 0;
  	string waveForm;
  	int segmentNumber = 0;
  	float initX = 0.0f;
  	float initY = 0.0f;
  	float dragX = 0.0f;
  	float dragY = 0.0f;
  
  CLACOSDisplay() {}
  
  void onDragStart(EventDragStart &e) override {
  	dragX = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.x;
  	dragY = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.y;
  }
  
  void onDragMove(EventDragMove &e) override {
  	if ((!module->inputs[CLACOS::DIST_X_INPUT + segmentNumber].active) && (!module->inputs[CLACOS::DIST_X_INPUT + segmentNumber].active)) {
  		float newDragX = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.x;
  		float newDragY = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.y;
  		module->phaseDistX[segmentNumber] = rescale(clamp(initX+(newDragX-dragX),0.0f,70.0f), 0.0f, 70.0f, 0.01f,0.99f);
  		module->phaseDistY[segmentNumber]  = rescale(clamp(initY-(newDragY-dragY),0.0f,70.0f), 0.0f, 70.0f, 0.01f,0.99f);
  	}
  }
  
  void onMouseDown(EventMouseDown &e) override {
  	if (e.button == 0) {
  		e.consumed = true;
  		e.target = this;
  		initX = e.pos.x;
  		initY = 70.0f - e.pos.y;
  	}
  }
  
  void draw(NVGcontext *vg) override {
  	if (++frame >= 4) {
  		frame = 0;
  		if (module->waveFormIndex[segmentNumber] == 0)
  			waveForm="SIN";
  		else if (module->waveFormIndex[segmentNumber] == 1)
  			waveForm="TRI";
  		else if (module->waveFormIndex[segmentNumber] == 2)
  			waveForm="SAW";
  		else if (module->waveFormIndex[segmentNumber] == 3)
  			waveForm="SQR";
  	}
  	nvgFontSize(vg, 10.0f);
  	nvgFillColor(vg, nvgRGBA(42, 87, 117, 255));
  	nvgText(vg, 12.0f, 79.0f, waveForm.c_str(), NULL);
  
  	// Draw ref lines
  	nvgStrokeColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x80));
  	{
  		nvgBeginPath(vg);
  		nvgMoveTo(vg, 0.0f, 35.0f);
  		nvgLineTo(vg, 70.0f, 35.0f);
  		nvgMoveTo(vg, 35.0f, 0.0f);
  		nvgLineTo(vg, 35.0f, 70.0f);
  		nvgClosePath(vg);
  	}
  	nvgStroke(vg);
  
  	// Draw phase distortion
  	nvgStrokeColor(vg, nvgRGBA(42, 87, 117, 255));
  	{
  		nvgBeginPath(vg);
  		nvgMoveTo(vg, 0.0f, 70.0f);
  		nvgLineTo(vg, (int)(rescale(module->phaseDistX[segmentNumber], 0.0f,1.0f,0.0f,70.0f)) , 70.0f - (int)(rescale(module->phaseDistY[segmentNumber], 0.0f,1.0f,0.01f,70.0f)));
  		nvgMoveTo(vg, (int)(rescale(module->phaseDistX[segmentNumber], 0.0f,1.0f,0.0f,70.0f)) , 70.0f - (int)(rescale(module->phaseDistY[segmentNumber], 0.0f,1.0f,0.01f,70.0f)));
  		nvgLineTo(vg, 70.0f, 0.0f);
  		nvgClosePath(vg);
  	}
  	nvgStroke(vg);
  }
  };
  
  
  struct CLACOSWidget : ModuleWidget {
  	CLACOSWidget(CLACOS *module) : ModuleWidget(module) {
  		setPanel(SVG::load(assetPlugin(plugin, "res/CLACOS.svg")));
  
  		addChild(Widget::create<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
  		addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
  		addChild(Widget::create<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  		addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  
  		for (int i = 0; i < 4; i++)
  		{
  			{
  				CLACOSDisplay *display = new CLACOSDisplay();
  				display->module = module;
  				display->segmentNumber = i;
  				display->box.pos = Vec(3.0f + 74.0f * (i%2), 113.0f + 102.0f * round(i/2));
  				display->box.size = Vec(70.0f, 70.0f);
  				addChild(display);
  				addParam(ParamWidget::create<BidooBlueTrimpot>(Vec(2.0f + 74.0f * (i%2), 194.0f + 102.0f * round(i/2)), module, CLACOS::WAVEFORM_PARAM + i, 0.0f, 3.0f, 0.0f));
  				addInput(Port::create<TinyPJ301MPort>(Vec(22.0f + 74.0f * (i%2), 196.0f + 102.0f * round(i/2)), Port::INPUT, module, CLACOS::WAVEFORM_INPUT + i));
  				addInput(Port::create<TinyPJ301MPort>(Vec(40.0f + 74.0f * (i%2), 196.0f + 102.0f * round(i/2)), Port::INPUT, module, CLACOS::DIST_X_INPUT + i));
  				addInput(Port::create<TinyPJ301MPort>(Vec(57.0f + 74.0f * (i%2), 196.0f + 102.0f * round(i/2)), Port::INPUT, module, CLACOS::DIST_Y_INPUT + i));
  			}
  		}
  
  		addParam(ParamWidget::create<CKSS>(Vec(15.0f, 80.0f), module, CLACOS::MODE_PARAM, 0.0f, 1.0f, 1.0f));
  		addParam(ParamWidget::create<CKSS>(Vec(119.0f, 80.0f), module, CLACOS::SYNC_PARAM, 0.0f, 1.0f, 1.0f));
  
  		addParam(ParamWidget::create<BidooLargeBlueKnob>(Vec(57.0f, 45.0f), module, CLACOS::PITCH_PARAM, -54.0f, 54.0f, 0.0f));
  	  addParam(ParamWidget::create<BidooBlueTrimpot>(Vec(114.0f,45.0f), module, CLACOS::FINE_PARAM, -1.0f, 1.0f, 0.0f));
  		addParam(ParamWidget::create<BidooBlueTrimpot>(Vec(18.0f,45.0f), module, CLACOS::FM_PARAM, 0.0f, 1.0f, 0.0f));
  		addInput(Port::create<TinyPJ301MPort>(Vec(38.0f, 83.0f), Port::INPUT, module, CLACOS::FM_INPUT));
  
  	  addInput(Port::create<PJ301MPort>(Vec(11.0f, 330.0f), Port::INPUT, module, CLACOS::PITCH_INPUT));
  		addInput(Port::create<PJ301MPort>(Vec(45.0f, 330.0f), Port::INPUT, module, CLACOS::SYNC_INPUT));
  		addOutput(Port::create<PJ301MPort>(Vec(114.0f, 330.0f), Port::OUTPUT, module, CLACOS::MAIN_OUTPUT));
  	}
  };
  
  } // namespace rack_plugin_Bidoo
  
  using namespace rack_plugin_Bidoo;
  
  RACK_PLUGIN_MODEL_INIT(Bidoo, CLACOS) {
     Model *modelCLACOS = Model::create<CLACOS, CLACOSWidget>("Bidoo", "clACos", "clACos oscillator", OSCILLATOR_TAG);
     return modelCLACOS;
  }
  