/* globals NGL, $ */
if (typeof window.structureData === 'undefined') {
	// eslint-disable-next-line no-undef
	window.structureData = {};
}
if (typeof antibodyColorMap === 'undefined') {
	// eslint-disable-next-line no-undef
	antibodyColorMap = {};
}
function AAshortToLong(abbr, long) {
	let translate = {
		A: {s: 'Ala', l: 'Alanine'},
		R: {s: 'Arg', l: 'Arginine'},
		N: {s: 'Asn', l: 'Asparagine'},
		D: {s: 'Asp', l: 'Aspartate'},
		C: {s: 'Cys', l: 'Cysteine'},
		Q: {s: 'Gln', l: 'Glutamine'},
		E: {s: 'Glu', l: 'Glutamate'},
		G: {s: 'Gly', l: 'Glycine'},
		H: {s: 'His', l: 'Histidine'},
		I: {s: 'Ile', l: 'Isoleucine'},
		L: {s: 'Leu', l: 'Leucine'},
		K: {s: 'Lys', l: 'Lysine'},
		M: {s: 'Met', l: 'Methionine'},
		F: {s: 'Phe', l: 'Phenylalanine'},
		P: {s: 'Pro', l: 'Proline'},
		S: {s: 'Ser', l: 'Serine'},
		T: {s: 'Thr', l: 'Threonine'},
		W: {s: 'Trp', l: 'Tryptophan'},
		Y: {s: 'Tyr', l: 'Tyrosine'},
		V: {s: 'Val', l: 'Valine'},
	};
	return translate[abbr][long?'l':'s'];
}

function translateShortToLong(str, long) {
	let s = str.split('');
	let n = s.map(function(c) {
		return AAshortToLong(c, long);
	});
	return n.join('');
}

// eslint-disable-next-line no-unused-vars
function createNGLViewer(top, options) {
	options = options || {};
	let stage;
	let loaded = {};
	let shapeComp = {};
	let missenseData = {};
	let colorSchemes = {};
	let combinedSchemeKey = '';
	let volumeRepr = {};
	let highlightParts = {};
	let currentStructure;
	// eslint-disable-next-line no-underscore-dangle
	let _events = {};
	// eslint-disable-next-line no-underscore-dangle
	let _structureData = {};
	if (options.structureData) {
		addStructureData(options.structureData);
	}

	const defaultColorScheme = 'bfactor';
	const structureVisType = 'cartoon';
	let colorNames = [
		0xFF0000, // Red
		0x00FF00, // Green
		0x0000FF, // Blue
		0xFFA500, // Orange
		0x762A83, // Purple-ish
		0x8e7f00, // Yellow-ish
	];
	let transcriptColor = 0xdddddd;
	let defaultColor = 0xFF67EE;

	let keyClinical = 'c';
	let keyPopulation = 'p';
	let keyAMClinical = 'ac';
	let keyAMPopulation = 'ap';

	let alphaFoldColors = {
		90: ['Very high (pLDDT > 90)', 0x0053d6, '#0053d6'],
		70: ['Confident (90 > pLDDT > 70)', 0x65cbf3, '#65cbf3'],
		50: ['Low (70 > pLDDT > 50)', 0xffdb13, '#ffdb13'],
		0: ['Very low (pLDDT < 50)', 0xff7d45, '#ff7d45']
	};
	let alphaFoldConfidenceSchemeId;

	// eslint-disable-next-line no-underscore-dangle
	function _isFunction(obj) {
		return typeof obj === "function";
	}

	/* Event functions taken from OpenSeadragon */
	function addHandler(eventName, handler, userData) {
		if (!_events[eventName]) {
			_events[eventName] = [];
		}
		let events = _events[eventName];
		if (handler && _isFunction(handler)) {
			events.push({handler: handler, userData: userData || null});
		}
		return true;
	}
	function addOnceHandler(eventName, handler, userData, times) {
		times = times || 1;
		var count = 0;
		var onceHandler = function(event) {
			count++;
			if (count === times) {
				removeHandler(eventName, onceHandler);
			}
			return handler(event);
		};
		return addHandler(eventName, onceHandler, userData);
	}
	function removeHandler(eventName, handler) {
		var events = _events[eventName];
		var handlers = [];
		if (!events) {
			return;
		}
		if (Array.isArray(events)) {
			for (let i = 0; i < events.length; i++) {
				if (events[i].handler !== handler) {
					handlers.push(events[i]);
				}
			}
			_events[eventName] = handlers;
		}
	}

	// eslint-disable-next-line no-underscore-dangle
	function _getHandler(eventName) {
		var events = _events[eventName];
		if (!events || !events.length) {
			return null;
		}
		events = events.length === 1 ? [events[0]] : Array(...events);
		return function (args) {
			let length = events.length;
			for (let i = 0; i < length; i++) {
				if (events[i]) {
					args.userData = events[i].userData;
					events[i].handler(args);
				}
			}
		};
	}
	function raiseEvent(eventName, eventArgs) {
		var handler = _getHandler(eventName);
		if (handler) {
			handler(eventArgs || {});
		}
	}

	function addStructureData(newStructureData) {
		Object.entries(newStructureData).forEach(([idx, val]) => _structureData[idx] = val);
	}

	function getStructureData(structure) {
		return _structureData[structure];
	}

	function getStructureDataEntries() {
		return _structureData;
	}

	function getSelectedColor() {
		let scheme = $('.color.active').attr('class').replace('color', '').replace('active', '').trim();
		if (scheme) {
			return scheme;
		}
		return undefined;
	}

	function hideComponents() {
		stage.eachComponent(function (comp) {
			// if (comp.name !== structure) {
			comp.setVisibility(false);
			// }
		});
	}

	async function loadmodel(modelToLoad) {
		hideComponents();
		if (!modelToLoad) {
			return;
		}
		let structure = modelToLoad;
		let thisStructureData = getStructureData(structure);
		stage.removeAllComponents();
		combinedSchemeKey = '';
		highlightParts = {};
		if (loaded[structure]) {
			setLoadedStructure(modelToLoad);
			await handleMissense();
			handleColors();
			stage.getComponentsByName(structure).forEach(function (e) {
				e.setVisibility(true);
				e.setDefaultAssembly('');
				e.autoView();
			});
			raiseEvent('model-loaded', {structure: structure});
		} else {
			let pr = stage.loadFile(thisStructureData.file, {defaultRepresentation: false});
			let s = await pr;
			// pr.then(function (s) {
				setLoadedStructure(modelToLoad);
				// loaded[structure] = 1;
				s.setVisibility(false);
				s.setName(structure);
				setRepresentation(s, getSelectedColor());

				try {
					await handleMissense();
				} catch (e) {
					// eslint-disable-next-line no-console
					console.log(e);
				}
				try {
					handleColors();
				} catch (e) {
					// eslint-disable-next-line no-console
					console.log(e);
				}
				stage.getComponentsByName(structure).forEach(function (e) {
					e.setVisibility(true);
					e.setDefaultAssembly('');
					e.autoView();
				});
				raiseEvent('model-loaded', {structure: structure});
			// });
			// await pr;
		}
	}

	function getPositionForResidue(structure, residueID, chainName) {
		let pos;
		stage.getComponentsByName(structure).forEach(function (e) {
			e.structure.getResidueProxy(residueID - 1).eachAtom(function (atom) {
				if (atom.chainname !== chainName) {
					return;
				}
				if (atom.atomname === 'CA') {
					pos = atom.positionToVector3();
				}
			});
		});
		return pos;
	}

	async function getMissenseDataForStructure(structure) {
		if (!missenseData.hasOwnProperty(structure)) {
			missenseData[structure] = fetchMissenseData(structure);
		}
		return missenseData[structure];
	}

	function fetchMissenseData(structure) {
		return new Promise((resolve) => {
			$.getJSON('/structure/structure_data.php?structure_id='+structure, (json) => {
				resolve(json);
			});
		});
	}

	function getAntigensForStructure(structure) {
		let ds = getStructureData(structure);
		if (ds) {
			return ds.antigens;
		}
		return [];
	}

	function loadStructure(structure_id, showAntigens, assembly, showClinical, showPopulation, showAMClinical, showAMPopulation) {
		loadmodelsimple(structure_id, '/proteinstructures/'+structure_id+'.mmtf', showAntigens, assembly, showClinical, showPopulation, showAMClinical, showAMPopulation);
	}

	async function loadmodelsimple(structure, file, showAntigens, assembly, showClinical, showPopulation, showAMClinical, showAMPopulation) {
		assembly = assembly || "";
		hideComponents();
		if (loaded[structure]) {
			setLoadedStructure(structure);
			if (showAntigens) {
				handleColors(showAntigens);
			}
			if (showClinical || showPopulation || showAMClinical || showAMPopulation) {
				await handleMissense(showClinical, showPopulation, showAMClinical, showAMPopulation);
			}
			stage.getComponentsByName(structure).forEach(function (e) {
				e.setVisibility(true);
				e.setDefaultAssembly(assembly);
				e.autoView();
			});
			raiseEvent('model-loaded', {structure: structure});
		} else {
			let pr = stage.loadFile(file, {defaultRepresentation: false});
			pr.then(function (s) {
				loaded[structure] = 1;
				setLoadedStructure(structure);
				s.setVisibility(false);
				s.setName(structure);
				setRepresentation(s, defaultColorScheme);
				if (showAntigens) {
					handleColors(showAntigens);
				}
				if (showClinical || showPopulation || showAMClinical || showAMPopulation) {
					handleMissense(showClinical, showPopulation, showAMClinical, showAMPopulation);
				}
				stage.getComponentsByName(structure).forEach(function (e) {
					e.setVisibility(true);
					e.setDefaultAssembly(assembly);
					e.autoView();
				});
				raiseEvent('model-loaded', {structure: structure});
			});
		}
	}

	async function handleMissense(showClinical, showPopulation, showAMClinical, showAMPopulation) {
		if (typeof showClinical === 'undefined') {
			showClinical = $('.variants.active').hasClass('clinical');
		}
		if (typeof showPopulation === 'undefined') {
			showPopulation = $('.variants.active').hasClass('population');
		}
		if (typeof showAMClinical === 'undefined') {
			showAMClinical = $('.alphavariants.active').hasClass('pathogenic');
		}
		if (typeof showAMPopulation === 'undefined') {
			showAMPopulation = $('.alphavariants.active').hasClass('benign');
		}

		let structure = getLoadedStructure();
		let name = structure +
			(showClinical ? keyClinical : "_") +
			(showPopulation ? keyPopulation : "_") +
			(showAMClinical ? keyAMClinical : "_") +
			(showAMPopulation ? keyAMPopulation : "_");
		if (showClinical || showPopulation || showAMClinical || showAMPopulation) {
			if (!shapeComp.hasOwnProperty(name)) {
				let shape = new NGL.Shape('shape');
				let missense = await getMissenseDataForStructure(structure);
				Object.keys(missense).forEach(function (chain) {
					let residues = missense[chain];
					Object.keys(residues).forEach(function(residue) {
						let variants = residues[residue];
						let pos = getPositionForResidue(structure, residue, chain);
						if (typeof pos === "undefined") {
							return;
						}
						let title_parts = {
							c: [],
							p: [],
							ac: [],
							ap: []
						};
						let createShape = false;
						let oldaa = '';
						Object.keys(variants).forEach(function(k) {
							let d = variants[k];
							let t = '=>' + translateShortToLong(d.n, true) +
								(d.s_t ? '\nSIFT: '+d.s_t + ' ' + d.s_v : '') +
								(d.p_t ? '\nPolyPhen: '+d.p_t + ' ' + d.p_v : '') +
								(d.a_t ? ': '+d.a_t + ' ' + d.a_v : '');
							if (d.v_t === keyClinical && d.pa.length > 0) {
								t += '\nPathology: '+d.pa.join(', ');
							}
							title_parts[d.v_t].push(t);
							if (
								(d.v_t === keyClinical && showClinical) ||
								(d.v_t === keyPopulation && showPopulation) ||
								(d.v_t === keyAMClinical && showAMClinical) ||
								(d.v_t === keyAMPopulation && showAMPopulation)) {
								createShape = true;
								if (oldaa === '') {
									oldaa = translateShortToLong(d.o, true);
								}
							}
						});
						if (createShape) {
							let title = oldaa+':\n';
							let haveText = false;
							if (title_parts.c.length) {
								title += 'Clinical variations:\n' + title_parts.c.join('\n');
								haveText = true;
							}
							if (title_parts.p.length) {
								title += (haveText ? '\n\n' : '')+'Population variations:\n' + title_parts.p.join('\n');
								haveText = true;
							}
							if (title_parts.ap.length) {
								title += (haveText ? '\n\n' : '')+'AlphaMissense benign variations:\n' + title_parts.ap.join('\n');
								haveText = true;
							}
							if (title_parts.ac.length) {
								title += (haveText ? '\n\n' : '')+'AlphaMissense pathogenic variations:\n' + title_parts.ac.join('\n');
							}
							let withClinical = showClinical && title_parts.c.length>0;
							let withPopulation = showPopulation && title_parts.p.length>0;
							let withAMClinical = showAMClinical && title_parts.ac.length>0;
							let withAMPopulation = showAMPopulation && title_parts.ap.length>0;
							let color;
							if (
								(withClinical || withPopulation) &&
								(withAMClinical || withAMPopulation)
							) {
								// Color pink when both type of variants in the same position
								color = [1, 0, 1];
							} else if (withClinical || withPopulation) {
								color = [0, 0, 1];
							} else {
								color = [1, 0, 0];
							}
							shape.addSphere(pos, color, 1, title.trim());
						}
					});
				});
				shapeComp[name] = stage.addComponentFromObject(shape);
				shapeComp[name].addRepresentation('buffer');
			}
		}
		Object.keys(shapeComp).forEach(function(v) {
			shapeComp[v].setVisibility(name===v);
		});
		let legend = [];
		if (showClinical) {
			legend.push(keyClinical);
		}
		if (showPopulation) {
			legend.push(keyPopulation);
		}
		if (showAMClinical) {
			legend.push(keyAMClinical);
		}
		if (showAMPopulation) {
			legend.push(keyAMPopulation);
		}
		let legendHTML = updateVariantLegend(structure, legend);
		raiseEvent('variant-changed', {legendData: legendHTML, legend: legend});
	}

	function handleColors(showAntigens) {
		if (typeof showAntigens === 'undefined') {
			showAntigens = $('.antigens.active').hasClass('on');
		}
		let legendType;
		let structure = getLoadedStructure();
		if (showAntigens) {
			let antigens = getAntigensForStructure(structure);
			let key = structure+'-'+antigens.join('-');
			if (!colorSchemes.hasOwnProperty(key)) {
				let colors = {};
				antigens.forEach(function(v) {
					if (!colors.hasOwnProperty(v.chain_name)) {
						colors[v.chain_name] = [];
					}
					if (!window.antibodyColorMap.hasOwnProperty(v.ab)) {
						window.antibodyColorMap[v.ab] = colorNames[Object.keys(window.antibodyColorMap).length] || 0xFF0000;
					}
					let color = window.antibodyColorMap[v.ab];
					colors[v.chain_name].push([color, v.start_pos, v.end_pos]);
				});
				colorSchemes[key] = NGL.ColormakerRegistry.addScheme(function () {
					this.atomColor = function (atom) {
						let r = colors[atom.chainname];
						if (r) {
							for (let i = 0; i < r.length; i++) {
								if (atom.resno >= r[i][1] && atom.resno <= r[i][2]) {
									return r[i][0];
								}
							}
						}
						return 0xFFFFFF;
					};
				});
			}
			stage.getComponentsByName(structure).forEach(function (e) {
				setRepresentation(e, colorSchemes[key]);
			});
			// show
			legendType = 'antibody';
		} else {
			// default color
			let color = getSelectedColor();
			stage.getComponentsByName(structure).forEach(function (e) {
				setRepresentation(e, color);
			});
			legendType = color;
		}
		let legendData = createColorLegend(legendType);
		raiseEvent('color-changed', {legendType: legendType, legendData: legendData});
	}

	function handleSurface() {
		var showSurface = $('.surface.active').hasClass('on');
		let structure = getLoadedStructure();
		if (showSurface) {
			stage.getComponentsByName(structure).forEach(function (e) {
				if (volumeRepr.hasOwnProperty(structure)) {
					volumeRepr[structure].setVisibility(true);
				} else {
					var molsurf = new NGL.MolecularSurface(e.structure);
					var surf = molsurf.getSurface({
						type: 'ses',
						probeRadius: 1.4,
						name: 'molsurf'
					});
					var o = stage.addComponentFromObject(surf);
					volumeRepr[structure] = o.addRepresentation('surface');
				}
			});
		} else if (volumeRepr.hasOwnProperty(structure)) {
			volumeRepr[structure].setVisibility(false);
		}
		raiseEvent('surface-toggled', {display: showSurface});
	}

	function highlightSection(transcript, startPos, stopPos, highlightedName, in_color, in_key) {
		let structure = getLoadedStructure();
		let key;
		let ds = getStructureData(structure);
		if (!ds) {
			return;
		}
		if (typeof in_key !== 'undefined') {
			key = in_key;
			startPos = highlightParts[key].startPos;
			stopPos = highlightParts[key].stopPos;
			highlightedName = highlightParts[key].highlightedName;
			transcript = highlightParts[key].transcript;
		} else if (typeof transcript !== 'undefined') {
				key = structure + '-' + transcript + '-' + highlightedName + '-' + startPos + '-' + stopPos;
		} else {
			transcript = $('.tab_content:not(.hidden)').find('svg').data('transcript');
		}
		// toggle the highlight
		if (highlightParts.hasOwnProperty(key)) {
			delete highlightParts[key];
		} else if (typeof startPos !== 'undefined') {
			let nextColor;
			if (in_color) {
				nextColor = in_color;
			} else {
				nextColor = colorNames.find(function (v) {
					for (var k in highlightParts) {
						if (highlightParts[k].color === v) {
							return false;
						}
					}
					return true;
				});
			}
			if (nextColor === undefined) {
				nextColor = 0xFF0000;
			}
			highlightParts[key] = {
				s: structure,
				startPos: startPos,
				stopPos: stopPos,
				highlightedName: highlightedName,
				key: key,
				transcript: transcript,
				color: nextColor
			};
		}

		let keys = Object.keys(highlightParts);
		combinedSchemeKey = transcript+'-'+structure;
		if (keys.length) {
			combinedSchemeKey = keys.reduce(function(previousValue, currentValue) {
				return previousValue + highlightParts[currentValue].key;
			});
		}

		let transcripts = ds.transcriptMappings;
		let offset;
		let haveOverlaps = false;
		if (transcripts.hasOwnProperty(transcript)) {
			offset = transcripts[transcript].offset;
		} else {
			throw Error('Transcript does not map');
		}
		if (!colorSchemes.hasOwnProperty(combinedSchemeKey)) {
			let resCount = stage.getComponentsByName(structure).list[0].structure.residueStore.length;
			let residues = Array(resCount+1).fill(defaultColor);
			// There is probably a more performant way to do this...

			keys.forEach(function (v) {
				let hStartPos = highlightParts[v].startPos - 1 + offset;
				let hStopPos = highlightParts[v].stopPos - 1 + offset;
				let color = highlightParts[v].color;
				for (let k = hStartPos; k <= hStopPos; k++) {
					if (residues[k] === defaultColor) {
						residues[k] = color;
					} else {
						// Show overlaps in black
						residues[k] = 0x000000;
						haveOverlaps = true;
					}
				}
			});
			let aa_seq_length = transcripts[transcript].aa_seq_length;
			for (let i=offset; i <= offset+aa_seq_length; i++) {
				if (residues[i] === defaultColor) {
					residues[i] = transcriptColor;
				}
			}
			let schemaId = NGL.ColormakerRegistry.addScheme(function () {
				this.atomColor = function (atom) {
					return residues[atom.resno];
				};
			});
			colorSchemes[combinedSchemeKey] = {
				overlaps: haveOverlaps,
				schemaId: schemaId
			};
		}
		raiseEvent('highlight-changed');
	}

	function setRepresentation(s, color) {
		if (color === 'highlight') {
			if (combinedSchemeKey === '') {
				highlightSection();
			}
			let structure = getLoadedStructure();
			stage.getComponentsByName(structure).forEach(function (e) {
				setRepresentation(e, colorSchemes[combinedSchemeKey].schemaId);
			});
			return;
		}
		let opts = {color: color};
		let structureData = getStructureData(s.name);
		if (color === 'bfactor' && structureData['structureType'] === 'prediction') {
			if (!alphaFoldConfidenceSchemeId) {
				alphaFoldConfidenceSchemeId = NGL.ColormakerRegistry.addScheme(function () {
					this.atomColor = function (atom) {
						let bfactor = atom.bfactor;
						if (bfactor >= 90) {
							return alphaFoldColors[90][1];
						} if (bfactor >= 70) {
							return alphaFoldColors[70][1];
						} if (bfactor >= 50) {
							return alphaFoldColors[50][1];
						}
						return alphaFoldColors[0][1];
					};
				});
			}
			opts['color'] = alphaFoldConfidenceSchemeId;
		}
		s.addRepresentation(structureVisType, opts);
		raiseEvent('representation-changed');
	}

	function getLoadedStructure() {
		return currentStructure;
	}

	function setLoadedStructure(newStructure) {
		currentStructure = newStructure;
	}

	function init() {
		let viewer = top.querySelector('div.viewport');
		// Create NGL Stage object
		stage = new NGL.Stage(viewer, {'backgroundColor': 'white'});

		// Handle window resizing
		window.addEventListener('resize', function () {
			stage.handleResize();
		}, false);
		viewer.addEventListener('resize', function() {
			stage.handleResize();
		});
		const resizeObserver = new ResizeObserver(function(entries) {
			for (const entry of entries) {
				if (document.fullscreenElement) {
					return;
				}
				if (entry.target.classList.contains('viewport')) {
					stage.handleResize();
				}
			}
		});
		resizeObserver.observe(viewer);
		if (typeof options.skipEvents !== "undefined" && options.skipEvents) {
			return;
		}
		$('div.structureSelector select').on('change', function() {
			loadmodel($(this).val());
		});
		// $('div.structureSelector select.assembly').on('change', function () {
		// 	let opt = viewer.querySelector('.assembly').value;
		// 	stage.compList.forEach(function (e) {
		// 		if (e.parameters.visible) {
		// 			e.setDefaultAssembly(opt);
		// 		}
		// 	});
		// });

		$(top).on('resetHighlight', function() {
			highlightParts = {};
			if (getSelectedColor() === 'highlight') {
				highlightSection();
			}
		});

		$('.variants, .alphavariants').on('toggle', function() {
			handleMissense();
		});
		$('.antigens').on('toggle', function() {
			handleColors();
		});
		$('.color').on('toggle', function() {
			handleColors();
		});
		$('.surface').on('toggle', function() {
			handleSurface();
		});
		$('.autorotate').on('toggle', function() {
			stage.setSpin($('.autorotate.active').hasClass('on'));
		});
		$(top).on('highlightSection', function(e, data) {
			highlightSection(data.transcript, data.startPos, data.stopPos, data.name, data.color, data.key);
		});
	}
	async function run(modelToLoad) {
		if (modelToLoad) {
			await loadmodel(modelToLoad);
		} else {
			hideComponents();
		}
	}

	function createColorLegend(showLegend) {
		let newHtml = "";
		if (showLegend === 'antibody') {
			Object.keys(window.antibodyColorMap).forEach(function (v) {
				let color = "#" + window.antibodyColorMap[v].toString(16).padStart(6, "0");
				newHtml += `<div class="colorLegendEntry"><div class='colorLegendColor' style='background-color: ${color}'></div><div class='colorLegendText'>${v}</div></div>`;
			});
			if (newHtml) {
				newHtml = "<div class='title'>Legend:</div>"+newHtml;
			}
		} else if (showLegend === 'bfactor') {
			if ($(".viewport.Predicted").length) {
				newHtml = "<div class='title'>Confidence for predicted structure:</div>" + newHtml;
				Object.keys(alphaFoldColors).sort(function (a, b) {
					return b - a;
				}).forEach(function (v) {
					let color = alphaFoldColors[v][2];
					let text = alphaFoldColors[v][0];
					newHtml += `<div class="colorLegendEntry"><div class='colorLegendColor' style='background-color: ${color}'></div><div class='colorLegendText'>${text}</div></div>`;
				});
			}
		} else if (showLegend === 'highlight') {
			newHtml = $("<div><div class='title'>Highlighted:</div></div>");
			let keys = Object.keys(highlightParts);
			let legendItems = [];
			keys.forEach(function(v) {
				legendItems.push({
					name: highlightParts[v].highlightedName,
					color: '#'+highlightParts[v].color.toString(16).padStart(6, '0'),
					key: v
				});
			});
			if (!legendItems.length) {
				newHtml.append('<div class="center italic">-- Select items in the protein browser above to highlight --</div>');
			}
			legendItems.push({
				name: "Transcript",
				color: '#'+transcriptColor.toString(16).padStart(6, '0')
			});
			// FIXME: Implement a better check but this is good enough for now
			if (!getLoadedStructure().startsWith('ENSP')) {
				legendItems.push({
					name: "Region outside transcript",
					color: '#' + defaultColor.toString(16).padStart(6, '0')
				});
			}
			if (combinedSchemeKey && colorSchemes[combinedSchemeKey].overlaps) {
				legendItems.push({
					name: 'Overlapping items',
					color: '#000000'
				});
			}
			Object.keys(legendItems).sort(function (a, b) {
				return legendItems[b]['name'] - legendItems[a]['name'];
			}).forEach(function (v) {
				let color = legendItems[v]['color'];
				let title = legendItems[v]['name'];
				let entry = $(`<div class="colorLegendEntry"><div class='colorLegendColor' style='background-color: ${color}'></div><div class='colorLegendText'>${title}</div></div>`);
				if (typeof legendItems[v]['key'] !== 'undefined' && legendItems[v]['key'].length) {
					entry.attr('data-key', legendItems[v]['key']);
					entry.attr('title', 'Click to remove highlight');
					entry.addClass('structureHL');
				}
				newHtml.append(entry);
			});
		}
		return newHtml;
	}

	function updateVariantLegend(structure, showLegend) {
		if (!showLegend.length) {
			return "";
		}
		let data = getStructureData(structure);
		let o = "";
		for (let i in showLegend) {
			let ltype = showLegend[i];
			let counts = (data.variantCount && typeof data.variantCount[ltype] !== "undefined") ? data.variantCount[ltype] : {};
			let sites = (typeof counts['sites'] !== "undefined") ? counts['sites'] : 'NA';
			let variants = (typeof counts['variants'] !== "undefined") ? counts['variants'] : 'NA';
			let prefix;
			let color;
			if (ltype === 'c' || ltype === 'p') {
				prefix = 'Experimental';
				color = 'blue';
			} else {
				prefix = 'AlphaMissense';
				color = 'red';
			}
			o += `<div class='title'>${prefix} variants for structure (${color} spheres):</div><div>${variants} variants in ${sites} sites</div>`;
		}
		return o;
	}

	init();
	return {
		stage: stage,
		loadStructure: loadStructure,
		run: run,
		getLoadedStructure: getLoadedStructure,
		addHandler: addHandler,
		addOnceHandler: addOnceHandler,
		removeHandler: removeHandler,
		getStructureData: getStructureData,
		addStructureData: addStructureData,
		getStructureDataEntries: getStructureDataEntries,
		loadmodel: loadmodel,
	};
}