// for Toolkit.js
function gettext(val) { return val; };

Ext.onReady(function() {
    const NOW = new Date();
    const COLORS = {
	'keep-last': 'orange',
	'keep-hourly': 'purple',
	'keep-daily': 'yellow',
	'keep-weekly': 'green',
	'keep-monthly': 'blue',
	'keep-yearly': 'red',
	'all zero': 'white',
    };
    const TEXT_COLORS = {
	'keep-last': 'black',
	'keep-hourly': 'white',
	'keep-daily': 'black',
	'keep-weekly': 'white',
	'keep-monthly': 'white',
	'keep-yearly': 'white',
	'all zero': 'black',
    };

    Ext.define('PBS.prunesimulator.Documentation', {
	extend: 'Ext.Panel',
	alias: 'widget.prunesimulatorDocumentation',

	html: '<iframe style="width:100%;height:100%;border:0px;" src="./documentation.html"/>',
    });

    Ext.define('PBS.prunesimulator.CalendarEvent', {
	extend: 'Ext.form.field.ComboBox',
	alias: 'widget.prunesimulatorCalendarEvent',

	editable: true,

	valueField: 'value',
	queryMode: 'local',

	store: {
	    field: ['value', 'text'],
	    data: [
		{ value: '0/2:00', text: "Every two hours" },
		{ value: '0/6:00', text: "Every six hours" },
		{ value: '2,22:30', text: "At 02:30 and 22:30" },
		{ value: '00:00', text: "At 00:00" },
		{ value: '08..17:00/30', text: "From 08:00 to 17:30 every 30 minutes" },
		{ value: 'HOUR:MINUTE', text: "Custom schedule" },
	    ],
	},

	tpl: [
	    '<ul class="x-list-plain"><tpl for=".">',
	    '<li role="option" class="x-boundlist-item">{text}</li>',
	    '</tpl></ul>',
	],

	displayTpl: [
	    '<tpl for=".">',
	    '{value}',
	    '</tpl>',
	],
    });

    Ext.define('PBS.prunesimulator.DayOfWeekSelector', {
	extend: 'Ext.form.field.ComboBox',
	alias: 'widget.prunesimulatorDayOfWeekSelector',

	editable: false,

	displayField: 'text',
	valueField: 'value',
	queryMode: 'local',

	store: {
	    field: ['value', 'text'],
	    data: [
		{ value: 'mon', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[1]) },
		{ value: 'tue', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[2]) },
		{ value: 'wed', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[3]) },
		{ value: 'thu', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[4]) },
		{ value: 'fri', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[5]) },
		{ value: 'sat', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[6]) },
		{ value: 'sun', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[0]) },
	    ],
	},
    });

    Ext.define('pbs-prune-list', {
	extend: 'Ext.data.Model',
	fields: [
	    {
		name: 'backuptime',
		type: 'date',
		dateFormat: 'timestamp',
	    },
	    {
		name: 'mark',
		type: 'string',
	    },
	    {
		name: 'keepName',
		type: 'string',
	    },
	],
    });

    Ext.define('PBS.prunesimulator.PruneList', {
	extend: 'Ext.panel.Panel',
	alias: 'widget.prunesimulatorPruneList',

	initComponent: function() {
	    let me = this;

	    if (!me.store) {
		throw "no store specified";
	    }

	    me.items = [
		{
		    xtype: 'grid',
		    store: me.store,
		    border: false,
		    columns: [
			{
			    header: 'Backup Time',
			    dataIndex: 'backuptime',
			    renderer: function(value, metaData, record) {
				let text = Ext.Date.format(value, 'Y-m-d H:i:s');
				if (record.data.mark === 'keep') {
				    if (me.useColors) {
					let bgColor = COLORS[record.data.keepName];
					let textColor = TEXT_COLORS[record.data.keepName];
					return '<div style="background-color: ' + bgColor + '; ' +
							    'color: ' + textColor + ';">' + text + '</div>';
				    } else {
					return text;
				    }
				} else {
				    return '<div style="text-decoration: line-through;">' + text + '</div>';
				}
			    },
			    flex: 1,
			    sortable: false,
			},
			{
			    header: 'Keep (reason)',
			    dataIndex: 'mark',
			    renderer: function(value, metaData, record) {
				if (record.data.mark === 'keep') {
				    if (record.data.keepCount) {
					return 'keep (' + record.data.keepName +
					       ': ' + record.data.keepCount + ')';
				    } else {
					return 'keep (' + record.data.keepName + ')';
				    }
				} else {
				    return value;
				}
			    },
			    width: 200,
			    sortable: false,
			},
		    ],
		},
	    ];

	    me.callParent();
	},
    });

    Ext.define('PBS.prunesimulator.WeekTable', {
	extend: 'Ext.panel.Panel',
	alias: 'widget.prunesimulatorWeekTable',

	reload: function() {
	    let me = this;
	    let backups = me.store.data.items;

	    let html = '<table class="cal">';

	    let now = new Date(NOW.getTime());
	    let skip = 7 - parseInt(Ext.Date.format(now, 'N'), 10);
	    let tableStartDate = Ext.Date.add(now, Ext.Date.DAY, skip);

	    let bIndex = 0;

	    for (let i = 0; bIndex < backups.length; i++) {
		html += '<tr>';

		for (let j = 0; j < 7; j++) {
		    let date = Ext.Date.subtract(tableStartDate, Ext.Date.DAY, j + 7 * i);
		    let currentDay = Ext.Date.format(date, 'd/m/Y');

		    let dayOfWeekCls = Ext.Date.format(date, 'D').toLowerCase();
		    let firstOfMonthCls = Ext.Date.format(date, 'd') === '01'
		        ? 'first-of-month'
		        : '';
		    html += `<td class="cal-day ${dayOfWeekCls} ${firstOfMonthCls}">`;

		    const isBackupOnDay = function(backup, day) {
			return backup && Ext.Date.format(backup.data.backuptime, 'd/m/Y') === day;
		    };

		    let backup = backups[bIndex];

		    html += '<table><tr>';
		    html += `<th class="cal-day-date">${Ext.Date.format(date, 'D, d M Y')}</th>`;

		    while (isBackupOnDay(backup, currentDay)) {
			html += '<tr><td>';

			let text = Ext.Date.format(backup.data.backuptime, 'H:i');
			if (backup.data.mark === 'remove') {
			    html += `<span class="strikethrough">${text}</span>`;
			} else {
			    if (backup.data.keepCount) {
				text += ` (${backup.data.keepName} ${backup.data.keepCount})`;
			    } else {
				text += ` (${backup.data.keepName})`;
			    }
			    if (me.useColors) {
				let bgColor = COLORS[backup.data.keepName];
				let textColor = TEXT_COLORS[backup.data.keepName];
				html += `<span style="background-color: ${bgColor};
				    color: ${textColor};">${text}</span>`;
			    } else {
				html += `<span class="black">${text}</span>`;
			    }
			}
			html += '</td></tr>';
			backup = backups[++bIndex];
		    }
		    html += '</table>';
		    html += '</div>';
		    html += '</td>';
		}

		html += '</tr>';
	    }

	    me.setHtml(html);
	},

	initComponent: function() {
	    let me = this;

	    if (!me.store) {
		throw "no store specified";
	    }

	    let reload = function() {
		me.reload();
	    };

	    me.store.on("datachanged", reload);

	    me.callParent();

	    me.reload();
	},
    });

    Ext.define('PBS.PruneSimulatorKeepInput', {
	extend: 'Ext.form.field.Number',
	alias: 'widget.prunesimulatorKeepInput',

	allowBlank: true,
	fieldGroup: 'keep',
	minValue: 1,

	listeners: {
	    afterrender: function(field) {
		this.triggers.clear.setVisible(field.value !== null);
	    },
	    change: function(field, newValue, oldValue) {
		this.triggers.clear.setVisible(newValue !== null);
	    },
	},
	triggers: {
	    clear: {
		cls: 'clear-trigger',
		weight: -1,
		handler: function() {
		    this.triggers.clear.setVisible(false);
		    this.setValue(null);
		},
	    },
	},
    });

    Ext.define('PBS.PruneSimulatorPanel', {
	extend: 'Ext.panel.Panel',
	alias: 'widget.prunesimulatorPanel',

	viewModel: {
	},

	getValues: function() {
	    let me = this;

	    let values = {};

	    Ext.Array.each(me.query('[isFormField]'), function(field) {
		let data = field.getSubmitData();
		Ext.Object.each(data, function(name, val) {
		    values[name] = val;
		});
	    });

	    return values;
	},

	controller: {
	    xclass: 'Ext.app.ViewController',

	    init: function(view) {
		this.reloadFull(); // initial load
		this.switchColor(true);
	    },

	    control: {
		'field[fieldGroup=keep]': { change: 'reloadPrune' },
	    },

	    reloadFull: function() {
		let me = this;
		let view = me.getView();

		let params = view.getValues();

		let [hourSpec, minuteSpec] = params['schedule-time'].split(':');

		if (!hourSpec || !minuteSpec) {
		    Ext.Msg.alert('Error', 'Invalid schedule');
		    return;
		}

		let matchTimeSpec = function(timeSpec, rangeMin, rangeMax) {
		    let specValues = timeSpec.split(',');
		    let matches = {};

		    let assertValid = function(value) {
			let num = Number(value);
			if (isNaN(num)) {
			    throw value + " is not an integer";
			} else if (value < rangeMin || value > rangeMax) {
			    throw "number '" + value + "' is not in the range '" + rangeMin + ".." + rangeMax + "'";
			}
			return num;
		    };

		    specValues.forEach(function(value) {
			if (value.includes('..')) {
			    let [start, end] = value.split('..');
			    start = assertValid(start);
			    end = assertValid(end);
			    if (start > end) {
				throw "interval start is bigger then interval end '" + start + " > " + end + "'";
			    }
			    for (let i = start; i <= end; i++) {
				matches[i] = 1;
			    }
			} else if (value.includes('/')) {
			    let [start, step] = value.split('/');
			    start = assertValid(start);
			    step = assertValid(step);
			    for (let i = start; i <= rangeMax; i += step) {
				matches[i] = 1;
			    }
			} else if (value === '*') {
			    for (let i = rangeMin; i <= rangeMax; i++) {
				matches[i] = 1;
			    }
			} else {
			    value = assertValid(value);
			    matches[value] = 1;
			}
		    });

		    return Object.keys(matches);
		};

		let hours, minutes;

		try {
		    hours = matchTimeSpec(hourSpec, 0, 23);
		    minutes = matchTimeSpec(minuteSpec, 0, 59);
		} catch (err) {
		    Ext.Msg.alert('Error', err);
		    return;
		}

		let backups = me.populateFromSchedule(
		    params['schedule-weekdays'],
		    hours,
		    minutes,
		    params.numberOfWeeks,
		);

		me.pruneSelect(backups, params);

		view.pruneStore.setData(backups);
	    },

	    reloadPrune: function() {
		let me = this;
		let view = me.getView();

		let params = view.getValues();

		let backups = [];
		view.pruneStore.getData().items.forEach(function(item) {
		    backups.push({
			backuptime: item.data.backuptime,
		    });
		});

		me.pruneSelect(backups, params);

		view.pruneStore.setData(backups);
	    },

	    // backups are sorted descending by date
	    populateFromSchedule: function(weekdays, hours, minutes, weekCount) {
		let weekdayFlags = [
		    weekdays.includes('sun'),
		    weekdays.includes('mon'),
		    weekdays.includes('tue'),
		    weekdays.includes('wed'),
		    weekdays.includes('thu'),
		    weekdays.includes('fri'),
		    weekdays.includes('sat'),
		];

		let todaysDate = new Date(NOW.getTime());

		let timesOnSingleDay = [];

		hours.forEach(function(hour) {
		    minutes.forEach(function(minute) {
			todaysDate.setHours(hour);
			todaysDate.setMinutes(minute);
			timesOnSingleDay.push(todaysDate.getTime());
		    });
		});

		// sort recent times first, backups array below is ordered now -> past
		timesOnSingleDay.sort((a, b) => b - a);

		let backups = [];

		for (let i = 0; i < 7 * weekCount; i++) {
		    let daysDate = Ext.Date.subtract(todaysDate, Ext.Date.DAY, i);
		    let weekday = parseInt(Ext.Date.format(daysDate, 'w'), 10);
		    if (weekdayFlags[weekday]) {
			timesOnSingleDay.forEach(function(time) {
			    backups.push({
				backuptime: Ext.Date.subtract(new Date(time), Ext.Date.DAY, i),
			    });
			});
		    }
		}

		return backups;
	    },

	    pruneMark: function(backups, keepCount, keepName, idFunc) {
		if (!keepCount) {
		    return;
		}

		let alreadyIncluded = {};
		let newlyIncluded = {};
		let newlyIncludedCount = 0;

		let finished = false;

		backups.forEach(function(backup) {
		    let mark = backup.mark;
		    if (mark && mark === 'keep') {
			let id = idFunc(backup);
			alreadyIncluded[id] = true;
		    }
		});

		backups.forEach(function(backup) {
		    let mark = backup.mark;
		    let id = idFunc(backup);

		    if (finished || alreadyIncluded[id] || mark) {
			return;
		    }

		    if (!newlyIncluded[id]) {
			if (newlyIncludedCount >= keepCount) {
			    finished = true;
			    return;
			}
			newlyIncluded[id] = true;
			newlyIncludedCount++;
			backup.mark = 'keep';
			backup.keepName = keepName;
			backup.keepCount = newlyIncludedCount;
		    } else {
			backup.mark = 'remove';
		    }
		});
	    },

	    // backups need to be sorted descending by date
	    pruneSelect: function(backups, keepParams) {
		let me = this;

		if (Number(keepParams['keep-last']) +
		    Number(keepParams['keep-hourly']) +
		    Number(keepParams['keep-daily']) +
		    Number(keepParams['keep-weekly']) +
		    Number(keepParams['keep-monthly']) +
		    Number(keepParams['keep-yearly']) === 0) {
		    backups.forEach(function(backup) {
			backup.mark = 'keep';
			backup.keepName = 'keep-all';
		    });

		    return;
		}

		me.pruneMark(backups, keepParams['keep-last'], 'keep-last', function(backup) {
		    return backup.backuptime;
		});
		me.pruneMark(backups, keepParams['keep-hourly'], 'keep-hourly', function(backup) {
		    return Ext.Date.format(backup.backuptime, 'H/d/m/Y');
		});
		me.pruneMark(backups, keepParams['keep-daily'], 'keep-daily', function(backup) {
		    return Ext.Date.format(backup.backuptime, 'd/m/Y');
		});
		me.pruneMark(backups, keepParams['keep-weekly'], 'keep-weekly', function(backup) {
		    // ISO-8601 week and week-based year
		    return Ext.Date.format(backup.backuptime, 'W/o');
		});
		me.pruneMark(backups, keepParams['keep-monthly'], 'keep-monthly', function(backup) {
		    return Ext.Date.format(backup.backuptime, 'm/Y');
		});
		me.pruneMark(backups, keepParams['keep-yearly'], 'keep-yearly', function(backup) {
		    return Ext.Date.format(backup.backuptime, 'Y');
		});

		backups.forEach(function(backup) {
		    backup.mark = backup.mark || 'remove';
		});
	    },

	    toggleColors: function(checkbox, checked) {
		this.switchColor(checked);
	    },

	    switchColor: function(useColors) {
		let me = this;
		let view = me.getView();

		const getStyle = name =>
		    `background-color: ${COLORS[name]}; color: ${TEXT_COLORS[name]};`;

		for (const field of view.query('[isFormField]')) {
		    if (field.fieldGroup !== 'keep') {
			continue;
		    }
		    if (useColors) {
		    field.setFieldStyle(getStyle(field.name));
		    } else {
			field.setFieldStyle('background-color: white; color: #444;');
		    }
		}

		me.lookup('weekTable').useColors = useColors;
		me.lookup('pruneList').useColors = useColors;

		me.reloadPrune();
	    },
	},

	keepItems: [
	    {
		xtype: 'prunesimulatorKeepInput',
		name: 'keep-last',
		fieldLabel: 'keep-last',
		value: 4,
	    },
	    {
		xtype: 'prunesimulatorKeepInput',
		name: 'keep-hourly',
		fieldLabel: 'keep-hourly',
	    },
	    {
		xtype: 'prunesimulatorKeepInput',
		name: 'keep-daily',
		fieldLabel: 'keep-daily',
		value: 5,
	    },
	    {
		xtype: 'prunesimulatorKeepInput',
		name: 'keep-weekly',
		fieldLabel: 'keep-weekly',
		value: 2,
	    },
	    {
		xtype: 'prunesimulatorKeepInput',
		name: 'keep-monthly',
		fieldLabel: 'keep-monthly',
	    },
	    {
		xtype: 'prunesimulatorKeepInput',
		name: 'keep-yearly',
		fieldLabel: 'keep-yearly',
	    },
	],

	initComponent: function() {
	    var me = this;

	    me.pruneStore = Ext.create('Ext.data.Store', {
		model: 'pbs-prune-list',
		sorters: { property: 'backuptime', direction: 'DESC' },
	    });

	    me.items = [
		{
		    xtype: 'panel',
		    layout: {
			type: 'hbox',
			align: 'stretch',
		    },
		    border: false,
		    items: [
			{
			    title: 'View',
			    layout: 'anchor',
			    flex: 1,
			    border: false,
			    bodyPadding: 10,
			    items: [
				{
				    xtype: 'checkbox',
				    name: 'showCalendar',
				    reference: 'showCalendar',
				    fieldLabel: 'Show Calendar:',
				    checked: true,
				},
				{
				    xtype: 'checkbox',
				    name: 'showColors',
				    reference: 'showColors',
				    fieldLabel: 'Show Colors:',
				    checked: true,
				    handler: 'toggleColors',
				},
			    ],
			},
			{ xtype: "panel", width: 1, border: 1 },
			{
			    xtype: 'form',
			    layout: 'anchor',
			    flex: 1,
			    border: false,
			    title: 'Simulated Backup Schedule',
			    defaults: {
				labelWidth: 120,
			    },
			    bodyPadding: 10,
			    items: [
				{
				    xtype: 'prunesimulatorDayOfWeekSelector',
				    name: 'schedule-weekdays',
				    fieldLabel: 'Day of week',
				    value: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
				    allowBlank: false,
				    multiSelect: true,
				    padding: '0 0 0 10',
				},
				{
				    xtype: 'prunesimulatorCalendarEvent',
				    name: 'schedule-time',
				    allowBlank: false,
				    value: '0/6:00',
				    fieldLabel: 'Backup schedule',
				    padding: '0 0 0 10',
				},
				{
				    xtype: 'numberfield',
				    name: 'numberOfWeeks',
				    allowBlank: false,
				    fieldLabel: 'Number of weeks',
				    minValue: 1,
				    value: 15,
				    maxValue: 260, // five years
				    padding: '0 0 0 10',
				},
				{
				    xtype: 'button',
				    name: 'schedule-button',
				    text: 'Update Schedule',
				    formBind: true,
				    handler: 'reloadFull',
				},
			    ],
			},
		    ],
		},
		{
		    xtype: 'panel',
		    layout: {
			type: 'hbox',
			align: 'stretch',
		    },
		    flex: 1,
		    border: false,
		    items: [
			{
			    layout: 'anchor',
			    title: 'Prune Options',
			    border: false,
			    bodyPadding: 10,
			    scrollable: true,
			    items: me.keepItems,
			    flex: 1,
			},
			{ xtype: "panel", width: 1, border: 1 },
			{
			    layout: 'fit',
			    title: 'Backups',
			    border: false,
			    xtype: 'prunesimulatorPruneList',
			    store: me.pruneStore,
			    reference: 'pruneList',
			    flex: 1,
			},
		    ],
		},
		{
		    layout: 'anchor',
		    title: 'Calendar',
		    autoScroll: true,
		    flex: 2,
		    xtype: 'prunesimulatorWeekTable',
		    reference: 'weekTable',
		    store: me.pruneStore,
		    bind: {
			hidden: '{!showCalendar.checked}',
		    },
		},
	    ];

	    me.callParent();
	},
    });

    Ext.create('Ext.container.Viewport', {
	layout: 'border',
	renderTo: Ext.getBody(),
	items: [
	    {
		xtype: 'prunesimulatorPanel',
		title: 'Proxmox Backup Server - Prune Simulator',
		region: 'west',
		layout: {
		    type: 'vbox',
		    align: 'stretch',
		    pack: 'start',
		},
		flex: 3,
		maxWidth: 1090,
	    },
	    {
		xtype: 'prunesimulatorDocumentation',
		title: 'Usage',
		border: false,
		flex: 2,
		region: 'center',
	    },
	],
    });
});