#include "plugin.hpp"

// Be warned that, as the immortal programmar Roast Beef said, "My code is a Dog's code."
// Gratitude to Mannequins for letting me create this humble clone of their awesome work as a way to learn VCVrack :D

// Soft clipping algo for Cheese output
float clip(float x) {
    // Pade approximant of x/(1 + x^12)^(1/12)
    const float limit = 1.16691853009184f;
    x = clamp(x, -limit, limit);
    return (x + 1.45833f*std::pow(x, 13) + 0.559028f*std::pow(x, 25) + 0.0427035f*std::pow(x, 37))
        / (1.f + 1.54167f*std::pow(x, 12) + 0.642361f*std::pow(x, 24) + 0.0579909f*std::pow(x, 36));
}

// Introduce simple filter class which blocks DC signal (for mixer)
class DCBlocker
{
public:
    DCBlocker() {}
    float process(float input)
    {
        float y = 0.005 * xm1 + 0.995 * ym1;
        xm1 = input;
        ym1 = y;
        return input - y;
        /* ALTERNATE METHOD
        float y = input - xm1 + 0.995 * ym1;
        xm1 = input;
        ym1 = y;
        return y;
        */
    }
private:
    float xm1 = 0.0f;
    float ym1 = 0.0f;
};



struct Chilly_cheese : Module {
  // context menu stuff so you can switch between different modes and suchlike
  int mode = 0;

	void onReset() override {
		mode = 0;
	}

	json_t* dataToJson() override {
		json_t* rootJ = json_object();
		json_object_set_new(rootJ, "mode", json_integer(mode));
		return rootJ;
	}

	void dataFromJson(json_t* rootJ) override {
		json_t* modeJ = json_object_get(rootJ, "mode");
		if (modeJ)
			mode = json_integer_value(modeJ);
	}

  // initialize location, slew, dc blockers, etc
	float location = 0.f;
  dsp::BiquadFilter slew;
  DCBlocker dc1, dc2, dc3, dc4, dc5, dc6;
  bool firstsample = true;

	enum ParamIds {
		MACRO_PARAM,
		NUM_PARAMS
	};
	enum InputIds {
		MACRO_INPUT,
		FADE_INPUT,
		LEFT_INPUT,
		OFFSET_INPUT,
		RIGHT_INPUT,
		OR2_INPUT,
		OR1_INPUT,
		AND2_INPUT,
		AND1_INPUT,
		SLOPE_INPUT,
		CREASE_INPUT,
		NUM_INPUTS
	};
	enum OutputIds {
		LEFT_OUTPUT,
		RIGHT_OUTPUT,
		OR_OUTPUT,
		AND_OUTPUT,
		SLOPE_OUTPUT,
		FOLLOW_OUTPUT,
		CREASE_OUTPUT,
		LOCATION_OUTPUT,
		CHEESE_OUTPUT,
		NUM_OUTPUTS
	};
	enum LightIds {
		NUM_LIGHTS
	};

	Chilly_cheese() {
		config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
		configParam(MACRO_PARAM, 0.f, 1.f, 0.5f, "MACRO");

    configInput(MACRO_INPUT, "Macro CV");
    configInput(FADE_INPUT, "Fade (Normalled to Macro)");
    configInput(LEFT_INPUT, "Left (Normalled to -5v)");
    configInput(OFFSET_INPUT, "Offset");
    configInput(RIGHT_INPUT, "Right (Normalled to +5v)");
    configInput(OR1_INPUT, "OR 1");
    configInput(OR2_INPUT, "OR 2 (Normalled to Macro)");
    configInput(AND1_INPUT, "AND 1 (Normalled to OR 1)");
    configInput(AND2_INPUT, "AND 2 (Normalled to Macro)");
    configInput(SLOPE_INPUT, "Slope (Normalled to Macro)");
    configInput(CREASE_INPUT, "Crease (Normalled to Slope)");

    configOutput(LEFT_OUTPUT, "Left");
    configOutput(RIGHT_OUTPUT, "Right");
    configOutput(OR_OUTPUT, "OR");
    configOutput(AND_OUTPUT, "AND");
    configOutput(SLOPE_OUTPUT, "Slope");
    configOutput(FOLLOW_OUTPUT, "Follow");
    configOutput(CREASE_OUTPUT, "Crease");
    configOutput(LOCATION_OUTPUT, "Location");
    configOutput(CHEESE_OUTPUT, "Cheese");
	}

	void process(const ProcessArgs& args) override {
    if (firstsample) {
      slew.setParameters(dsp::BiquadFilter::LOWPASS_1POLE, 5.f / args.sampleRate, 0.5f, 1.f);
      firstsample = false;
    }
		// setup macro and macro cv
		float macro = params[MACRO_PARAM].getValue();
		float macrovolts = (macro * 10) - 5;
		float macro_cvin = 0.f;
		if ( inputs[MACRO_INPUT].isConnected() ) {
			macro_cvin = inputs[MACRO_INPUT].getVoltage();
		}
		macro += (macro_cvin / 10);
		macro = clamp(macro, 0.f, 1.f);
		macrovolts += macro_cvin;
		macrovolts = clamp(macrovolts, -5.f, 5.f);
		// initial jack normalling
		float left = -5.f;
		float right = 5.f;
		float fade = macro;
		float or2, and2, slope;
		or2 = and2 = slope = macrovolts;
		// left and right crossfader calc and normalling
		if ( inputs[LEFT_INPUT].isConnected() ) {
			left = inputs[LEFT_INPUT].getVoltage();
		}
		if ( inputs[RIGHT_INPUT].isConnected() ) {
			right = inputs[RIGHT_INPUT].getVoltage();
		}
		if ( inputs[FADE_INPUT].isConnected() ) {
			fade = inputs[FADE_INPUT].getVoltage();
			fade = (fade / 10.0f) + 0.5f;
			fade = clamp(fade, 0.f, 1.f);
		}
		float offset = inputs[OFFSET_INPUT].getVoltage();

    // updated crossfader algorithm (thanks trent!)
    float fadeddiff = fade * (right - left);
    float leftfaded  = left  + fadeddiff + offset;
    float rightfaded = right - fadeddiff + offset;
		leftfaded = clamp(leftfaded, -5.f, 5.f);
		rightfaded = clamp(rightfaded, -5.f, 5.f);
		outputs[LEFT_OUTPUT].setVoltage(leftfaded);
		outputs[RIGHT_OUTPUT].setVoltage(rightfaded);
		// begin the cheese mixture
		float cheese = dc1.process(left) + dc2.process(right);
		// or + and calc and normalling
		float or1, or_output, and1, and_output;
		or1 = and1 = inputs[OR1_INPUT].getVoltage();
		if ( inputs[OR2_INPUT].isConnected() ) {
			or2 = inputs[OR2_INPUT].getVoltage();
		}
		or_output = (or1 >= or2) ? or1 : or2;
		or_output = clamp(or_output, -5.f, 5.f);
		outputs[OR_OUTPUT].setVoltage(or_output);
		if ( inputs[AND1_INPUT].isConnected() ) {
			and1 = inputs[AND1_INPUT].getVoltage();
		}
		if ( inputs[AND2_INPUT].isConnected() ) {
			and2 = inputs[AND2_INPUT].getVoltage();
		}
		if (and1 <= and2) {
			and_output = and1;
		} else {
			and_output = and2;
		}
		and_output = clamp(and_output, -5.f, 5.f);
		outputs[AND_OUTPUT].setVoltage(and_output);
		// continue the cheese mixture
		cheese = cheese + dc3.process(or1) + dc4.process(and1);
		// slope and follow calc
		if ( inputs[SLOPE_INPUT].isConnected() ) {
			slope = inputs[SLOPE_INPUT].getVoltage();
		}
		cheese += dc5.process(slope); // penultimate cheese ingredient added
		float crease = slope;
		slope = abs(slope);
    slope = clamp(slope, 0.f, 5.f);
    float follow = slew.process(slope);
    follow = clamp(follow * 1.5, 0.f, 10.f);
		outputs[SLOPE_OUTPUT].setVoltage(slope);
		outputs[FOLLOW_OUTPUT].setVoltage(follow);
		// crease/location calc and normalling
		if ( inputs[CREASE_INPUT].isConnected() ) {
			crease = inputs[CREASE_INPUT].getVoltage();
		}
    // "location" aka Integrator has 6 different modes - default, glacial, sluggish, slowish, quickish, and snappy
    // fun fact: according to Trent, Sluggish Mode is the most accurate to the physical module's integrator!
    const float CREASES_[] = {1.f/25000, 1.f/300000, 1.f/115200, 1.f/50000, 1.f/12500, 1.f/6250};
		location += crease * CREASES_[mode];
    location = clamp(location, -5.f, 5.f);
    outputs[LOCATION_OUTPUT].setVoltage(location);
		cheese += dc6.process(crease); // final cheese ingredient added
		if ( crease >= 0.f ) {
			crease -= 5.f;
		} else {
			crease += 5.f;
		}
		crease = clamp(crease, -5.f, 5.f);
		outputs[CREASE_OUTPUT].setVoltage(crease);
		// SAY CHEESE!!! ^_^
		cheese = clip(cheese*macro*0.1f)*10.f;
		outputs[CHEESE_OUTPUT].setVoltage(cheese);
	}
};


struct Chilly_cheeseWidget : ModuleWidget {
	Chilly_cheeseWidget(Chilly_cheese* module) {
		setModule(module);
		setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/chilly_cheese.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<RoundHugeBlackKnob>(mm2px(Vec(20.026, 20.232)), module, Chilly_cheese::MACRO_PARAM));

		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.863, 38.04)), module, Chilly_cheese::MACRO_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(19.73, 44.832)), module, Chilly_cheese::FADE_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.688, 51.383)), module, Chilly_cheese::LEFT_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(19.622, 58.037)), module, Chilly_cheese::OFFSET_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.723, 64.571)), module, Chilly_cheese::RIGHT_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(19.664, 71.15)), module, Chilly_cheese::OR2_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.555, 77.472)), module, Chilly_cheese::OR1_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(19.58, 84.346)), module, Chilly_cheese::AND2_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.642, 90.446)), module, Chilly_cheese::AND1_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.304, 103.82)), module, Chilly_cheese::SLOPE_INPUT));
		addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.22, 117.04)), module, Chilly_cheese::CREASE_INPUT));

		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(32.877, 38.213)), module, Chilly_cheese::LEFT_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(33.056, 51.271)), module, Chilly_cheese::RIGHT_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(33.024, 64.772)), module, Chilly_cheese::OR_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(32.997, 77.686)), module, Chilly_cheese::AND_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(33.056, 90.685)), module, Chilly_cheese::SLOPE_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(19.369, 97.079)), module, Chilly_cheese::FOLLOW_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(33.056, 103.504)), module, Chilly_cheese::CREASE_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(19.538, 110.361)), module, Chilly_cheese::LOCATION_OUTPUT));
		addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(32.997, 116.503)), module, Chilly_cheese::CHEESE_OUTPUT));
	}

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

    menu->addChild(new MenuEntry);
    menu->addChild(createMenuLabel("Mode"));

    struct ModeItem : MenuItem {
      Chilly_cheese* module;
      int mode;
      void onAction(const event::Action& e) override {
        module->mode = mode;
      }
    };

    std::string modeNames[6] = {"Default Location Speed", "Glacial Location Speed", "Sluggish Location Speed", "Slowish Location Speed", "Quickish Location Speed", "Snappy Location Speed"};
    for (int i = 0; i < 6; i++) {
      ModeItem* modeItem = createMenuItem<ModeItem>(modeNames[i]);
      modeItem->rightText = CHECKMARK(module->mode == i);
      modeItem->module = module;
      modeItem->mode = i;
      menu->addChild(modeItem);
    }
  }
};


Model* modelChilly_cheese = createModel<Chilly_cheese, Chilly_cheeseWidget>("chilly_cheese");
