Experimenting with more structured ways to handle command-line input/output in Rust
use crate::{LocalizationError, Localize};

use fixed_decimal::{Decimal, FloatPrecision};
use icu_experimental::relativetime::{
    options::Numeric, RelativeTimeFormatter, RelativeTimeFormatterOptions,
};
use icu_locale::{langid, LanguageIdentifier};
use jiff::{tz::TimeZone, SpanRound, Timestamp, Unit};

/// Allow the formatter to use non-numeric output (e.g. "tomorrow", "yesterday")
const FORMATTER_OPTIONS: RelativeTimeFormatterOptions = RelativeTimeFormatterOptions {
    numeric: Numeric::Auto,
};

impl<W: std::io::Write> Localize<W> for Timestamp {
    const CANONICAL_LOCALE: LanguageIdentifier = langid!("en-US");

    fn available_locales(&self) -> Vec<LanguageIdentifier> {
        // TODO: keep track of all locales with Fluent data, and return only those
        vec![<Self as Localize<W>>::CANONICAL_LOCALE]
    }

    fn message_for_locale(
        &self,
        writer: &mut W,
        locale: &LanguageIdentifier,
    ) -> Result<(), LocalizationError> {
        // Get the current time
        let current_timestamp = Timestamp::now();
        let current_datetime = current_timestamp.to_zoned(TimeZone::UTC).datetime();

        // Calculate the difference, rounded to the largest unit
        let unformatted_span = current_timestamp
            .since(*self)
            .unwrap()
            // Make sure the span is rounded to the largest available unit
            .round(
                SpanRound::new()
                    .largest(Unit::Year)
                    .relative(current_datetime),
            )
            .unwrap();

        // Find the largest "component": year, month, week etc
        let units: [(Unit, i64); 7] = [
            (Unit::Year, unformatted_span.get_years() as i64),
            (Unit::Month, unformatted_span.get_months() as i64),
            (Unit::Week, unformatted_span.get_weeks() as i64),
            (Unit::Day, unformatted_span.get_days() as i64),
            (Unit::Hour, unformatted_span.get_hours() as i64),
            (Unit::Minute, unformatted_span.get_minutes()),
            (Unit::Second, unformatted_span.get_seconds()),
        ];

        // Use the largest non-zero unit
        let selected_unit = units
            .iter()
            .find(|(_unit, value)| value.abs() > 0)
            .map(|(unit, _value)| *unit)
            .unwrap_or(Unit::Second);

        // Round the span to that selected unit
        let rounding_options = SpanRound::new()
            .smallest(selected_unit)
            .largest(selected_unit)
            .relative(current_datetime);
        let formatted_span = current_timestamp
            .since(*self)
            .unwrap()
            .round(rounding_options)
            .unwrap();

        // We can finally get the actual rounded value
        let selected_value = match selected_unit {
            Unit::Year => formatted_span.get_years() as i64,
            Unit::Month => formatted_span.get_months() as i64,
            Unit::Week => formatted_span.get_weeks() as i64,
            Unit::Day => formatted_span.get_days() as i64,
            Unit::Hour => formatted_span.get_hours() as i64,
            Unit::Minute => formatted_span.get_minutes(),
            Unit::Second => formatted_span.get_seconds(),
            _ => unreachable!(),
        };

        let formatter = match selected_unit {
            Unit::Year => {
                RelativeTimeFormatter::try_new_long_year(locale.into(), FORMATTER_OPTIONS)
            }
            Unit::Month => {
                RelativeTimeFormatter::try_new_long_month(locale.into(), FORMATTER_OPTIONS)
            }
            Unit::Week => {
                RelativeTimeFormatter::try_new_long_week(locale.into(), FORMATTER_OPTIONS)
            }
            Unit::Day => RelativeTimeFormatter::try_new_long_day(locale.into(), FORMATTER_OPTIONS),
            Unit::Hour => {
                RelativeTimeFormatter::try_new_long_hour(locale.into(), FORMATTER_OPTIONS)
            }
            Unit::Minute => {
                RelativeTimeFormatter::try_new_long_minute(locale.into(), FORMATTER_OPTIONS)
            }
            Unit::Second => {
                RelativeTimeFormatter::try_new_long_second(locale.into(), FORMATTER_OPTIONS)
            }
            _ => unreachable!(),
        }
        .unwrap();

        let decimal =
            Decimal::try_from_f64(selected_value as f64, FloatPrecision::Integer).unwrap();
        let formatted_text = formatter.format(decimal);

        writer.write_all(formatted_text.to_string().as_bytes())?;
        writer.flush()?;

        Ok(())
    }
}