import { memo } from "react";

// This component always renders the server time / UTC.
// It does not (yet) support rendering some parts in the time zone of the client.
// It is also not language-/locale-aware.

// For DE locale, usually returns something like "vor 10 Tagen"
// `numeric: "auto"` results in additional wordings to be used.
// For example, instead of "vor 1 Tag", it will return "gestern"
// `style: "long"` will return "vor 6 Minuten"
// `style: "short"` will return "vor 6 Min."
const relativeDateFormatter = new Intl.RelativeTimeFormat(["de", "en"], {
	style: "long",
	numeric: "auto",
});

const relativeDateFormatterNarrow = new Intl.RelativeTimeFormat(["de", "en"], {
	style: "narrow",
	numeric: "auto",
});

const absoluteFormatter = new Intl.DateTimeFormat(["de", "en"], {
	dateStyle: "long",
	timeStyle: "long",
});

export type RelativeTimestampStyle = "long" | "narrow" | "short";

type ValidUnit = Intl.RelativeTimeFormatUnit &
	("year" | "month" | "week" | "day" | "hour" | "minute" | "second");

interface Formatter {
	format(value: number, unit: ValidUnit): string;
}

const formatterMap = {
	long: relativeDateFormatter,
	narrow: relativeDateFormatterNarrow,
	short: {
		// Intl does not offer a meaningful "short" style for relative time. We use our own.
		format(value: number, unit: ValidUnit) {
			const v = Math.abs(Math.round(value));
			switch (unit) {
				// The char " " between the number and the unit is a narrow no-break space (U+202F).
				case "day":
					return `${v} T`;
				case "hour":
					return `${v} Std.`;
				case "minute":
					return `${v} min`;
				case "second":
					return `${v} s`;
				case "month":
					return `${v} M`;
				case "week":
					return `${v} W`;
				case "year":
					return `${v} J`;
			}
		},
	},
} satisfies Record<RelativeTimestampStyle, Formatter>;

/**
 * @remarks This component uses api.Timestamp instead of `Date` to avoid re-renders (Dates are objects and therefore compared by reference).
 */
export interface RelativeTimestampProps {
	/**
	 * **Unix time in seconds**.
	 * @remarks We use a `number` instead of `Date` because `Date`s are objects and would cause unnecessary re-renders. `number`s are easier to memoize and to compare.
	 */
	date: number;

	/**
	 * **Unix time in seconds**. Does not have a default, so items in a list can all use the same exact date. Use something like `(Date.getTime() / 1000) | 0` to get some date to compare with.
	 * @remarks We use a `number` instead of `Date` because `Date`s are objects and would cause unnecessary re-renders. `number`s are easier to memoize and to compare.
	 */
	relativeToDate: number;

	style?: RelativeTimestampStyle;
}

export default memo(function RelativeTimestamp({
	date,
	relativeToDate,
	style,
}: RelativeTimestampProps) {
	const d = new Date(date * 1000); // TODO: We seem to have time zone issues

	const longDateTime = absoluteFormatter.format(d);
	const relativeDate = getRelativeDate(
		d,
		typeof relativeToDate === "number"
			? new Date(relativeToDate * 1000)
			: relativeToDate,
		style,
	);
	return (
		<time dateTime={d.toISOString()} title={longDateTime}>
			{relativeDate}
		</time>
	);
});

const unitsInMs: Record<ValidUnit, number> = {
	year: 365 * 24 * 60 * 60 * 1000,
	month: (365 * 24 * 60 * 60 * 1000) / 12,
	week: 7 * 24 * 60 * 60 * 1000,
	day: 24 * 60 * 60 * 1000,
	hour: 60 * 60 * 1000,
	minute: 60 * 1000,
	second: 1000,
};

function getRelativeDate(
	date: Date,
	relativeTo: Date,
	style: RelativeTimestampStyle = "long",
): string {
	const elapsed = date.getTime() - relativeTo.getTime();
	for (const [unit, divisor] of Object.entries(unitsInMs)) {
		if (Math.abs(elapsed) > divisor || unit === "second") {
			const formatter = formatterMap[style];
			return formatter.format(
				Math.round(elapsed / divisor),
				unit as ValidUnit,
			);
		}
	}
	return absoluteFormatter.format(date);
}
