Ext.ns('PVE');

console.log("Starting Proxmox VE Manager");

Ext.Ajax.defaultHeaders = {
    'Accept': 'application/json',
};

Ext.define('PVE.Utils', {
 utilities: {

    // this singleton contains miscellaneous utilities

    toolkit: undefined, // (extjs|touch), set inside Toolkit.js

    bus_match: /^(ide|sata|virtio|scsi)(\d+)$/,

    log_severity_hash: {
	0: "panic",
	1: "alert",
	2: "critical",
	3: "error",
	4: "warning",
	5: "notice",
	6: "info",
	7: "debug",
    },

    support_level_hash: {
	'c': gettext('Community'),
	'b': gettext('Basic'),
	's': gettext('Standard'),
	'p': gettext('Premium'),
    },

    noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit '
      +'<a target="_blank" href="https://www.proxmox.com/en/proxmox-virtual-environment/pricing">'
      +'www.proxmox.com</a> to get a list of available options.',

    getClusterSubscriptionLevel: async function() {
	let { result } = await Proxmox.Async.api2({ url: '/cluster/status' });
	let levelMap = Object.fromEntries(
	  result.data.filter(v => v.type === 'node').map(v => [v.name, v.level]),
	);
	return levelMap;
    },

    kvm_ostypes: {
	'Linux': [
	    { desc: '6.x - 2.6 Kernel', val: 'l26' },
	    { desc: '2.4 Kernel - x86', val: 'l24' },
	],
	'Microsoft Windows': [
	    { desc: '11/2022', val: 'win11' },
	    { desc: '10/2016/2019 - x86', val: 'win10' },
	    { desc: '8.x/2012/2012r2 - x86', val: 'win8' },
	    { desc: '7/2008r2 - x86', val: 'win7' },
	    { desc: 'Vista/2008 - x86', val: 'w2k8' },
	    { desc: 'XP/2003 - x86', val: 'wxp' },
	    { desc: '2000 - x86', val: 'w2k' },
	],
	'Other': [
	    { desc: '-', val: 'other' },
	],
	'Solaris Kernel - x86': [
	    { desc: '-', val: 'solaris' },
	],
    },

    is_windows: function(ostype) {
	for (let entry of PVE.Utils.kvm_ostypes['Microsoft Windows']) {
	    if (entry.val === ostype) {
		return true;
	    }
	}
	return false;
    },

    get_health_icon: function(state, circle) {
	if (circle === undefined) {
	    circle = false;
	}

	if (state === undefined) {
	    state = 'uknown';
	}

	var icon = 'faded fa-question';
	switch (state) {
	    case 'good':
		icon = 'good fa-check';
		break;
	    case 'upgrade':
		icon = 'warning fa-upload';
		break;
	    case 'old':
		icon = 'warning fa-refresh';
		break;
	    case 'warning':
		icon = 'warning fa-exclamation';
		break;
	    case 'critical':
		icon = 'critical fa-times';
		break;
	    default: break;
	}

	if (circle) {
	    icon += '-circle';
	}

	return icon;
    },

    parse_ceph_version: function(service) {
	if (service.ceph_version_short) {
	    return service.ceph_version_short;
	}

	if (service.ceph_version) {
	    // See PVE/Ceph/Tools.pm - get_local_version
	    const match = service.ceph_version.match(/^ceph.*\sv?(\d+(?:\.\d+)+)/);
	    if (match) {
		return match[1];
	    }
	}

	return undefined;
    },

    compare_ceph_versions: function(a, b) {
	let avers = [];
	let bvers = [];

	if (a === b) {
	    return 0;
	}

	if (Ext.isArray(a)) {
	    avers = a.slice(); // copy array
	} else {
	    avers = a.toString().split('.');
	}

	if (Ext.isArray(b)) {
	    bvers = b.slice(); // copy array
	} else {
	    bvers = b.toString().split('.');
	}

	for (;;) {
	    let av = avers.shift();
	    let bv = bvers.shift();

	    if (av === undefined && bv === undefined) {
		return 0;
	    } else if (av === undefined) {
		return -1;
	    } else if (bv === undefined) {
		return 1;
	    } else {
		let diff = parseInt(av, 10) - parseInt(bv, 10);
		if (diff !== 0) return diff;
		// else we need to look at the next parts
	    }
	}
    },

    get_ceph_icon_html: function(health, fw) {
	var state = PVE.Utils.map_ceph_health[health];
	var cls = PVE.Utils.get_health_icon(state);
	if (fw) {
	    cls += ' fa-fw';
	}
	return "<i class='fa " + cls + "'></i> ";
    },

    map_ceph_health: {
	'HEALTH_OK': 'good',
	'HEALTH_UPGRADE': 'upgrade',
	'HEALTH_OLD': 'old',
	'HEALTH_WARN': 'warning',
	'HEALTH_ERR': 'critical',
    },

    render_sdn_pending: function(rec, value, key, index) {
	if (rec.data.state === undefined || rec.data.state === null) {
	    return value;
	}

	if (rec.data.state === 'deleted') {
	    if (value === undefined) {
		return ' ';
	    } else {
		return '<div style="text-decoration: line-through;">'+ value +'</div>';
	    }
	} else if (rec.data.pending[key] !== undefined && rec.data.pending[key] !== null) {
	    if (rec.data.pending[key] === 'deleted') {
		return ' ';
	    } else {
		return rec.data.pending[key];
	    }
	}
	return value;
    },

    render_sdn_pending_state: function(rec, value) {
	if (value === undefined || value === null) {
	    return ' ';
	}

	let icon = `<i class="fa fa-fw fa-refresh warning"></i>`;

	if (value === 'deleted') {
	    return '<span>' + icon + value + '</span>';
	}

	let tip = gettext('Pending Changes') + ': <br>';

	for (const [key, keyvalue] of Object.entries(rec.data.pending)) {
	    if ((rec.data[key] !== undefined && rec.data.pending[key] !== rec.data[key]) ||
		rec.data[key] === undefined
	    ) {
		tip += `${key}: ${keyvalue} <br>`;
	    }
	}
	return '<span data-qtip="' + tip + '">'+ icon + value + '</span>';
    },

    render_ceph_health: function(healthObj) {
	var state = {
	    iconCls: PVE.Utils.get_health_icon(),
	    text: '',
	};

	if (!healthObj || !healthObj.status) {
	    return state;
	}

	var health = PVE.Utils.map_ceph_health[healthObj.status];

	state.iconCls = PVE.Utils.get_health_icon(health, true);
	state.text = healthObj.status;

	return state;
    },

    render_zfs_health: function(value) {
	if (typeof value === 'undefined') {
	    return "";
	}
	var iconCls = 'question-circle';
	switch (value) {
	    case 'AVAIL':
	    case 'ONLINE':
		iconCls = 'check-circle good';
		break;
	    case 'REMOVED':
	    case 'DEGRADED':
		iconCls = 'exclamation-circle warning';
		break;
	    case 'UNAVAIL':
	    case 'FAULTED':
	    case 'OFFLINE':
		iconCls = 'times-circle critical';
		break;
	    default: //unknown
	}

	return '<i class="fa fa-' + iconCls + '"></i> ' + value;
    },

    render_pbs_fingerprint: fp => fp.substring(0, 23),

    render_backup_encryption: function(v, meta, record) {
	if (!v) {
	    return gettext('No');
	}

	let tip = '';
	if (v.match(/^[a-fA-F0-9]{2}:/)) { // fingerprint
	    tip = `Key fingerprint ${PVE.Utils.render_pbs_fingerprint(v)}`;
	}
	let icon = `<i class="fa fa-fw fa-lock good"></i>`;
	return `<span data-qtip="${tip}">${icon} ${gettext('Encrypted')}</span>`;
    },

    render_backup_verification: function(v, meta, record) {
	let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
	if (v === undefined || v === null) {
	    return i('question-circle-o warning', gettext('None'));
	}
	let tip = "";
	let txt = gettext('Failed');
	let iconCls = 'times critical';
	if (v.state === 'ok') {
	    txt = gettext('OK');
	    iconCls = 'check good';
	    let now = Date.now() / 1000;
	    let task = Proxmox.Utils.parse_task_upid(v.upid);
	    let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
	    tip = `Last verify task started on ${verify_time}`;
	    if (now - v.starttime > 30 * 24 * 60 * 60) {
		tip = `Last verify task over 30 days ago: ${verify_time}`;
		iconCls = 'check warning';
	    }
	}
	return `<span data-qtip="${tip}"> ${i(iconCls, txt)} </span>`;
    },

    render_backup_status: function(value, meta, record) {
	if (typeof value === 'undefined') {
	    return "";
	}

	let iconCls = 'check-circle good';
	let text = gettext('Yes');

	if (!PVE.Parser.parseBoolean(value.toString())) {
	    iconCls = 'times-circle critical';

	    text = gettext('No');

	    let reason = record.get('reason');
	    if (typeof reason !== 'undefined') {
		if (reason in PVE.Utils.backup_reasons_table) {
		    reason = PVE.Utils.backup_reasons_table[record.get('reason')];
		}
		text = `${text} - ${reason}`;
	    }
	}

	return `<i class="fa fa-${iconCls}"></i> ${text}`;
    },

    render_backup_days_of_week: function(val) {
	var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
	var selected = [];
	var cur = -1;
	val.split(',').forEach(function(day) {
	    cur++;
	    var dow = (dows.indexOf(day)+6)%7;
	    if (cur === dow) {
		if (selected.length === 0 || selected[selected.length-1] === 0) {
		    selected.push(1);
		} else {
		    selected[selected.length-1]++;
		}
	    } else {
		while (cur < dow) {
		    cur++;
		    selected.push(0);
		}
		selected.push(1);
	    }
	});

	cur = -1;
	var days = [];
	selected.forEach(function(item) {
	    cur++;
	    if (item > 2) {
		days.push(Ext.Date.dayNames[cur+1] + '-' + Ext.Date.dayNames[(cur+item)%7]);
		cur += item-1;
	    } else if (item === 2) {
		days.push(Ext.Date.dayNames[cur+1]);
		days.push(Ext.Date.dayNames[(cur+2)%7]);
		cur++;
	    } else if (item === 1) {
		days.push(Ext.Date.dayNames[(cur+1)%7]);
	    }
	});
	return days.join(', ');
    },

    render_backup_selection: function(value, metaData, record) {
	let allExceptText = gettext('All except {0}');
	let allText = '-- ' + gettext('All') + ' --';
	if (record.data.all) {
	    if (record.data.exclude) {
		return Ext.String.format(allExceptText, record.data.exclude);
	    }
	    return allText;
	}
	if (record.data.vmid) {
	    return record.data.vmid;
	}

	if (record.data.pool) {
	    return "Pool '"+ record.data.pool + "'";
	}

	return "-";
    },

    backup_reasons_table: {
	'backup=yes': gettext('Enabled'),
	'backup=no': gettext('Disabled'),
	'enabled': gettext('Enabled'),
	'disabled': gettext('Disabled'),
	'not a volume': gettext('Not a volume'),
	'efidisk but no OMVF BIOS': gettext('EFI Disk without OMVF BIOS'),
    },

    renderNotFound: what => Ext.String.format(gettext("No {0} found"), what),

    get_kvm_osinfo: function(value) {
	var info = { base: 'Other' }; // default
	if (value) {
	    Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) {
		Ext.each(PVE.Utils.kvm_ostypes[k], function(e) {
		    if (e.val === value) {
			info = { desc: e.desc, base: k };
		    }
		});
	    });
	}
	return info;
    },

    render_kvm_ostype: function(value) {
	var osinfo = PVE.Utils.get_kvm_osinfo(value);
	if (osinfo.desc && osinfo.desc !== '-') {
	    return osinfo.base + ' ' + osinfo.desc;
	} else {
	    return osinfo.base;
	}
    },

    render_hotplug_features: function(value) {
	var fa = [];

	if (!value || value === '0') {
	    return gettext('Disabled');
	}

	if (value === '1') {
	    value = 'disk,network,usb';
	}

	Ext.each(value.split(','), function(el) {
	    if (el === 'disk') {
		fa.push(gettext('Disk'));
	    } else if (el === 'network') {
		fa.push(gettext('Network'));
	    } else if (el === 'usb') {
		fa.push('USB');
	    } else if (el === 'memory') {
		fa.push(gettext('Memory'));
	    } else if (el === 'cpu') {
		fa.push(gettext('CPU'));
	    } else {
		fa.push(el);
	    }
	});

	return fa.join(', ');
    },

    render_localtime: function(value) {
	if (value === '__default__') {
	    return Proxmox.Utils.defaultText + ' (' + gettext('Enabled for Windows') + ')';
	}
	return Proxmox.Utils.format_boolean(value);
    },

    render_qga_features: function(config) {
	if (!config) {
	    return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
	}
	let qga = PVE.Parser.parsePropertyString(config, 'enabled');
	if (!PVE.Parser.parseBoolean(qga.enabled)) {
	    return Proxmox.Utils.disabledText;
	}
	delete qga.enabled;

	let agentstring = Proxmox.Utils.enabledText;

	for (const [key, value] of Object.entries(qga)) {
	    let displayText = Proxmox.Utils.disabledText;
	    if (key === 'type') {
		let map = {
		    isa: "ISA",
		    virtio: "VirtIO",
		};
		displayText = map[value] || Proxmox.Utils.unknownText;
	    } else if (key === 'freeze-fs-on-backup' && PVE.Parser.parseBoolean(value)) {
		continue;
	    } else if (PVE.Parser.parseBoolean(value)) {
		displayText = Proxmox.Utils.enabledText;
	    }
	    agentstring += `, ${key}: ${displayText}`;
	}

	return agentstring;
    },

    render_qemu_machine: function(value) {
	return value || Proxmox.Utils.defaultText;
    },

    render_qemu_bios: function(value) {
	if (!value) {
	    return "OVMF (UEFI)";
	} else if (value === 'seabios') {
	    return "SeaBIOS";
	} else if (value === 'ovmf') {
	    return "OVMF (UEFI)";
	} else {
	    return value;
	}
    },

    render_dc_ha_opts: function(value) {
	if (!value) {
	    return Proxmox.Utils.defaultText;
	} else {
	    return PVE.Parser.printPropertyString(value);
	}
    },
    render_as_property_string: v => !v ? Proxmox.Utils.defaultText : PVE.Parser.printPropertyString(v),

    render_scsihw: function(value) {
	if (!value || value === '__default__') {
	    return Proxmox.Utils.defaultText + '(VirtIO SCSI)';
	} else if (value === 'virtio-scsi-pci') {
	    return 'VirtIO SCSI';
	} else if (value === 'virtio-scsi-single') {
	    return 'VirtIO SCSI single';
	} else {
	    return value;
	}
    },

    render_spice_enhancements: function(values) {
	let props = PVE.Parser.parsePropertyString(values);
	if (Ext.Object.isEmpty(props)) {
	    return Proxmox.Utils.noneText;
	}

	let output = [];
	if (PVE.Parser.parseBoolean(props.foldersharing)) {
	    output.push('Folder Sharing: ' + gettext('Enabled'));
	}
	if (props.videostreaming === 'all' || props.videostreaming === 'filter') {
	    output.push('Video Streaming: ' + props.videostreaming);
	}
	return output.join(', ');
    },

    // fixme: auto-generate this
    // for now, please keep in sync with PVE::Tools::kvmkeymaps
    kvm_keymaps: {
	'__default__': Proxmox.Utils.defaultText,
	//ar: 'Arabic',
	da: 'Danish',
	de: 'German',
	'de-ch': 'German (Swiss)',
	'en-gb': 'English (UK)',
	'en-us': 'English (USA)',
	es: 'Spanish',
	//et: 'Estonia',
	fi: 'Finnish',
	//fo: 'Faroe Islands',
	fr: 'French',
	'fr-be': 'French (Belgium)',
	'fr-ca': 'French (Canada)',
	'fr-ch': 'French (Swiss)',
	//hr: 'Croatia',
	hu: 'Hungarian',
	is: 'Icelandic',
	it: 'Italian',
	ja: 'Japanese',
	lt: 'Lithuanian',
	//lv: 'Latvian',
	mk: 'Macedonian',
	nl: 'Dutch',
	//'nl-be': 'Dutch (Belgium)',
	no: 'Norwegian',
	pl: 'Polish',
	pt: 'Portuguese',
	'pt-br': 'Portuguese (Brazil)',
	//ru: 'Russian',
	sl: 'Slovenian',
	sv: 'Swedish',
	//th: 'Thai',
	tr: 'Turkish',
    },

    kvm_vga_drivers: {
	'__default__': Proxmox.Utils.defaultText,
	std: gettext('Standard VGA'),
	serial0: gettext('Serial terminal') + ' 0',
	serial1: gettext('Serial terminal') + ' 1',
	serial2: gettext('Serial terminal') + ' 2',
	serial3: gettext('Serial terminal') + ' 3',
	virtio: 'VirtIO-GPU',
	'virtio-gl': 'VirGL GPU',
	ramfb: 'ramfb',
        mdev: 'mdev',
	none: Proxmox.Utils.noneText,
    },

    render_kvm_language: function(value) {
	if (!value || value === '__default__') {
	    return Proxmox.Utils.defaultText;
	}
	let text = PVE.Utils.kvm_keymaps[value];
	return text ? `${text} (${value})` : value;
    },

    console_map: {
	'__default__': Proxmox.Utils.defaultText + ' (xterm.js)',
	'vv': 'SPICE (remote-viewer)',
	'html5': 'HTML5 (noVNC)',
	'xtermjs': 'xterm.js',
    },

    render_console_viewer: function(value) {
	value = value || '__default__';
	return PVE.Utils.console_map[value] || value;
    },

    render_kvm_vga_driver: function(value) {
	if (!value) {
	    return Proxmox.Utils.defaultText;
	}
	let vga = PVE.Parser.parsePropertyString(value, 'type');
	let text = PVE.Utils.kvm_vga_drivers[vga.type];
	if (!vga.type) {
	    text = Proxmox.Utils.defaultText;
	}
	return text ? `${text} (${value})` : value;
    },

    render_kvm_startup: function(value) {
	var startup = PVE.Parser.parseStartup(value);

	var res = 'order=';
	if (startup.order === undefined) {
	    res += 'any';
	} else {
	    res += startup.order;
	}
	if (startup.up !== undefined) {
	    res += ',up=' + startup.up;
	}
	if (startup.down !== undefined) {
	    res += ',down=' + startup.down;
	}

	return res;
    },

    extractFormActionError: function(action) {
	var msg;
	switch (action.failureType) {
	case Ext.form.action.Action.CLIENT_INVALID:
	    msg = gettext('Form fields may not be submitted with invalid values');
	    break;
	case Ext.form.action.Action.CONNECT_FAILURE:
	    msg = gettext('Connection error');
	    var resp = action.response;
	    if (resp.status && resp.statusText) {
		msg += " " + resp.status + ": " + resp.statusText;
	    }
	    break;
	case Ext.form.action.Action.LOAD_FAILURE:
	case Ext.form.action.Action.SERVER_INVALID:
	    msg = Proxmox.Utils.extractRequestError(action.result, true);
	    break;
	}
	return msg;
    },

    contentTypes: {
	'images': gettext('Disk image'),
	'backup': gettext('VZDump backup file'),
	'vztmpl': gettext('Container template'),
	'iso': gettext('ISO image'),
	'rootdir': gettext('Container'),
	'snippets': gettext('Snippets'),
	'import': gettext('Import'),
    },

    volume_is_qemu_backup: function(volid, format) {
	return format === 'pbs-vm' || volid.match(':backup/vzdump-qemu-');
    },

    volume_is_lxc_backup: function(volid, format) {
	return format === 'pbs-ct' || volid.match(':backup/vzdump-(lxc|openvz)-');
    },

    authSchema: {
	ad: {
	    name: gettext('Active Directory Server'),
	    ipanel: 'pveAuthADPanel',
	    syncipanel: 'pveAuthLDAPSyncPanel',
	    add: true,
	    tfa: true,
	    pwchange: true,
	},
	ldap: {
	    name: gettext('LDAP Server'),
	    ipanel: 'pveAuthLDAPPanel',
	    syncipanel: 'pveAuthLDAPSyncPanel',
	    add: true,
	    tfa: true,
	    pwchange: true,
	},
	openid: {
	    name: gettext('OpenID Connect Server'),
	    ipanel: 'pveAuthOpenIDPanel',
	    add: true,
	    tfa: false,
	    pwchange: false,
	    iconCls: 'pmx-itype-icon-openid-logo',
	},
	pam: {
	    name: 'Linux PAM',
	    ipanel: 'pveAuthBasePanel',
	    add: false,
	    tfa: true,
	    pwchange: true,
	},
	pve: {
	    name: 'Proxmox VE authentication server',
	    ipanel: 'pveAuthBasePanel',
	    add: false,
	    tfa: true,
	    pwchange: true,
	},
    },

    storageSchema: {
	dir: {
	    name: Proxmox.Utils.directoryText,
	    ipanel: 'DirInputPanel',
	    faIcon: 'folder',
	    backups: true,
	},
	lvm: {
	    name: 'LVM',
	    ipanel: 'LVMInputPanel',
	    faIcon: 'folder',
	    backups: false,
	},
	lvmthin: {
	    name: 'LVM-Thin',
	    ipanel: 'LvmThinInputPanel',
	    faIcon: 'folder',
	    backups: false,
	},
	btrfs: {
	    name: 'BTRFS',
	    ipanel: 'BTRFSInputPanel',
	    faIcon: 'folder',
	    backups: true,
	},
	nfs: {
	    name: 'NFS',
	    ipanel: 'NFSInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	cifs: {
	    name: 'SMB/CIFS',
	    ipanel: 'CIFSInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	glusterfs: {
	    name: 'GlusterFS',
	    ipanel: 'GlusterFsInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	iscsi: {
	    name: 'iSCSI',
	    ipanel: 'IScsiInputPanel',
	    faIcon: 'building',
	    backups: false,
	},
	cephfs: {
	    name: 'CephFS',
	    ipanel: 'CephFSInputPanel',
	    faIcon: 'building',
	    backups: true,
	},
	pvecephfs: {
	    name: 'CephFS (PVE)',
	    ipanel: 'CephFSInputPanel',
	    hideAdd: true,
	    faIcon: 'building',
	    backups: true,
	},
	rbd: {
	    name: 'RBD',
	    ipanel: 'RBDInputPanel',
	    faIcon: 'building',
	    backups: false,
	},
	pveceph: {
	    name: 'RBD (PVE)',
	    ipanel: 'RBDInputPanel',
	    hideAdd: true,
	    faIcon: 'building',
	    backups: false,
	},
	zfs: {
	    name: 'ZFS over iSCSI',
	    ipanel: 'ZFSInputPanel',
	    faIcon: 'building',
	    backups: false,
	},
	zfspool: {
	    name: 'ZFS',
	    ipanel: 'ZFSPoolInputPanel',
	    faIcon: 'folder',
	    backups: false,
	},
	pbs: {
	    name: 'Proxmox Backup Server',
	    ipanel: 'PBSInputPanel',
	    faIcon: 'floppy-o',
	    backups: true,
	},
	drbd: {
	    name: 'DRBD',
	    hideAdd: true,
	    backups: false,
	},
	esxi: {
	    name: 'ESXi',
	    ipanel: 'ESXIInputPanel',
	    faIcon: 'cloud-download',
	    backups: false,
	},
    },

    sdnvnetSchema: {
	vnet: {
	    name: 'vnet',
	    faIcon: 'folder',
	},
    },

    sdnzoneSchema: {
	zone: {
	     name: 'zone',
	     hideAdd: true,
	},
	simple: {
	    name: 'Simple',
	    ipanel: 'SimpleInputPanel',
	    faIcon: 'th',
	},
	vlan: {
	    name: 'VLAN',
	    ipanel: 'VlanInputPanel',
	    faIcon: 'th',
	},
	qinq: {
	    name: 'QinQ',
	    ipanel: 'QinQInputPanel',
	    faIcon: 'th',
	},
	vxlan: {
	    name: 'VXLAN',
	    ipanel: 'VxlanInputPanel',
	    faIcon: 'th',
	},
	evpn: {
	    name: 'EVPN',
	    ipanel: 'EvpnInputPanel',
	    faIcon: 'th',
	},
    },

    sdncontrollerSchema: {
	controller: {
	     name: 'controller',
	     hideAdd: true,
	},
	evpn: {
	    name: 'evpn',
	    ipanel: 'EvpnInputPanel',
	    faIcon: 'crosshairs',
	},
	bgp: {
	    name: 'bgp',
	    ipanel: 'BgpInputPanel',
	    faIcon: 'crosshairs',
	},
	isis: {
	    name: 'isis',
	    ipanel: 'IsisInputPanel',
	    faIcon: 'crosshairs',
	},
    },

    sdnipamSchema: {
	ipam: {
	     name: 'ipam',
	     hideAdd: true,
	},
	pve: {
	    name: 'PVE',
	    ipanel: 'PVEIpamInputPanel',
	    faIcon: 'th',
	    hideAdd: true,
	},
	netbox: {
	    name: 'Netbox',
	    ipanel: 'NetboxInputPanel',
	    faIcon: 'th',
	},
	phpipam: {
	    name: 'PhpIpam',
	    ipanel: 'PhpIpamInputPanel',
	    faIcon: 'th',
	},
    },

    sdndnsSchema: {
	dns: {
	     name: 'dns',
	     hideAdd: true,
	},
	powerdns: {
	    name: 'powerdns',
	    ipanel: 'PowerdnsInputPanel',
	    faIcon: 'th',
	},
    },

    format_sdnvnet_type: function(value, md, record) {
	var schema = PVE.Utils.sdnvnetSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },

    format_sdnzone_type: function(value, md, record) {
	var schema = PVE.Utils.sdnzoneSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },

    format_sdncontroller_type: function(value, md, record) {
	var schema = PVE.Utils.sdncontrollerSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },

    format_sdnipam_type: function(value, md, record) {
	var schema = PVE.Utils.sdnipamSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },

    format_sdndns_type: function(value, md, record) {
	var schema = PVE.Utils.sdndnsSchema[value];
	if (schema) {
	    return schema.name;
	}
	return Proxmox.Utils.unknownText;
    },

    format_storage_type: function(value, md, record) {
	if (value === 'rbd') {
	    value = !record || record.get('monhost') ? 'rbd' : 'pveceph';
	} else if (value === 'cephfs') {
	    value = !record || record.get('monhost') ? 'cephfs' : 'pvecephfs';
	}

	let schema = PVE.Utils.storageSchema[value];
	return schema?.name ?? value;
    },

    format_ha: function(value) {
	var text = Proxmox.Utils.noneText;

	if (value.managed) {
	    text = value.state || Proxmox.Utils.noneText;

	    text += ', ' + Proxmox.Utils.groupText + ': ';
	    text += value.group || Proxmox.Utils.noneText;
	}

	return text;
    },

    format_content_types: function(value) {
	return value.split(',').sort().map(function(ct) {
	    return PVE.Utils.contentTypes[ct] || ct;
	}).join(', ');
    },

    render_storage_content: function(value, metaData, record) {
	let data = record.data;
	let result;
	if (Ext.isNumber(data.channel) &&
	    Ext.isNumber(data.id) &&
	    Ext.isNumber(data.lun)) {
	    result = "CH " +
		Ext.String.leftPad(data.channel, 2, '0') +
		" ID " + data.id + " LUN " + data.lun;
	} else if (data.content === 'import') {
	    if (data.volid.match(/^.*?:import\//)) {
		// dir-based storages
		result = data.volid.replace(/^.*?:import\//, '');
	    } else {
		// esxi storage
		result = data.volid.replace(/^.*?:/, '');
	    }
	} else {
	    result = data.volid.replace(/^.*?:(.*?\/)?/, '');
	}
	return Ext.String.htmlEncode(result);
    },

    render_serverity: function(value) {
	return PVE.Utils.log_severity_hash[value] || value;
    },

    calculate_hostcpu: function(data) {
	if (!(data.uptime && Ext.isNumeric(data.cpu))) {
	    return -1;
	}

	if (data.type !== 'qemu' && data.type !== 'lxc') {
	    return -1;
	}

	var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
	var node = PVE.data.ResourceStore.getAt(index);
	if (!Ext.isDefined(node) || node === null) {
	    return -1;
	}
	var maxcpu = node.data.maxcpu || 1;

	if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
	    return -1;
	}

	return (data.cpu/maxcpu) * data.maxcpu;
    },

    render_hostcpu: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!(record.data.uptime && Ext.isNumeric(record.data.cpu))) {
	    return '';
	}

	if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
	    return '';
	}

	var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
	var node = PVE.data.ResourceStore.getAt(index);
	if (!Ext.isDefined(node) || node === null) {
	    return '';
	}
	var maxcpu = node.data.maxcpu || 1;

	if (!Ext.isNumeric(maxcpu) || maxcpu < 1) {
	    return '';
	}

	var per = (record.data.cpu/maxcpu) * record.data.maxcpu * 100;
	const cpu_label = maxcpu > 1 ? 'CPUs' : 'CPU';

	return `${per.toFixed(1)}% of ${maxcpu} ${cpu_label}`;
    },

    render_bandwidth: function(value) {
	if (!Ext.isNumeric(value)) {
	    return '';
	}

	return Proxmox.Utils.format_size(value) + '/s';
    },

    render_timestamp_human_readable: function(value) {
	return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s');
    },

    // render a timestamp or pending
    render_next_event: function(value) {
	if (!value) {
	    return '-';
	}
	let now = new Date(), next = new Date(value * 1000);
	if (next < now) {
	    return gettext('pending');
	}
	return Proxmox.Utils.render_timestamp(value);
    },

    calculate_mem_usage: function(data) {
	if (!Ext.isNumeric(data.mem) ||
	    data.maxmem === 0 ||
	    data.uptime < 1) {
	    return -1;
	}

	return data.mem / data.maxmem;
    },

    calculate_hostmem_usage: function(data) {
	if (data.type !== 'qemu' && data.type !== 'lxc') {
	    return -1;
	}

        var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
	var node = PVE.data.ResourceStore.getAt(index);

        if (!Ext.isDefined(node) || node === null) {
	    return -1;
        }
	var maxmem = node.data.maxmem || 0;

	if (!Ext.isNumeric(data.mem) ||
	    maxmem === 0 ||
	    data.uptime < 1) {
	    return -1;
	}

	return data.mem / maxmem;
    },

    render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!Ext.isNumeric(value) || value === -1) {
	    return '';
	}
	if (value > 1) {
	    // we got no percentage but bytes
	    var mem = value;
	    var maxmem = record.data.maxmem;
	    if (!record.data.uptime ||
		maxmem === 0 ||
		!Ext.isNumeric(mem)) {
		return '';
	    }

	    return (mem*100/maxmem).toFixed(1) + " %";
	}
	return (value*100).toFixed(1) + " %";
    },

    render_hostmem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!Ext.isNumeric(record.data.mem) || value === -1) {
	    return '';
	}

	if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
	    return '';
	}

	var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
	var node = PVE.data.ResourceStore.getAt(index);
	var maxmem = node.data.maxmem || 0;

	if (record.data.mem > 1) {
	    // we got no percentage but bytes
	    var mem = record.data.mem;
	    if (!record.data.uptime ||
		maxmem === 0 ||
		!Ext.isNumeric(mem)) {
		return '';
	    }

	    return ((mem*100)/maxmem).toFixed(1) + " %";
	}
	return (value*100).toFixed(1) + " %";
    },

    render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) {
	var mem = value;
	var maxmem = record.data.maxmem;

	if (!record.data.uptime) {
	    return '';
	}

	if (!(Ext.isNumeric(mem) && maxmem)) {
	    return '';
	}

	return Proxmox.Utils.render_size(value);
    },

    calculate_disk_usage: function(data) {
	if (!Ext.isNumeric(data.disk) ||
	    ((data.type === 'qemu' || data.type === 'lxc') && data.uptime === 0) ||
	    data.maxdisk === 0
	) {
	    return -1;
	}

	return data.disk / data.maxdisk;
    },

    render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
	if (!Ext.isNumeric(value) || value === -1) {
	    return '';
	}

	return (value * 100).toFixed(1) + " %";
    },

    render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) {
	var disk = value;
	var maxdisk = record.data.maxdisk;
	var type = record.data.type;

	if (!Ext.isNumeric(disk) ||
	    maxdisk === 0 ||
	    ((type === 'qemu' || type === 'lxc') && record.data.uptime === 0)
	) {
	    return '';
	}

	return Proxmox.Utils.render_size(value);
    },

    get_object_icon_class: function(type, record) {
	var status = '';
	var objType = type;

	if (type === 'type') {
	    // for folder view
	    objType = record.groupbyid;
	} else if (record.template) {
	    // templates
	    objType = 'template';
	    status = type;
	} else if (type === 'storage' && record.content === 'import') {
	    return 'fa fa-cloud-download';
	} else {
	    // everything else
	    status = record.status + ' ha-' + record.hastate;
	}

	if (record.lock) {
	    status += ' locked lock-' + record.lock;
	}

	var defaults = PVE.tree.ResourceTree.typeDefaults[objType];
	if (defaults && defaults.iconCls) {
	    var retVal = defaults.iconCls + ' ' + status;
	    return retVal;
	}

	return '';
    },

    render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) {
	var cls = PVE.Utils.get_object_icon_class(value, record.data);

	var fa = '<i class="fa-fw x-grid-icon-custom ' + cls + '"></i> ';
	return fa + value;
    },

    render_support_level: function(value, metaData, record) {
	return PVE.Utils.support_level_hash[value] || '-';
    },

    render_upid: function(value, metaData, record) {
	var type = record.data.type;
	var id = record.data.id;

	return Proxmox.Utils.format_task_description(type, id);
    },

    render_optional_url: function(value) {
	if (value && value.match(/^https?:\/\//)) {
	    return '<a target="_blank" href="' + value + '">' + value + '</a>';
	}
	return value;
    },

    render_san: function(value) {
	var names = [];
	if (Ext.isArray(value)) {
	    value.forEach(function(val) {
		if (!Ext.isNumber(val)) {
		    names.push(val);
		}
	    });
	    return names.join('<br>');
	}
	return value;
    },

    render_full_name: function(firstname, metaData, record) {
	var first = firstname || '';
	var last = record.data.lastname || '';
	return Ext.htmlEncode(first + " " + last);
    },

    // expecting the following format:
    // [v2:10.10.10.1:6802/2008,v1:10.10.10.1:6803/2008]
    render_ceph_osd_addr: function(value) {
	value = value.trim();
	if (value.startsWith('[') && value.endsWith(']')) {
	    value = value.slice(1, -1); // remove []
	}
	value = value.replaceAll(',', '\n'); // split IPs in lines
	let retVal = '';
	for (const i of value.matchAll(/^(v[0-9]):(.*):([0-9]*)\/([0-9]*)$/gm)) {
	    retVal += `${i[1]}: ${i[2]}:${i[3]}<br>`;
	}
	return retVal.length < 1 ? value : retVal;
    },

    render_get_arch: function(value) {
	if (!value){
		return Proxmox.Utils.defaultText;
	}else{
		return value;
	}
    },

    windowHostname: function() {
	return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
            function(m, addr, offset, original) { return addr; });
    },

    openDefaultConsoleWindow: function(consoles, consoleType, vmid, nodename, vmname, cmd) {
	var dv = PVE.Utils.defaultViewer(consoles, consoleType);
	PVE.Utils.openConsoleWindow(dv, consoleType, vmid, nodename, vmname, cmd);
    },

    openConsoleWindow: function(viewer, consoleType, vmid, nodename, vmname, cmd) {
	if (vmid === undefined && (consoleType === 'kvm' || consoleType === 'lxc')) {
	    throw "missing vmid";
	}
	if (!nodename) {
	    throw "no nodename specified";
	}

	if (viewer === 'html5') {
	    PVE.Utils.openVNCViewer(consoleType, vmid, nodename, vmname, cmd);
	} else if (viewer === 'xtermjs') {
	    Proxmox.Utils.openXtermJsViewer(consoleType, vmid, nodename, vmname, cmd);
	} else if (viewer === 'vv') {
	    let url = '/nodes/' + nodename + '/spiceshell';
	    let params = {
		proxy: PVE.Utils.windowHostname(),
	    };
	    if (consoleType === 'kvm') {
		url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
	    } else if (consoleType === 'lxc') {
		url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
	    } else if (consoleType === 'upgrade') {
		params.cmd = 'upgrade';
	    } else if (consoleType === 'cmd') {
		params.cmd = cmd;
	    } else if (consoleType !== 'shell') {
		throw `unknown spice viewer type '${consoleType}'`;
	    }
	    PVE.Utils.openSpiceViewer(url, params);
	} else {
	    throw `unknown viewer type '${viewer}'`;
	}
    },

    defaultViewer: function(consoles, type) {
	var allowSpice, allowXtermjs;

	if (consoles === true) {
	    allowSpice = true;
	    allowXtermjs = true;
	} else if (typeof consoles === 'object') {
	    allowSpice = consoles.spice;
	    allowXtermjs = !!consoles.xtermjs;
	}
	let dv = PVE.UIOptions.options.console || (type === 'kvm' ? 'vv' : 'xtermjs');
	if (dv === 'vv' && !allowSpice) {
	    dv = allowXtermjs ? 'xtermjs' : 'html5';
	} else if (dv === 'xtermjs' && !allowXtermjs) {
	    dv = allowSpice ? 'vv' : 'html5';
	}

	return dv;
    },

    openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) {
	let scaling = 'off';
	if (Proxmox.Utils.toolkit !== 'touch') {
	    var sp = Ext.state.Manager.getProvider();
	    scaling = sp.get('novnc-scaling', 'off');
	}
	var url = Ext.Object.toQueryString({
	    console: vmtype, // kvm, lxc, upgrade or shell
	    novnc: 1,
	    vmid: vmid,
	    vmname: vmname,
	    node: nodename,
	    resize: scaling,
	    cmd: cmd,
	});
	var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427");
	if (nw) {
	    nw.focus();
	}
    },

    openSpiceViewer: function(url, params) {
	var downloadWithName = function(uri, name) {
	    var link = Ext.DomHelper.append(document.body, {
		tag: 'a',
		href: uri,
		css: 'display:none;visibility:hidden;height:0px;',
	    });

	    // Note: we need to tell Android, AppleWebKit and Chrome
	    // the correct file name extension
	    // but we do not set 'download' tag for other environments, because
	    // It can have strange side effects (additional user prompt on firefox)
	    if (navigator.userAgent.match(/Android|AppleWebKit|Chrome/i)) {
		link.download = name;
	    }

	    if (link.fireEvent) {
		link.fireEvent('onclick');
	    } else {
		let evt = document.createEvent("MouseEvents");
		evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
		link.dispatchEvent(evt);
	    }
	};

	Proxmox.Utils.API2Request({
	    url: url,
	    params: params,
	    method: 'POST',
	    failure: function(response, opts) {
		Ext.Msg.alert('Error', response.htmlStatus);
	    },
	    success: function(response, opts) {
		let cfg = response.result.data;
		let raw = Object.entries(cfg).reduce((acc, [k, v]) => acc + `${k}=${v}\n`, "[virt-viewer]\n");
		let spiceDownload = 'data:application/x-virt-viewer;charset=UTF-8,' + encodeURIComponent(raw);
		downloadWithName(spiceDownload, "pve-spice.vv");
	    },
	});
    },

    openTreeConsole: function(tree, record, item, index, e) {
	e.stopEvent();
	let nodename = record.data.node;
	let vmid = record.data.vmid;
	let vmname = record.data.name;
	if (record.data.type === 'qemu' && !record.data.template) {
	    Proxmox.Utils.API2Request({
		url: `/nodes/${nodename}/qemu/${vmid}/status/current`,
		failure: response => Ext.Msg.alert('Error', response.htmlStatus),
		success: function(response, opts) {
		    let conf = response.result.data;
		    let consoles = {
			spice: !!conf.spice,
			xtermjs: !!conf.serial,
		    };
		    PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
		},
	    });
	} else if (record.data.type === 'lxc' && !record.data.template) {
	    PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
	}
    },

    // test automation helper
    call_menu_handler: function(menu, text) {
	let item = menu.query('menuitem').find(el => el.text === text);
	if (item && item.handler) {
	    item.handler();
	}
    },

    createCmdMenu: function(v, record, item, index, event) {
	event.stopEvent();
	if (!(v instanceof Ext.tree.View)) {
	    v.select(record);
	}
	let menu;
	let type = record.data.type;

	if (record.data.template) {
	    if (type === 'qemu' || type === 'lxc') {
		menu = Ext.create('PVE.menu.TemplateMenu', {
		    pveSelNode: record,
		});
	    }
	} else if (type === 'qemu' || type === 'lxc' || type === 'node') {
	    menu = Ext.create('PVE.' + type + '.CmdMenu', {
		pveSelNode: record,
		nodename: record.data.node,
	    });
	} else {
	    return undefined;
	}

	menu.showAt(event.getXY());
	return menu;
    },

    // helper for deleting field which are set to there default values
    delete_if_default: function(values, fieldname, default_val, create) {
	if (values[fieldname] === '' || values[fieldname] === default_val) {
	    if (!create) {
		if (values.delete) {
		    if (Ext.isArray(values.delete)) {
			values.delete.push(fieldname);
		    } else {
			values.delete += ',' + fieldname;
		    }
		} else {
		    values.delete = fieldname;
		}
	    }

	    delete values[fieldname];
	}
    },

    loadSSHKeyFromFile: function(file, callback) {
	// ssh-keygen produces ~ 740 bytes for a 4096 bit RSA key,  current max is 16 kbit, so assume:
	// 740 * 8 for max. 32kbit (5920 bytes), round upwards to 8192 bytes, leaves lots of comment space
	PVE.Utils.loadFile(file, callback, 8192);
    },

    loadFile: function(file, callback, maxSize) {
	maxSize = maxSize || 32 * 1024;
	if (file.size > maxSize) {
	    Ext.Msg.alert(gettext('Error'), `${gettext("Invalid file size")}: ${file.size} > ${maxSize}`);
	    return;
	}
	let reader = new FileReader();
	reader.onload = evt => callback(evt.target.result);
	reader.readAsText(file);
    },

    loadTextFromFile: function(file, callback, maxBytes) {
	let maxSize = maxBytes || 8192;
	if (file.size > maxSize) {
	    Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
	    return;
	}
	let reader = new FileReader();
	reader.onload = evt => callback(evt.target.result);
	reader.readAsText(file);
    },

    diskControllerMaxIDs: {
	ide: 4,
	sata: 6,
	scsi: 31,
	virtio: 16,
	unused: 256,
    },

    // types is either undefined (all busses), an array of busses, or a single bus
    forEachBus: function(types, func) {
	let busses = Object.keys(PVE.Utils.diskControllerMaxIDs);

	if (Ext.isArray(types)) {
	    busses = types;
	} else if (Ext.isDefined(types)) {
	    busses = [types];
	}

	// check if we only have valid busses
	for (let i = 0; i < busses.length; i++) {
	    if (!PVE.Utils.diskControllerMaxIDs[busses[i]]) {
		throw "invalid bus: '" + busses[i] + "'";
	    }
	}

	for (let i = 0; i < busses.length; i++) {
	    let count = PVE.Utils.diskControllerMaxIDs[busses[i]];
	    for (let j = 0; j < count; j++) {
		let cont = func(busses[i], j);
		if (!cont && cont !== undefined) {
		    return;
		}
	    }
	}
    },

    lxc_mp_counts: {
	mp: 256,
	unused: 256,
    },

    forEachLxcMP: function(func, includeUnused) {
	for (let i = 0; i < PVE.Utils.lxc_mp_counts.mp; i++) {
	    let cont = func('mp', i, `mp${i}`);
	    if (!cont && cont !== undefined) {
		return;
	    }
	}

	if (!includeUnused) {
	    return;
	}

	for (let i = 0; i < PVE.Utils.lxc_mp_counts.unused; i++) {
	    let cont = func('unused', i, `unused${i}`);
	    if (!cont && cont !== undefined) {
		return;
	    }
	}
    },

    lxc_dev_count: 256,

    forEachLxcDev: function(func) {
	for (let i = 0; i < PVE.Utils.lxc_dev_count; i++) {
	    let cont = func(i, `dev${i}`);
	    if (!cont && cont !== undefined) {
		return;
	    }
	}
    },

    hardware_counts: {
	net: 32,
	usb: 14,
	usb_old: 5,
	hostpci: 16,
	audio: 1,
	efidisk: 1,
	serial: 4,
	rng: 1,
	tpmstate: 1,
    },

    // we can have usb6 and up only for specific machine/ostypes
    get_max_usb_count: function(ostype, machine) {
	if (!ostype) {
	    return PVE.Utils.hardware_counts.usb_old;
	}

	let match = /-(\d+).(\d+)/.exec(machine ?? '');
	if (!match || PVE.Utils.qemu_min_version([match[1], match[2]], [7, 1])) {
	    if (ostype === 'l26') {
		return PVE.Utils.hardware_counts.usb;
	    }
	    let os_match = /^win(\d+)$/.exec(ostype);
	    if (os_match && os_match[1] > 7) {
		return PVE.Utils.hardware_counts.usb;
	    }
	}

	return PVE.Utils.hardware_counts.usb_old;
    },

    // parameters are expected to be arrays, e.g. [7,1], [4,0,1]
    // returns true if toCheck is equal or greater than minVersion
    qemu_min_version: function(toCheck, minVersion) {
	let i;
	for (i = 0; i < toCheck.length && i < minVersion.length; i++) {
	    if (toCheck[i] < minVersion[i]) {
		return false;
	    }
	}

	if (minVersion.length > toCheck.length) {
	    for (; i < minVersion.length; i++) {
		if (minVersion[i] !== 0) {
		    return false;
		}
	    }
	}

	return true;
    },

    cleanEmptyObjectKeys: function(obj) {
	for (const propName of Object.keys(obj)) {
	    if (obj[propName] === null || obj[propName] === undefined) {
		delete obj[propName];
	    }
	}
    },

    acmedomain_count: 5,

    add_domain_to_acme: function(acme, domain) {
	if (acme.domains === undefined) {
	    acme.domains = [domain];
	} else {
	    acme.domains.push(domain);
	    acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index);
	}
	return acme;
    },

    remove_domain_from_acme: function(acme, domain) {
	if (acme.domains !== undefined) {
	    acme.domains = acme
		.domains
		.filter((value, index, self) => self.indexOf(value) === index && value !== domain);
	}
	return acme;
    },

    handleStoreErrorOrMask: function(view, store, regex, callback) {
	view.mon(store, 'load', function(proxy, response, success, operation) {
	    if (success) {
		Proxmox.Utils.setErrorMask(view, false);
		return;
	    }
	    let msg;
	    if (operation.error.statusText) {
		if (operation.error.statusText.match(regex)) {
		    callback(view, operation.error);
		    return;
		} else {
		    msg = operation.error.statusText + ' (' + operation.error.status + ')';
		}
	    } else {
		msg = gettext('Connection error');
	    }
	    Proxmox.Utils.setErrorMask(view, msg);
	});
    },

    showCephInstallOrMask: function(container, msg, nodename, callback) {
	if (msg.match(/not (installed|initialized)/i)) {
	    if (Proxmox.UserName === 'root@pam') {
		container.el.mask();
		if (!container.down('pveCephInstallWindow')) {
		    var isInstalled = !!msg.match(/not initialized/i);
		    var win = Ext.create('PVE.ceph.Install', {
			nodename: nodename,
		    });
		    win.getViewModel().set('isInstalled', isInstalled);
		    container.add(win);
		    win.on('close', () => {
			container.el.unmask();
		    });
		    win.show();
		    callback(win);
		}
	    } else {
		container.mask(Ext.String.format(gettext('{0} not installed.') +
		    ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
	    }
	    return true;
	} else {
	    return false;
	}
    },

    monitor_ceph_installed: function(view, rstore, nodename, maskOwnerCt) {
	PVE.Utils.handleStoreErrorOrMask(
	    view,
	    rstore,
	    /not (installed|initialized)/i,
	    (_, error) => {
		nodename = nodename || Proxmox.NodeName;
		let maskTarget = maskOwnerCt ? view.ownerCt : view;
		rstore.stopUpdate();
		PVE.Utils.showCephInstallOrMask(maskTarget, error.statusText, nodename, win => {
		    view.mon(win, 'cephInstallWindowClosed', () => rstore.startUpdate());
		});
	    },
	);
    },


    propertyStringSet: function(target, source, name, value) {
	if (source) {
	    if (value === undefined) {
		target[name] = source;
	    } else {
		target[name] = value;
	    }
	} else {
	    delete target[name];
	}
    },

    forEachCorosyncLink: function(nodeinfo, cb) {
	let re = /(?:ring|link)(\d+)_addr/;
	Ext.iterate(nodeinfo, (prop, val) => {
	    let match = re.exec(prop);
	    if (match) {
		cb(Number(match[1]), val);
	    }
	});
    },

    cpu_vendor_map: {
	'default': 'QEMU',
	'AuthenticAMD': 'AMD',
	'GenuineIntel': 'Intel',
    },

    cpu_vendor_order: {
	"AMD": 1,
	"Intel": 2,
	"QEMU": 3,
	"Host": 4,
	"_default_": 5, // includes custom models
    },

    verify_ip64_address_list: function(value, with_suffix) {
	for (let addr of value.split(/[ ,;]+/)) {
	    if (addr === '') {
		continue;
	    }

	    if (with_suffix) {
		let parts = addr.split('%');
		addr = parts[0];

		if (parts.length > 2) {
		    return false;
		}

		if (parts.length > 1 && !addr.startsWith('fe80:')) {
		    return false;
		}
	    }

	    if (!Proxmox.Utils.IP64_match.test(addr)) {
		return false;
	    }
	}

	return true;
    },

    sortByPreviousUsage: function(vmconfig, controllerList) {
	if (!controllerList) {
	    controllerList = ['ide', 'virtio', 'scsi', 'sata'];
	}
	let usedControllers = {};
	for (const type of Object.keys(PVE.Utils.diskControllerMaxIDs)) {
	    usedControllers[type] = 0;
	}

	for (const property of Object.keys(vmconfig)) {
	    if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) {
		const foundController = property.match(PVE.Utils.bus_match)[1];
		usedControllers[foundController]++;
	    }
	}

	let sortPriority = PVE.qemu.OSDefaults.getDefaults(vmconfig.ostype).busPriority;

	let sortedList = Ext.clone(controllerList);
	sortedList.sort(function(a, b) {
	    if (usedControllers[b] === usedControllers[a]) {
		return sortPriority[b] - sortPriority[a];
	    }
	    return usedControllers[b] - usedControllers[a];
	});

	return sortedList;
    },

    nextFreeDisk: function(controllers, config) {
	for (const controller of controllers) {
	    for (let i = 0; i < PVE.Utils.diskControllerMaxIDs[controller]; i++) {
		let confid = controller + i.toString();
		if (!Ext.isDefined(config[confid])) {
		    return {
			controller,
			id: i,
			confid,
		    };
		}
	    }
	}

	return undefined;
    },

    nextFreeLxcMP: function(type, config) {
	for (let i = 0; i < PVE.Utils.lxc_mp_counts[type]; i++) {
	    let confid = `${type}${i}`;
	    if (!Ext.isDefined(config[confid])) {
		return {
		    type,
		    id: i,
		    confid,
		};
	    }
	}

	return undefined;
    },

    escapeNotesTemplate: function(value) {
	let replace = {
	    '\\': '\\\\',
	    '\n': '\\n',
	};
	return value.replace(/(\\|[\n])/g, match => replace[match]);
    },

    unEscapeNotesTemplate: function(value) {
	let replace = {
	    '\\\\': '\\',
	    '\\n': '\n',
	};
	return value.replace(/(\\\\|\\n)/g, match => replace[match]);
    },

    notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],

    renderTags: function(tagstext, overrides) {
	let text = '';
	if (tagstext) {
	    let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
	    if (PVE.UIOptions.shouldSortTags()) {
		tags = tags.sort((a, b) => {
		    let alc = a.toLowerCase();
		    let blc = b.toLowerCase();
		    return alc < blc ? -1 : blc < alc ? 1 : a.localeCompare(b);
		});
	    }
	    text += ' ';
	    tags.forEach((tag) => {
		text += Proxmox.Utils.getTagElement(tag, overrides);
	    });
	}
	return text;
    },

    tagCharRegex: /^[a-z0-9+_.-]+$/i,

    verificationStateOrder: {
	'failed': 0,
	'none': 1,
	'ok': 2,
	'__default__': 3,
    },

    isStandaloneNode: function() {
	return PVE.data.ResourceStore.getNodes().length < 2;
    },

    // main use case of this helper is the login window
    getUiLanguage: function() {
	let languageCookie = Ext.util.Cookies.get('PVELangCookie');
	if (languageCookie === 'kr') {
	    // fix-up 'kr' being used for Korean by mistake FIXME: remove with PVE 9
	    let dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
	    languageCookie = 'ko';
	    Ext.util.Cookies.set('PVELangCookie', languageCookie, dt);
	}
	return languageCookie || Proxmox.defaultLang || 'en';
    },

    formatGuestTaskConfirmation: function(taskType, vmid, guestName) {
	return Proxmox.Utils.format_task_description(taskType, `${vmid} (${guestName})`);
    },
},

    singleton: true,
    constructor: function() {
	var me = this;
	Ext.apply(me, me.utilities);

	Proxmox.Utils.override_task_descriptions({
	    acmedeactivate: ['ACME Account', gettext('Deactivate')],
	    acmenewcert: ['SRV', gettext('Order Certificate')],
	    acmerefresh: ['ACME Account', gettext('Refresh')],
	    acmeregister: ['ACME Account', gettext('Register')],
	    acmerenew: ['SRV', gettext('Renew Certificate')],
	    acmerevoke: ['SRV', gettext('Revoke Certificate')],
	    acmeupdate: ['ACME Account', gettext('Update')],
	    'auth-realm-sync': [gettext('Realm'), gettext('Sync')],
	    'auth-realm-sync-test': [gettext('Realm'), gettext('Sync Preview')],
	    cephcreatemds: ['Ceph Metadata Server', gettext('Create')],
	    cephcreatemgr: ['Ceph Manager', gettext('Create')],
	    cephcreatemon: ['Ceph Monitor', gettext('Create')],
	    cephcreateosd: ['Ceph OSD', gettext('Create')],
	    cephcreatepool: ['Ceph Pool', gettext('Create')],
	    cephdestroymds: ['Ceph Metadata Server', gettext('Destroy')],
	    cephdestroymgr: ['Ceph Manager', gettext('Destroy')],
	    cephdestroymon: ['Ceph Monitor', gettext('Destroy')],
	    cephdestroyosd: ['Ceph OSD', gettext('Destroy')],
	    cephdestroypool: ['Ceph Pool', gettext('Destroy')],
	    cephdestroyfs: ['CephFS', gettext('Destroy')],
	    cephfscreate: ['CephFS', gettext('Create')],
	    cephsetpool: ['Ceph Pool', gettext('Edit')],
	    cephsetflags: ['', gettext('Change global Ceph flags')],
	    clustercreate: ['', gettext('Create Cluster')],
	    clusterjoin: ['', gettext('Join Cluster')],
	    dircreate: [gettext('Directory Storage'), gettext('Create')],
	    dirremove: [gettext('Directory'), gettext('Remove')],
	    download: [gettext('File'), gettext('Download')],
	    hamigrate: ['HA', gettext('Migrate')],
	    hashutdown: ['HA', gettext('Shutdown')],
	    hastart: ['HA', gettext('Start')],
	    hastop: ['HA', gettext('Stop')],
	    imgcopy: ['', gettext('Copy data')],
	    imgdel: ['', gettext('Erase data')],
	    lvmcreate: [gettext('LVM Storage'), gettext('Create')],
	    lvmremove: ['Volume Group', gettext('Remove')],
	    lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
	    lvmthinremove: ['Thinpool', gettext('Remove')],
	    migrateall: ['', gettext('Bulk migrate VMs and Containers')],
	    'move_volume': ['CT', gettext('Move Volume')],
	    'pbs-download': ['VM/CT', gettext('File Restore Download')],
	    pull_file: ['CT', gettext('Pull file')],
	    push_file: ['CT', gettext('Push file')],
	    qmclone: ['VM', gettext('Clone')],
	    qmconfig: ['VM', gettext('Configure')],
	    qmcreate: ['VM', gettext('Create')],
	    qmdelsnapshot: ['VM', gettext('Delete Snapshot')],
	    qmdestroy: ['VM', gettext('Destroy')],
	    qmigrate: ['VM', gettext('Migrate')],
	    qmmove: ['VM', gettext('Move disk')],
	    qmpause: ['VM', gettext('Pause')],
	    qmreboot: ['VM', gettext('Reboot')],
	    qmreset: ['VM', gettext('Reset')],
	    qmrestore: ['VM', gettext('Restore')],
	    qmresume: ['VM', gettext('Resume')],
	    qmrollback: ['VM', gettext('Rollback')],
	    qmshutdown: ['VM', gettext('Shutdown')],
	    qmsnapshot: ['VM', gettext('Snapshot')],
	    qmstart: ['VM', gettext('Start')],
	    qmstop: ['VM', gettext('Stop')],
	    qmsuspend: ['VM', gettext('Hibernate')],
	    qmtemplate: ['VM', gettext('Convert to template')],
	    resize: ['VM/CT', gettext('Resize')],
	    spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'],
	    spiceshell: ['', gettext('Shell') + ' (Spice)'],
	    startall: ['', gettext('Bulk start VMs and Containers')],
	    stopall: ['', gettext('Bulk shutdown VMs and Containers')],
	    suspendall: ['', gettext('Suspend all VMs')],
	    unknownimgdel: ['', gettext('Destroy image from unknown guest')],
	    wipedisk: ['Device', gettext('Wipe Disk')],
	    vncproxy: ['VM/CT', gettext('Console')],
	    vncshell: ['', gettext('Shell')],
	    vzclone: ['CT', gettext('Clone')],
	    vzcreate: ['CT', gettext('Create')],
	    vzdelsnapshot: ['CT', gettext('Delete Snapshot')],
	    vzdestroy: ['CT', gettext('Destroy')],
	    vzdump: (type, id) => id ? `VM/CT ${id} - ${gettext('Backup')}` : gettext('Backup Job'),
	    vzmigrate: ['CT', gettext('Migrate')],
	    vzmount: ['CT', gettext('Mount')],
	    vzreboot: ['CT', gettext('Reboot')],
	    vzrestore: ['CT', gettext('Restore')],
	    vzresume: ['CT', gettext('Resume')],
	    vzrollback: ['CT', gettext('Rollback')],
	    vzshutdown: ['CT', gettext('Shutdown')],
	    vzsnapshot: ['CT', gettext('Snapshot')],
	    vzstart: ['CT', gettext('Start')],
	    vzstop: ['CT', gettext('Stop')],
	    vzsuspend: ['CT', gettext('Suspend')],
	    vztemplate: ['CT', gettext('Convert to template')],
	    vzumount: ['CT', gettext('Unmount')],
	    zfscreate: [gettext('ZFS Storage'), gettext('Create')],
	    zfsremove: ['ZFS Pool', gettext('Remove')],
	});

	Proxmox.Utils.overrideNotificationFieldName({
	    'job-id': gettext('Job ID'),
	});

	Proxmox.Utils.overrideNotificationFieldValue({
	    'package-updates': gettext('Package updates are available'),
	    'vzdump': gettext('Backup notifications'),
	    'replication': gettext('Replication job notifications'),
	    'fencing': gettext('Node fencing notifications'),
	});
    },

});
