/* eslint no-tabs: 0, no-var: 0, indent: [2, tab, {VariableDeclarator: 0, SwitchCase: 1}], quotes: 0 */
/* global CodeMirror */
"use strict";

var styleId = null;
var dirty = {};       // only the actually dirty items here
var editors = [];     // array of all CodeMirror instances
var saveSizeOnClose;
var useHistoryBack;   // use browser history back when "back to manage" is clicked

// direct & reverse mapping of @-moz-document keywords and internal property names
var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"};
var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"};

// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
onBackgroundReady();

// make querySelectorAll enumeration code readable
["forEach", "some", "indexOf", "map"].forEach(function(method) {
	NodeList.prototype[method]= Array.prototype[method];
});

// Chrome pre-34
Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector;

// Chrome pre-41 polyfill
Element.prototype.closest = Element.prototype.closest || function(selector) {
	for (var e = this; e && !e.matches(selector); e = e.parentElement) {}
	return e;
};

Array.prototype.rotate = function(amount) { // negative amount == rotate left
	var r = this.slice(-amount, this.length);
	Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
	return r;
};

Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }});

// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
new MutationObserver((mutations, observer) => {
	const themeElement = document.getElementById("cm-theme");
	if (themeElement) {
		themeElement.href = prefs.get("editor.theme") == "default" ? ""
			: "codemirror/theme/" + prefs.get("editor.theme") + ".css";
		observer.disconnect();
	}
}).observe(document, {subtree: true, childList: true});

// reroute handling to nearest editor when keypress resolves to one of these commands
var hotkeyRerouter = {
	commands: {
		save: true, jumpToLine: true, nextEditor: true, prevEditor: true,
		find: true, findNext: true, findPrev: true, replace: true, replaceAll: true
	},
	setState: function(enable) {
		setTimeout(function() {
			document[(enable ? "add" : "remove") + "EventListener"]("keydown", hotkeyRerouter.eventHandler);
		}, 0);
	},
	eventHandler: function(event) {
		var keyName = CodeMirror.keyName(event);
		if ("handled" == CodeMirror.lookupKey(keyName, CodeMirror.getOption("keyMap"), handleCommand)
		 || "handled" == CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, handleCommand)) {
			event.preventDefault();
			event.stopPropagation();
		}
		function handleCommand(command) {
			if (hotkeyRerouter.commands[command] === true) {
				CodeMirror.commands[command](getEditorInSight(event.target));
				return true;
			}
		}
	}
};

function onChange(event) {
	var node = event.target;
	if ("savedValue" in node) {
		var currentValue = "checkbox" === node.type ? node.checked : node.value;
		setCleanItem(node, node.savedValue === currentValue);
	} else {
		// the manually added section's applies-to is dirty only when the value is non-empty
		setCleanItem(node, node.localName != "input" || !node.value.trim());
		delete node.savedValue; // only valid when actually saved
	}
	updateTitle();
}

// Set .dirty on stylesheet contributors that have changed
function setDirtyClass(node, isDirty) {
	node.classList.toggle("dirty", isDirty);
}

function setCleanItem(node, isClean) {
	if (!node.id) {
		node.id = Date.now().toString(32).substr(-6);
	}

	if (isClean) {
		delete dirty[node.id];
		// code sections have .CodeMirror property
		if (node.CodeMirror) {
			node.savedValue = node.CodeMirror.changeGeneration();
		} else {
			node.savedValue = "checkbox" === node.type ? node.checked : node.value;
		}
	} else {
		dirty[node.id] = true;
	}

	setDirtyClass(node, !isClean);
}

function isCleanGlobal() {
	var clean = Object.keys(dirty).length == 0;
	setDirtyClass(document.body, !clean);
    var saveBtn = document.getElementById("save-button")
    if (clean){
        //saveBtn.removeAttribute('disabled');
    }else{
        //saveBtn.setAttribute('disabled', true);
    }
	return clean;
}

function setCleanGlobal() {
	document.querySelectorAll("#header, #sections > div").forEach(setCleanSection);
	dirty = {}; // forget the dirty applies-to ids from a deleted section after the style was saved
}

function setCleanSection(section) {
	section.querySelectorAll(".style-contributor").forEach(function(node) { setCleanItem(node, true) });

	// #header section has no codemirror
	var cm = section.CodeMirror;
	if (cm) {
		section.savedValue = cm.changeGeneration();
		indicateCodeChange(cm);
	}
}

function initCodeMirror() {
	var CM = CodeMirror;
	var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0;

	// CodeMirror miserably fails on keyMap="" so let's ensure it's not
	if (!prefs.get('editor.keyMap')) {
		prefs.reset('editor.keyMap');
	}

	// default option values
	Object.assign(CM.defaults, {
		mode: 'css',
		lineNumbers: true,
		lineWrapping: true,
		foldGutter: true,
		gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
		matchBrackets: true,
		highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
		lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")},
		lintReportDelay: prefs.get("editor.lintReportDelay"),
		styleActiveLine: true,
		theme: "default",
		keyMap: prefs.get("editor.keyMap"),
		extraKeys: { // independent of current keyMap
			"Alt-PageDown": "nextEditor",
			"Alt-PageUp": "prevEditor"
		}
	}, prefs.get("editor.options"));

	// additional commands
	CM.commands.jumpToLine = jumpToLine;
	CM.commands.nextEditor = function(cm) { nextPrevEditor(cm, 1) };
	CM.commands.prevEditor = function(cm) { nextPrevEditor(cm, -1) };
	CM.commands.save = save;
	CM.commands.blockComment = function(cm) {
		cm.blockComment(cm.getCursor("from"), cm.getCursor("to"), {fullLines: false});
	};

	// "basic" keymap only has basic keys by design, so we skip it

	var extraKeysCommands = {};
	Object.keys(CM.defaults.extraKeys).forEach(function(key) {
		extraKeysCommands[CM.defaults.extraKeys[key]] = true;
	});
	if (!extraKeysCommands.jumpToLine) {
		CM.keyMap.sublime["Ctrl-G"] = "jumpToLine";
		CM.keyMap.emacsy["Ctrl-G"] = "jumpToLine";
		CM.keyMap.pcDefault["Ctrl-J"] = "jumpToLine";
		CM.keyMap.macDefault["Cmd-J"] = "jumpToLine";
	}
	if (!extraKeysCommands.autocomplete) {
		CM.keyMap.pcDefault["Ctrl-Space"] = "autocomplete"; // will be used by "sublime" on PC via fallthrough
		CM.keyMap.macDefault["Alt-Space"] = "autocomplete"; // OSX uses Ctrl-Space and Cmd-Space for something else
		CM.keyMap.emacsy["Alt-/"] = "autocomplete"; // copied from "emacs" keymap
		// "vim" and "emacs" define their own autocomplete hotkeys
	}
	if (!extraKeysCommands.blockComment) {
		CM.keyMap.sublime["Shift-Ctrl-/"] = "blockComment";
	}

	if (isWindowsOS) {
		// "pcDefault" keymap on Windows should have F3/Shift-F3
		if (!extraKeysCommands.findNext) {
			CM.keyMap.pcDefault["F3"] = "findNext";
		}
		if (!extraKeysCommands.findPrev) {
			CM.keyMap.pcDefault["Shift-F3"] = "findPrev";
		}

		// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys
		["N", "T", "W"].forEach(function(char) {
			[{from: "Ctrl-", to: ["Alt-", "Ctrl-Alt-"]},
			 {from: "Shift-Ctrl-", to: ["Ctrl-Alt-", "Shift-Ctrl-Alt-"]} // Note: modifier order in CM is S-C-A
			].forEach(function(remap) {
				var oldKey = remap.from + char;
				Object.keys(CM.keyMap).forEach(function(keyMapName) {
					var keyMap = CM.keyMap[keyMapName];
					var command = keyMap[oldKey];
					if (!command) {
						return;
					}
					remap.to.some(function(newMod) {
						var newKey = newMod + char;
						if (!(newKey in keyMap)) {
							delete keyMap[oldKey];
							keyMap[newKey] = command;
							return true;
						}
					});
				});
			});
		});
	}

	// user option values
	CM.getOption = function (o) {
		return CodeMirror.defaults[o];
	};
	CM.setOption = function (o, v) {
		CodeMirror.defaults[o] = v;
		editors.forEach(function(editor) {
			editor.setOption(o, v);
		});
	};

	CM.prototype.getSection = function() {
		return this.display.wrapper.parentNode;
	};

	// initialize global editor controls
	function optionsHtmlFromArray(options) {
		return options.map(function(opt) { return "<option>" + opt + "</option>"; }).join("");
	}
	var themeControl = document.getElementById("editor.theme");
	if (BG && BG.codeMirrorThemes) {
		themeControl.innerHTML = optionsHtmlFromArray(BG.codeMirrorThemes);
	} else {
		// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
		const theme = prefs.get("editor.theme");
		themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
		BG.getCodeMirrorThemes().then(themes => {
			BG.codeMirrorThemes = themes;
			themeControl.innerHTML = optionsHtmlFromArray(themes);
			themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
		});
	}
	document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort());
	document.getElementById("options").addEventListener("change", acmeEventListener, false);
	setupLivePrefs(
		document.querySelectorAll("#options *[data-option][id^='editor.']")
			.map(function(option) { return option.id })
	);

	hotkeyRerouter.setState(true);
}

function acmeEventListener(event) {
	var el = event.target;
	var option = el.dataset.option;
	//console.log("acmeEventListener heard %s on %s", event.type, el.id);
	if (!option) {
		console.error("acmeEventListener: no 'cm_option' %O", el);
		return;
	}
	var value = el.type == "checkbox" ? el.checked : el.value;
	switch (option) {
		case "tabSize":
			CodeMirror.setOption("indentUnit", value);
			break;
		case "theme":
			var themeLink = document.getElementById("cm-theme");
			// use non-localized "default" internally
			if (!value || value == "default" || value == t("defaultTheme")) {
				value = "default";
				if (prefs.get(el.id) != value) {
					prefs.set(el.id, value);
				}
				themeLink.href = "";
				el.selectedIndex = 0;
				break;
			}
			var url = chrome.runtime.getURL("codemirror/theme/" + value + ".css");
			if (themeLink.href == url) { // preloaded in initCodeMirror()
				break;
			}
			// avoid flicker: wait for the second stylesheet to load, then apply the theme
			document.head.insertAdjacentHTML("beforeend",
				'<link id="cm-theme2" rel="stylesheet" href="' + url + '">');
			(function() {
				setTimeout(function() {
					CodeMirror.setOption(option, value);
					themeLink.remove();
					document.getElementById("cm-theme2").id = "cm-theme";
				}, 100);
			})();
			return;
		case "highlightSelectionMatches":
			switch (value) {
				case 'token':
				case 'selection':
					document.body.dataset[option] = value;
					value = {showToken: value == 'token' && /[#.\-\w]/, annotateScrollbar: true};
					break;
				default:
					value = null;
			}
	}
	CodeMirror.setOption(option, value);
}

// replace given textarea with the CodeMirror editor
function setupCodeMirror(textarea, index) {
	var cm = CodeMirror.fromTextArea(textarea, {lint: null});

	cm.on("change", indicateCodeChange);
	cm.on("blur", function(cm) {
		editors.lastActive = cm;
		hotkeyRerouter.setState(true);
		setTimeout(function() {
			var cm = editors.lastActive;
			var childFocused = cm.display.wrapper.contains(document.activeElement);
			cm.display.wrapper.classList.toggle("CodeMirror-active", childFocused);
		}, 0);
	});
	cm.on("focus", function() {
		hotkeyRerouter.setState(false);
		cm.display.wrapper.classList.add("CodeMirror-active");
	});
	cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));

	var resizeGrip = cm.display.wrapper.appendChild(document.createElement("div"));
	resizeGrip.className = "resize-grip";
	resizeGrip.addEventListener("mousedown", function(e) {
		e.preventDefault();
		var cm = e.target.parentNode.CodeMirror;
		var minHeight = cm.defaultTextHeight()
			+ cm.display.lineDiv.offsetParent.offsetTop /* .CodeMirror-lines padding */
			+ cm.display.wrapper.offsetHeight - cm.display.wrapper.scrollHeight /* borders */;
		function resize(e) {
			cm.setSize(null, Math.max(minHeight, cm.display.wrapper.scrollHeight + e.movementY));
		}
		document.addEventListener("mousemove", resize);
		document.addEventListener("mouseup", function resizeStop() {
			document.removeEventListener("mouseup", resizeStop);
			document.removeEventListener("mousemove", resize);
		});
	});
	// resizeGrip has enough space when scrollbars.horiz is visible
	if (cm.display.scrollbars.horiz.style.display != "") {
		cm.display.scrollbars.vert.style.marginBottom = "0";
	}
	// resizeGrip space adjustment in case a long line was entered/deleted by a user
	new MutationObserver(function(mutations) {
		var hScrollbar = mutations[0].target;
		var hScrollbarVisible = hScrollbar.style.display != "";
		var vScrollbar = hScrollbar.parentNode.CodeMirror.display.scrollbars.vert;
		vScrollbar.style.marginBottom = hScrollbarVisible ? "0" : "";
	}).observe(cm.display.scrollbars.horiz, {
		attributes: true,
		attributeFilter: ["style"]
	});

	editors.splice(index || editors.length, 0, cm);
	return cm;
}

function indicateCodeChange(cm) {
	var section = cm.getSection();
	setCleanItem(section, cm.isClean(section.savedValue));
	updateTitle();
	updateLintReport(cm);
}

function getSectionForChild(e) {
	return e.closest("#sections > div");
}

function getSections() {
	return document.querySelectorAll("#sections > div");
}

// remind Chrome to repaint a previously invisible editor box by toggling any element's transform
// this bug is present in some versions of Chrome (v37-40 or something)
document.addEventListener("scroll", function(event) {
	var style = document.getElementById("name").style;
	style.webkitTransform = style.webkitTransform ? "" : "scale(1)";
});

// Shift-Ctrl-Wheel scrolls entire page even when mouse is over a code editor
document.addEventListener("wheel", function(event) {
	if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
		// Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
		window.scrollBy(0, event.deltaX || event.deltaY);
		event.preventDefault();
	}
});

chrome.tabs.query({currentWindow: true}, function(tabs) {
	var windowId = tabs[0].windowId;
	if (prefs.get("openEditInWindow")) {
		if (sessionStorage.saveSizeOnClose && 'left' in prefs.get('windowPosition', {})) {
			// window was reopened via Ctrl-Shift-T etc.
			chrome.windows.update(windowId, prefs.get('windowPosition'));
		}
		if (tabs.length == 1 && window.history.length == 1) {
			chrome.windows.getAll(function(windows) {
				if (windows.length > 1) {
					sessionStorageHash("saveSizeOnClose").set(windowId, true);
					saveSizeOnClose = true;
				}
			});
		} else {
			saveSizeOnClose = sessionStorageHash("saveSizeOnClose").value[windowId];
		}
	}
	chrome.tabs.onRemoved.addListener(function(tabId, info) {
		sessionStorageHash("manageStylesHistory").unset(tabId);
		if (info.windowId == windowId && info.isWindowClosing) {
			sessionStorageHash("saveSizeOnClose").unset(windowId);
		}
	});
});

getActiveTab().then(tab => {
	useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href;
});

function goBackToManage(event) {
	if (useHistoryBack) {
		event.stopPropagation();
		event.preventDefault();
		history.back();
	}
}

window.onbeforeunload = function() {
	if (saveSizeOnClose) {
		prefs.set("windowPosition", {
			left: screenLeft,
			top: screenTop,
			width: outerWidth,
			height: outerHeight
		});
	}
	document.activeElement.blur();
	if (isCleanGlobal()) {
		return;
	}
	updateLintReport(null, 0);
	return confirm(t('styleChangesNotSaved'));
};

function addAppliesTo(list, name, value) {
	var showingEverything = list.querySelector(".applies-to-everything") != null;
	// blow away "Everything" if it's there
	if (showingEverything) {
		list.removeChild(list.firstChild);
	}
	var e;
	if (name && value) {
		e = template.appliesTo.cloneNode(true);
		e.querySelector("[name=applies-type]").value = name;
		e.querySelector("[name=applies-value]").value = value;
		e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false);
	} else if (showingEverything || list.hasChildNodes()) {
		e = template.appliesTo.cloneNode(true);
		if (list.hasChildNodes()) {
			e.querySelector("[name=applies-type]").value = list.querySelector("li:last-child [name='applies-type']").value;
		}
		e.querySelector(".remove-applies-to").addEventListener("click", removeAppliesTo, false);
	} else {
		e = template.appliesToEverything.cloneNode(true);
	}
	e.querySelector(".add-applies-to").addEventListener("click", function() {addAppliesTo(this.parentNode.parentNode)}, false);
	list.appendChild(e);
}

function addSection(event, section) {
	var div = template.section.cloneNode(true);
	div.querySelector(".applies-to-help").addEventListener("click", showAppliesToHelp, false);
	div.querySelector(".remove-section").addEventListener("click", removeSection, false);
	div.querySelector(".add-section").addEventListener("click", addSection, false);
	div.querySelector(".beautify-section").addEventListener("click", beautify);

	var codeElement = div.querySelector(".code");
	var appliesTo = div.querySelector(".applies-to-list");
	var appliesToAdded = false;

	if (section) {
		codeElement.value = section.code;
		for (var i in propertyToCss) {
			if (section[i]) {
				section[i].forEach(function(url) {
					addAppliesTo(appliesTo, propertyToCss[i], url);
					appliesToAdded = true;
				});
			}
		}
	}
	if (!appliesToAdded) {
		addAppliesTo(appliesTo);
	}

	appliesTo.addEventListener("change", onChange);
	appliesTo.addEventListener("input", onChange);

	toggleTestRegExpVisibility();
	appliesTo.addEventListener('change', toggleTestRegExpVisibility);
	div.querySelector('.test-regexp').onclick = showRegExpTester;
	function toggleTestRegExpVisibility() {
		const show = [...appliesTo.children].some(item =>
			!item.matches('.applies-to-everything') &&
			item.querySelector('.applies-type').value == 'regexp' &&
			item.querySelector('.applies-value').value.trim());
		div.classList.toggle('has-regexp', show);
		appliesTo.oninput = appliesTo.oninput || show && (event => {
			if (event.target.matches('.applies-value')
			&& event.target.parentElement.querySelector('.applies-type').value == 'regexp') {
				showRegExpTester(null, div);
			}
		});
	}

	var sections = document.getElementById("sections");
	if (event) {
		var clickedSection = getSectionForChild(event.target);
		sections.insertBefore(div, clickedSection.nextElementSibling);
		var newIndex = getSections().indexOf(clickedSection) + 1;
		var cm = setupCodeMirror(codeElement, newIndex);
		makeSectionVisible(cm);
		cm.focus()
		renderLintReport();
	} else {
		sections.appendChild(div);
		var cm = setupCodeMirror(codeElement);
	}

	div.CodeMirror = cm;
	setCleanSection(div);
	return div;
}

function removeAppliesTo(event) {
	var appliesTo = event.target.parentNode,
	    appliesToList = appliesTo.parentNode;
	removeAreaAndSetDirty(appliesTo);
	if (!appliesToList.hasChildNodes()) {
		addAppliesTo(appliesToList);
	}
}

function removeSection(event) {
	var section = getSectionForChild(event.target);
	var cm = section.CodeMirror;
	removeAreaAndSetDirty(section);
	editors.splice(editors.indexOf(cm), 1);
	renderLintReport();
}

function removeAreaAndSetDirty(area) {
	var contributors = area.querySelectorAll('.style-contributor');
	if(!contributors.length){
		setCleanItem(area, false);
	}
	contributors.some(function(node) {
		if (node.savedValue) {
			// it's a saved section, so make it dirty and stop the enumeration
			setCleanItem(area, false);
			return true;
		} else {
			// it's an empty section, so undirty the applies-to items,
			// otherwise orphaned ids would keep the style dirty
			setCleanItem(node, true);
		}
	});
	updateTitle();
	area.parentNode.removeChild(area);
}

function makeSectionVisible(cm) {
	var section = cm.getSection();
	var bounds = section.getBoundingClientRect();
	if ((bounds.bottom > window.innerHeight && bounds.top > 0) || (bounds.top < 0 && bounds.bottom < window.innerHeight)) {
		if (bounds.top < 0) {
			window.scrollBy(0, bounds.top - 1);
		} else {
			window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
		}
	}
}

function setupGlobalSearch() {
	var originalCommand = {
		find: CodeMirror.commands.find,
		findNext: CodeMirror.commands.findNext,
		findPrev: CodeMirror.commands.findPrev,
		replace: CodeMirror.commands.replace
	};
	var originalOpenDialog = CodeMirror.prototype.openDialog;
	var originalOpenConfirm = CodeMirror.prototype.openConfirm;

	var curState; // cm.state.search for last used 'find'

	function shouldIgnoreCase(query) { // treat all-lowercase non-regexp queries as case-insensitive
		return typeof query == "string" && query == query.toLowerCase();
	}

	function updateState(cm, newState) {
		if (!newState) {
			if (cm.state.search) {
				return cm.state.search;
			}
			if (!curState) {
				return null;
			}
			newState = curState;
		}
		cm.state.search = {
			query: newState.query,
			overlay: newState.overlay,
			annotate: cm.showMatchesOnScrollbar(newState.query, shouldIgnoreCase(newState.query))
		}
		cm.addOverlay(newState.overlay);
		return cm.state.search;
	}

	// temporarily overrides the original openDialog with the provided template's innerHTML
	function customizeOpenDialog(cm, template, callback) {
		cm.openDialog = function(tmpl, cb, opt) {
			// invoke 'callback' and bind 'this' to the original callback
			originalOpenDialog.call(cm, template.innerHTML, callback.bind(cb), opt);
		};
		setTimeout(function() { cm.openDialog = originalOpenDialog; }, 0);
		refocusMinidialog(cm);
	}

	function focusClosestCM(activeCM) {
		editors.lastActive = activeCM;
		var cm = getEditorInSight();
		if (cm != activeCM) {
			cm.focus();
		}
		return cm;

	}

	function find(activeCM) {
		activeCM = focusClosestCM(activeCM);
		customizeOpenDialog(activeCM, template.find, function(query) {
			this(query);
			curState = activeCM.state.search;
			if (editors.length == 1 || !curState.query) {
				return;
			}
			editors.forEach(function(cm) {
				if (cm != activeCM) {
					cm.execCommand("clearSearch");
					updateState(cm, curState);
				}
			});
			if (CodeMirror.cmpPos(curState.posFrom, curState.posTo) == 0) {
				findNext(activeCM);
			}
		});
		originalCommand.find(activeCM);
	}

	function findNext(activeCM, reverse) {
		var state = updateState(activeCM);
		if (!state || !state.query) {
			find(activeCM);
			return;
		}
		var pos = activeCM.getCursor(reverse ? "from" : "to");
		activeCM.setSelection(activeCM.getCursor()); // clear the selection, don't move the cursor

		var rxQuery = typeof state.query == "object"
			? state.query : stringAsRegExp(state.query, shouldIgnoreCase(state.query) ? "i" : "");

		if (document.activeElement && document.activeElement.name == "applies-value"
			&& searchAppliesTo(activeCM)) {
			return;
		}
		for (var i=0, cm=activeCM; i < editors.length; i++) {
			state = updateState(cm);
			if (!cm.hasFocus()) {
				pos = reverse ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(0, 0);
			}
			var searchCursor = cm.getSearchCursor(state.query, pos, shouldIgnoreCase(state.query));
			if (searchCursor.find(reverse)) {
				if (editors.length > 1) {
					makeSectionVisible(cm);
					cm.focus();
				}
				// speedup the original findNext
				state.posFrom = reverse ? searchCursor.to() : searchCursor.from();
				state.posTo = CodeMirror.Pos(state.posFrom.line, state.posFrom.ch);
				originalCommand[reverse ? "findPrev" : "findNext"](cm);
				return;
			} else if (!reverse && searchAppliesTo(cm)) {
				return;
			}
			cm = editors[(editors.indexOf(cm) + (reverse ? -1 + editors.length : 1)) % editors.length];
			if (reverse && searchAppliesTo(cm)) {
				return;
			}
		}
		// nothing found so far, so call the original search with wrap-around
		originalCommand[reverse ? "findPrev" : "findNext"](activeCM);

		function searchAppliesTo(cm) {
			var inputs = [].slice.call(cm.getSection().querySelectorAll(".applies-value"));
			if (reverse) {
				inputs = inputs.reverse();
			}
			inputs.splice(0, inputs.indexOf(document.activeElement) + 1);
			return inputs.some(function(input) {
				var match = rxQuery.exec(input.value);
				if (match) {
					input.focus();
					var end = match.index + match[0].length;
					// scroll selected part into view in long inputs,
					// works only outside of current event handlers chain, hence timeout=0
					setTimeout(function() {
						input.setSelectionRange(end, end);
						input.setSelectionRange(match.index, end)
					}, 0);
					return true;
				}
			});
		}
	}

	function findPrev(cm) {
		findNext(cm, true);
	}

	function replace(activeCM, all) {
		var queue, query, replacement;
		activeCM = focusClosestCM(activeCM);
		customizeOpenDialog(activeCM, template[all ? "replaceAll" : "replace"], function(txt) {
			query = txt;
			customizeOpenDialog(activeCM, template.replaceWith, function(txt) {
				replacement = txt;
				queue = editors.rotate(-editors.indexOf(activeCM));
				all ? editors.forEach(doReplace) : doReplace();
			});
			this(query);
		});
		originalCommand.replace(activeCM, all);

		function doReplace() {
			var cm = queue.shift();
			if (!cm) {
				if (!all) {
					editors.lastActive.focus();
				}
				return;
			}
			// hide the first two dialogs (replace, replaceWith)
			cm.openDialog = function(tmpl, callback, opt) {
				cm.openDialog = function(tmpl, callback, opt) {
					cm.openDialog = originalOpenDialog;
					if (all) {
						callback(replacement);
					} else {
						doConfirm(cm);
						callback(replacement);
						if (!cm.getWrapperElement().querySelector(".CodeMirror-dialog")) {
							// no dialog == nothing found in the current CM, move to the next
							doReplace();
						}
					}
				};
				callback(query);
			};
			originalCommand.replace(cm, all);
		}
		function doConfirm(cm) {
			var wrapAround = false;
			var origPos = cm.getCursor();
			cm.openConfirm = function overrideConfirm(tmpl, callbacks, opt) {
				var ovrCallbacks = callbacks.map(function(callback) {
					return function() {
						makeSectionVisible(cm);
						cm.openConfirm = overrideConfirm;
						setTimeout(function() { cm.openConfirm = originalOpenConfirm; }, 0);

						var pos = cm.getCursor();
						callback();
						var cmp = CodeMirror.cmpPos(cm.getCursor(), pos);
						wrapAround |= cmp <= 0;

						var dlg = cm.getWrapperElement().querySelector(".CodeMirror-dialog");
						if (!dlg || cmp == 0 || wrapAround && CodeMirror.cmpPos(cm.getCursor(), origPos) >= 0) {
							if (dlg) {
								dlg.remove();
							}
							doReplace();
						}
					}
				});
				originalOpenConfirm.call(cm, template.replaceConfirm.innerHTML, ovrCallbacks, opt);
			};
		}
	}

	function replaceAll(cm) {
		replace(cm, true);
	}

	CodeMirror.commands.find = find;
	CodeMirror.commands.findNext = findNext;
	CodeMirror.commands.findPrev = findPrev;
	CodeMirror.commands.replace = replace;
	CodeMirror.commands.replaceAll = replaceAll;
}

function jumpToLine(cm) {
	var cur = cm.getCursor();
	refocusMinidialog(cm);
	cm.openDialog(template.jumpToLine.innerHTML, function(str) {
		var m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
		if (m) {
			cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
		}
	}, {value: cur.line+1});
}

function refocusMinidialog(cm) {
	var section = cm.getSection();
	if (!section.querySelector(".CodeMirror-dialog")) {
		return;
	}
	// close the currently opened minidialog
	cm.focus();
	// make sure to focus the input in newly opened minidialog
	setTimeout(function() {
		section.querySelector(".CodeMirror-dialog").focus();
	}, 0);
}

function nextPrevEditor(cm, direction) {
	cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
	makeSectionVisible(cm);
	cm.focus();
}

function getEditorInSight(nearbyElement) {
	// priority: 1. associated CM for applies-to element 2. last active if visible 3. first visible
	var cm;
	if (nearbyElement && nearbyElement.className.indexOf("applies-") >= 0) {
		cm = getSectionForChild(nearbyElement).CodeMirror;
	} else {
		cm = editors.lastActive;
	}
	if (!cm || offscreenDistance(cm) > 0) {
		var sorted = editors
			.map(function(cm, index) { return {cm: cm, distance: offscreenDistance(cm), index: index} })
			.sort(function(a, b) { return a.distance - b.distance || a.index - b.index });
		cm = sorted[0].cm;
		if (sorted[0].distance > 0) {
			makeSectionVisible(cm)
		}
	}
	return cm;

	function offscreenDistance(cm) {
		var LINES_VISIBLE = 2; // closest editor should have at least # lines visible
		var bounds = cm.getSection().getBoundingClientRect();
		if (bounds.top < 0) {
			return -bounds.top;
		} else if (bounds.top < window.innerHeight - cm.defaultTextHeight() * LINES_VISIBLE) {
			return 0;
		} else {
			return bounds.top - bounds.height;
		}
	}
}

function updateLintReport(cm, delay) {
	if (delay == 0) {
		// immediately show pending csslint messages in onbeforeunload and save
		update(cm);
		return;
	}
	if (delay > 0) {
		setTimeout(cm => { cm.performLint(); update(cm) }, delay, cm);
		return;
	}
	var state = cm.state.lint;
	if (!state) {
		return;
	}
	// user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms)
	// or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed
	clearTimeout(state.reportTimeout);
	state.reportTimeout = setTimeout(update, state.options.delay + 100, cm);
	state.postponeNewIssues = delay == undefined || delay == null;

	function update(cm) {
		var scope = cm ? [cm] : editors;
		var changed = false;
		var fixedOldIssues = false;
		scope.forEach(function(cm) {
			var state = cm.state.lint || {};
			var oldMarkers = state.markedLast || {};
			var newMarkers = {};
			var html = !state.marked || state.marked.length == 0 ? "" : "<tbody>" +
				state.marked.map(function(mark) {
					var info = mark.__annotation;
					var isActiveLine = info.from.line == cm.getCursor().line;
					var pos = isActiveLine ? "cursor" : (info.from.line + "," + info.from.ch);
					var message = escapeHtml(info.message.replace(/ at line \d.+$/, ""));
					if (message.length > 100) {
						message = message.substr(0, 100) + "...";
					}
					if (isActiveLine || oldMarkers[pos] == message) {
						delete oldMarkers[pos];
					}
					newMarkers[pos] = message;
					return "<tr class='" + info.severity + "'>" +
						"<td role='severity' class='CodeMirror-lint-marker-" + info.severity + "'>" +
							info.severity + "</td>" +
						"<td role='line'>" + (info.from.line+1) + "</td>" +
						"<td role='sep'>:</td>" +
						"<td role='col'>" + (info.from.ch+1) + "</td>" +
						"<td role='message'>" + message + "</td></tr>";
				}).join("") + "</tbody>";
			state.markedLast = newMarkers;
			fixedOldIssues |= state.reportDisplayed && Object.keys(oldMarkers).length > 0;
			if (state.html != html) {
				state.html = html;
				changed = true;
			}
		});
		if (changed) {
			clearTimeout(state ? state.renderTimeout : undefined);
			if (!state || !state.postponeNewIssues || fixedOldIssues) {
				renderLintReport(true);
			} else {
				state.renderTimeout = setTimeout(function() {
					renderLintReport(true);
				}, CodeMirror.defaults.lintReportDelay);
			}
		}
	}
	function escapeHtml(html) {
		var chars = {"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': '&quot;', "'": '&#39;', "/": '&#x2F;'};
		return html.replace(/[&<>"'\/]/g, function(char) { return chars[char] });
	}
}

function renderLintReport(someBlockChanged) {
	var container = document.getElementById("lint");
	var content = container.children[1];
	var label = t("sectionCode");
	var newContent = content.cloneNode(false);
	var issueCount = 0;
	editors.forEach(function(cm, index) {
		if (cm.state.lint && cm.state.lint.html) {
			var newBlock = newContent.appendChild(document.createElement("table"));
			var html = "<caption>" + label + " " + (index+1) + "</caption>" + cm.state.lint.html;
			newBlock.innerHTML = html;
			newBlock.cm = cm;
			issueCount += newBlock.rows.length;

			var block = content.children[newContent.children.length - 1];
			var blockChanged = !block || cm != block.cm || html != block.innerHTML;
			someBlockChanged |= blockChanged;
			cm.state.lint.reportDisplayed = blockChanged;
		}
	});
	if (someBlockChanged || newContent.children.length != content.children.length) {
		document.getElementById('issue-count').textContent = issueCount;
		container.replaceChild(newContent, content);
		container.style.display = newContent.children.length ? "block" : "none";
		resizeLintReport(null, newContent);
	}
}

function resizeLintReport(event, content) {
	content = content || document.getElementById("lint").children[1];
	if (content.children.length) {
		var bounds = content.getBoundingClientRect();
		var newMaxHeight = bounds.bottom <= innerHeight ? '' : (innerHeight - bounds.top) + "px";
		if (newMaxHeight != content.style.maxHeight) {
			content.style.maxHeight = newMaxHeight;
		}
	}
}

function gotoLintIssue(event) {
	var issue = event.target.closest("tr");
	if (!issue) {
		return;
	}
	var block = issue.closest("table");
	makeSectionVisible(block.cm);
	block.cm.focus();
	block.cm.setSelection({
		line: parseInt(issue.querySelector("td[role='line']").textContent) - 1,
		ch: parseInt(issue.querySelector("td[role='col']").textContent) - 1
	});
}

function toggleLintReport() {
	document.getElementById("lint").classList.toggle("collapsed");
}

function beautify(event) {
	if (exports.css_beautify) { // thanks to csslint's definition of 'exports'
		doBeautify();
	} else {
		var script = document.head.appendChild(document.createElement("script"));
		script.src = "beautify/beautify-css-mod.js";
		script.onload = doBeautify;
	}
	function doBeautify() {
		var tabs = prefs.get("editor.indentWithTabs");
		var options = prefs.get("editor.beautify");
		options.indent_size = tabs ? 1 : prefs.get("editor.tabSize");
		options.indent_char = tabs ? "\t" : " ";

		var section = getSectionForChild(event.target);
		var scope = section ? [section.CodeMirror] : editors;

		showHelp(t("styleBeautify"), "<div class='beautify-options'>" +
			optionHtml(".selector1,", "selector_separator_newline") +
			optionHtml(".selector2,", "newline_before_open_brace") +
			optionHtml("{", "newline_after_open_brace") +
			optionHtml("border: none;", "newline_between_properties", true) +
			optionHtml("display: block;", "newline_before_close_brace", true) +
			optionHtml("}", "newline_between_rules") +
			"</div>" +
			"<div><button role='undo'></button></div>");

		var undoButton = document.querySelector("#help-popup button[role='undo']");
		undoButton.textContent = t(scope.length == 1 ? "undo" : "undoGlobal");
		undoButton.addEventListener("click", function() {
			var undoable = false;
			scope.forEach(function(cm) {
				if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
					delete cm.beautifyChange[cm.changeGeneration()];
					cm.undo();
					cm.scrollIntoView(cm.getCursor());
					undoable |= cm.beautifyChange[cm.changeGeneration()];
				}
			});
			undoButton.disabled = !undoable;
		});

		scope.forEach(function(cm) {
			setTimeout(function() {
				const pos = options.translate_positions =
					[].concat.apply([], cm.doc.sel.ranges.map(r =>
						[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
				var text = cm.getValue();
				var newText = exports.css_beautify(text, options);
				if (newText != text) {
					if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
						// clear the list if last change wasn't a css-beautify
						cm.beautifyChange = {};
					}
					cm.setValue(newText);
					const selections = [];
					for (let i = 0; i < pos.length; i += 2) {
						selections.push({anchor: pos[i], head: pos[i + 1]});
					}
					cm.setSelections(selections);
					cm.beautifyChange[cm.changeGeneration()] = true;
					undoButton.disabled = false;
				}
			}, 0);
		});

		document.querySelector(".beautify-options").addEventListener("change", function(event) {
			var value = event.target.selectedIndex > 0;
			options[event.target.dataset.option] = value;
			prefs.set("editor.beautify", options);
			event.target.parentNode.setAttribute("newline", value.toString());
			doBeautify();
		});

		function optionHtml(label, optionName, indent) {
			var value = options[optionName];
			return "<div newline='" + value.toString() + "'>" +
				"<span" + (indent ? " indent" : "") + ">" + label + "</span>" +
				"<select data-option='" + optionName + "'>" +
					"<option" + (value ? "" : " selected") + ">&nbsp;</option>" +
					"<option" + (value ? " selected" : "") + ">\\n</option>" +
				"</select></div>";
		}
	}
}

document.addEventListener("DOMContentLoaded", init);

function init() {
	initCodeMirror();
	var params = getParams();
	if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses
		// This is an add
		tE("heading", "addStyleTitle");
		var section = {code: ""}
		for (var i in CssToProperty) {
			if (params[i]) {
				section[CssToProperty[i]] = [params[i]];
			}
		}
		window.onload = () => {
			window.onload = null;
			addSection(null, section);
			// default to enabled
			document.getElementById("enabled").checked = true
			initHooks();
		};
		return;
	}
	// This is an edit
	tE("heading", "editStyleHeading", null, false);
	getStylesSafe({id: params.id}).then(styles => {
		let style = styles[0];
		if (!style) {
			style = {id: null, sections: []};
			history.replaceState({}, document.title, location.pathname);
		}
		styleId = style.id;
		setStyleMeta(style);
		window.onload = () => {
			window.onload = null;
			initWithStyle({style});
		};
		if (document.readyState != 'loading') {
			window.onload();
		}
	});
}

function setStyleMeta(style) {
	document.getElementById("name").value = style.name;
	document.getElementById("enabled").checked = style.enabled;
	document.getElementById("url").href = style.url;
}

function initWithStyle({style, codeIsUpdated}) {
	setStyleMeta(style);

	if (codeIsUpdated === false) {
		setCleanGlobal();
		updateTitle();
		return;
	}

	// if this was done in response to an update, we need to clear existing sections
	getSections().forEach(function(div) { div.remove(); });
	var queue = style.sections.length ? style.sections.slice() : [{code: ""}];
	var queueStart = new Date().getTime();
	// after 100ms the sections will be added asynchronously
	while (new Date().getTime() - queueStart <= 100 && queue.length) {
		add();
	}
	(function processQueue() {
		if (queue.length) {
			add();
			setTimeout(processQueue, 0);
		}
	})();
	initHooks();

	function add() {
		var sectionDiv = addSection(null, queue.shift());
		maximizeCodeHeight(sectionDiv, !queue.length);
		const cm = sectionDiv.CodeMirror;
		setTimeout(() => {
			cm.setOption('lint', CodeMirror.defaults.lint);
			updateLintReport(cm, 0);
		}, prefs.get("editor.lintDelay"));
	}
}

function initHooks() {
	document.querySelectorAll("#header .style-contributor").forEach(function(node) {
		node.addEventListener("change", onChange);
		node.addEventListener("input", onChange);
	});
	document.getElementById("to-mozilla").addEventListener("click", showMozillaFormat, false);
	document.getElementById("to-mozilla-help").addEventListener("click", showToMozillaHelp, false);
	document.getElementById("from-mozilla").addEventListener("click", fromMozillaFormat);
	document.getElementById("beautify").addEventListener("click", beautify);
	document.getElementById("save-button").addEventListener("click", save, false);
	document.getElementById("sections-help").addEventListener("click", showSectionHelp, false);
	document.getElementById("keyMap-help").addEventListener("click", showKeyMapHelp, false);
	document.getElementById("cancel-button").addEventListener("click", goBackToManage);
	document.getElementById("lint-help").addEventListener("click", showLintHelp);
	document.getElementById("lint").addEventListener("click", gotoLintIssue);
	window.addEventListener("resize", resizeLintReport);

	// touch devices don't have onHover events so the element we'll be toggled via clicking (touching)
	if ("ontouchstart" in document.body) {
		document.querySelector("#lint h2").addEventListener("click", toggleLintReport);
	}

	document.querySelectorAll(
		'input:not([type]), input[type="text"], input[type="search"], input[type="number"]')
		.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));

	setupGlobalSearch();
	setCleanGlobal();
	updateTitle();
}


function toggleContextMenuDelete(event) {
	if (event.button == 2) {
		chrome.contextMenus.update('editDeleteText', {
			enabled: Boolean(
				this.selectionStart != this.selectionEnd ||
				this.somethingSelected && this.somethingSelected()
			),
		}, ignoreChromeError);
	}
}


function maximizeCodeHeight(sectionDiv, isLast) {
	var cm = sectionDiv.CodeMirror;
	var stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
	if (!stats.cmActualHeight) {
		stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
	}
	if (!stats.sectionMarginTop) {
		stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
	}
	var sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
	if (!stats.firstSectionTop) {
		stats.firstSectionTop = sectionTop;
	}
	var extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
	var cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
	var cmDesiredHeight = cm.display.sizer.clientHeight + 2*cm.defaultTextHeight();
	var cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
	stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
	stats.totalHeight += cmGrantableHeight + extrasHeight;
	if (!isLast) {
		return;
	}
	stats.totalHeight += stats.firstSectionTop;
	if (stats.totalHeight <= window.innerHeight) {
		editors.forEach(function(cm, index) {
			cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
		});
		return;
	}
	// scale heights to fill the gap between last section and bottom edge of the window
	var sections = document.getElementById("sections");
	var available = window.innerHeight - sections.getBoundingClientRect().bottom -
		parseFloat(getComputedStyle(sections).marginBottom);
	if (available <= 0) {
		return;
	}
	var totalDelta = stats.deltas.reduce(function(sum, d) { return sum + d; }, 0);
	var q = available / totalDelta;
	var baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
	stats.deltas.forEach(function(delta, index) {
		editors[index].setSize(null, baseHeight + Math.floor(q * delta));
	});
}

function updateTitle() {
	var DIRTY_TITLE = "* $";

	var name = document.getElementById("name").savedValue;
	var clean = isCleanGlobal();
	var title = styleId === null ? t("addStyleTitle") : t('editStyleTitle', [name]);
	document.title = clean ? title : DIRTY_TITLE.replace("$", title);
}

function validate() {
	var name = document.getElementById("name").value;
	if (name == "") {
		return t("styleMissingName");
	}
	// validate the regexps
	if (document.querySelectorAll(".applies-to-list").some(function(list) {
		return list.childNodes.some(function(li) {
			if (li.className == template.appliesToEverything.className) {
				return false;
			}
			var valueElement = li.querySelector("[name=applies-value]");
			var type = li.querySelector("[name=applies-type]").value;
			var value = valueElement.value;
			if (type && value) {
				if (type == "regexp") {
					try {
						new RegExp(value);
					} catch (ex) {
						valueElement.focus();
						return true;
					}
				}
			}
			return false;
		});
	})) {
		return t("styleBadRegexp");
	}
	return null;
}

function save() {
	updateLintReport(null, 0);

	// save the contents of the CodeMirror editors back into the textareas
	for (var i=0; i < editors.length; i++) {
		editors[i].save();
	}

	var error = validate();
	if (error) {
		alert(error);
		return;
	}
	var name = document.getElementById("name").value;
	var enabled = document.getElementById("enabled").checked;
	saveStyleSafe({
		id: styleId,
		name: name,
		enabled: enabled,
		reason: 'editSave',
		sections: getSectionsHashes()
	})
		.then(saveComplete);
}

function getSectionsHashes() {
	var sections = [];
	getSections().forEach(function(div) {
		var meta = getMeta(div);
		var code = div.CodeMirror.getValue();
		if (/^\s*$/.test(code) && Object.keys(meta).length == 0) {
			return;
		}
		meta.code = code;
		sections.push(meta);
	});
	return sections;
}

function getMeta(e) {
	var meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
	e.querySelector(".applies-to-list").childNodes.forEach(function(li) {
		if (li.className == template.appliesToEverything.className) {
			return;
		}
		var type = li.querySelector("[name=applies-type]").value;
		var value = li.querySelector("[name=applies-value]").value;
		if (type && value) {
			var property = CssToProperty[type];
			meta[property].push(value);
		}
	});
	return meta;
}

function saveComplete(style) {
	styleId = style.id;
	setCleanGlobal();

	// Go from new style URL to edit style URL
	if (location.href.indexOf("id=") == -1) {
		history.replaceState({}, document.title, "edit.html?id=" + style.id);
		tE("heading", "editStyleHeading", null, false);
	}
	updateTitle();
}

function showMozillaFormat() {
	var popup = showCodeMirrorPopup(t("styleToMozillaFormatTitle"), "", {readOnly: true});
	popup.codebox.setValue(toMozillaFormat());
	popup.codebox.execCommand("selectAll");
}

function toMozillaFormat() {
	return getSectionsHashes().map(function(section) {
		var cssMds = [];
		for (var i in propertyToCss) {
			if (section[i]) {
				cssMds = cssMds.concat(section[i].map(function (v){
					return propertyToCss[i] + "(\"" + v.replace(/\\/g, "\\\\") + "\")";
				}));
			}
		}
		return cssMds.length ? "@-moz-document " + cssMds.join(", ") + " {\n" + section.code + "\n}" : section.code;
	}).join("\n\n");
}

function fromMozillaFormat() {
	var popup = showCodeMirrorPopup(t("styleFromMozillaFormatPrompt"), tHTML("<div>\
		<button name='import-append' i18n-text='importAppendLabel' i18n-title='importAppendTooltip'></button>\
		<button name='import-replace' i18n-text='importReplaceLabel' i18n-title='importReplaceTooltip'></button>\
		</div>").innerHTML);

	var contents = popup.querySelector(".contents");
	contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
	popup.codebox.focus();

	popup.querySelector("[name='import-append']").addEventListener("click", doImport);
	popup.querySelector("[name='import-replace']").addEventListener("click", doImport);

	popup.codebox.on("change", function() {
		clearTimeout(popup.mozillaTimeout);
		popup.mozillaTimeout = setTimeout(function() {
			popup.classList.toggle("ready", trimNewLines(popup.codebox.getValue()));
		}, 100);
	});

	function doImport() {
		var replaceOldStyle = this.name == "import-replace";
		popup.querySelector(".dismiss").onclick();
		var mozStyle = trimNewLines(popup.codebox.getValue());
		var parser = new parserlib.css.Parser(), lines = mozStyle.split("\n");
		var sectionStack = [{code: "", start: {line: 1, col: 1}}];
		var errors = "", oldSectionCount = editors.length;
		var firstAddedCM;

		parser.addListener("startdocument", function(e) {
			var outerText = getRange(sectionStack.last.start, (--e.col, e));
			var gapComment = outerText.match(/(\/\*[\s\S]*?\*\/)[\s\n]*$/);
			var section = {code: "", start: backtrackTo(this, parserlib.css.Tokens.LBRACE, "end")};
			// move last comment before @-moz-document inside the section
			if (gapComment && !gapComment[1].match(/\/\*\s*AGENT_SHEET\s*\*\//)) {
				section.code = gapComment[1] + "\n";
				outerText = trimNewLines(outerText.substring(0, gapComment.index));
			}
			if (outerText.trim()) {
				sectionStack.last.code = outerText;
				doAddSection(sectionStack.last);
				sectionStack.last.code = "";
			}
			e.functions.forEach(function(f) {
				var m = f.match(/^(url|url-prefix|domain|regexp)\((['"]?)(.+?)\2?\)$/);
				var aType = CssToProperty[m[1]];
				var aValue = aType != "regexps" ? m[3] : m[3].replace(/\\\\/g, "\\");
				(section[aType] = section[aType] || []).push(aValue);
			});
			sectionStack.push(section);
		});

		parser.addListener("enddocument", function(e) {
			var end = backtrackTo(this, parserlib.css.Tokens.RBRACE, "start");
			var section = sectionStack.pop();
			section.code += getRange(section.start, end);
			sectionStack.last.start = (++end.col, end);
			doAddSection(section);
		});

		parser.addListener("endstylesheet", function() {
			// add nonclosed outer sections (either broken or the last global one)
			var endOfText = {line: lines.length, col: lines.last.length + 1};
			sectionStack.last.code += getRange(sectionStack.last.start, endOfText);
			sectionStack.forEach(doAddSection);

			delete maximizeCodeHeight.stats;
			editors.forEach(function(cm) {
				maximizeCodeHeight(cm.getSection(), cm == editors.last);
			});

			makeSectionVisible(firstAddedCM);
			firstAddedCM.focus();

			if (errors) {
				showHelp(t("issues"), errors);
			}
		});

		parser.addListener("error", function(e) {
			errors += e.line + ":" + e.col + " " + e.message.replace(/ at line \d.+$/, "") + "<br>";
		});

		parser.parse(mozStyle);

		function getRange( start, end) {
			const L1 = start.line - 1, C1 = start.col - 1;
			const L2 = end.line - 1, C2 = end.col - 1;
			if (L1 == L2) {
				return lines[L1].substr(C1, C2 - C1 + 1);
			} else {
				const middle = lines.slice(L1 + 1, L2).join('\n');
				return lines[L1].substr(C1) + '\n' + middle +
					(L2 >= lines.length ? '' : ((middle ? '\n' : '') + lines[L2].substring(0, C2)));
			}
		}
		function doAddSection(section) {
			section.code = section.code.trim();
			// don't add empty sections
			if (!section.code
			&& !section.urls
			&& !section.urlPrefixes
			&& !section.domains
			&& !section.regexps) {
				return;
			}
			if (!firstAddedCM) {
				if (!initFirstSection(section)) {
					return;
				}
			}
			setCleanItem(addSection(null, section), false);
			firstAddedCM = firstAddedCM || editors.last;
		}
		// do onetime housekeeping as the imported text is confirmed to be a valid style
		function initFirstSection(section) {
			// skip adding the first global section when there's no code/comments
			if (!section.code.replace("@namespace url(http://www.w3.org/1999/xhtml);", "") /* ignore boilerplate NS */
					.replace(/[\s\n]/g, "")) { /* ignore all whitespace including new lines */
				return false;
			}
			if (replaceOldStyle) {
				editors.slice(0).reverse().forEach(function(cm) {
					removeSection({target: cm.getSection().firstElementChild});
				});
			} else if (!editors.last.getValue()) {
				// nuke the last blank section
				if (editors.last.getSection().querySelector(".applies-to-everything")) {
					removeSection({target: editors.last.getSection()});
				}
			}
			return true;
		}
	}
	function backtrackTo(parser, tokenType, startEnd) {
		var tokens = parser._tokenStream._lt;
		for (var i = tokens.length - 2; i >= 0; --i) {
			if (tokens[i].type == tokenType) {
				return {line: tokens[i][startEnd+"Line"], col: tokens[i][startEnd+"Col"]};
			}
		}
	}
	function trimNewLines(s) {
		return s.replace(/^[\s\n]+/, "").replace(/[\s\n]+$/, "");
	}
}

function showSectionHelp() {
	showHelp(t("styleSectionsTitle"), t("sectionHelp"));
}

function showAppliesToHelp() {
	showHelp(t("appliesLabel"), t("appliesHelp"));
}

function showToMozillaHelp() {
	showHelp(t("styleMozillaFormatHeading"), t("styleToMozillaFormatHelp"));
}

function showKeyMapHelp() {
	var keyMap = mergeKeyMaps({}, prefs.get("editor.keyMap"), CodeMirror.defaults.extraKeys);
	var keyMapSorted = Object.keys(keyMap)
		.map(function(key) { return {key: key, cmd: keyMap[key]} })
		.concat([{key: "Shift-Ctrl-Wheel", cmd: "scrollWindow"}])
		.sort(function(a, b) { return a.cmd < b.cmd || (a.cmd == b.cmd && a.key < b.key) ? -1 : 1 });
	showHelp(t("cm_keyMap") + ": " + prefs.get("editor.keyMap"),
		'<table class="keymap-list">' +
			'<thead><tr><th><input placeholder="' + t("helpKeyMapHotkey") + '" type="search"></th>' +
				'<th><input placeholder="' + t("helpKeyMapCommand") + '" type="search"></th></tr></thead>' +
			"<tbody>" + keyMapSorted.map(function(value) {
				return "<tr><td>" + value.key + "</td><td>" + value.cmd + "</td></tr>";
			}).join("") +
			"</tbody>" +
		"</table>");

	var table = document.querySelector("#help-popup table");
	table.addEventListener("input", filterTable);

	var inputs = table.querySelectorAll("input");
	inputs[0].addEventListener("keydown", hotkeyHandler);
	inputs[1].focus();

	function hotkeyHandler(event) {
		var keyName = CodeMirror.keyName(event);
		if (keyName == "Esc" || keyName == "Tab" || keyName == "Shift-Tab") {
			return;
		}
		event.preventDefault();
		event.stopPropagation();
		// normalize order of modifiers,
		// for modifier-only keys ("Ctrl-Shift") a dummy main key has to be temporarily added
		var keyMap = {};
		keyMap[keyName.replace(/(Shift|Ctrl|Alt|Cmd)$/, "$&-dummy")] = "";
		var normalizedKey = Object.keys(CodeMirror.normalizeKeyMap(keyMap))[0];
		this.value = normalizedKey.replace("-dummy", "");
		filterTable(event);
	}
	function filterTable(event) {
		var input = event.target;
		var query = stringAsRegExp(input.value, "gi");
		var col = input.parentNode.cellIndex;
		inputs[1 - col].value = "";
		table.tBodies[0].childNodes.forEach(function(row) {
			var cell = row.children[col];
			cell.innerHTML = cell.textContent.replace(query, "<mark>$&</mark>");
			row.style.display = query.test(cell.textContent) ? "" : "none";
			// clear highlight from the other column
			cell = row.children[1 - col];
			cell.innerHTML = cell.textContent;
		});
	}
	function mergeKeyMaps(merged, ...more) {
		more.forEach(keyMap => {
			if (typeof keyMap == "string") {
				keyMap = CodeMirror.keyMap[keyMap];
			}
			Object.keys(keyMap).forEach(function(key) {
				var cmd = keyMap[key];
				// filter out '...', 'attach', etc. (hotkeys start with an uppercase letter)
				if (!merged[key] && !key.match(/^[a-z]/) && cmd != "...") {
					if (typeof cmd == "function") {
						// for 'emacs' keymap: provide at least something meaningful (hotkeys and the function body)
						// for 'vim*' keymaps: almost nothing as it doesn't rely on CM keymap mechanism
						cmd = cmd.toString().replace(/^function.*?\{[\s\r\n]*([\s\S]+?)[\s\r\n]*\}$/, "$1");
						merged[key] = cmd.length <= 200 ? cmd : cmd.substr(0, 200) + "...";
					} else {
						merged[key] = cmd;
					}
				}
			});
			if (keyMap.fallthrough) {
				merged = mergeKeyMaps(merged, keyMap.fallthrough);
			}
		});
		return merged;
	}
}

function showLintHelp() {
	showHelp(t("issues"), t("issuesHelp") + "<ul>" +
		CSSLint.getRules().map(function(rule) {
			return "<li><b>" + rule.name + "</b><br>" + rule.desc + "</li>";
		}).join("") + "</ul>"
	);
}

function showRegExpTester(event, section = getSectionForChild(this)) {
	const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
	const OWN_ICON = chrome.runtime.getManifest().icons['16'];
	const cachedRegexps = showRegExpTester.cachedRegexps =
		showRegExpTester.cachedRegexps || new Map();
	const regexps = [...section.querySelector('.applies-to-list').children]
		.map(item =>
			!item.matches('.applies-to-everything') &&
			item.querySelector('.applies-type').value == 'regexp' &&
			item.querySelector('.applies-value').value.trim())
		.filter(item => item)
		.map(text => {
			const rxData = Object.assign({text}, cachedRegexps.get(text));
			if (!rxData.urls) {
				cachedRegexps.set(text, Object.assign(rxData, {
					rx: tryRegExp(text),
					urls: new Map(),
				}));
			}
			return rxData;
		});
	chrome.tabs.onUpdated.addListener(function _(tabId, info) {
		if (document.querySelector('.regexp-report')) {
			if (info.url) {
				showRegExpTester(event, section);
			}
		} else {
			chrome.tabs.onUpdated.removeListener(_);
		}
	});
	chrome.tabs.query({}, tabs => {
		const supported = tabs.map(tab => tab.url)
			.filter(url => URLS.supported.test(url));
		const unique = [...new Set(supported).values()];
		for (const rxData of regexps) {
			const {rx, urls} = rxData;
			if (rx) {
				const urlsNow = new Map();
				for (const url of unique) {
					const match = urls.get(url) || (url.match(rx) || [])[0];
					if (match) {
						urlsNow.set(url, match);
					}
				}
				rxData.urls = urlsNow;
			}
		}
		const moreInfoLink = template.regexpTestPartial.outerHTML;
		const stats = {
			full: {data: [], label: t('styleRegexpTestFull')},
			partial: {data: [], label: t('styleRegexpTestPartial') + moreInfoLink},
			none: {data: [], label: t('styleRegexpTestNone')},
			invalid: {data: [], label: t('styleRegexpTestInvalid')},
		};
		for (const {text, rx, urls} of regexps) {
			if (!rx) {
				stats.invalid.data.push({text});
				continue;
			}
			if (!urls.size) {
				stats.none.data.push({text});
				continue;
			}
			const full = [];
			const partial = [];
			for (const [url, match] of urls.entries()) {
				const faviconUrl = url.startsWith(URLS.ownOrigin)
					? OWN_ICON
					: GET_FAVICON_URL + new URL(url).hostname;
				const icon = `<img src="${faviconUrl}">`;
				if (match.length == url.length) {
					full.push(`<div>${icon + url}</div>`);
				} else {
					partial.push(`<div>${icon}<mark>${match}</mark>` +
						url.substr(match.length) + '</div>');
				}
			}
			if (full.length) {
				stats.full.data.push({text, urls: full});
			}
			if (partial.length) {
				stats.partial.data.push({text, urls: partial});
			}
		}
		showHelp(t('styleRegexpTestTitle'),
			'<div class="regexp-report">' +
			Object.keys(stats).map(type => (!stats[type].data.length ? '' :
				`<details open data-type="${type}">
					<summary>${stats[type].label}</summary>` +
					stats[type].data.map(({text, urls}) => (!urls ? text :
						`<details open><summary>${text}</summary>${urls.join('')}</details>`
					)).join('<br>') +
				'</details>'
			)).join('') +
			'</div>');
		document.querySelector('.regexp-report').onclick = event => {
			const target = event.target.closest('a, .regexp-report div');
			if (target) {
				openURL({url: target.href || target.textContent});
				event.preventDefault();
			}
		};
	});
}

function showHelp(title, text) {
	var div = document.getElementById("help-popup");
	div.classList.remove("big");
	div.querySelector(".contents").innerHTML = text;
	div.querySelector(".title").innerHTML = title;

	if (getComputedStyle(div).display == "none") {
		document.addEventListener("keydown", closeHelp);
		div.querySelector(".dismiss").onclick = closeHelp; // avoid chaining on multiple showHelp() calls
	}

	div.style.display = "block";
	return div;

	function closeHelp(e) {
		if (!e || e.type == "click" || (e.keyCode == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) {
			div.style.display = "";
			document.querySelector(".contents").innerHTML = "";
			document.removeEventListener("keydown", closeHelp);
		}
	}
}

function showCodeMirrorPopup(title, html, options) {
	var popup = showHelp(title, html);
	popup.classList.add("big");

	popup.codebox = CodeMirror(popup.querySelector(".contents"), Object.assign({
		mode: "css",
		lineNumbers: true,
		lineWrapping: true,
		foldGutter: true,
		gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
		matchBrackets: true,
		lint: {getAnnotations: CodeMirror.lint.css, delay: 0},
		styleActiveLine: true,
		theme: prefs.get("editor.theme"),
		keyMap: prefs.get("editor.keyMap")
	}, options));
	popup.codebox.focus();
	popup.codebox.on("focus", function() { hotkeyRerouter.setState(false) });
	popup.codebox.on("blur", function() { hotkeyRerouter.setState(true) });
	return popup;
}

function getParams() {
	var params = {};
	var urlParts = location.href.split("?", 2);
	if (urlParts.length == 1) {
		return params;
	}
	urlParts[1].split("&").forEach(function(keyValue) {
		var splitKeyValue = keyValue.split("=", 2);
		params[decodeURIComponent(splitKeyValue[0])] = decodeURIComponent(splitKeyValue[1]);
	});
	return params;
}

chrome.runtime.onMessage.addListener(onRuntimeMessage);

function onRuntimeMessage(request) {
	switch (request.method) {
		case "styleUpdated":
			if (styleId && styleId == request.style.id && request.reason != 'editSave') {
				if ((request.style.sections[0] || {}).code === null) {
					// the code-less style came from notifyAllTabs
					onBackgroundReady().then(() => {
						request.style = BG.cachedStyles.byId.get(request.style.id);
						initWithStyle(request);
					});
				} else {
					initWithStyle(request);
				}
			}
			break;
		case "styleDeleted":
			if (styleId && styleId == request.id) {
				window.onbeforeunload = function() {};
				window.close();
				break;
			}
			break;
		case "prefChanged":
			if ('editor.smartIndent' in request.prefs) {
				CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
			}
			break;
		case 'editDeleteText':
			document.execCommand('delete');
			break;
	}
}

function getComputedHeight(el) {
	var compStyle = getComputedStyle(el);
	return el.getBoundingClientRect().height +
		parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
}