<svelte:head>
	<title>Crontab Generator</title>
</svelte:head>

<script lang="js">
	import { copy } from '$lib/clipboard.js';

	/**
	 * @typedef {{ label: string, value: string }[]} Presets
	 */

	/** @type {{ label: string, name: string, placeholder: string, presets: Presets }[]} */
	const fields = [
		{
			label: 'Minute',
			name: 'minute',
			placeholder: '0-59',
			presets: [
				{ label: '*', value: '*' },
				{ label: '*/5', value: '*/5' },
				{ label: '*/10', value: '*/10' },
				{ label: '*/15', value: '*/15' },
				{ label: '*/30', value: '*/30' },
				{ label: '0', value: '0' },
			],
		},
		{
			label: 'Hour',
			name: 'hour',
			placeholder: '0-23',
			presets: [
				{ label: '*', value: '*' },
				{ label: '*/2', value: '*/2' },
				{ label: '*/4', value: '*/4' },
				{ label: '*/6', value: '*/6' },
				{ label: '*/12', value: '*/12' },
				{ label: '0', value: '0' },
				{ label: '6', value: '6' },
				{ label: '12', value: '12' },
				{ label: '18', value: '18' },
			],
		},
		{
			label: 'Day (month)',
			name: 'dom',
			placeholder: '1-31',
			presets: [
				{ label: '*', value: '*' },
				{ label: '1', value: '1' },
				{ label: '15', value: '15' },
				{ label: 'L', value: 'L' },
			],
		},
		{
			label: 'Month',
			name: 'month',
			placeholder: '1-12',
			presets: [
				{ label: '*', value: '*' },
				{ label: '1', value: '1' },
				{ label: '3', value: '3' },
				{ label: '6', value: '6' },
				{ label: '9', value: '9' },
				{ label: '12', value: '12' },
				{ label: '1-6', value: '1-6' },
				{ label: '7-12', value: '7-12' },
			],
		},
		{
			label: 'Day (week)',
			name: 'dow',
			placeholder: '0-7',
			presets: [
				{ label: '*', value: '*' },
				{ label: 'Mon–Fri', value: '1-5' },
				{ label: 'Sat–Sun', value: '6,0' },
				{ label: 'Mon', value: '1' },
				{ label: 'Wed', value: '3' },
				{ label: 'Fri', value: '5' },
				{ label: 'Sat', value: '6' },
				{ label: 'Sun', value: '0' },
			],
		},
	];

	/** @type {Record<string, string>} */
	let values = $state({
		minute: '*',
		hour: '*',
		dom: '*',
		month: '*',
		dow: '*',
	});

	const expression = $derived(
		`${values.minute} ${values.hour} ${values.dom} ${values.month} ${values.dow}`
	);

	/** @param {string} field @param {string} val */
	const set = (field, val) => { values[field] = val; };

	const DOW_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
	const MON_NAMES = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

	/** @param {string} v @returns {string} */
	function describeMinute(v) {
		if (v === '*') return 'every minute';
		if (v === '0') return 'at minute 0';
		if (v.startsWith('*/')) return `every ${v.slice(2)} minutes`;
		return `at minute ${v}`;
	}

	/** @param {string} v @returns {string} */
	function describeHour(v) {
		if (v === '*') return 'every hour';
		if (v.startsWith('*/')) return `every ${v.slice(2)} hours`;
		if (/^\d+$/.test(v)) return `at ${v.padStart(2, '0')}:xx`;
		return `at hour ${v}`;
	}

	/** @param {string} v @returns {string} */
	function describeDom(v) {
		if (v === '*') return '';
		if (v === 'L') return 'on the last day of the month';
		if (/^\d+$/.test(v)) return `on day ${v} of the month`;
		return `on day-of-month ${v}`;
	}

	/** @param {string} v @returns {string} */
	function describeMonth(v) {
		if (v === '*') return '';
		if (/^\d+$/.test(v)) return `in ${MON_NAMES[+v] ?? v}`;
		if (v.includes('-')) {
			const [a, b] = v.split('-');
			return `from ${MON_NAMES[+a] ?? a} to ${MON_NAMES[+b] ?? b}`;
		}
		return `in month ${v}`;
	}

	/** @param {string} v @returns {string} */
	function describeDow(v) {
		if (v === '*') return '';
		if (v === '1-5') return 'on weekdays (Mon–Fri)';
		if (v === '6,0' || v === '0,6') return 'on weekends';
		if (/^\d+$/.test(v)) return `on ${DOW_NAMES[+v] ?? v}`;
		if (v.includes('-')) {
			const [a, b] = v.split('-');
			return `from ${DOW_NAMES[+a] ?? a} to ${DOW_NAMES[+b] ?? b}`;
		}
		return `on day-of-week ${v}`;
	}

	const description = $derived((() => {
		const min = describeMinute(values.minute);
		const hr  = describeHour(values.hour);
		const dom = describeDom(values.dom);
		const mon = describeMonth(values.month);
		const dow = describeDow(values.dow);

		let parts = [];

		if (values.minute === '*' && values.hour === '*') {
			parts.push('every minute');
		} else if (values.minute === '0' && values.hour !== '*') {
			parts.push(`at ${values.hour.padStart(2,'0')}:00`);
		} else if (values.hour === '*') {
			parts.push(min);
		} else {
			parts.push(`${min}, ${hr}`);
		}

		if (dom) parts.push(dom);
		if (mon) parts.push(mon);
		if (dow) parts.push(dow);

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

	/** Common presets */
	const quickPresets = [
		{ label: 'Every minute',      expr: '* * * * *' },
		{ label: 'Every 5 minutes',   expr: '*/5 * * * *' },
		{ label: 'Every 15 minutes',  expr: '*/15 * * * *' },
		{ label: 'Every 30 minutes',  expr: '*/30 * * * *' },
		{ label: 'Every hour',        expr: '0 * * * *' },
		{ label: 'Every 6 hours',     expr: '0 */6 * * *' },
		{ label: 'Every day at midnight', expr: '0 0 * * *' },
		{ label: 'Every day at noon', expr: '0 12 * * *' },
		{ label: 'Every Mon at 09:00',expr: '0 9 * * 1' },
		{ label: 'Weekdays at 08:00', expr: '0 8 * * 1-5' },
		{ label: 'Every Sunday at 03:00', expr: '0 3 * * 0' },
		{ label: '1st of each month', expr: '0 0 1 * *' },
		{ label: 'Every quarter',     expr: '0 0 1 */3 *' },
		{ label: 'Once a year (Jan 1)',expr: '0 0 1 1 *' },
	];

	/** @param {string} expr */
	function applyPreset(expr) {
		const parts = expr.split(' ');
		values.minute = parts[0];
		values.hour   = parts[1];
		values.dom    = parts[2];
		values.month  = parts[3];
		values.dow    = parts[4];
	}

	/** @param {string} expr */
	function parseExpression(expr) {
		const parts = expr.trim().split(/\s+/);
		if (parts.length === 5) applyPreset(parts.join(' '));
	}

	let manualInput = $state('');

	$effect(() => {
		manualInput = expression;
	});
</script>

<h1>Cron</h1>

<h2>Quick presets</h2>
<div class="presets">
	{#each quickPresets as p}
		<button onclick={() => applyPreset(p.expr)}>{p.label}</button>
	{/each}
</div>

<h2>Builder</h2>
<table class="builder">
	<thead>
		<tr>
			{#each fields as f}
				<th>{f.label}</th>
			{/each}
		</tr>
	</thead>
	<tbody>
		<tr>
			{#each fields as f}
				<td>
					<input
						type="text"
						value={values[f.name]}
						oninput={(e) => set(f.name, /** @type {HTMLInputElement} */ (e.target).value)}
						size="6"
						spellcheck="false"
					/>
				</td>
			{/each}
		</tr>
		<tr>
			{#each fields as f}
				<td class="chips">
					{#each f.presets as p}
						<button
							class:selected={values[f.name] === p.value}
							onclick={() => set(f.name, p.value)}
						>{p.label}</button>
					{/each}
				</td>
			{/each}
		</tr>
	</tbody>
</table>

<h2>Expression</h2>
<p>
	<code class="expr">{expression}</code>
	<button onclick={() => copy(expression)}>copy</button>
</p>
<p class="desc">{description}</p>

<h2>Paste &amp; parse</h2>
<p>
	<input
		type="text"
		bind:value={manualInput}
		size="30"
		placeholder="* * * * *"
		spellcheck="false"
		oninput={() => parseExpression(manualInput)}
	/>
	<button onclick={() => parseExpression(manualInput)}>parse</button>
</p>

<h2>Reference</h2>
<table class="ref">
	<thead>
		<tr><th>Field</th><th>Range</th><th>Special</th></tr>
	</thead>
	<tbody>
		<tr><td>Minute</td><td>0–59</td><td rowspan="5"><code>*</code> any &nbsp; <code>,</code> list &nbsp; <code>-</code> range &nbsp; <code>/</code> step</td></tr>
		<tr><td>Hour</td><td>0–23</td></tr>
		<tr><td>Day (month)</td><td>1–31</td></tr>
		<tr><td>Month</td><td>1–12</td></tr>
		<tr><td>Day (week)</td><td>0–7 (0,7=Sun)</td></tr>
	</tbody>
</table>

<style>
	.presets {
		display: flex;
		flex-wrap: wrap;
		gap: 4px;
		margin-bottom: 8px;
	}

	.builder {
		border-collapse: collapse;
		margin-bottom: 8px;
	}

	.builder th,
	.builder td {
		border: 1px solid var(--border);
		padding: 4px 8px;
		vertical-align: top;
	}

	.builder th {
		background: var(--bg-raised);
		font-size: 0.85em;
	}

	.builder input {
		width: 60px;
		font-family: monospace;
	}

	.chips {
		display: flex;
		flex-wrap: wrap;
		gap: 2px;
		min-width: 80px;
	}

	.chips button.selected {
		background: var(--active-bg);
		color: var(--active-fg);
		border-color: var(--active-bg);
	}

	.expr {
		font-size: 1.3em;
		margin-right: 8px;
		letter-spacing: 0.05em;
	}

	.desc {
		color: var(--fg-dim);
		font-style: italic;
		margin-top: 0;
	}

	.ref {
		border-collapse: collapse;
		font-size: 0.9em;
	}

	.ref th,
	.ref td {
		border: 1px solid var(--border);
		padding: 3px 8px;
	}

	.ref th {
		background: var(--bg-raised);
	}
</style>
