// ago.js: JavaScript port of russell ballestrini's ago (https://pypi.org/project/ago/)
// "he might have died by now but his code should last."
//
// Copyright (C) 2025 TimeHexOn & foxhop & russell@unturf
// https://www.permacomputer.com
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// Define time units as a data structure
// Each unit contains:
// - name: full name of the unit (e.g., "year")
// - abbr: abbreviation (e.g., "y")
// - seconds: number of seconds in this unit (for reference)
// - extract: function to extract this unit's value from a delta object {days, seconds, microseconds}
const TIME_UNITS = [
	{
		name: "year",
		abbr: "y",
		seconds: 31536000,
		extract: (td) => Math.floor(td.days / 365),
	},
	{
		name: "day",
		abbr: "d",
		seconds: 86400,
		extract: (td) => td.days % 365,
	},
	{
		name: "hour",
		abbr: "h",
		seconds: 3600,
		extract: (td) => Math.floor(td.seconds / 3600),
	},
	{
		name: "minute",
		abbr: "m",
		seconds: 60,
		extract: (td) => Math.floor(td.seconds / 60) % 60,
	},
	{
		name: "second",
		abbr: "s",
		seconds: 1,
		extract: (td) => td.seconds % 60,
	},
	{
		name: "millisecond",
		abbr: "ms",
		seconds: 0.001,
		extract: (td) => Math.floor(td.microseconds / 1000),
	},
	{
		name: "microsecond",
		abbr: "μs",
		seconds: 0.000001,
		extract: (td) => td.microseconds % 1000,
	},
];

// Convert a millisecond delta to a Python-style timedelta object {days, seconds, microseconds}
function msToTimedelta(ms) {
	const totalMs = Math.abs(ms);
	const days = Math.floor(totalMs / 86400000);
	const remainderMs = totalMs - days * 86400000;
	const seconds = Math.floor(remainderMs / 1000);
	const microseconds = (remainderMs % 1000) * 1000;
	return { days, seconds, microseconds };
}

// Convert various input types to a timedelta and determine if it's in the past.
// Accepts: Date object, {days, seconds, microseconds} timedelta, or Unix timestamp (number)
// Returns: [timedelta, isPast]
function getDeltaFromSubject(subject) {
	if (subject && typeof subject.days === "number" && typeof subject.seconds === "number") {
		// timedelta object
		const totalMs = subject.days * 86400000 + subject.seconds * 1000 + (subject.microseconds || 0) / 1000;
		return [
			{ days: Math.abs(subject.days), seconds: Math.abs(subject.seconds), microseconds: Math.abs(subject.microseconds || 0) },
			totalMs >= 0,
		];
	}

	if (subject instanceof Date) {
		const deltaMs = Date.now() - subject.getTime();
		return [msToTimedelta(deltaMs), deltaMs >= 0];
	}

	// Assume it's a timestamp (seconds or milliseconds)
	const ts = Number(subject);
	if (Number.isNaN(ts)) {
		throw new TypeError(`Cannot convert ${typeof subject} to a time delta`);
	}
	// Detect if timestamp is in milliseconds (> year 2100 in seconds)
	const tsMs = ts > 4102444800 ? ts : ts * 1000;
	const deltaMs = Date.now() - tsMs;
	return [msToTimedelta(deltaMs), deltaMs >= 0];
}

// Accepts a delta, returns a dictionary of units.
export function delta2dict(delta) {
	const td = typeof delta.days === "number" ? delta : msToTimedelta(delta);
	const result = {};
	for (const unit of TIME_UNITS) {
		result[unit.name] = unit.extract(td);
	}
	return result;
}

// Extract time components from a timedelta, filtering out zero values.
function extractComponents(delta) {
	const td = typeof delta.days === "number" ? delta : msToTimedelta(delta);
	const timeDict = delta2dict(td);
	const components = [];
	for (const unit of TIME_UNITS) {
		const value = timeDict[unit.name];
		if (value > 0) {
			components.push({
				unit: unit.name,
				abbr: unit.abbr,
				value,
			});
		}
	}
	return components;
}

// Format the time components into a human-readable string.
function formatComponents(components, precision = 2, abbreviate = false) {
	const result = [];
	for (const component of components.slice(0, precision)) {
		if (abbreviate) {
			result.push(`${component.value}${component.abbr}`);
		} else {
			let unitName = component.unit;
			if (component.value !== 1) {
				unitName += "s";
			}
			result.push(`${component.value} ${unitName}`);
		}
	}
	return result.join(", ");
}

// Accept a subject, return a human readable timedelta string.
//
// subject: Date object, timedelta {days, seconds, microseconds}, or timestamp number
// precision: the desired amount of unit precision (default: 2)
// pastTense: format string for past timedeltas (default: "{} ago")
// futureTense: format string for future timedeltas (default: "in {}")
// abbreviate: boolean to abbreviate units (default: false)
export function human(subject, precision = 2, pastTense = "{} ago", futureTense = "in {}", abbreviate = false) {
	const [delta, isPast] = getDeltaFromSubject(subject);
	const components = extractComponents(delta);

	if (components.length === 0) {
		return "just now";
	}

	const formatted = formatComponents(components, precision, abbreviate);

	if (isPast) {
		return pastTense.replace("{}", formatted);
	}
	return futureTense.replace("{}", formatted);
}
