/**
 * Manage downloads
 */

(function() {

var cache = {};
var form = $('form');
var minified = true;

var dependencies = {};

var treeURL = 'https://api.github.com/repos/PrismJS/prism/git/trees/master?recursive=1';
var treePromise = new Promise(function(resolve) {
	$u.xhr({
		url: treeURL,
		callback: function(xhr) {
			if (xhr.status < 400) {
				resolve(JSON.parse(xhr.responseText).tree);
			}
		}
	});
});

/**
 * Converts the given value into an array.
 *
 * @param {T | T[] | null | undefined} value
 * @returns {T[]}
 * @template T
 */
function toArray(value) {
	if (Array.isArray(value)) {
		return value;
	} else {
		return value == null ? [] : [value];
	}
}

var hstr = window.location.hash.match(/(?:languages|plugins)=[-+\w]+|themes=[-\w]+/g);
if (hstr) {
	hstr.forEach(function(str) {
		var kv = str.split('=', 2),
		    category = kv[0],
		    ids = kv[1].split('+');
		if (category !== 'meta' && category !== 'core' && components[category]) {
			for (var id in components[category]) {
				if (components[category][id].option) {
					delete components[category][id].option;
				}
			}
			if (category === 'themes' && ids.length) {
				var themeInput = $('#theme input[value="' + ids[0] + '"]');
				if (themeInput) {
					themeInput.checked = true;
				}
				setTheme(ids[0]);
			}
			var makeDefault = function (id) {
				if (id !== 'meta') {
					if (components[category][id]) {
						if (components[category][id].option !== 'default') {
							if (typeof components[category][id] === 'string') {
								components[category][id] = { title: components[category][id] }
							}
							components[category][id].option = 'default';
						}

						toArray(components[category][id].require).forEach(makeDefault);
					}
				}
			};
			ids.forEach(makeDefault);
		}
	});
}

// Stay compatible with old querystring feature
var qstr = window.location.search.match(/(?:languages|plugins)=[-+\w]+|themes=[-\w]+/g);
if (qstr && !hstr) {
	window.location.hash = window.location.search.replace(/^\?/, '');
	window.location.search = '';
}

var storedTheme = localStorage.getItem('theme');

for (var category in components) {
	var all = components[category];

	all.meta.section = $u.element.create('section', {
		className: 'options',
		id: 'category-' + category,
		contents: {
			tag: 'h1',
			contents: category.charAt(0).toUpperCase() + category.slice(1)
		},
		inside: '#components'
	});

	if (all.meta.addCheckAll) {
		$u.element.create('label', {
			attributes: {
				'data-id': 'check-all-' + category
			},
			contents: [
				{
					tag: 'input',
					properties: {
						type: 'checkbox',
						name: 'check-all-' + category,
						value: '',
						checked: false,
						onclick: (function(category, all){
							return function () {
								var checkAll = this;
								$$('input[name="download-' + category + '"]').forEach(function(input) {
									all[input.value].enabled = input.checked = checkAll.checked;
								});

								update(category);
							};
						})(category, all)
					}
				},
				'Select/unselect all'
			],
			inside: all.meta.section
		});
	}

	for (var id in all) {
		if(id === 'meta') {
			continue;
		}

		var checked = false, disabled = false;
		var option = all[id].option || all.meta.option;

		switch (option) {
			case 'mandatory': disabled = true; // fallthrough
			case 'default': checked = true;
		}
		if (category === 'themes' && storedTheme) {
			checked = id === storedTheme;
		}

		var filepath = all.meta.path.replace(/\{id}/g, id);

		var info = all[id] = {
			title: all[id].title || all[id],
			aliasTitles: all[id].aliasTitles,
			noCSS: all[id].noCSS || all.meta.noCSS,
			noJS: all[id].noJS || all.meta.noJS,
			enabled: checked,
			require: toArray(all[id].require),
			after: toArray(all[id].after),
			modify: toArray(all[id].modify),
			owner: all[id].owner,
			files: {
				minified: {
					paths: [],
					size: 0
				},
				dev: {
					paths: [],
					size: 0
				}
			}
		};

		info.require.forEach(function (v) {
			dependencies[v] = (dependencies[v] || []).concat(id);
		});

		if (!all[id].noJS && !/\.css$/.test(filepath)) {
			info.files.minified.paths.push(filepath.replace(/(\.js)?$/, '.min.js'));
			info.files.dev.paths.push(filepath.replace(/(\.js)?$/, '.js'));
		}


		if ((!all[id].noCSS && !/\.js$/.test(filepath)) || /\.css$/.test(filepath)) {
			var cssFile = filepath.replace(/(\.css)?$/, '.css');

			info.files.minified.paths.push(cssFile);
			info.files.dev.paths.push(cssFile);
		}

		function getLanguageTitle(lang) {
			if (!lang.aliasTitles)
				return lang.title;

			var titles = [lang.title];
			for (var alias in lang.aliasTitles)
				if (lang.aliasTitles.hasOwnProperty(alias))
					titles.push(lang.aliasTitles[alias]);
			return titles.join(" + ");
		}

		var label = $u.element.create('label', {
			attributes: {
				'data-id': id
			},
			contents: [
				{
					tag: 'input',
					properties: {
						type: all.meta.exclusive? 'radio' : 'checkbox',
						name: 'download-' + category,
						value: id,
						checked: checked,
						disabled: disabled,
						onclick: (function(id, category, all){
							return function () {
								$$('input[name="' + this.name + '"]').forEach(function(input) {
									all[input.value].enabled = input.checked;
								});

								if (all[id].require && this.checked) {
									all[id].require.forEach(function(v) {
										var input = $('label[data-id="' + v + '"] > input');
										input.checked = true;

										input.onclick();
									});
								}

								if (dependencies[id] && !this.checked) { // It’s required by others
									dependencies[id].forEach(function(dependent) {
										var input = $('label[data-id="' + dependent + '"] > input');
										input.checked = false;

										input.onclick();
									});
								}

								update(category, id);
							};
						})(id, category, all)
					}
				},
				all.meta.link? {
					tag: 'a',
					properties: {
						href: all.meta.link.replace(/\{id}/g, id),
						className: 'name'
					},
					contents: info.title
				} : {
					tag: 'span',
					properties: {
						className: 'name'
					},
					contents: getLanguageTitle(info)
				},
				' ',
				all[id].owner? {
					tag: 'a',
					properties: {
						href: 'https://github.com/' + all[id].owner,
						className: 'owner',
						target: '_blank'
					},
					contents: all[id].owner
				} : ' ',
				{
					tag: 'strong',
					className: 'filesize'
				}
			],
			inside: all.meta.section
		});

		// Add click events on main theme selector too.
		(function (label) {
			if (category === 'themes') {
				var themeInput = $('#theme input[value="' + id + '"]');
				var input = $('input', label);
				if (themeInput) {
					var themeInputOnclick = themeInput.onclick;
					themeInput.onclick = function () {
						input.checked = true;
						input.onclick();
						themeInputOnclick && themeInputOnclick.call(themeInput);
					};
				}
			}
		}(label));
	}
}

form.elements.compression[0].onclick =
form.elements.compression[1].onclick = function() {
	minified = !!+this.value;

	getFilesSizes();
};

function getFileSize(filepath) {
	return treePromise.then(function(tree) {
		for(var i=0, l=tree.length; i<l; i++) {
			if(tree[i].path === filepath) {
				return tree[i].size;
			}
		}
	});
}

function getFilesSizes() {
	for (var category in components) {
		var all = components[category];

		for (var id in all) {
			if(id === 'meta') {
				continue;
			}

			var distro = all[id].files[minified? 'minified' : 'dev'],
			    files = distro.paths;

			files.forEach(function (filepath) {
				var file = cache[filepath] = cache[filepath] || {};

				if(!file.size) {

					(function(category, id) {
					getFileSize(filepath).then(function(size) {
						if(size) {
							file.size = size;
							distro.size += file.size;

							update(category, id);
						}
					});
					}(category, id));
				}
				else {
					update(category, id);
				}
			});
		}
	}
}

getFilesSizes();

function getFileContents(filepath) {
	return new Promise(function(resolve, reject) {
		$u.xhr({
			url: filepath,
			callback: function(xhr) {
				if (xhr.status < 400 && xhr.responseText) {
					resolve(xhr.responseText);
				} else {
					reject();
				}
			}
		});
	});
}

function prettySize(size) {
	return Math.round(100 * size / 1024)/100 + 'KB';
}

function update(updatedCategory, updatedId){
	// Update total size
	var total = {js: 0, css: 0}, updated = {js: 0, css: 0};

	for (var category in components) {
		var all = components[category];
		var allChecked = true;

		for (var id in all) {
			var info = all[id];

			if (info.enabled || id == updatedId) {
				var distro = info.files[minified? 'minified' : 'dev'];

				distro.paths.forEach(function(path) {
					if (cache[path]) {
						var file = cache[path];

						var type = path.match(/\.(\w+)$/)[1],
						    size = file.size || 0;

						if (info.enabled) {

							if (!file.contentsPromise) {
								file.contentsPromise = getFileContents(path);
							}

							total[type] += size;
						}

						if (id == updatedId) {
							updated[type] += size;
						}
					}
				});
			}
			if (id !== 'meta' && !info.enabled) {
				allChecked = false;
			}

			// Select main theme
			if (category === 'themes' && id === updatedId && info.enabled) {
				var themeInput = $('#theme input[value="' + updatedId + '"]');
				if (themeInput) {
					themeInput.checked = true;
				}
				setTheme(updatedId);
			}
		}

		if (all.meta.addCheckAll) {
			$('input[name="check-all-' + category + '"]').checked = allChecked;
		}
	}

	total.all = total.js + total.css;

	if (updatedId) {
		updated.all = updated.js + updated.css;

		$u.element.prop($('label[data-id="' + updatedId + '"] .filesize'), {
			textContent: prettySize(updated.all),
			title: (updated.js ? Math.round(100 * updated.js / updated.all) + '% JavaScript' : '') +
				(updated.js && updated.css ? ' + ' : '') +
				(updated.css ? Math.round(100 * updated.css / updated.all) + '% CSS' : '')
		});
	}

	$('#filesize').textContent = prettySize(total.all);

	$u.element.prop($('#percent-js'), {
		textContent: Math.round(100 * total.js / total.all) + '%',
		title: prettySize(total.js)
	});

	$u.element.prop($('#percent-css'), {
		textContent: Math.round(100 * total.css / total.all) + '%',
		title: prettySize(total.css)
	});

	delayedGenerateCode();
}

var timerId = 0;
// "debounce" multiple rapid requests to generate and highlight code
function delayedGenerateCode(){
	if ( timerId !== 0 ) {
		clearTimeout(timerId);
	}
	timerId = setTimeout(generateCode, 500);
}

function generateCode(){
	/** @type {CodePromiseInfo[]} */
	var promises = [];
	var redownload = {};

	for (var category in components) {
		for (var id in components[category]) {
			if (id === 'meta') {
				continue;
			}

			var info = components[category][id];
			if (info.enabled) {
				if (category !== 'core') {
					redownload[category] = redownload[category] || [];
					redownload[category].push(id);
				}
				info.files[minified ? 'minified' : 'dev'].paths.forEach(function (path) {
					if (cache[path]) {
						var type = path.match(/\.(\w+)$/)[1];

						promises.push({
							contentsPromise: cache[path].contentsPromise,
							id: id,
							category: category,
							path: path,
							type: type
						});
					}
				});
			}
		}
	}

	// Hide error message if visible
	var error = $('#download .error');
	error.style.display = '';

	Promise.all([buildCode(promises), getVersion()]).then(function(arr) {
		var res = arr[0];
		var version = arr[1];
		var code = res.code;
		var errors = res.errors;

		if (errors.length) {
			error.style.display = 'block';
			error.innerHTML = '';
			$u.element.contents(error, errors);
		}

		var redownloadUrl = window.location.href.split("#")[0] + "#";
		for (var category in redownload) {
			redownloadUrl += category + "=" + redownload[category].join('+') + "&";
		}
		redownloadUrl = redownloadUrl.replace(/&$/,"");
		window.location.replace(redownloadUrl);

		var versionComment = "/* PrismJS " + version + "\n" + redownloadUrl + " */";

		for (var type in code) {
			(function (type) {
				var text = versionComment + "\n" + code[type];
				var fileName = 'prism.' + type;

				var codeElement = $('#download-' + type + ' code');
				var pre = codeElement.parentElement;

				var newCode = document.createElement('CODE');
				newCode.className = codeElement.className;
				newCode.textContent = text;

				Prism.highlightElement(newCode, true, function () {
					pre.replaceChild(newCode, codeElement);
				});


				$('#download-' + type + ' .download-button').onclick = function () {
					saveAs(new Blob([text], { type: "application/octet-stream;charset=utf-8" }), fileName);
				};
			})(type);
		}
	});
}

/**
 * Returns a promise of the code of the Prism bundle.
 *
 * @param {CodePromiseInfo[]} promises
 * @returns {Promise<{ code: { js: string, css: string }, errors: HTMLElement[] }>}
 *
 * @typedef CodePromiseInfo
 * @property {Promise} contentsPromise
 * @property {string} id
 * @property {string} category
 * @property {string} path
 * @property {string} type
 */
function buildCode(promises) {
	// sort the promises

	/** @type {CodePromiseInfo[]} */
	var finalPromises = [];
	/** @type {Object<string, CodePromiseInfo[]>} */
	var toSortMap = {};

	promises.forEach(function (p) {
		if (p.category == "core" || p.category == "themes") {
			finalPromises.push(p);
		} else {
			var infos = toSortMap[p.id];
			if (!infos) {
				toSortMap[p.id] = infos = [];
			}
			infos.push(p);
		}
	});

	// this assumes that the ids in `toSortMap` are complete under transitive requirements
	getLoader(components, Object.keys(toSortMap)).getIds().forEach(function (id) {
		if (!toSortMap[id]) {
			console.error(id + " not found.");
		}
		finalPromises.push.apply(finalPromises, toSortMap[id]);
	});
	promises = finalPromises;

	// build
	var i = 0,
	    l = promises.length;
	var code = {js: '', css: ''};
	var errors = [];

	var f = function(resolve) {
		if(i < l) {
			var p = promises[i];
			p.contentsPromise.then(function(contents) {
				code[p.type] += contents + (p.type === 'js' && !/;\s*$/.test(contents) ? ';' : '') + '\n';
				i++;
				f(resolve);
			});
			p.contentsPromise['catch'](function() {
				errors.push($u.element.create({
					tag: 'p',
					prop: {
						textContent: 'An error occurred while fetching the file "' + p.path + '".'
					}
				}));
				i++;
				f(resolve);
			});
		} else {
			resolve({code: code, errors: errors});
		}
	};

	return new Promise(f);
}

/**
 * @returns {Promise<string>}
 */
function getVersion() {
	return getFileContents('./package.json').then(function (jsonStr) {
		return JSON.parse(jsonStr).version;
	});
}

})();
