#include <app/CableWidget.hpp>
#include <widget/SvgWidget.hpp>
#include <widget/TransformWidget.hpp>
#include <app/Scene.hpp>
#include <app/RackWidget.hpp>
#include <app/ModuleWidget.hpp>
#include <context.hpp>
#include <asset.hpp>
#include <settings.hpp>
#include <engine/Engine.hpp>
#include <engine/Port.hpp>
#include <app/MultiLightWidget.hpp>
#include <componentlibrary.hpp>


namespace rack {
namespace app {


struct TintWidget : widget::Widget {
	NVGcolor color = color::WHITE;
	void draw(const DrawArgs& args) override {
		nvgTint(args.vg, color);
		Widget::draw(args);
	}
};


struct PlugLight : componentlibrary::TRedGreenBlueLight<app::MultiLightWidget> {
	PlugLight() {
		box.size = math::Vec(9, 9);
	}
};


struct PlugWidget::Internal {
	CableWidget* cableWidget;
	engine::Port::Type type;

	/** Initially pointing upward. */
	float angle = 0.5f * M_PI;

	widget::FramebufferWidget* fb;
	widget::TransformWidget* plugTransform;
	TintWidget* plugTint;
	widget::SvgWidget* plug;
	widget::SvgWidget* plugPort;
	app::MultiLightWidget* plugLight;
};


PlugWidget::PlugWidget() {
	internal = new Internal;

	internal->fb = new widget::FramebufferWidget;
	addChild(internal->fb);

	internal->plugTransform = new widget::TransformWidget;
	internal->fb->addChild(internal->plugTransform);

	internal->plugTint = new TintWidget;
	internal->plugTransform->addChild(internal->plugTint);

	internal->plug = new widget::SvgWidget;
	internal->plug->setSvg(window::Svg::load(asset::system("res/ComponentLibrary/Plug.svg")));
	internal->plugTint->addChild(internal->plug);
	internal->plugTransform->setSize(internal->plug->getSize());
	internal->plugTransform->setPosition(internal->plug->getSize().mult(-0.5));
	internal->plugTint->setSize(internal->plug->getSize());

	internal->plugPort = new widget::SvgWidget;
	internal->plugPort->setSvg(window::Svg::load(asset::system("res/ComponentLibrary/PlugPort.svg")));
	internal->plugPort->setPosition(internal->plugPort->getSize().mult(-0.5));
	internal->fb->addChild(internal->plugPort);

	internal->plugLight = new PlugLight;
	internal->plugLight->setPosition(internal->plugLight->getSize().mult(-0.5));
	addChild(internal->plugLight);

	setSize(internal->plug->getSize());
}


PlugWidget::~PlugWidget() {
	delete internal;
}

void PlugWidget::step() {
	std::vector<float> values(3);

	PortWidget* pw = internal->cableWidget->getPort(internal->type);
	if (pw && internal->plugLight->isVisible()) {
		engine::Port* port = pw->getPort();
		if (port) {
			for (int i = 0; i < 3; i++) {
				values[i] = port->plugLights[i].getBrightness();
			}
		}
	}
	internal->plugLight->setBrightnesses(values);

	Widget::step();
}

void PlugWidget::setColor(NVGcolor color) {
	if (color::isEqual(color, internal->plugTint->color))
		return;
	internal->plugTint->color = color;
	internal->fb->setDirty();
}

void PlugWidget::setAngle(float angle) {
	if (angle == internal->angle)
		return;
	internal->angle = angle;
	internal->plugTransform->identity();
	internal->plugTransform->rotate(angle - 0.5f * M_PI, internal->plug->getSize().div(2));
	internal->fb->setDirty();
}

void PlugWidget::setTop(bool top) {
	internal->plugLight->setVisible(top);
}

CableWidget* PlugWidget::getCable() {
	return internal->cableWidget;
}

engine::Port::Type PlugWidget::getType() {
	return internal->type;
}


struct CableWidget::Internal {
	/** For making history consistent when disconnecting and reconnecting cable. */
	int64_t cableId = -1;
};


CableWidget::CableWidget() {
	internal = new Internal;
	color = color::BLACK_TRANSPARENT;

	outputPlug = new PlugWidget;
	outputPlug->internal->cableWidget = this;
	outputPlug->internal->type = engine::Port::OUTPUT;

	inputPlug = new PlugWidget;
	inputPlug->internal->cableWidget = this;
	inputPlug->internal->type = engine::Port::INPUT;
}


CableWidget::~CableWidget() {
	delete outputPlug;
	delete inputPlug;

	setCable(NULL);
	delete internal;
}


bool CableWidget::isComplete() {
	return outputPort && inputPort;
}


void CableWidget::updateCable() {
	if (cable) {
		APP->engine->removeCable(cable);
		delete cable;
		cable = NULL;
	}
	if (inputPort && outputPort) {
		cable = new engine::Cable;
		cable->id = internal->cableId;
		cable->inputModule = inputPort->module;
		cable->inputId = inputPort->portId;
		cable->outputModule = outputPort->module;
		cable->outputId = outputPort->portId;
		APP->engine->addCable(cable);
		internal->cableId = cable->id;
	}
}


void CableWidget::setCable(engine::Cable* cable) {
	if (this->cable) {
		APP->engine->removeCable(this->cable);
		delete this->cable;
		this->cable = NULL;
		internal->cableId = -1;
	}
	if (cable) {
		app::ModuleWidget* outputMw = APP->scene->rack->getModule(cable->outputModule->id);
		if (!outputMw)
			throw Exception("Cable cannot find output ModuleWidget %lld", (long long) cable->outputModule->id);
		outputPort = outputMw->getOutput(cable->outputId);
		if (!outputPort)
			throw Exception("Cable cannot find output port %d", cable->outputId);

		app::ModuleWidget* inputMw = APP->scene->rack->getModule(cable->inputModule->id);
		if (!inputMw)
			throw Exception("Cable cannot find input ModuleWidget %lld", (long long) cable->inputModule->id);
		inputPort = inputMw->getInput(cable->inputId);
		if (!inputPort)
			throw Exception("Cable cannot find input port %d", cable->inputId);

		this->cable = cable;
		internal->cableId = cable->id;
	}
	else {
		outputPort = NULL;
		inputPort = NULL;
	}
}


engine::Cable* CableWidget::getCable() {
	return cable;
}


math::Vec CableWidget::getInputPos() {
	if (inputPort) {
		return inputPort->getRelativeOffset(inputPort->box.zeroPos().getCenter(), APP->scene->rack);
	}
	else if (hoveredInputPort) {
		return hoveredInputPort->getRelativeOffset(hoveredInputPort->box.zeroPos().getCenter(), APP->scene->rack);
	}
	else {
		return APP->scene->rack->getMousePos();
	}
}


math::Vec CableWidget::getOutputPos() {
	if (outputPort) {
		return outputPort->getRelativeOffset(outputPort->box.zeroPos().getCenter(), APP->scene->rack);
	}
	else if (hoveredOutputPort) {
		return hoveredOutputPort->getRelativeOffset(hoveredOutputPort->box.zeroPos().getCenter(), APP->scene->rack);
	}
	else {
		return APP->scene->rack->getMousePos();
	}
}


void CableWidget::mergeJson(json_t* rootJ) {
	std::string s = color::toHexString(color);
	json_object_set_new(rootJ, "color", json_string(s.c_str()));
}


void CableWidget::fromJson(json_t* rootJ) {
	json_t* colorJ = json_object_get(rootJ, "color");
	if (colorJ && json_is_string(colorJ)) {
		color = color::fromHexString(json_string_value(colorJ));
	}
	else {
		// In <v0.6.0, cables used JSON objects instead of hex strings. Just ignore them if so and use the existing cable color.
		// In <=v1, cable colors were not serialized.
		color = APP->scene->rack->getNextCableColor();
	}
}


static math::Vec getSlumpPos(math::Vec pos1, math::Vec pos2) {
	float dist = pos1.minus(pos2).norm();
	math::Vec avg = pos1.plus(pos2).div(2);
	// Lower average point as distance increases
	avg.y += (1.0 - settings::cableTension) * (150.0 + 1.0 * dist);
	return avg;
}


void CableWidget::step() {
	math::Vec outputPos = getOutputPos();
	math::Vec inputPos = getInputPos();
	math::Vec slump = getSlumpPos(outputPos, inputPos);

	NVGcolor colorOpaque = color;
	colorOpaque.a = 1.f;

	// Draw output plug
	outputPlug->setPosition(outputPos);
	bool outputTop = outputPort && (APP->scene->rack->getTopPlug(outputPort) == outputPlug);
	outputPlug->setTop(outputTop);
	outputPlug->setAngle(slump.minus(outputPos).arg());
	outputPlug->setColor(colorOpaque);

	// Draw input plug
	inputPlug->setPosition(inputPos);
	bool inputTop = inputPort && (APP->scene->rack->getTopPlug(inputPort) == inputPlug);
	inputPlug->setTop(inputTop);
	inputPlug->setAngle(slump.minus(inputPos).arg());
	inputPlug->setColor(colorOpaque);

	Widget::step();
}


void CableWidget::draw(const DrawArgs& args) {
	CableWidget::drawLayer(args, 0);
}


void CableWidget::drawLayer(const DrawArgs& args, int layer) {
	// Cable shadow and cable
	float opacity = settings::cableOpacity;
	bool thick = false;

	if (isComplete()) {
		engine::Output* output = &cable->outputModule->outputs[cable->outputId];
		// Increase thickness if output port is polyphonic
		if (output->isPolyphonic()) {
			thick = true;
		}

		// Draw opaque if mouse is hovering over a connected port
		Widget* hoveredWidget = APP->event->hoveredWidget;
		if (outputPort == hoveredWidget || inputPort == hoveredWidget) {
			opacity = 1.0;
		}
		// Draw translucent cable if not active (i.e. 0 channels)
		else if (output->getChannels() == 0) {
			opacity *= 0.5;
		}
	}
	else {
		// Draw opaque if the cable is incomplete
		opacity = 1.0;
	}

	if (opacity <= 0.0)
		return;
	nvgAlpha(args.vg, std::pow(opacity, 1.5));

	math::Vec outputPos = getOutputPos();
	math::Vec inputPos = getInputPos();

	float thickness = thick ? 9.0 : 6.0;

	// The endpoints are off-center
	math::Vec slump = getSlumpPos(outputPos, inputPos);
	float dist = 14.f;
	outputPos = outputPos.plus(slump.minus(outputPos).normalize().mult(dist));
	inputPos = inputPos.plus(slump.minus(inputPos).normalize().mult(dist));

	nvgLineCap(args.vg, NVG_ROUND);
	// Avoids glitches when cable is bent
	nvgLineJoin(args.vg, NVG_ROUND);

	if (layer == -1) {
		// Draw cable shadow
		math::Vec shadowSlump = slump.plus(math::Vec(0, 30));
		nvgBeginPath(args.vg);
		nvgMoveTo(args.vg, VEC_ARGS(outputPos));
		nvgQuadTo(args.vg, VEC_ARGS(shadowSlump), VEC_ARGS(inputPos));
		NVGcolor shadowColor = nvgRGBAf(0, 0, 0, 0.10);
		nvgStrokeColor(args.vg, shadowColor);
		nvgStrokeWidth(args.vg, thickness - 1.0);
		nvgStroke(args.vg);
	}
	else if (layer == 0) {
		// Draw cable outline
		nvgBeginPath(args.vg);
		nvgMoveTo(args.vg, VEC_ARGS(outputPos));
		nvgQuadTo(args.vg, VEC_ARGS(slump), VEC_ARGS(inputPos));
		// nvgStrokePaint(args.vg, nvgLinearGradient(args.vg, VEC_ARGS(outputPos), VEC_ARGS(inputPos), color::mult(color, 0.5), color));
		nvgStrokeColor(args.vg, color::mult(color, 0.8));
		nvgStrokeWidth(args.vg, thickness);
		nvgStroke(args.vg);

		// Draw cable
		nvgStrokeColor(args.vg, color::mult(color, 0.95));
		nvgStrokeWidth(args.vg, thickness - 1.0);
		nvgStroke(args.vg);
	}

	Widget::drawLayer(args, layer);
}


engine::Cable* CableWidget::releaseCable() {
	engine::Cable* cable = this->cable;
	this->cable = NULL;
	internal->cableId = -1;
	return cable;
}


void CableWidget::onAdd(const AddEvent& e) {
	Widget* plugContainer = APP->scene->rack->getPlugContainer();
	plugContainer->addChild(outputPlug);
	plugContainer->addChild(inputPlug);
	Widget::onAdd(e);
}


void CableWidget::onRemove(const RemoveEvent& e) {
	Widget* plugContainer = APP->scene->rack->getPlugContainer();
	plugContainer->removeChild(outputPlug);
	plugContainer->removeChild(inputPlug);
	Widget::onRemove(e);
}


} // namespace app
} // namespace rack
