'use strict';
'require baseclass';
'require dom';
'require ui';
'require uci';
'require form';
'require network';
'require firewall';
'require validation';
'require tools.prng as random';

var protocols = [
	'ip', 0, 'IP',
	'hopopt', 0, 'HOPOPT',
	'icmp', 1, 'ICMP',
	'igmp', 2, 'IGMP',
	'ggp', 3 , 'GGP',
	'ipencap', 4, 'IP-ENCAP',
	'st', 5, 'ST',
	'tcp', 6, 'TCP',
	'egp', 8, 'EGP',
	'igp', 9, 'IGP',
	'pup', 12, 'PUP',
	'udp', 17, 'UDP',
	'hmp', 20, 'HMP',
	'xns-idp', 22, 'XNS-IDP',
	'rdp', 27, 'RDP',
	'iso-tp4', 29, 'ISO-TP4',
	'dccp', 33, 'DCCP',
	'xtp', 36, 'XTP',
	'ddp', 37, 'DDP',
	'idpr-cmtp', 38, 'IDPR-CMTP',
	'ipv6', 41, 'IPv6',
	'ipv6-route', 43, 'IPv6-Route',
	'ipv6-frag', 44, 'IPv6-Frag',
	'idrp', 45, 'IDRP',
	'rsvp', 46, 'RSVP',
	'gre', 47, 'GRE',
	'esp', 50, 'IPSEC-ESP',
	'ah', 51, 'IPSEC-AH',
	'skip', 57, 'SKIP',
	'icmpv6', 58, 'IPv6-ICMP',
	'ipv6-icmp', 58, 'IPv6-ICMP',
	'ipv6-nonxt', 59, 'IPv6-NoNxt',
	'ipv6-opts', 60, 'IPv6-Opts',
	'rspf', 73, 'RSPF',
	'rspf', 73, 'CPHB',
	'vmtp', 81, 'VMTP',
	'eigrp', 88, 'EIGRP',
	'ospf', 89, 'OSPFIGP',
	'ax.25', 93, 'AX.25',
	'ipip', 94, 'IPIP',
	'etherip', 97, 'ETHERIP',
	'encap', 98, 'ENCAP',
	'pim', 103, 'PIM',
	'ipcomp', 108, 'IPCOMP',
	'vrrp', 112, 'VRRP',
	'l2tp', 115, 'L2TP',
	'isis', 124, 'ISIS',
	'sctp', 132, 'SCTP',
	'fc', 133, 'FC',
	'mh', 135, 'Mobility-Header',
	'ipv6-mh', 135, 'Mobility-Header',
	'mobility-header', 135, 'Mobility-Header',
	'udplite', 136, 'UDPLite',
	'mpls-in-ip', 137, 'MPLS-in-IP',
	'manet', 138, 'MANET',
	'hip', 139, 'HIP',
	'shim6', 140, 'Shim6',
	'wesp', 141, 'WESP',
	'rohc', 142, 'ROHC',
];

function lookupProto(x) {
	if (x == null || x === '')
		return null;

	var s = String(x).toLowerCase();

	for (var i = 0; i < protocols.length; i += 3)
		if (s == protocols[i] || s == protocols[i+1])
			return [ protocols[i+1], protocols[i+2], protocols[i] ];

	return [ -1, x, x ];
}

return baseclass.extend({
	fmt: function(fmtstr, args, values) {
		var repl = [],
		    wrap = false,
		    tokens = [];

		if (values == null) {
			values = [];
			wrap = true;
		}

		var get = function(args, key) {
			var names = key.trim().split(/\./),
			    obj = args,
			    ctx = obj;

			for (var i = 0; i < names.length; i++) {
				if (!L.isObject(obj))
					return null;

				ctx = obj;
				obj = obj[names[i]];
			}

			if (typeof(obj) == 'function')
				return obj.call(ctx);

			return obj;
		};

		var isset = function(val) {
			if (L.isObject(val) && !dom.elem(val)) {
				for (var k in val)
					if (val.hasOwnProperty(k))
						return true;

				return false;
			}
			else if (Array.isArray(val)) {
				return (val.length > 0);
			}
			else {
				return (val !== null && val !== undefined && val !== '' && val !== false);
			}
		};

		var parse = function(tokens, text) {
			if (dom.elem(text)) {
				tokens.push('<span data-fmt-placeholder="%d"></span>'.format(values.length));
				values.push(text);
			}
			else {
				tokens.push(String(text).replace(/\\(.)/g, '$1'));
			}
		};

		for (var i = 0, last = 0; i <= fmtstr.length; i++) {
			if (fmtstr.charAt(i) == '%' && fmtstr.charAt(i + 1) == '{') {
				if (i > last)
					parse(tokens, fmtstr.substring(last, i));

				var j = i + 1,  nest = 0;

				var subexpr = [];

				for (var off = j + 1, esc = false; j <= fmtstr.length; j++) {
					var ch = fmtstr.charAt(j);

					if (esc) {
						esc = false;
					}
					else if (ch == '\\') {
						esc = true;
					}
					else if (ch == '{') {
						nest++;
					}
					else if (ch == '}') {
						if (--nest == 0) {
							subexpr.push(fmtstr.substring(off, j));
							break;
						}
					}
					else if (ch == '?' || ch == ':' || ch == '#') {
						if (nest == 1) {
							subexpr.push(fmtstr.substring(off, j));
							subexpr.push(ch);
							off = j + 1;
						}
					}
				}

				var varname  = subexpr[0].trim(),
				    op1      = (subexpr[1] != null) ? subexpr[1] : '?',
				    if_set   = (subexpr[2] != null && subexpr[2] != '') ? subexpr[2] : '%{' + varname + '}',
				    op2      = (subexpr[3] != null) ? subexpr[3] : ':',
				    if_unset = (subexpr[4] != null) ? subexpr[4] : '';

				/* Invalid expression */
				if (nest != 0 || subexpr.length > 5 || varname == '') {
					return fmtstr;
				}

				/* enumeration */
				else if (op1 == '#' && subexpr.length == 3) {
					var items = L.toArray(get(args, varname));

					for (var k = 0; k < items.length; k++) {
						tokens.push.apply(tokens, this.fmt(if_set, Object.assign({}, args, {
							first: k == 0,
							next:  k > 0,
							last:  (k + 1) == items.length,
							item:  items[k]
						}), values));
					}
				}

				/* ternary expression */
				else if (op1 == '?' && op2 == ':' && (subexpr.length == 1 || subexpr.length == 3 || subexpr.length == 5)) {
					var val = get(args, varname);

					if (subexpr.length == 1)
						parse(tokens, isset(val) ? val : '');
					else if (isset(val))
						tokens.push.apply(tokens, this.fmt(if_set, args, values));
					else
						tokens.push.apply(tokens, this.fmt(if_unset, args, values));
				}

				/* unrecognized command */
				else {
					return fmtstr;
				}

				last = j + 1;
				i = j;
			}
			else if (i >= fmtstr.length) {
				if (i > last)
					parse(tokens, fmtstr.substring(last, i));
			}
		}

		if (wrap) {
			var node = E('span', {}, tokens.join('')),
			    repl = node.querySelectorAll('span[data-fmt-placeholder]');

			for (var i = 0; i < repl.length; i++)
				repl[i].parentNode.replaceChild(values[repl[i].getAttribute('data-fmt-placeholder')], repl[i]);

			return node;
		}
		else {
			return tokens;
		}
	},

	map_invert: function(v, fn) {
		return L.toArray(v).map(function(v) {
			v = String(v);

			if (fn != null && typeof(v[fn]) == 'function')
				v = v[fn].call(v);

			return {
				ival: v,
				inv: v.charAt(0) == '!',
				val: v.replace(/^!\s*/, '')
			};
		});
	},

	lookupProto: lookupProto,

	addDSCPOption: function(s, is_target) {
		var o = s.taboption(is_target ? 'general' : 'advanced', form.Value, is_target ? 'set_dscp' : 'dscp',
			is_target ? _('DSCP mark') : _('Match DSCP'),
			is_target ? _('Apply the given DSCP class or value to established connections.') : _('Matches traffic carrying the specified DSCP marking.'));

		o.modalonly = true;
		o.rmempty = !is_target;
		o.placeholder = _('any');

		if (is_target)
			o.depends('target', 'DSCP');

		o.value('CS0');
		o.value('CS1');
		o.value('CS2');
		o.value('CS3');
		o.value('CS4');
		o.value('CS5');
		o.value('CS6');
		o.value('CS7');
		o.value('BE');
		o.value('AF11');
		o.value('AF12');
		o.value('AF13');
		o.value('AF21');
		o.value('AF22');
		o.value('AF23');
		o.value('AF31');
		o.value('AF32');
		o.value('AF33');
		o.value('AF41');
		o.value('AF42');
		o.value('AF43');
		o.value('EF');
		o.validate = function(section_id, value) {
			if (value == '')
				return is_target ? _('DSCP mark required') : true;

			if (!is_target)
				value = String(value).replace(/^!\s*/, '');

			var m = value.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);

			if (!m || (m[1] != null && +m[1] > 0x3f))
				return _('Invalid DSCP mark');

			return true;
		};

		return o;
	},

	addMarkOption: function(s, is_target) {
		var o = s.taboption(is_target ? 'general' : 'advanced', form.Value,
			(is_target > 1) ? 'set_xmark' : (is_target ? 'set_mark' : 'mark'),
			(is_target > 1) ? _('XOR mark') : (is_target ? _('Set mark') : _('Match mark')),
			(is_target > 1) ? _('Apply a bitwise XOR of the given value and the existing mark value on established connections. Format is value[/mask]. If a mask is specified then those bits set in the mask are zeroed out.') :
				(is_target ? _('Set the given mark value on established connections. Format is value[/mask]. If a mask is specified then only those bits set in the mask are modified.') :
						_('Matches a specific firewall mark or a range of different marks.')));

		o.modalonly = true;
		o.rmempty = true;

		if (is_target > 1)
			o.depends('target', 'MARK_XOR');
		else if (is_target)
			o.depends('target', 'MARK_SET');

		o.validate = function(section_id, value) {
			if (value == '')
				return is_target ? _('Valid firewall mark required') : true;

			if (!is_target)
				value = String(value).replace(/^!\s*/, '');

			var m = value.match(/^(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i);

			if (!m || +m[1] > 0xffffffff || (m[2] != null && +m[2] > 0xffffffff))
				return _('Expecting: %s').format(_('valid firewall mark'));

			return true;
		};

		return o;
	},

	addLimitOption: function(s) {
		var o = s.taboption('advanced', form.Value, 'limit',
			_('Limit matching'),
			_('Limits traffic matching to the specified rate.'));

		o.modalonly = true;
		o.rmempty = true;
		o.placeholder = _('unlimited');
		o.value('10/second');
		o.value('60/minute');
		o.value('3/hour');
		o.value('500/day');
		o.validate = function(section_id, value) {
			if (value == '')
				return true;

			var m = String(value).toLowerCase().match(/^(?:0x[0-9a-f]{1,8}|[0-9]{1,10})\/([a-z]+)$/),
			    u = ['second', 'minute', 'hour', 'day'],
			    i = 0;

			if (m)
				for (i = 0; i < u.length; i++)
					if (u[i].indexOf(m[1]) == 0)
						break;

			if (!m || i >= u.length)
				return _('Invalid limit value');

			return true;
		};

		return o;
	},

	addLimitBurstOption: function(s) {
		var o = s.taboption('advanced', form.Value, 'limit_burst',
			_('Limit burst'),
			_('Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number.'));

		o.modalonly = true;
		o.rmempty = true;
		o.placeholder = '5';
		o.datatype = 'uinteger';
		o.depends({ limit: null, '!reverse': true });

		return o;
	},

	transformHostHints: function(family, hosts) {
		var choice_values = [],
		    choice_labels = {},
		    ip6addrs = {},
		    ipaddrs = {};

		for (var mac in hosts) {
			L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4).forEach(function(ip) {
				ipaddrs[ip] = mac;
			});

			L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6).forEach(function(ip) {
				ip6addrs[ip] = mac;
			});
		}

		if (!family || family == 'ipv4') {
			L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ip) {
				var val = ip,
				    txt = hosts[ipaddrs[ip]].name || ipaddrs[ip];

				choice_values.push(val);
				choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
			});
		}

		if (!family || family == 'ipv6') {
			L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ip) {
				var val = ip,
				    txt = hosts[ip6addrs[ip]].name || ip6addrs[ip];

				choice_values.push(val);
				choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
			});
		}

		return [choice_values, choice_labels];
	},

	updateHostHints: function(map, section_id, option, family, hosts) {
		var opt = map.lookupOption(option, section_id)[0].getUIElement(section_id),
		    choices = this.transformHostHints(family, hosts);

		opt.clearChoices();
		opt.addChoices(choices[0], choices[1]);
	},

	CBIDynamicMultiValueList: form.DynamicList.extend({
		renderWidget: function(/* ... */) {
			var dl = form.DynamicList.prototype.renderWidget.apply(this, arguments),
			    inst = dom.findClassInstance(dl);

			inst.addItem = function(dl, value, text, flash) {
				var values = L.toArray(value);
				for (var i = 0; i < values.length; i++)
					ui.DynamicList.prototype.addItem.call(this, dl, values[i], null, true);
			};

			return dl;
		}
	}),

	addIPOption: function(s, tab, name, label, description, family, hosts, multiple) {
		var o = s.taboption(tab, multiple ? this.CBIDynamicMultiValueList : form.Value, name, label, description);
		var fw4 = L.hasSystemFeature('firewall4');

		o.modalonly = true;
		o.datatype = (fw4 && validation.types.iprange) ? 'list(neg(or(ipmask("true"),iprange)))' : 'list(neg(ipmask("true")))';
		o.placeholder = multiple ? _('-- add IP --') : _('any');

		if (family != null) {
			var choices = this.transformHostHints(family, hosts);

			for (var i = 0; i < choices[0].length; i++)
				o.value(choices[0][i], choices[1][choices[0][i]]);
		}

		/* force combobox rendering */
		o.transformChoices = function() {
			return this.super('transformChoices', []) || {};
		};

		return o;
	},

	addLocalIPOption: function(s, tab, name, label, description, devices) {
		var o = s.taboption(tab, form.Value, name, label, description);
		var fw4 = L.hasSystemFeature('firewall4');

		o.modalonly = true;
		o.datatype = !fw4?'ip4addr("nomask")':'ipaddr("nomask")';
		o.placeholder = _('any');

		L.sortedKeys(devices, 'name').forEach(function(dev) {
			var ip4addrs = devices[dev].ipaddrs;
			var ip6addrs = devices[dev].ip6addrs;

			if (!L.isObject(devices[dev].flags) || devices[dev].flags.loopback)
				return;

			for (var i = 0; Array.isArray(ip4addrs) && i < ip4addrs.length; i++) {
				if (!L.isObject(ip4addrs[i]) || !ip4addrs[i].address)
					continue;

				o.value(ip4addrs[i].address, E([], [
					ip4addrs[i].address, ' (', E('strong', {}, [dev]), ')'
				]));
			}
			for (var i = 0; fw4 && Array.isArray(ip6addrs) && i < ip6addrs.length; i++) {
				if (!L.isObject(ip6addrs[i]) || !ip6addrs[i].address)
					continue;

				o.value(ip6addrs[i].address, E([], [
					ip6addrs[i].address, ' (', E('strong', {}, [dev]), ')'
				]));
			}
		});

		return o;
	},

	addMACOption: function(s, tab, name, label, description, hosts) {
		var o = s.taboption(tab, this.CBIDynamicMultiValueList, name, label, description);

		o.modalonly = true;
		o.datatype = 'list(macaddr)';
		o.placeholder = _('-- add MAC --');

		L.sortedKeys(hosts).forEach(function(mac) {
			o.value(mac, E([], [ mac, ' (', E('strong', {}, [
				hosts[mac].name ||
				L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] ||
				L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0] ||
				'?'
			]), ')' ]));
		});

		return o;
	},

	CBIProtocolSelect: form.MultiValue.extend({
		__name__: 'CBI.ProtocolSelect',

		addChoice: function(value, label) {
			if (!Array.isArray(this.keylist) || this.keylist.indexOf(value) == -1)
				this.value(value, label);
		},

		load: function(section_id) {
			var cfgvalue = L.toArray(this.super('load', [section_id]) || this.default).sort();

			['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue).forEach(L.bind(function(value) {
				switch (value) {
				case 'all':
				case 'any':
				case '*':
					this.addChoice('all', _('Any'));
					break;

				case 'tcpudp':
					this.addChoice('tcp', 'TCP');
					this.addChoice('udp', 'UDP');
					break;

				default:
					var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
					    p = lookupProto(m ? +m[1] : value);

					this.addChoice(p[2], p[1]);
					break;
				}
			}, this));

			if (cfgvalue == '*' || cfgvalue == 'any' || cfgvalue == 'all')
				cfgvalue = 'all';

			return cfgvalue;
		},

		renderWidget: function(section_id, option_index, cfgvalue) {
			var value = (cfgvalue != null) ? cfgvalue : this.default,
			    choices = this.transformChoices();

			var widget = new ui.Dropdown(L.toArray(value), choices, {
				id: this.cbid(section_id),
				sort: this.keylist,
				multiple: true,
				optional: false,
				display_items: 10,
				dropdown_items: -1,
				create: true,
				disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
				validate: function(value) {
					var v = L.toArray(value);

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

						var m = v[i].match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);

						if (m ? (+m[1] > 255) : (lookupProto(v[i])[0] == -1))
							return _('Unrecognized protocol');
					}

					return true;
				}
			});

			widget.createChoiceElement = function(sb, value) {
				var p = lookupProto(value);

				return ui.Dropdown.prototype.createChoiceElement.call(this, sb, p[2], p[1]);
			};

			widget.createItems = function(sb, value) {
				var values = L.toArray(value).map(function(value) {
					var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
					    p = lookupProto(m ? +m[1] : value);

					return (p[0] > -1) ? p[2] : p[1];
				});

				values.sort();

				return ui.Dropdown.prototype.createItems.call(this, sb, values.join(' '));
			};

			widget.toggleItem = function(sb, li) {
				var value = li.getAttribute('data-value'),
				    toggleFn = ui.Dropdown.prototype.toggleItem;

				toggleFn.call(this, sb, li);

				if (value == 'all') {
					var items = li.parentNode.querySelectorAll('li[data-value]');

					for (var j = 0; j < items.length; j++)
						if (items[j] !== li)
							toggleFn.call(this, sb, items[j], false);
				}
				else {
					toggleFn.call(this, sb, li.parentNode.querySelector('li[data-value="all"]'), false);
				}
			};

			return widget.render();
		}
	}),

	checkLegacySNAT: function() {
		var redirects = uci.sections('firewall', 'redirect');

		for (var i = 0; i < redirects.length; i++)
			if ((redirects[i]['target'] || '').toLowerCase() == 'snat')
				return true;

		return false;
	},

	handleMigration: function(ev) {
		var redirects = uci.sections('firewall', 'redirect'),
		    tasks = [];

		var mapping = {
			dest: 'src',
			reflection: null,
			reflection_src: null,
			src_dip: 'snat_ip',
			src_dport: 'snat_port',
			src: null
		};

		for (var i = 0; i < redirects.length; i++) {
			if ((redirects[i]['target'] || '').toLowerCase() != 'snat')
				continue;

			var sid = uci.add('firewall', 'nat');

			for (var opt in redirects[i]) {
				if (opt.charAt(0) == '.')
					continue;

				if (mapping[opt] === null)
					continue;

				uci.set('firewall', sid, mapping[opt] || opt, redirects[i][opt]);
			}

			uci.remove('firewall', redirects[i]['.name']);
		}

		return uci.save()
			.then(L.bind(ui.changes.init, ui.changes))
			.then(L.bind(ui.changes.apply, ui.changes));
	},

	renderMigration: function() {
		ui.showModal(_('Firewall configuration migration'), [
			E('p', _('The existing firewall configuration needs to be changed for LuCI to function properly.')),
			E('p', _('Upon pressing "Continue", "redirect" sections with target "SNAT" will be converted to "nat" sections and the firewall will be restarted to apply the updated configuration.')),
			E('div', { 'class': 'right' },
				E('button', {
					'class': 'btn cbi-button-action important',
					'click': ui.createHandlerFn(this, 'handleMigration')
				}, _('Continue')))
		]);
	},
});
