'use strict';
'require baseclass';
'require fs';
'require ui';
'require uci';
'require rpc';
'require network';
'require firewall';

const callGetBuiltinEthernetPorts = rpc.declare({
	object: 'luci',
	method: 'getBuiltinEthernetPorts',
	expect: { result: [] }
});

const callNetworkDeviceStatus = rpc.declare({
	object: 'network.device',
	method: 'status',
	params: [ 'name' ],
	expect: { '': {} }
});

function isString(v)
{
	return typeof(v) === 'string' && v !== '';
}

function resolveVLANChain(ifname, bridges, mapping)
{
	while (!mapping[ifname]) {
		const m = ifname.match(/^(.+)\.([^.]+)$/);

		if (!m)
			break;

		if (bridges[m[1]]) {
			if (bridges[m[1]].vlan_filtering)
				mapping[ifname] = bridges[m[1]].vlans[m[2]];
			else
				mapping[ifname] = bridges[m[1]].ports;
		}
		else if (/^[0-9]{1,4}$/.test(m[2]) && m[2] <= 4095) {
			mapping[ifname] = [ m[1] ];
		}
		else {
			break;
		}

		ifname = m[1];
	}
}

function buildVLANMappings(mapping)
{
	const bridge_vlans = uci.sections('network', 'bridge-vlan');
	const vlan_devices = uci.sections('network', 'device');
	const interfaces = uci.sections('network', 'interface');
	const bridges = {};

	/* find bridge VLANs */
	for (let i = 0, s; (s = bridge_vlans[i]) != null; i++) {
		if (!isString(s.device) || !/^[0-9]{1,4}$/.test(s.vlan) || +s.vlan > 4095)
			continue;

		const aliases = L.toArray(s.alias);
		const ports = L.toArray(s.ports);
		const br = bridges[s.device] = (bridges[s.device] || { ports: [], vlans: {}, vlan_filtering: true });

		br.vlans[s.vlan] = [];

		for (let p of ports) {
			const port = p.replace(/:[ut*]+$/, '');

			if (br.ports.indexOf(port) === -1)
				br.ports.push(port);

			br.vlans[s.vlan].push(port);
		}

		for (let a of aliases)
			if (a != s.vlan)
				br.vlans[a] = br.vlans[s.vlan];
	}

	/* find bridges, VLAN devices */
	for (let i = 0, s; (s = vlan_devices[i]) != null; i++) {
		if (s.type == 'bridge') {
			if (!isString(s.name))
				continue;

			const ports = L.toArray(s.ports);
			const br = bridges[s.name] || (bridges[s.name] = { ports: [], vlans: {}, vlan_filtering: false });

			if (s.vlan_filtering == '0')
				br.vlan_filtering = false;
			else if (s.vlan_filtering == '1')
				br.vlan_filtering = true;

			for (let p of ports)
				if (br.ports.indexOf(p) === -1)
					br.ports.push(p);

			mapping[s.name] = br.ports;
		}
		else if (s.type == '8021q' || s.type == '8021ad') {
			if (!isString(s.name) || !isString(s.vid) || !isString(s.ifname))
				continue;

			/* parent device is a bridge */
			if (bridges[s.ifname]) {
				/* parent bridge is VLAN enabled, device refers to VLAN ports */
				if (bridges[s.ifname].vlan_filtering)
					mapping[s.name] = bridges[s.ifname].vlans[s.vid];

				/* parent bridge is not VLAN enabled, device refers to all bridge ports */
				else
					mapping[s.name] = bridges[s.ifname].ports;
			}

			/* parent is a simple netdev */
			else {
				mapping[s.name] = [ s.ifname ];
			}

			resolveVLANChain(s.ifname, bridges, mapping);
		}
	}

	/* resolve VLAN tagged interfaces in bridge ports */
	for (let brname in bridges) {
		for (let bp of bridges[brname].ports)
			resolveVLANChain(bp, bridges, mapping);

		for (let vid in bridges[brname].vlans)
			for (let v of bridges[brname].vlans[vid])
				resolveVLANChain(v, bridges, mapping);
	}

	/* find implicit VLAN devices */
	for (let i = 0, s; (s = interfaces[i]) != null; i++) {
		if (!isString(s.device))
			continue;

		resolveVLANChain(s.device, bridges, mapping);
	}
}

function resolveVLANPorts(ifname, mapping, seen)
{
	const ports = [];

	if (!seen)
		seen = {};

	if (mapping[ifname]) {
		for (let m of mapping[ifname]) {
			if (!seen[m]) {
				seen[m] = true;
				ports.push.apply(ports, resolveVLANPorts(m, mapping, seen));
			}
		}
	}
	else {
		ports.push(ifname);
	}

	return ports.sort(L.naturalCompare);
}

function buildInterfaceMapping(zones, networks) {
	const vlanmap = {};
	const portmap = {};
	const netmap = {};

	buildVLANMappings(vlanmap);

	for (let net of networks) {
		const l3dev = net.getDevice();

		if (!l3dev)
			continue;

		const ports = resolveVLANPorts(l3dev.getName(), vlanmap);

		for (let p of ports) {
			portmap[p] = portmap[p] || { networks: [], zones: [] };
			portmap[p].networks.push(net);
		}

		netmap[net.getName()] = net;
	}

	for (let z of zones) {
		const networknames = z.getNetworks();

		for (let nn of networknames) {
			if (!netmap[nn])
				continue;

			const l3dev = netmap[nn].getDevice();

			if (!l3dev)
				continue;

			const ports = resolveVLANPorts(l3dev.getName(), vlanmap);

			for (let p of ports) {
				portmap[p] = portmap[p] || { networks: [], zones: [] };

				if (portmap[p].zones.indexOf(z) === -1)
					portmap[p].zones.push(z);
			}
		}
	}

	return portmap;
}

function formatSpeed(carrier, speed, duplex) {
	if ((speed > 0) && duplex) {
		const d = (duplex == 'half') ? '\u202f(H)' : '';
		const e = E('span', { 'title': _('Speed: %d Mbit/s, Duplex: %s').format(speed, duplex) });

		switch (true) {
		case (speed < 1000):
			e.innerText = '%d\u202fM%s'.format(speed, d);
			break;
		case (speed == 1000):
			e.innerText = '1\u202fGbE' + d;
			break;
		case (speed >= 1e6 && speed < 1e9):
			e.innerText = '%f\u202fTbE'.format(speed / 1e6);
			break;
		case (speed >= 1e9):
			e.innerText = '%f\u202fPbE'.format(speed / 1e9);
			break;
		default: e.innerText = '%f\u202fGbE'.format(speed / 1000);
		}

		return e;
	}

	return carrier ? _('Connected') : _('no link');
}

function getPSEStatus(pse) {
	if (!pse)
		return null;

	const status = pse['c33-power-status'] || pse['podl-power-status'],
	    power = pse['c33-actual-power'];

	return {
		status: status,
		power: power,
		isDelivering: status === 'delivering' && power > 0
	};
}

function formatPSEPower(pse) {
	if (!pse)
		return null;

	const status = pse['c33-power-status'] || pse['podl-power-status'],
	    power = pse['c33-actual-power'];

	if (status === 'delivering' && power) {
		const watts = (power / 1000).toFixed(1);
		/* Format: "⚡ 15.4 W" - lightning bolt + narrow space + watts + narrow space + W */
		return E('span', { 'style': 'color:#000' },
			[ '\u26a1\ufe0e\u202f%s\u202fW'.format(watts) ]);
	}
	else if (status === 'searching') {
		return E('span', { 'style': 'color:#000' },
			[ '\u26a1\ufe0e\u202f' + _('searching') ]);
	}
	else if (status === 'fault' || status === 'otherfault' || status === 'error') {
		return E('span', { 'style': 'color:#d9534f' },
			[ '\u26a1\ufe0e\u202f' + _('fault') ]);
	}
	else if (status === 'disabled') {
		return E('span', { 'style': 'color:#888' },
			[ '\u26a1\ufe0e\u202f' + _('off') ]);
	}

	return null;
}

function formatStats(portdev, pse) {
	const stats = portdev._devstate('stats') || {};
	const items = [
		_('Received bytes'), '%1024mB'.format(stats.rx_bytes),
		_('Received packets'), '%1000mPkts.'.format(stats.rx_packets),
		_('Received multicast'), '%1000mPkts.'.format(stats.multicast),
		_('Receive errors'), '%1000mPkts.'.format(stats.rx_errors),
		_('Receive dropped'), '%1000mPkts.'.format(stats.rx_dropped),

		_('Transmitted bytes'), '%1024mB'.format(stats.tx_bytes),
		_('Transmitted packets'), '%1000mPkts.'.format(stats.tx_packets),
		_('Transmit errors'), '%1000mPkts.'.format(stats.tx_errors),
		_('Transmit dropped'), '%1000mPkts.'.format(stats.tx_dropped),

		_('Collisions seen'), stats.collisions
	];

	if (pse) {
		const status = pse['c33-power-status'] || pse['podl-power-status'],
		    power = pse['c33-actual-power'],
		    powerClass = pse['c33-power-class'],
		    powerLimit = pse['c33-available-power-limit'];

		items.push(_('PoE status'), status || _('unknown'));

		if (power)
			items.push(_('PoE power'), '%.1f W'.format(power / 1000));

		if (powerClass)
			items.push(_('PoE class'), powerClass);

		if (powerLimit)
			items.push(_('PoE limit'), '%.1f W'.format(powerLimit / 1000));
	}

	return ui.itemlist(E('span'), items);
}

function renderNetworkBadge(network, zonename) {
	const l3dev = network.getDevice();
	const span = E('span', { 'class': 'ifacebadge', 'style': 'margin:.125em 0' }, [
		E('span', {
			'class': 'zonebadge',
			'title': zonename ? _('Part of zone %q').format(zonename) : _('No zone assigned'),
			'style': firewall.getZoneColorStyle(zonename)
		}, '\u202f'),
		'\u202f', network.getName(), ': '
	]);

	if (l3dev)
		span.appendChild(E('img', {
			'title': l3dev.getI18n(),
			'src': L.resource('icons/%s%s.svg'.format(l3dev.getType(), l3dev.isUp() ? '' : '_disabled'))
		}));
	else
		span.appendChild(E('em', _('(no interfaces attached)')));

	return span;
}

function renderNetworksTooltip(pmap) {
	const res = [ null ];
	const zmap = {};

	const zones = (pmap && Array.isArray(pmap.zones)) ? pmap.zones : [];
	const networks = (pmap && Array.isArray(pmap.networks)) ? pmap.networks : [];

	for (let pmz of zones) {
		const networknames = pmz.getNetworks();

		for (let nn of networknames)
			zmap[nn] = pmz.getName();
	}

	for (let pmn of networks)
		res.push(E('br'), renderNetworkBadge(pmn, zmap[pmn.getName()]));

	if (res.length > 1)
		res[0] = N_((res.length - 1) / 2, 'Part of network:', 'Part of networks:');
	else
		res[0] = _('Port is not part of any network');

	return E([], res);
}

return baseclass.extend({
	title: _('Port status'),

	load() {
		return Promise.all([
			L.resolveDefault(callGetBuiltinEthernetPorts(), []),
			L.resolveDefault(fs.read('/etc/board.json'), '{}'),
			firewall.getZones(),
			network.getNetworks(),
			uci.load('network')
		]).then((data) => {
			/* Get all known port names from builtin ports or board.json */
			const builtinPorts = data[0] || [];
			const board = JSON.parse(data[1] || '{}');
			const allPorts = new Set();

			/* Collect port names from builtin ethernet ports */
			builtinPorts.forEach((port) => {
				if (port.device)
					allPorts.add(port.device);
			});

			/* Collect port names from board.json if no builtin ports */
			if (allPorts.size === 0 && board.network) {
				['lan', 'wan'].forEach((role) => {
					if (board.network[role]) {
						if (Array.isArray(board.network[role].ports))
							board.network[role].ports.forEach((p) => allPorts.add(p));
						else if (board.network[role].device)
							allPorts.add(board.network[role].device);
					}
				});
			}

			/* Query PSE status from netifd for all known ports */
			const psePromises = Array.from(allPorts).map((devname) => {
				return L.resolveDefault(callNetworkDeviceStatus(devname), {}).then((status) => {
					return { name: devname, pse: status.pse || null };
				});
			});

			return Promise.all(psePromises).then((pseResults) => {
				const pseMap = {};
				pseResults.forEach((r) => {
					if (r.pse)
						pseMap[r.name] = r.pse;
				});
				data.push(pseMap);
				return data;
			});
		});
	},

	render(data) {
		if (L.hasSystemFeature('swconfig'))
			return null;

		const board = JSON.parse(data[1]),
		      port_map = buildInterfaceMapping(data[2], data[3]),
		      pseMap = data[5] || {};
		let known_ports = [];

		if (Array.isArray(data[0]) && data[0].length > 0) {
			known_ports = data[0].map(port => ({
				...port,
				netdev: network.instantiateDevice(port.device)
			}));
		}
		else {
			if (L.isObject(board) && L.isObject(board.network)) {
				for (let k = 'lan'; k != null; k = (k == 'lan') ? 'wan' : null) {
					if (!L.isObject(board.network[k]))
						continue;

					if (Array.isArray(board.network[k].ports))
						for (let i = 0; i < board.network[k].ports.length; i++)
							known_ports.push({
								role: k,
								device: board.network[k].ports[i],
								netdev: network.instantiateDevice(board.network[k].ports[i])
							});
					else if (typeof(board.network[k].device) == 'string')
						known_ports.push({
							role: k,
							device: board.network[k].device,
							netdev: network.instantiateDevice(board.network[k].device)
						});
				}
			}
		}

		known_ports.sort(function(a, b) {
			return L.naturalCompare(a.device, b.device);
		});

		return E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(70px, 1fr));margin-bottom:1em' }, known_ports.map(function(port) {
			const speed = port.netdev.getSpeed();
			const duplex = port.netdev.getDuplex();
			const carrier = port.netdev.getCarrier();
			const pmap = port_map[port.netdev.getName()];
			const pzones = (pmap && pmap.zones.length) ? pmap.zones.sort((a, b) => L.naturalCompare(a.getName(), b.getName())) : [ null ];
			const pse = pseMap[port.device];
			const pseInfo = getPSEStatus(pse);
			const psePower = formatPSEPower(pse);

			/* Select port icon based on carrier and PSE status */
			let portIcon;
			if (pseInfo && pseInfo.isDelivering) {
				portIcon = carrier ? 'pse_up' : 'pse_down';
			} else {
				portIcon = carrier ? 'up' : 'down';
			}

			const statsContent = [
				'\u25b2\u202f%1024.1mB'.format(port.netdev.getTXBytes()),
				E('br'),
				'\u25bc\u202f%1024.1mB'.format(port.netdev.getRXBytes())
			];

			if (psePower) {
				statsContent.push(E('br'));
				statsContent.push(psePower);
			}

			statsContent.push(E('span', { 'class': 'cbi-tooltip' }, formatStats(port.netdev, pse)));

			return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;min-width:70px;max-width:100px' }, [
				E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.netdev.getName() ]),
				E('div', { 'class': 'ifacebox-body' }, [
					E('img', { 'src': L.resource('icons/port_%s.svg').format(portIcon) }),
					E('br'),
					formatSpeed(carrier, speed, duplex)
				]),
				E('div', { 'class': 'ifacebox-head cbi-tooltip-container', 'style': 'display:flex' }, [
					E([], pzones.map(function(zone) {
						return E('div', {
							'class': 'zonebadge',
							'style': 'cursor:help;flex:1;height:3px;opacity:' + (carrier ? 1 : 0.25) + ';' + firewall.getZoneColorStyle(zone)
						});
					})),
					E('span', { 'class': 'cbi-tooltip left' }, [ renderNetworksTooltip(pmap) ])
				]),
				E('div', { 'class': 'ifacebox-body' }, [
					E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, statsContent)
				])
			]);
		}));
	}
});
