#pragma once
#include "comm/comm_module.hh"
#include "hub/hub_module.hh"
#include "hub/jack_alias.hh"
#include "hub_knob_mappings.hh"
#include "mapping/module_directory.hh"
#include "plugin.hh"
#include "util/edge_detector.hh"
#include "util/math.hh"
#include "util/string_util.hh"
#include <map>
#include <osdialog.h>
#include <span>

namespace MetaModule
{

struct MetaModuleHubBase : public rack::Module {

	MetaModuleHubBase(unsigned num_pots)
		: mappings{num_pots}
		, last_knob_val(num_pots) {
	}

	std::function<void()> updatePatchName;
	std::string patchNameText = "";
	std::string patchDescText = "";
	MappingMode mappingMode = pluginSettings.defaultMappingMode;

	bool should_save = false;
	bool should_send_wifi = false;

	std::optional<int> inProgressMapParamId{};

	static constexpr uint32_t MaxMapsPerPot = 8;
	static constexpr uint32_t MaxKnobSets = 8;
	HubKnobMappings<MaxMapsPerPot, MaxKnobSets> mappings;

	JackAlias jack_alias{};

	bool use_glue_labels = pluginSettings.defaultUseGlueLabels;
	bool use_builtin_midi = pluginSettings.defaultUseBuiltinMidi;
	bool auto_map_audio_outs = pluginSettings.defaultAutoMapAudioOuts;
	std::map<int64_t, std::string> module_aliases;
	std::map<int64_t, int> module_alias_colors;

	std::vector<float> last_knob_val{};

	const std::vector<std::string> sampleRates{"Any", "24kHz", "32kHz", "48kHz", "96kHz"};
	const std::array<unsigned, 5> sampleRateNums{0, 24000, 32000, 48000, 96000};
	const std::vector<std::string> blockSizes{"Any", "16", "32", "64", "128", "256", "512"};
	const std::array<unsigned, 7> blockSizeNums{0, 16, 32, 64, 128, 256, 512};
	int suggested_samplerate_idx = 0;
	int suggested_blocksize_idx = 0;

	// Settings setters: mirror each change into pluginSettings so that newly
	// created hubs inherit it

	void setMappingMode(int index) {
		mappingMode = MappingMode(index);
		pluginSettings.defaultMappingMode = mappingMode;
	}

	void setUseGlueLabels(bool use) {
		use_glue_labels = use;
		pluginSettings.defaultUseGlueLabels = use;
	}

	void setUseBuiltinMidi(bool use) {
		use_builtin_midi = use;
		pluginSettings.defaultUseBuiltinMidi = use;
	}

	void setAutoMapAudioOuts(bool auto_map) {
		auto_map_audio_outs = auto_map;
		pluginSettings.defaultAutoMapAudioOuts = auto_map;
	}

	// Mapping State/Progress

	void startMappingFrom(int hubParamId) {
		inProgressMapParamId = hubParamId;
	}

	void endMapping() {
		inProgressMapParamId = {};
	}

	std::optional<int> getMappingSource() {
		return inProgressMapParamId;
	}

	bool isMappingInProgress() {
		return inProgressMapParamId.has_value();
	}

	// Always locks
	bool registerMap(int hubParamId, rack::Module *module, int64_t moduleParamId) {
		if (!isMappingInProgress()) {
			pr_dbg("registerMap() called but we aren't mapping\n");
			return false;
		}

		if (!module) {
			pr_dbg("Error: Dest module ptr is null. Aborting mapping.\n");
			endMapping();
			return false;
		}

		if (ModuleDirectory::isHub(module)) {
			pr_dbg("Dest module is a hub. Aborting mapping.\n");
			endMapping();
			return false;
		}

		mappings.linkToModule(id);
		auto *map = mappings.addMap(hubParamId, module->id, moduleParamId, ShouldLock::Yes);
		if (!map) {
			pr_dbg("Error: could not create mapping\n");
			return false;
		}

		map->range_max = 1.f;
		map->range_min = 0.0f;
		map->curve_type = 0;
		endMapping();

		return true;
	}

	// Runtime applying maps

	void processMaps() {
		for (int hubParamId = 0; auto &knob : mappings) {
			if (std::fabs(last_knob_val[hubParamId] - params[hubParamId].getValue()) > 0.0001f) {
				auto new_val = params[hubParamId].getValue();
				auto last_val = last_knob_val[hubParamId];

				for (auto &mapset : knob) {

					int paramId = mapset.paramHandle.paramId;
					auto module = mapset.paramHandle.module;
					if (!module)
						continue;

					rack::ParamQuantity *paramQuantity = module->paramQuantities[paramId];
					if (!paramQuantity)
						continue;
					if (!paramQuantity->isBounded())
						continue;

					auto &map = mappings.activeMap(mapset);

					if (map.curve_type == 1) {
						// Toggle/latching mode:

						if (new_val > 0.5f && last_val < 0.5f) {
							// if param is currently closer to min, then set it to max (and vice-versa)
							auto cur_val = paramQuantity->getScaledValue();
							if (std::abs(cur_val - map.range_min) < std::abs(cur_val - map.range_max)) {
								paramQuantity->setScaledValue(map.range_max);
							} else {
								paramQuantity->setScaledValue(map.range_min);
							}
						}

					} else {
						auto val = MathTools::map_value(new_val, 0.f, 1.f, map.range_min, map.range_max);
						paramQuantity->setScaledValue(val);
					}
				}

				last_knob_val[hubParamId] = new_val;
			}
			hubParamId++;
		}
	}

	// VCV Rack calls this periodically on auto-save
	json_t *dataToJson() override {
		json_t *rootJ = mappings.encodeJson();

		json_t *aliasJ = jack_alias.encodeJson();
		json_object_set_new(rootJ, "Alias", aliasJ);

		if (updatePatchName) {
			updatePatchName();
			json_t *patchNameJ = json_string(patchNameText.c_str());
			json_object_set_new(rootJ, "PatchName", patchNameJ);

			json_t *patchDescJ = json_string(patchDescText.c_str());
			json_object_set_new(rootJ, "PatchDesc", patchDescJ);
		} else {
			pr_warn("Hub Widget has not been constructed, but dataToJson is being called\n");
		}

		json_t *mappingModeJ = json_integer(this->mappingMode);
		json_object_set_new(rootJ, "MappingMode", mappingModeJ);

		if ((size_t)suggested_samplerate_idx < sampleRateNums.size()) {
			json_t *suggSampleRateJ = json_integer(sampleRateNums[suggested_samplerate_idx]);
			json_object_set_new(rootJ, "SuggestedSampleRate", suggSampleRateJ);
		}

		if ((size_t)suggested_blocksize_idx < blockSizeNums.size()) {
			json_t *suggBlockSizeJ = json_integer(blockSizeNums[suggested_blocksize_idx]);
			json_object_set_new(rootJ, "SuggestedBlockSize", suggBlockSizeJ);
		}

		json_t *defaultKnobSetJ = json_integer(mappings.getActiveKnobSetIdx());
		json_object_set_new(rootJ, "DefaultKnobSet", defaultKnobSetJ);

		json_object_set_new(rootJ, "UseGlueLabels", json_boolean(use_glue_labels));
		json_object_set_new(rootJ, "UseBuiltinMidi", json_boolean(use_builtin_midi));
		json_object_set_new(rootJ, "AutoMapAudioOuts", json_boolean(auto_map_audio_outs));

		json_t *moduleAliasesJ = json_object();
		for (auto const &[id, alias] : module_aliases)
			json_object_set_new(moduleAliasesJ, std::to_string(id).c_str(), json_string(alias.c_str()));
		json_object_set_new(rootJ, "ModuleAliases", moduleAliasesJ);

		json_t *moduleAliasColorsJ = json_object();
		for (auto const &[id, color] : module_alias_colors)
			json_object_set_new(moduleAliasColorsJ, std::to_string(id).c_str(), json_integer(color));
		json_object_set_new(rootJ, "ModuleAliasColors", moduleAliasColorsJ);

		return rootJ;
	}

	// VCV Rack calls this on startup, and on loading a new patch file
	// Do not use setMappingMode(), etc here -- that would change user's default settings
	void dataFromJson(json_t *rootJ) override {
		auto patchNameJ = json_object_get(rootJ, "PatchName");
		if (json_is_string(patchNameJ)) {
			patchNameText = json_string_value(patchNameJ);
		}

		auto patchDescJ = json_object_get(rootJ, "PatchDesc");
		if (json_is_string(patchDescJ)) {
			patchDescText = json_string_value(patchDescJ);
		}

		auto defaultKnobSetJ = json_object_get(rootJ, "DefaultKnobSet");
		if (json_is_integer(defaultKnobSetJ)) {
			unsigned idx = json_integer_value(defaultKnobSetJ);
			mappings.changeActiveKnobSet(idx, ShouldLock::No);
		}

		// For settings with global user defaults: a patch saved before the setting
		// existed has no key for it, so use the legacy default, not the user default,
		// to keep old patches working as they always did
		auto mappingModeJ = json_object_get(rootJ, "MappingMode");
		if (json_is_integer(mappingModeJ)) {
			mappingMode = MappingMode(json_integer_value(mappingModeJ));
		} else {
			mappingMode = MappingMode::ALL;
		}

		auto suggSampleRateJ = json_object_get(rootJ, "SuggestedSampleRate");
		if (json_is_integer(suggSampleRateJ)) {
			size_t sr = json_integer_value(suggSampleRateJ);
			if (auto it = std::ranges::find(sampleRateNums, sr); it != sampleRateNums.end()) {
				suggested_samplerate_idx = std::distance(sampleRateNums.begin(), it);
			} else {
				suggested_samplerate_idx = 0;
			}
		} else {
			suggested_samplerate_idx = 0;
		}

		auto suggBlockSizeJ = json_object_get(rootJ, "SuggestedBlockSize");
		if (json_is_integer(suggBlockSizeJ)) {
			size_t bs = json_integer_value(suggBlockSizeJ);
			if (auto it = std::ranges::find(blockSizeNums, bs); it != blockSizeNums.end()) {
				suggested_blocksize_idx = std::distance(blockSizeNums.begin(), it);
			} else {
				suggested_blocksize_idx = 0;
			}
		} else {
			suggested_blocksize_idx = 0;
		}

		auto aliasJ = json_object_get(rootJ, "Alias");
		jack_alias.decodeJson(aliasJ);

		auto useGlueLabelsJ = json_object_get(rootJ, "UseGlueLabels");
		use_glue_labels = json_is_boolean(useGlueLabelsJ) ? json_boolean_value(useGlueLabelsJ) : true;

		auto useBuiltinMidiJ = json_object_get(rootJ, "UseBuiltinMidi");
		use_builtin_midi = json_is_boolean(useBuiltinMidiJ) ? json_boolean_value(useBuiltinMidiJ) : true;

		auto autoMapAudioOutsJ = json_object_get(rootJ, "AutoMapAudioOuts");
		auto_map_audio_outs = json_is_boolean(autoMapAudioOutsJ) ? json_boolean_value(autoMapAudioOutsJ) : false;

		auto moduleAliasesJ = json_object_get(rootJ, "ModuleAliases");
		if (json_is_object(moduleAliasesJ)) {
			module_aliases.clear();
			const char *key;
			json_t *val;
			json_object_foreach(moduleAliasesJ, key, val) {
				if (json_is_string(val)) {
					try {
						module_aliases[std::stoll(key)] = json_string_value(val);
					} catch (...) {
					}
				}
			}
		}

		auto moduleAliasColorsJ = json_object_get(rootJ, "ModuleAliasColors");
		if (json_is_object(moduleAliasColorsJ)) {
			module_alias_colors.clear();
			const char *key;
			json_t *val;
			json_object_foreach(moduleAliasColorsJ, key, val) {
				if (json_is_integer(val)) {
					try {
						module_alias_colors[std::stoll(key)] = json_integer_value(val);
					} catch (...) {
					}
				}
			}
		}

		mappings.decodeJson(rootJ);
	}

	void onReset(const ResetEvent &e) override {
		Module::onReset(e);
		patchNameText = "";
		patchDescText = "";
		module_aliases.clear();
		module_alias_colors.clear();
		mappings.clear_all(ShouldLock::No);
		mappings.setActiveKnobSetIdx(0);
		mappings.refreshParamHandles(ShouldLock::No);
	}

	enum class JackDir { In, Out };

	void set_jack_alias(JackDir dir, unsigned idx, std::string_view text) {
		if (dir == JackDir::In) {
			if (idx < jack_alias.in.size()) {
				jack_alias.in[idx] = text;
				// jacks are virtually reversed: Ins are seen as Outs in VCV
				outputInfos[idx]->name = text;
			}
		} else {
			if (idx < jack_alias.out.size()) {
				jack_alias.out[idx] = text;
				// jacks are virtually reversed: Ins are seen as Outs in VCV
				inputInfos[idx]->name = text;
			}
		}
	}

	std::string_view get_jack_alias(JackDir dir, unsigned idx) {
		if (dir == JackDir::In) {
			if (idx < jack_alias.in.size())
				return jack_alias.in[idx];

		} else if (dir == JackDir::Out) {
			if (idx < jack_alias.out.size())
				return jack_alias.out[idx];
		}

		return "";
	}

	static void change_button_expander_knobset(unsigned idx, ShouldLock do_lock) {
		auto moduleids = APP->engine->getModuleIds();
		for (auto id : moduleids) {
			if (auto module = APP->engine->getModule(id)) {
				if (module->getModel()->slug == "MMButtonExpander") {
					if (auto button_exp = dynamic_cast<MetaModuleHubBase *>(module)) {
						button_exp->mappings.changeActiveKnobSet(idx, do_lock);
					}
				}
			}
		}
	}
};

} // namespace MetaModule
