#include <set>
#include <algorithm>
#include <thread>

#include <app/Browser.hpp>
#include <widget/OpaqueWidget.hpp>
#include <widget/TransparentWidget.hpp>
#include <widget/ZoomWidget.hpp>
#include <ui/MenuOverlay.hpp>
#include <ui/ScrollWidget.hpp>
#include <ui/SequentialLayout.hpp>
#include <ui/Label.hpp>
#include <ui/Slider.hpp>
#include <ui/TextField.hpp>
#include <ui/MenuItem.hpp>
#include <ui/MenuSeparator.hpp>
#include <ui/Button.hpp>
#include <ui/ChoiceButton.hpp>
#include <ui/RadioButton.hpp>
#include <ui/OptionButton.hpp>
#include <ui/Tooltip.hpp>
#include <app/ModuleWidget.hpp>
#include <app/Scene.hpp>
#include <plugin.hpp>
#include <context.hpp>
#include <engine/Engine.hpp>
#include <plugin/Model.hpp>
#include <string.hpp>
#include <history.hpp>
#include <settings.hpp>
#include <system.hpp>
#include <tag.hpp>
#include <helpers.hpp>
#include <FuzzySearchDatabase.hpp>
#include <componentlibrary.hpp>


namespace rack {
namespace app {
namespace browser {


static fuzzysearch::Database<plugin::Model*> modelDb;


static void modelDbInit() {
	modelDb = fuzzysearch::Database<plugin::Model*>();
	modelDb.setWeights({0.9f, 0.75f, 1.0f, 0.8f, 0.9f});
	modelDb.setThreshold(0.5f);

	// Iterate plugins
	for (plugin::Plugin* plugin : plugin::plugins) {
		// Iterate model in plugin
		for (plugin::Model* model : plugin->models) {
			// Get search fields for model
			std::string tagStr;
			for (int tagId : model->tagIds) {
				// If non-English, add translation of tag before English tags
				if (settings::language != "en") {
					tagStr += string::translate("tag." + tag::getTag(tagId), settings::language);
					tagStr += " ";
				}
				// Add all aliases of a tag
				for (const std::string& tagAlias : tag::tagAliases[tagId]) {
					tagStr += tagAlias;
					tagStr += " ";
				}
			}
			std::vector<std::string> fields = {
				model->plugin->brand,
				model->plugin->name,
				model->name,
				model->description,
				tagStr,
			};
			// DEBUG("%s; %s; %s; %s; %s", fields[0].c_str(), fields[1].c_str(), fields[2].c_str(), fields[3].c_str(), fields[4].c_str());
			modelDb.addEntry(model, fields);
		}
	}
}


static ModuleWidget* chooseModel(plugin::Model* model) {
	// Record usage
	settings::ModuleInfo& mi = settings::moduleInfos[model->plugin->slug][model->slug];
	mi.added++;
	mi.lastAdded = system::getUnixTime();

	history::ComplexAction* h = new history::ComplexAction;
	h->name = string::translate("Browser.history.addModule");

	// Create Module and ModuleWidget
	INFO("Creating module %s", model->getFullName().c_str());
	engine::Module* module = model->createModule();
	APP->engine->addModule(module);

	INFO("Creating module widget %s", model->getFullName().c_str());
	ModuleWidget* moduleWidget = model->createModuleWidget(module);

	APP->scene->rack->deselectAll();
	APP->scene->rack->updateModuleOldPositions();
	APP->scene->rack->addModuleAtMouse(moduleWidget);
	h->push(APP->scene->rack->getModuleDragAction());

	// Load template preset
	moduleWidget->loadTemplate();

	// history::ModuleAdd
	history::ModuleAdd* ha = new history::ModuleAdd;
	// This serializes the module so redoing returns to the current state.
	ha->setModule(moduleWidget);
	h->push(ha);

	APP->history->push(h);

	// Hide Module Browser
	APP->scene->browser->hide();

	return moduleWidget;
}


// Widgets


struct Browser;


struct BrowserOverlay : ui::MenuOverlay {
	void step() override {
		// Only step if visible, since there are potentially thousands of descendants that don't need to be stepped.
		if (isVisible())
			MenuOverlay::step();
	}

	void onAction(const ActionEvent& e) override {
		// Hide instead of requestDelete()
		hide();
	}
};


struct ModuleWidgetContainer : widget::Widget {
	void draw(const DrawArgs& args) override {
		Widget::draw(args);
		Widget::drawLayer(args, 1);
	}
};


struct ModelBox : widget::OpaqueWidget {
	plugin::Model* model;
	ui::Tooltip* tooltip = NULL;
	// Lazily created widgets
	widget::Widget* previewWidget = NULL;
	widget::ZoomWidget* zoomWidget = NULL;
	widget::FramebufferWidget* fb = NULL;
	ModuleWidgetContainer* mwc = NULL;
	ModuleWidget* moduleWidget = NULL;

	ModelBox() {
		updateZoom();
	}

	void setModel(plugin::Model* model) {
		this->model = model;
	}

	void updateZoom() {
		float zoom = std::pow(2.f, settings::browserZoom);

		if (previewWidget) {
			fb->setDirty();
			zoomWidget->setZoom(zoom);
			box.size.x = moduleWidget->box.size.x * zoom;
		}
		else {
			// Approximate size as 12HP before we know the actual size.
			// We need a nonzero size, otherwise too many ModelBoxes will lazily render in the same frame.
			box.size.x = 12 * RACK_GRID_WIDTH * zoom;
		}
		box.size.y = RACK_GRID_HEIGHT * zoom;
		box.size = box.size.ceil();
	}

	void createPreview() {
		if (previewWidget)
			return;

		previewWidget = new widget::TransparentWidget;
		addChild(previewWidget);

		zoomWidget = new widget::ZoomWidget;
		previewWidget->addChild(zoomWidget);

		fb = new widget::FramebufferWidget;
		if (APP->window->pixelRatio < 2.0) {
			// Small details draw poorly at low DPI, so oversample when drawing to the framebuffer
			fb->oversample = 2.0;
		}
		zoomWidget->addChild(fb);

		mwc = new ModuleWidgetContainer;
		fb->addChild(mwc);

		INFO("Creating module widget %s", model->getFullName().c_str());
		moduleWidget = model->createModuleWidget(NULL);
		mwc->addChild(moduleWidget);
		mwc->box.size = moduleWidget->box.size;

		// Step ModuleWidget so it can set its default appearance.
		moduleWidget->step();

		updateZoom();
	}

	void draw(const DrawArgs& args) override {
		// Lazily create preview when drawn
		createPreview();

		// Draw shadow
		nvgBeginPath(args.vg);
		float r = 10; // Blur radius
		float c = 5; // Corner radius
		math::Rect shadowBox = box.zeroPos().grow(math::Vec(r, r));
		nvgRect(args.vg, RECT_ARGS(shadowBox));
		NVGcolor shadowColor = nvgRGBAf(0, 0, 0, 0.5);
		nvgFillPaint(args.vg, nvgBoxGradient(args.vg, 0, 0, box.size.x, box.size.y, c, r, shadowColor, color::BLACK_TRANSPARENT));
		nvgFill(args.vg);

		// To avoid blinding the user when rack brightness is low, draw framebuffer with the same brightness.
		float b = math::clamp(settings::rackBrightness + 0.2f, 0.f, 1.f);
		nvgGlobalTint(args.vg, nvgRGBAf(b, b, b, 1));

		OpaqueWidget::draw(args);

		// Draw favorite border
		const settings::ModuleInfo* mi = settings::getModuleInfo(model->plugin->slug, model->slug);
		if (mi && mi->favorite) {
			nvgBeginPath(args.vg);
			math::Rect borderBox = box.zeroPos();
			nvgRect(args.vg, RECT_ARGS(borderBox));
			nvgStrokeWidth(args.vg, 2);
			nvgStrokeColor(args.vg, componentlibrary::SCHEME_YELLOW);
			nvgStroke(args.vg);
		}
	}

	void step() override {
		OpaqueWidget::step();
	}

	void setTooltip(ui::Tooltip* tooltip) {
		if (this->tooltip) {
			this->tooltip->requestDelete();
			this->tooltip = NULL;
		}

		if (tooltip) {
			APP->scene->addChild(tooltip);
			this->tooltip = tooltip;
		}
	}

	void onButton(const ButtonEvent& e) override {
		if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == 0) {
			ModuleWidget* mw = chooseModel(model);

			// Pretend the moduleWidget was clicked so it can be dragged in the RackWidget
			e.consume(mw);

			// Set the drag position at the center of the module
			mw->dragOffset() = mw->box.size.div(2);
			// Disable dragging temporarily until the mouse has moved a bit.
			mw->dragEnabled() = false;
		}

		// Toggle favorite
		if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
			model->setFavorite(!model->isFavorite());
			e.consume(this);
		}

		// Open context menu on right-click
		if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) {
			createContextMenu();
			e.consume(this);
		}
	}

	void onHoverKey(const HoverKeyEvent& e) override {
		if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
			if (e.isKeyCommand(GLFW_KEY_F1, RACK_MOD_CTRL)) {
				system::openBrowser(model->getManualUrl());
				e.consume(this);
			}
		}

		if (e.isConsumed())
			return;
		OpaqueWidget::onHoverKey(e);
	}

	ui::Tooltip* createTooltip() {
		std::string text;
		text += model->name;
		text += "\n";
		text += model->plugin->brand;
		// Description
		if (model->description != "") {
			text += "\n" + model->description;
		}
		// Tags
		text += "\n\n";
		text += string::translate("Browser.tooltipTags");
		std::vector<std::string> tags;
		for (int tagId : model->tagIds) {
			std::string tag = string::translate("tag." + tag::getTag(tagId));
			tags.push_back(tag);
		}
		text += string::join(tags, ", ");
		ui::Tooltip* tooltip = new ui::Tooltip;
		tooltip->text = text;
		return tooltip;
	}

	void onEnter(const EnterEvent& e) override {
		setTooltip(createTooltip());
	}

	void onLeave(const LeaveEvent& e) override {
		setTooltip(NULL);
	}

	void onHide(const HideEvent& e) override {
		// Hide tooltip
		setTooltip(NULL);
		OpaqueWidget::onHide(e);
	}

	void createContextMenu() {
		ui::Menu* menu = createMenu();

		menu->addChild(createMenuLabel(model->name));
		menu->addChild(createMenuLabel(model->plugin->brand));
		model->appendContextMenu(menu, true);
	}
};


struct BrowserSearchField : ui::TextField {
	Browser* browser;

	void step() override {
		// Steal focus when step is called
		APP->event->setSelectedWidget(this);
		TextField::step();
	}

	void onSelectKey(const SelectKeyEvent& e) override;
	void onChange(const ChangeEvent& e) override;
	void onAction(const ActionEvent& e) override;

	void onHide(const HideEvent& e) override {
		APP->event->setSelectedWidget(NULL);
		ui::TextField::onHide(e);
	}

	void onShow(const ShowEvent& e) override {
		selectAll();
		TextField::onShow(e);
	}
};


struct FavoriteQuantity : Quantity {
	Browser* browser;
	void setValue(float value) override;
	float getValue() override;
};


struct ClearButton : ui::Button {
	Browser* browser;
	void onAction(const ActionEvent& e) override;
};


struct BrandItem : ui::MenuItem {
	Browser* browser;
	std::string brand;
	void onAction(const ActionEvent& e) override;
	void step() override;
};


struct BrandButton : ui::ChoiceButton {
	Browser* browser;

	void onAction(const ActionEvent& e) override;
	void step() override;
};


struct TagItem : ui::MenuItem {
	Browser* browser;
	int tagId;
	void onAction(const ActionEvent& e) override;
	void step() override;
};


struct TagButton : ui::ChoiceButton {
	Browser* browser;

	void onAction(const ActionEvent& e) override;
	void step() override;
};


static std::vector<std::string> getSortNames() {
	return {
		string::translate("Browser.sort.lastUpdated"),
		string::translate("Browser.sort.lastUsed"),
		string::translate("Browser.sort.mostUsed"),
		string::translate("Browser.sort.brand"),
		string::translate("Browser.sort.moduleName"),
		string::translate("Browser.sort.random"),
	};
};


struct SortButton : ui::ChoiceButton {
	Browser* browser;

	void onAction(const ActionEvent& e) override;

	void step() override {
		text = string::translate("Browser.sort");
		text += getSortNames()[settings::browserSort];
		text = string::ellipsize(text, 20);
		ChoiceButton::step();
	}
};


struct ZoomButton : ui::ChoiceButton {
	Browser* browser;

	void onAction(const ActionEvent& e) override;

	void step() override {
		text = string::translate("Browser.zoom");
		text += string::f("%.0f%%", std::pow(2.f, settings::browserZoom) * 100.f);
		ChoiceButton::step();
	}
};


struct UrlButton : ui::Button {
	std::string url;
	void onAction(const ActionEvent& e) override {
		system::openBrowser(url);
	}
};


struct Browser : widget::OpaqueWidget {
	ui::SequentialLayout* headerLayout;
	BrowserSearchField* searchField;
	BrandButton* brandButton;
	TagButton* tagButton;
	FavoriteQuantity* favoriteQuantity;
	ui::OptionButton* favoriteButton;
	ClearButton* clearButton;
	ui::Label* countLabel;

	ui::ScrollWidget* modelScroll;
	widget::Widget* modelMargin;
	ui::SequentialLayout* modelContainer;

	std::string search;
	std::string brand;
	std::set<int> tagIds = {};
	bool favorite = false;
	bool lastPreferDarkPanels = false;

	// Caches and temporary state
	std::map<plugin::Model*, float> prefilteredModelScores;
	std::map<plugin::Model*, int> modelOrders;

	Browser() {
		const float margin = 10;

		// Header
		headerLayout = new ui::SequentialLayout;
		headerLayout->box.pos = math::Vec(0, 0);
		headerLayout->box.size.y = 0;
		headerLayout->margin = math::Vec(margin, margin);
		headerLayout->spacing = math::Vec(margin, margin);
		addChild(headerLayout);

		searchField = new BrowserSearchField;
		searchField->box.size.x = 150;
		searchField->placeholder = string::translate("Browser.searchModules");
		searchField->browser = this;
		headerLayout->addChild(searchField);

		brandButton = new BrandButton;
		brandButton->box.size.x = 150;
		brandButton->browser = this;
		headerLayout->addChild(brandButton);

		tagButton = new TagButton;
		tagButton->box.size.x = 150;
		tagButton->browser = this;
		headerLayout->addChild(tagButton);

		favoriteQuantity = new FavoriteQuantity;
		favoriteQuantity->browser = this;

		favoriteButton = new ui::OptionButton;
		favoriteButton->quantity = favoriteQuantity;
		favoriteButton->text = string::translate("Browser.favorites");
		favoriteButton->box.size.x = 70;
		headerLayout->addChild(favoriteButton);

		clearButton = new ClearButton;
		clearButton->box.size.x = 130;
		clearButton->text = string::translate("Browser.resetFilters");
		clearButton->browser = this;
		headerLayout->addChild(clearButton);

		countLabel = new ui::Label;
		countLabel->box.size.x = 110;
		headerLayout->addChild(countLabel);

		SortButton* sortButton = new SortButton;
		sortButton->box.size.x = 150;
		sortButton->browser = this;
		headerLayout->addChild(sortButton);

		ZoomButton* zoomButton = new ZoomButton;
		zoomButton->box.size.x = 100;
		zoomButton->browser = this;
		headerLayout->addChild(zoomButton);

		UrlButton* libraryButton = new UrlButton;
		libraryButton->box.size.x = 150;
		libraryButton->text = string::translate("Browser.browseLibrary");
		libraryButton->url = "https://library.vcvrack.com/";
		headerLayout->addChild(libraryButton);

		// Model container
		modelScroll = new ui::ScrollWidget;
		modelScroll->box.pos.y = BND_WIDGET_HEIGHT;
		addChild(modelScroll);

		modelMargin = new widget::Widget;
		modelScroll->container->addChild(modelMargin);

		modelContainer = new ui::SequentialLayout;
		// Add 2 pixels for favorites border
		modelContainer->margin = math::Vec(margin, 2);
		modelContainer->spacing = math::Vec(margin, margin);
		modelMargin->addChild(modelContainer);

		resetModelBoxes();
		clear();
	}

	~Browser() {
		delete favoriteQuantity;
	}

	void resetModelBoxes() {
		modelContainer->clearChildren();
		modelOrders.clear();
		// Iterate plugins
		// for (int i = 0; i < 100; i++)
		for (plugin::Plugin* plugin : plugin::plugins) {
			// Iterate models in plugin
			int modelIndex = 0;
			for (plugin::Model* model : plugin->models) {
				// Create ModelBox
				ModelBox* modelBox = new ModelBox;
				modelBox->setModel(model);
				modelContainer->addChild(modelBox);

				modelOrders[model] = modelIndex;
				modelIndex++;
			}
		}
	}

	void updateZoom() {
		modelScroll->offset = math::Vec();

		for (Widget* w : modelContainer->children) {
			ModelBox* mb = reinterpret_cast<ModelBox*>(w);
			assert(mb);
			mb->updateZoom();
		}
	}

	void step() override {
		box = parent->box.zeroPos().grow(math::Vec(-40, -40));

		headerLayout->box.size.x = box.size.x;

		const float margin = 10;
		modelScroll->box.pos = headerLayout->box.getBottomLeft();
		modelScroll->box.size = box.size.minus(modelScroll->box.pos);
		modelMargin->box.size.x = modelScroll->box.size.x;
		modelMargin->box.size.y = modelContainer->box.size.y + margin;
		modelContainer->box.size.x = modelMargin->box.size.x - margin;

		// Check if preferDarkPanels has changed
		if (settings::preferDarkPanels != lastPreferDarkPanels) {
			lastPreferDarkPanels = settings::preferDarkPanels;
			// Request module framebuffers to re-render
			Widget::DirtyEvent eDirty;
			modelContainer->onDirty(eDirty);
		}

		OpaqueWidget::step();
	}

	void draw(const DrawArgs& args) override {
		bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0);
		Widget::draw(args);
	}

	bool isModelVisible(plugin::Model* model, const std::string& brand, std::set<int> tagIds, bool favorite) {
		// Filter hidden
		if (model->hidden)
			return false;

		// Filter moduleInfo setting if enabled is false
		settings::ModuleInfo* mi = settings::getModuleInfo(model->plugin->slug, model->slug);
		if (mi && !mi->enabled)
			return false;

		// Filter if not whitelisted by library
		if (!settings::isModuleWhitelisted(model->plugin->slug, model->slug))
			return false;

		// Filter favorites
		if (favorite) {
			if (!mi || !mi->favorite)
				return false;
		}

		// Filter brand
		if (!brand.empty()) {
			if (model->plugin->brand != brand)
				return false;
		}

		// Filter tag
		for (int tagId : tagIds) {
			auto it = std::find(model->tagIds.begin(), model->tagIds.end(), tagId);
			if (it == model->tagIds.end())
				return false;
		}

		return true;
	};

	// Determines if there is at least 1 visible Model with a given brand and tag
	bool hasVisibleModel(const std::string& brand, std::set<int> tagIds, bool favorite) {
		for (const auto& pair : prefilteredModelScores) {
			plugin::Model* model = pair.first;
			if (isModelVisible(model, brand, tagIds, favorite))
				return true;
		}
		return false;
	};

	template <typename F>
	void sortModels(F f) {
		modelContainer->children.sort([&](Widget* w1, Widget* w2) {
			ModelBox* m1 = reinterpret_cast<ModelBox*>(w1);
			ModelBox* m2 = reinterpret_cast<ModelBox*>(w2);
			return f(m1) < f(m2);
		});
	}

	void refresh() {
		// Reset scroll position
		modelScroll->offset = math::Vec();

		prefilteredModelScores.clear();

		// Filter ModelBoxes by brand and tag
		for (Widget* w : modelContainer->children) {
			ModelBox* m = reinterpret_cast<ModelBox*>(w);
			m->setVisible(isModelVisible(m->model, brand, tagIds, favorite));
		}

		// Filter and sort by search results
		if (search.empty()) {
			// Add all models to prefilteredModelScores with scores of 1
			for (Widget* w : modelContainer->children) {
				ModelBox* m = reinterpret_cast<ModelBox*>(w);
				prefilteredModelScores[m->model] = 1.f;
			}

			// Sort ModelBoxes
			if (settings::browserSort == settings::BROWSER_SORT_UPDATED) {
				sortModels([this](ModelBox* m) {
					plugin::Plugin* p = m->model->plugin;
					int modelOrder = get(modelOrders, m->model, 0);
					return std::make_tuple(-p->modifiedTimestamp, p->brand, p->name, modelOrder);
				});
			}
			else if (settings::browserSort == settings::BROWSER_SORT_LAST_USED) {
				sortModels([this](ModelBox* m) {
					plugin::Plugin* p = m->model->plugin;
					const settings::ModuleInfo* mi = settings::getModuleInfo(p->slug, m->model->slug);
					double lastAdded = mi ? mi->lastAdded : -INFINITY;
					int modelOrder = get(modelOrders, m->model, 0);
					return std::make_tuple(-lastAdded, -p->modifiedTimestamp, p->brand, p->name, modelOrder);
				});
			}
			else if (settings::browserSort == settings::BROWSER_SORT_MOST_USED) {
				sortModels([this](ModelBox* m) {
					plugin::Plugin* p = m->model->plugin;
					const settings::ModuleInfo* mi = settings::getModuleInfo(p->slug, m->model->slug);
					int added = mi ? mi->added : 0;
					double lastAdded = mi ? mi->lastAdded : -INFINITY;
					int modelOrder = get(modelOrders, m->model, 0);
					return std::make_tuple(-added, -lastAdded, -p->modifiedTimestamp, p->brand, p->name, modelOrder);
				});
			}
			else if (settings::browserSort == settings::BROWSER_SORT_BRAND) {
				sortModels([this](ModelBox* m) {
					plugin::Plugin* p = m->model->plugin;
					int modelOrder = get(modelOrders, m->model, 0);
					return std::make_tuple(p->brand, p->name, modelOrder);
				});
			}
			else if (settings::browserSort == settings::BROWSER_SORT_NAME) {
				sortModels([](ModelBox* m) {
					plugin::Plugin* p = m->model->plugin;
					return std::make_tuple(m->model->name, p->brand);
				});
			}
			else if (settings::browserSort == settings::BROWSER_SORT_RANDOM) {
				std::map<ModelBox*, uint64_t> randomOrder;
				for (Widget* w : modelContainer->children) {
					ModelBox* m = reinterpret_cast<ModelBox*>(w);
					randomOrder[m] = random::u64();
				}
				sortModels([&](ModelBox* m) {
					return get(randomOrder, m, 0);
				});
			}
		}
		else {
			// Score results against search query
			auto results = modelDb.search(search);
			// DEBUG("=============");
			for (auto& result : results) {
				prefilteredModelScores[result.key] = result.score;
				// DEBUG("%s %s\t\t%f", result.key->plugin->slug.c_str(), result.key->slug.c_str(), result.score);
			}
			// Sort by score
			sortModels([&](ModelBox* m) {
				return -get(prefilteredModelScores, m->model, 0.f);
			});
			// Filter by whether the score is above the threshold
			for (Widget* w : modelContainer->children) {
				ModelBox* m = reinterpret_cast<ModelBox*>(w);
				assert(m);
				if (m->isVisible()) {
					if (prefilteredModelScores.find(m->model) == prefilteredModelScores.end())
						m->hide();
				}
			}
		}

		// Count visible modules
		int count = 0;
		for (Widget* w : modelContainer->children) {
			if (w->isVisible())
				count++;
		}
		countLabel->text = (count == 1) ? string::translate("Browser.modulesOne") : string::f(string::translate("Browser.modulesMany"), count);
	}

	void clear() {
		search = "";
		searchField->setText("");
		brand = "";
		tagIds = {};
		favorite = false;
		refresh();
	}

	void onButton(const ButtonEvent& e) override {
		Widget::onButton(e);
		e.stopPropagating();
		// Consume all mouse buttons
		if (!e.isConsumed())
			e.consume(this);
	}

	void onHoverKey(const HoverKeyEvent& e) override {
		if (e.action == GLFW_PRESS) {
			// Secret key command to dump all visible modules into rack
			if (e.isKeyCommand(GLFW_KEY_F2, RACK_MOD_CTRL | GLFW_MOD_SHIFT | GLFW_MOD_ALT)) {
				int count = 0;
				for (widget::Widget* w : modelContainer->children) {
					ModelBox* mb = dynamic_cast<ModelBox*>(w);
					if (!mb)
						continue;
					if (!mb->visible)
						continue;
					count++;
					DEBUG("Dumping into rack (%d): %s/%s", count, mb->model->plugin->slug.c_str(), mb->model->slug.c_str());
					chooseModel(mb->model);
				}
				e.consume(this);
			}
		}

		if (e.isConsumed())
			return;
		OpaqueWidget::onHoverKey(e);
	}
};


// Implementations to resolve dependencies


inline void FavoriteQuantity::setValue(float value) {
	browser->favorite = value;
	browser->refresh();
}

inline float FavoriteQuantity::getValue() {
	return browser->favorite;
}

inline void ClearButton::onAction(const ActionEvent& e) {
	browser->clear();
}

inline void BrowserSearchField::onSelectKey(const SelectKeyEvent& e) {
	if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
		// Backspace when the field is empty to clear filters.
		if (e.isKeyCommand(GLFW_KEY_BACKSPACE) || e.isKeyCommand(GLFW_KEY_BACKSPACE, RACK_MOD_CTRL)) {
			if (text == "") {
				browser->clear();
				e.consume(this);
			}
		}
	}

	if (!e.getTarget())
		ui::TextField::onSelectKey(e);
}

inline void BrowserSearchField::onChange(const ChangeEvent& e) {
	browser->search = string::trim(text);
	browser->refresh();
}

inline void BrowserSearchField::onAction(const ActionEvent& e) {
	// Get first ModelBox
	ModelBox* mb = NULL;
	for (Widget* w : browser->modelContainer->children) {
		if (w->isVisible()) {
			mb = reinterpret_cast<ModelBox*>(w);
			break;
		}
	}

	if (mb) {
		chooseModel(mb->model);
	}
}

inline void BrandItem::onAction(const ActionEvent& e) {
	if (browser->brand == brand)
		browser->brand = "";
	else
		browser->brand = brand;
	browser->refresh();
}

inline void BrandItem::step() {
	rightText = CHECKMARK(browser->brand == brand);
	MenuItem::step();
}

inline void BrandButton::onAction(const ActionEvent& e) {
	ui::Menu* menu = createMenu();
	menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y));
	menu->box.size.x = box.size.x;

	BrandItem* noneItem = new BrandItem;
	noneItem->text = string::translate("Browser.allBrands");
	noneItem->brand = "";
	noneItem->browser = browser;
	menu->addChild(noneItem);

	menu->addChild(new ui::MenuSeparator);

	// Collect brands from all plugins
	std::set<std::string, string::CaseInsensitiveCompare> brands;
	for (plugin::Plugin* plugin : plugin::plugins) {
		brands.insert(plugin->brand);
	}

	for (const std::string& brand : brands) {
		BrandItem* brandItem = new BrandItem;
		brandItem->text = brand;
		brandItem->brand = brand;
		brandItem->browser = browser;
		brandItem->disabled = !browser->hasVisibleModel(brand, browser->tagIds, browser->favorite);
		menu->addChild(brandItem);
	}
}

inline void BrandButton::step() {
	text = string::translate("Browser.brand");
	if (!browser->brand.empty()) {
		text += ": ";
		text += browser->brand;
	}
	text = string::ellipsize(text, 20);
	ChoiceButton::step();
}

inline void TagItem::onAction(const ActionEvent& e) {
	auto it = browser->tagIds.find(tagId);
	bool isSelected = (it != browser->tagIds.end());

	if (tagId >= 0) {
		// Specific tag
		if (!e.isConsumed()) {
			// Multi select
			if (isSelected)
				browser->tagIds.erase(tagId);
			else
				browser->tagIds.insert(tagId);
			e.unconsume();
		}
		else {
			// Single select
			if (isSelected)
				browser->tagIds = {};
			else {
				browser->tagIds = {tagId};
			}
		}
	}
	else {
		// All tags
		browser->tagIds = {};
	}

	browser->refresh();
}

inline void TagItem::step() {
	// TODO Disable tags with no modules
	if (tagId >= 0) {
		auto it = browser->tagIds.find(tagId);
		bool isSelected = (it != browser->tagIds.end());
		rightText = CHECKMARK(isSelected);
	}
	else {
		rightText = CHECKMARK(browser->tagIds.empty());
	}
	MenuItem::step();
}

inline void TagButton::onAction(const ActionEvent& e) {
	ui::Menu* menu = createMenu();
	menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y));
	menu->box.size.x = box.size.x;

	TagItem* noneItem = new TagItem;
	noneItem->text = string::translate("Browser.allTags");
	noneItem->tagId = -1;
	noneItem->browser = browser;
	menu->addChild(noneItem);

	menu->addChild(createMenuLabel(widget::getKeyCommandName(0, RACK_MOD_CTRL) + string::translate("key.click") + string::translate("Browser.tagsSelectMultiple")));
	menu->addChild(new ui::MenuSeparator);

	for (int tagId = 0; tagId < (int) tag::tagAliases.size(); tagId++) {
		TagItem* tagItem = new TagItem;
		std::string tag = string::translate("tag." + tag::getTag(tagId));
		tagItem->text = tag;
		tagItem->tagId = tagId;
		tagItem->browser = browser;
		tagItem->disabled = !browser->hasVisibleModel(browser->brand, {tagId}, browser->favorite);
		menu->addChild(tagItem);
	}
}

inline void TagButton::step() {
	text = string::translate("Browser.tags");
	if (!browser->tagIds.empty()) {
		text += ": ";
		bool firstTag = true;
		for (int tagId : browser->tagIds) {
			if (!firstTag)
				text += ", ";
			std::string tag = string::translate("tag." + tag::getTag(tagId));
			text += tag;
			firstTag = false;
		}
	}
	text = string::ellipsize(text, 20);
	ChoiceButton::step();
}

inline void SortButton::onAction(const ActionEvent& e) {
	ui::Menu* menu = createMenu();
	menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y));
	menu->box.size.x = box.size.x;

	for (int sortId = 0; sortId <= settings::BROWSER_SORT_RANDOM; sortId++) {
		menu->addChild(createCheckMenuItem(getSortNames()[sortId], "",
			[=]() {return settings::browserSort == sortId;},
			[=]() {
				settings::browserSort = (settings::BrowserSort) sortId;
				browser->refresh();
			}
		));
	}
}

inline void ZoomButton::onAction(const ActionEvent& e) {
	ui::Menu* menu = createMenu();
	menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y));
	menu->box.size.x = box.size.x;

	for (float zoom = 1.f; zoom >= -2.f; zoom -= 0.5f) {
		menu->addChild(createCheckMenuItem(string::f("%.0f%%", std::pow(2.f, zoom) * 100.f), "",
			[=]() {return settings::browserZoom == zoom;},
			[=]() {
				if (zoom == settings::browserZoom)
					return;
				settings::browserZoom = zoom;
				browser->updateZoom();
			}
		));
	}
}


} // namespace browser



void browserInit() {
	browser::modelDbInit();
}


widget::Widget* browserCreate() {
	browser::BrowserOverlay* overlay = new browser::BrowserOverlay;
	overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33);

	browser::Browser* browser = new browser::Browser;
	overlay->addChild(browser);

	return overlay;
}


} // namespace app
} // namespace rack
