var packages = {
	available: { providers: {}, pkgs: {} },
	installed: { providers: {}, pkgs: {} }
};

var currentDisplayMode = 'available', currentDisplayRows = [];

function parseList(s, dest)
{
	var re = /([^\n]*)\n/g,
	    pkg = null, key = null, val = null, m;

	while ((m = re.exec(s)) !== null) {
		if (m[1].match(/^\s(.*)$/)) {
			if (pkg !== null && key !== null && val !== null)
				val += '\n' + RegExp.$1.trim();

			continue;
		}

		if (key !== null && val !== null) {
			switch (key) {
			case 'package':
				pkg = { name: val };
				break;

			case 'depends':
			case 'provides':
				var list = val.split(/\s*,\s*/);
				if (list.length !== 1 || list[0].length > 0)
					pkg[key] = list;
				break;

			case 'installed-time':
				pkg.installtime = new Date(+val * 1000);
				break;

			case 'installed-size':
				pkg.installsize = +val;
				break;

			case 'status':
				var stat = val.split(/\s+/),
				    mode = stat[1],
				    installed = stat[2];

				switch (mode) {
				case 'user':
				case 'hold':
					pkg[mode] = true;
					break;
				}

				switch (installed) {
				case 'installed':
					pkg.installed = true;
					break;
				}
				break;

			case 'essential':
				if (val === 'yes')
					pkg.essential = true;
				break;

			case 'size':
				pkg.size = +val;
				break;

			case 'architecture':
			case 'auto-installed':
			case 'filename':
			case 'sha256sum':
			case 'section':
				break;

			default:
				pkg[key] = val;
				break;
			}

			key = val = null;
		}

		if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
			key = RegExp.$1.toLowerCase();
			val = RegExp.$2.trim();
		}
		else {
			dest.pkgs[pkg.name] = pkg;

			var provides = dest.providers[pkg.name] ? [] : [ pkg.name ];

			if (pkg.provides)
				provides.push.apply(provides, pkg.provides);

			provides.forEach(function(p) {
				dest.providers[p] = dest.providers[p] || [];
				dest.providers[p].push(pkg);
			});
		}
	}
}

function display(pattern)
{
	var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode],
	    table = document.querySelector('#packages'),
	    pager = document.querySelector('#pager');

	currentDisplayRows.length = 0;

	if (typeof(pattern) === 'string' && pattern.length > 0)
		pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');

	for (var name in src.pkgs) {
		var pkg = src.pkgs[name],
		    desc = pkg.description || '',
		    altsize = null;

		if (!pkg.size && packages.available.pkgs[name])
			altsize = packages.available.pkgs[name].size;

		if (!desc && packages.available.pkgs[name])
			desc = packages.available.pkgs[name].description || '';

		desc = desc.split(/\n/);
		desc = desc[0].trim() + (desc.length > 1 ? '…' : '');

		if ((pattern instanceof RegExp) &&
		    !name.match(pattern) && !desc.match(pattern))
			continue;

		var btn, ver;

		if (currentDisplayMode === 'updates') {
			var avail = packages.available.pkgs[name];
			if (!avail || avail.version === pkg.version)
				continue;

			ver = '%s » %s'.format(
				truncateVersion(pkg.version || '-'),
				truncateVersion(avail.version || '-'));

			btn = E('div', {
				'class': 'btn cbi-button-positive',
				'data-package': name,
				'click': handleInstall
			}, _('Upgrade…'));
		}
		else if (currentDisplayMode === 'installed') {
			ver = truncateVersion(pkg.version || '-');
			btn = E('div', {
				'class': 'btn cbi-button-negative',
				'data-package': name,
				'click': handleRemove
			}, _('Remove'));
		}
		else {
			ver = truncateVersion(pkg.version || '-');

			if (!packages.installed.pkgs[name])
				btn = E('div', {
					'class': 'btn cbi-button-action',
					'data-package': name,
					'click': handleInstall
				}, _('Install…'));
			else if (packages.installed.pkgs[name].version != pkg.version)
				btn = E('div', {
					'class': 'btn cbi-button-positive',
					'data-package': name,
					'click': handleInstall
				}, _('Upgrade…'));
			else
				btn = E('div', {
					'class': 'btn cbi-button-neutral',
					'disabled': 'disabled'
				}, _('Installed'));
		}

		name = '%h'.format(name);
		desc = '%h'.format(desc || '-');

		if (pattern) {
			name = name.replace(pattern, '<ins>$&</ins>');
			desc = desc.replace(pattern, '<ins>$&</ins>');
		}

		currentDisplayRows.push([
			name,
			ver,
			pkg.size ? '%.1024mB'.format(pkg.size)
			         : (altsize ? '~%.1024mB'.format(altsize) : '-'),
			desc,
			btn
		]);
	}

	currentDisplayRows.sort(function(a, b) {
		if (a[0] < b[0])
			return -1;
		else if (a[0] > b[0])
			return 1;
		else
			return 0;
	});

	pager.parentNode.style.display = '';
	pager.setAttribute('data-offset', 100);
	handlePage({ target: pager.querySelector('.prev') });
}

function handlePage(ev)
{
	var filter = document.querySelector('input[name="filter"]'),
	    pager = ev.target.parentNode,
	    offset = +pager.getAttribute('data-offset'),
	    next = ev.target.classList.contains('next');

	if ((next && (offset + 100) >= currentDisplayRows.length) ||
	    (!next && (offset < 100)))
	    return;

	offset += next ? 100 : -100;
	pager.setAttribute('data-offset', offset);
	pager.querySelector('.text').firstChild.data = currentDisplayRows.length
		? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
		: _('No packages');

	if (offset < 100)
		pager.querySelector('.prev').setAttribute('disabled', 'disabled');
	else
		pager.querySelector('.prev').removeAttribute('disabled');

	if ((offset + 100) >= currentDisplayRows.length)
		pager.querySelector('.next').setAttribute('disabled', 'disabled');
	else
		pager.querySelector('.next').removeAttribute('disabled');

	var placeholder = _('No information available');

	if (filter.value)
		placeholder = [
			E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
			E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
		];

	cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
		placeholder);
}

function handleMode(ev)
{
	var tab = findParent(ev.target, 'li');
	if (tab.getAttribute('data-mode') === currentDisplayMode)
		return;

	tab.parentNode.querySelectorAll('li').forEach(function(li) {
		li.classList.remove('cbi-tab');
		li.classList.add('cbi-tab-disabled');
	});

	tab.classList.remove('cbi-tab-disabled');
	tab.classList.add('cbi-tab');

	currentDisplayMode = tab.getAttribute('data-mode');

	display(document.querySelector('input[name="filter"]').value);

	ev.target.blur();
	ev.preventDefault();
}

function orderOf(c)
{
	if (c === '~')
		return -1;
	else if (c === '' || c >= '0' && c <= '9')
		return 0;
	else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
		return c.charCodeAt(0);
	else
		return c.charCodeAt(0) + 256;
}

function compareVersion(val, ref)
{
	var vi = 0, ri = 0,
	    isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };

	val = val || '';
	ref = ref || '';

	while (vi < val.length || ri < ref.length) {
		var first_diff = 0;

		while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
		       (ri < ref.length && !isdigit[ref.charAt(ri)])) {
			var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
			if (vc !== rc)
				return vc - rc;

			vi++; ri++;
		}

		while (val.charAt(vi) === '0')
			vi++;

		while (ref.charAt(ri) === '0')
			ri++;

		while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
			first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
			vi++; ri++;
		}

		if (isdigit[val.charAt(vi)])
			return 1;
		else if (isdigit[ref.charAt(ri)])
			return -1;
		else if (first_diff)
			return first_diff;
	}

	return 0;
}

function versionSatisfied(ver, ref, vop)
{
	var r = compareVersion(ver, ref);

	switch (vop) {
	case '<':
	case '<=':
		return r <= 0;

	case '>':
	case '>=':
		return r >= 0;

	case '<<':
		return r < 0;

	case '>>':
		return r > 0;

	case '=':
		return r == 0;
	}

	return false;
}

function pkgStatus(pkg, vop, ver, info)
{
	info.errors = info.errors || [];
	info.install = info.install || [];

	if (pkg.installed) {
		if (vop && !versionSatisfied(pkg.version, ver, vop)) {
			var repl = null;

			(packages.available.providers[pkg.name] || []).forEach(function(p) {
				if (!repl && versionSatisfied(p.version, ver, vop))
					repl = p;
			});

			if (repl) {
				info.install.push(repl);
				return E('span', {
					'class': 'label',
					'data-tooltip': _('Requires update to %h %h')
						.format(repl.name, repl.version)
				}, _('Needs upgrade'));
			}

			info.errors.push(_('The installed version of package <em>%h</em> is not compatible, require %s while %s is installed.').format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));

			return E('span', {
				'class': 'label warning',
				'data-tooltip': _('Require version %h %h,\ninstalled %h')
					.format(vop, ver, pkg.version)
			}, _('Version incompatible'));
		}

		return E('span', { 'class': 'label notice' }, _('Installed'));
	}
	else if (!pkg.missing) {
		if (!vop || versionSatisfied(pkg.version, ver, vop)) {
			info.install.push(pkg);
			return E('span', { 'class': 'label' }, _('Not installed'));
		}

		info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
				.format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));

		return E('span', {
			'class': 'label warning',
			'data-tooltip': _('Require version %h %h,\ninstalled %h')
				.format(vop, ver, pkg.version)
		}, _('Version incompatible'));
	}
	else {
		info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));

		return E('span', { 'class': 'label warning' }, _('Not available'));
	}
}

function renderDependencyItem(dep, info)
{
	var li = E('li'),
	    vop = dep.version ? dep.version[0] : null,
	    ver = dep.version ? dep.version[1] : null,
	    depends = [];

	for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
		var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
		          packages.available.pkgs[dep.pkgs[i]] ||
		          { name: dep.name };

		if (i > 0)
			li.appendChild(document.createTextNode(' | '));

		var text = pkg.name;

		if (pkg.installsize)
			text += ' (%.1024mB)'.format(pkg.installsize);
		else if (pkg.size)
			text += ' (~%.1024mB)'.format(pkg.size);

		li.appendChild(E('span', { 'data-tooltip': pkg.description },
			[ text, ' ', pkgStatus(pkg, vop, ver, info) ]));

		(pkg.depends || []).forEach(function(d) {
			if (depends.indexOf(d) === -1)
				depends.push(d);
		});
	}

	if (!li.firstChild)
		li.appendChild(E('span', {},
			[ dep.name, ' ',
			  pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));

	var subdeps = renderDependencies(depends, info);
	if (subdeps)
		li.appendChild(subdeps);

	return li;
}

function renderDependencies(depends, info)
{
	var deps = depends || [],
	    items = [];

	info.seen = info.seen || [];

	for (var i = 0; i < deps.length; i++) {
		if (deps[i] === 'libc')
			continue;

		if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
			dep = RegExp.$1.trim();
			vop = RegExp.$2.trim();
			ver = RegExp.$3.trim();
		}
		else {
			dep = deps[i].trim();
			vop = ver = null;
		}

		if (info.seen[dep])
			continue;

		var pkgs = [];

		(packages.installed.providers[dep] || []).forEach(function(p) {
			if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
		});

		(packages.available.providers[dep] || []).forEach(function(p) {
			if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
		});

		info.seen[dep] = {
			name:    dep,
			pkgs:    pkgs,
			version: [vop, ver]
		};

		items.push(renderDependencyItem(info.seen[dep], info));
	}

	if (items.length)
		return E('ul', { 'class': 'deps' }, items);

	return null;
}

function truncateVersion(v, op)
{
	v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
		'<span data-tooltip="$1">$2…</span>');

	if (!op || op === '=')
		return v;

	return '%h %h'.format(op, v);
}

function handleReset(ev)
{
	var filter = document.querySelector('input[name="filter"]');

	filter.value = '';
	display();
}

function handleInstall(ev)
{
	var name = ev.target.getAttribute('data-package'),
	    pkg = packages.available.pkgs[name],
	    depcache = {},
	    size;

	if (pkg.installsize)
		size = _('~%.1024mB installed').format(pkg.installsize);
	else if (pkg.size)
		size = _('~%.1024mB compressed').format(pkg.size);
	else
		size = _('unknown');

	var deps = renderDependencies(pkg.depends, depcache),
	    tree = null, errs = null, inst = null, desc = null;

	if (depcache.errors && depcache.errors.length) {
		errs = E('ul', { 'class': 'errors' });
		depcache.errors.forEach(function(err) {
			errs.appendChild(E('li', {}, err));
		});
	}

	var totalsize = pkg.installsize || pkg.size || 0,
	    totalpkgs = 1;

	if (depcache.install && depcache.install.length)
		depcache.install.forEach(function(ipkg) {
			totalsize += ipkg.installsize || ipkg.size || 0;
			totalpkgs++;
		});

	inst = E('p', {},
		_('Require approx. %.1024mB size for %d package(s) to install.')
			.format(totalsize, totalpkgs));

	if (deps) {
		tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
		tree.appendChild(deps);
	}

	if (pkg.description) {
		desc = E('div', {}, [
			E('h5', {}, _('Description')),
			E('p', {}, pkg.description)
		]);
	}

	L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
		E('ul', {}, [
			E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
			E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
			tree || '',
		]),
		desc || '',
		errs || inst || '',
		E('div', { 'class': 'right' }, [
			E('div', {
				'class': 'btn',
				'click': L.hideModal
			}, _('Cancel')),
			' ',
			E('div', {
				'data-command': 'install',
				'data-package': name,
				'class': 'btn cbi-button-action',
				'click': handleOpkg
			}, _('Install'))
		])
	]);
}

function handleManualInstall(ev)
{
	var name_or_url = document.querySelector('input[name="install"]').value,
	    install = E('div', {
			'class': 'btn cbi-button-action',
			'data-command': 'install',
			'data-package': name_or_url,
			'click': function(ev) {
				document.querySelector('input[name="install"]').value = '';
				handleOpkg(ev);
			}
		}, _('Install')), warning;

	if (!name_or_url.length) {
		return;
	}
	else if (name_or_url.indexOf('/') !== -1) {
		warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
	}
	else if (!packages.available.providers[name_or_url]) {
		warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
		install = '';
	}
	else {
		warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
	}

	L.showModal(_('Manually install package'), [
		warning,
		E('div', { 'class': 'right' }, [
			E('div', {
				'click': L.hideModal,
				'class': 'btn cbi-button-neutral'
			}, _('Cancel')),
			' ', install
		])
	]);
}

function handleConfig(ev)
{
	L.showModal(_('OPKG Configuration'), [
		E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
	]);

	L.get('admin/system/opkg/config', null, function(xhr, conf) {
		var body = [
			E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.'))
		];

		Object.keys(conf).sort().forEach(function(file) {
			body.push(E('h5', {}, '%h'.format(file)));
			body.push(E('textarea', {
				'name': file,
				'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3)
			}, '%h'.format(conf[file])));
		});

		body.push(E('div', { 'class': 'right' }, [
			E('div', {
				'class': 'btn cbi-button-neutral',
				'click': L.hideModal
			}, _('Cancel')),
			' ',
			E('div', {
				'class': 'btn cbi-button-positive',
				'click': function(ev) {
					var data = {};
					findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
						.forEach(function(textarea) {
							data[textarea.getAttribute('name')] = textarea.value
						});

					L.showModal(_('OPKG Configuration'), [
						E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
					]);

					L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal);
				}
			}, _('Save')),
		]));

		L.showModal(_('OPKG Configuration'), body);
	});
}

function handleRemove(ev)
{
	var name = ev.target.getAttribute('data-package'),
	    pkg = packages.installed.pkgs[name],
	    avail = packages.available.pkgs[name] || {},
	    size, desc;

	if (avail.installsize)
		size = _('~%.1024mB installed').format(avail.installsize);
	else if (avail.size)
		size = _('~%.1024mB compressed').format(avail.size);
	else
		size = _('unknown');

	if (avail.description) {
		desc = E('div', {}, [
			E('h5', {}, _('Description')),
			E('p', {}, avail.description)
		]);
	}

	L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
		E('ul', {}, [
			E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
			E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
		]),
		desc || '',
		E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
			E('label', {}, [
				E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
				_('Automatically remove unused dependencies')
			]),
			E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
				E('div', {
					'class': 'btn',
					'click': L.hideModal
				}, _('Cancel')),
				' ',
				E('div', {
					'data-command': 'remove',
					'data-package': name,
					'class': 'btn cbi-button-negative',
					'click': handleOpkg
				}, _('Remove'))
			])
		])
	]);
}

function handleOpkg(ev)
{
	var cmd = ev.target.getAttribute('data-command'),
	    pkg = ev.target.getAttribute('data-package'),
	    rem = document.querySelector('input[name="autoremove"]'),
	    url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);

	var dlg = L.showModal(_('Executing package manager'), [
		E('p', { 'class': 'spinning' },
			_('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
	]);

	L.post(url, { package: pkg, autoremove: rem ? rem.checked : false }, function(xhr, res) {
		dlg.removeChild(dlg.lastChild);

		if (res.stdout)
			dlg.appendChild(E('pre', [ res.stdout ]));

		if (res.stderr) {
			dlg.appendChild(E('h5', _('Errors')));
			dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
		}

		if (res.code !== 0)
			dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));

		dlg.appendChild(E('div', { 'class': 'right' },
			E('div', {
				'class': 'btn',
				'click': function() {
					L.hideModal();
					updateLists();
				}
			}, _('Dismiss'))));
	});
}

function updateLists()
{
	cbi_update_table('#packages', [],
		E('div', { 'class': 'spinning' }, _('Loading package information…')));

	packages.available = { providers: {}, pkgs: {} };
	packages.installed = { providers: {}, pkgs: {} };

	L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
		var pg = document.querySelector('.cbi-progressbar'),
		    total = stat.blocks || 0,
		    free = stat.bfree || 0;

		pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
		pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));

		L.get('admin/system/opkg/list/available', null, function(xhr) {
			parseList(xhr.responseText, packages.available);
			L.get('admin/system/opkg/list/installed', null, function(xhr) {
				parseList(xhr.responseText, packages.installed);
				display(document.querySelector('input[name="filter"]').value);
			});
		});
	});
}

window.requestAnimationFrame(function() {
	var filter = document.querySelector('input[name="filter"]'),
	    keyTimeout = null;

	filter.value = filter.getAttribute('value');
	filter.addEventListener('keyup',
		function(ev) {
			if (keyTimeout !== null)
				window.clearTimeout(keyTimeout);

			keyTimeout = window.setTimeout(function() {
				display(ev.target.value);
			}, 250);
		});

	document.querySelector('#pager > .prev').addEventListener('click', handlePage);
	document.querySelector('#pager > .next').addEventListener('click', handlePage);
	document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);

	updateLists();
});
