// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/time/date_helper.h"

#include "ash/shell.h"
#include "ash/system/locale/locale_update_controller_impl.h"
#include "ash/system/model/clock_model.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_utils.h"
#include "base/i18n/icubridge/calendar.h"
#include "base/i18n/icubridge/date_time_formatter.h"
#include "base/i18n/icubridge/icu_bridge.h"
#include "base/i18n/unicodestring.h"
#include "base/memory/ptr_util.h"
#include "base/memory/singleton.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "third_party/icu/source/common/unicode/dtintrv.h"
#include "third_party/icu/source/i18n/unicode/dtitvfmt.h"
#include "third_party/icu/source/i18n/unicode/fieldpos.h"
#include "third_party/icu/source/i18n/unicode/gregocal.h"

namespace ash {

namespace {

// Milliseconds per minute.
constexpr int kMillisecondsPerMinute = 60000;

// Default week title for a few special languages that cannot find the start of
// a week. So far the known languages that cannot return their day of week are:
// 'bn', 'fa', 'mr', 'pa-PK'.
const std::vector<std::u16string> kDefaultWeekTitle = {u"S", u"M", u"T", u"W",
                                                       u"T", u"F", u"S"};

UDate TimeToUDate(const base::Time& time) {
  return static_cast<UDate>(time.InSecondsFSinceUnixEpoch() *
                            base::Time::kMillisecondsPerSecond);
}

// Receives an input `unicode_pattern` in the "Hm" format (HH:mm, aK:mm, h:mm a,
// a hh:mm, etc.) and extracts the hours part of the pattern.
icu::UnicodeString getHoursPattern(const icu::UnicodeString& unicode_pattern) {
  std::string pattern;
  unicode_pattern.toUTF8String(pattern);

  if (pattern.contains("hh")) {
    return icu::UnicodeString("hh");
  }
  if (pattern.contains("h")) {
    return icu::UnicodeString("h");
  }
  if (pattern.contains("HH")) {
    return icu::UnicodeString("HH");
  }
  if (pattern.contains("H")) {
    return icu::UnicodeString("H");
  }
  if (pattern.contains("KK")) {
    return icu::UnicodeString("KK");
  }
  if (pattern.contains("K")) {
    return icu::UnicodeString("K");
  }
  if (pattern.contains("kk")) {
    return icu::UnicodeString("kk");
  }
  if (pattern.contains("k")) {
    return icu::UnicodeString("k");
  }

  NOTREACHED() << "Hours pattern not found.";
}

}  // namespace

// static
DateHelper* DateHelper::GetInstance() {
  return base::Singleton<DateHelper>::get();
}

icu::SimpleDateFormat DateHelper::CreateSimpleDateFormatter(
    const char* pattern) {
  // Generate a locale-dependent format pattern. The generator will take
  // care of locale-dependent formatting issues like which separator to
  // use (some locales use '.' instead of ':'), and where to put the am/pm
  // marker.
  UErrorCode status = U_ZERO_ERROR;
  DCHECK(U_SUCCESS(status));
  std::unique_ptr<icu::DateTimePatternGenerator> generator(
      icu::DateTimePatternGenerator::createInstance(status));
  DCHECK(U_SUCCESS(status));
  icu::UnicodeString generated_pattern =
      generator->getBestPattern(icu::UnicodeString(pattern), status);
  DCHECK(U_SUCCESS(status));

  // Then, create a formatter object using the generated pattern.
  icu::SimpleDateFormat formatter(generated_pattern, status);
  DCHECK(U_SUCCESS(status));

  return formatter;
}

std::unique_ptr<icu::DateIntervalFormat>
DateHelper::CreateDateIntervalFormatter(const char* pattern) {
  UErrorCode status = U_ZERO_ERROR;
  icu::DateIntervalFormat* formatter =
      icu::DateIntervalFormat::createInstance(pattern, status);
  DCHECK(U_SUCCESS(status));
  return base::WrapUnique(formatter);
}

icu::SimpleDateFormat DateHelper::CreateHoursFormatter(const char* pattern) {
  UErrorCode status = U_ZERO_ERROR;
  DCHECK(U_SUCCESS(status));
  std::unique_ptr<icu::DateTimePatternGenerator> generator(
      icu::DateTimePatternGenerator::createInstance(status));
  DCHECK(U_SUCCESS(status));
  icu::UnicodeString generated_pattern =
      generator->getBestPattern(icu::UnicodeString(pattern), status);
  DCHECK(U_SUCCESS(status));
  // Since ICU 74, getBestPattern can return a gibberish pattern ""H
  // ├'Minute': m┤ ├'Dayperiod': a┤"" if the locale resource is missing. Instead
  // of using the gibberish pattern, this should fallback to the proposed
  // pattern.
  std::string gen_string;
  generated_pattern.toUTF8String(gen_string);
  if (gen_string.contains("├")) {
    // Fallback to the suggested pattern.
    generated_pattern = icu::UnicodeString(pattern);
  }
  // Extract the hours from the generated pattern.
  icu::UnicodeString hours_pattern = getHoursPattern(generated_pattern);
  icu::SimpleDateFormat formatter(hours_pattern, status);
  DCHECK(U_SUCCESS(status));

  return formatter;
}

std::u16string DateHelper::GetFormattedTime(const icu::DateFormat* formatter,
                                            const base::Time& time) {
  DCHECK(formatter);
  icu::UnicodeString date_string;

  formatter->format(TimeToUDate(time), date_string);
  return base::i18n::UnicodeStringToString16(date_string);
}

std::u16string DateHelper::GetFormattedInterval(
    const icu::DateIntervalFormat* formatter,
    const base::Time& start_time,
    const base::Time& end_time) {
  DCHECK(formatter);
  UErrorCode status = U_ZERO_ERROR;
  icu::DateInterval interval(TimeToUDate(start_time), TimeToUDate(end_time));
  icu::FieldPosition position = 0;
  icu::UnicodeString interval_string;
  formatter->format(&interval, interval_string, position, status);
  DCHECK(U_SUCCESS(status));
  return base::i18n::UnicodeStringToString16(interval_string);
}

base::TimeDelta DateHelper::GetTimeDifference(base::Time date) const {
  const icu::TimeZone& time_zone =
      system::TimezoneSettings::GetInstance()->GetTimezone();
  const base::TimeDelta raw_time_diff =
      base::Minutes(time_zone.getRawOffset() / kMillisecondsPerMinute);

  // Calculates the time difference adjust by the possible daylight savings
  // offset. If the status of any step fails, returns the default time
  // difference without considering daylight savings.
  if (!gregorian_calendar_) {
    return raw_time_diff;
  }

  UDate current_date = TimeToUDate(date);
  UErrorCode status = U_ZERO_ERROR;
  gregorian_calendar_->setTime(current_date, status);
  if (U_FAILURE(status)) {
    return raw_time_diff;
  }

  status = U_ZERO_ERROR;
  UBool day_light = gregorian_calendar_->inDaylightTime(status);
  if (U_FAILURE(status)) {
    return raw_time_diff;
  }

  int gmt_offset = time_zone.getRawOffset();
  if (day_light) {
    gmt_offset += time_zone.getDSTSavings();
  }

  return base::Minutes(gmt_offset / kMillisecondsPerMinute);
}

base::Time DateHelper::GetLocalMidnight(base::Time date) {
  base::TimeDelta time_difference = GetTimeDifference(date);
  return (date + time_difference).UTCMidnight() - time_difference;
}

DateHelper::DateHelper()
    : twelve_hour_clock_interval_formatter_(CreateDateIntervalFormatter("hm")),
      twenty_four_hour_clock_interval_formatter_(
          CreateDateIntervalFormatter("Hm")) {
  const icu::TimeZone& time_zone =
      system::TimezoneSettings::GetInstance()->GetTimezone();

  UErrorCode status = U_ZERO_ERROR;
  gregorian_calendar_ =
      std::make_unique<icu::GregorianCalendar>(time_zone, status);
  DCHECK(U_SUCCESS(status));
  CalculateLocalWeekTitles();
  time_zone_settings_observer_.Observe(system::TimezoneSettings::GetInstance());

  // Not using a scoped observer since the Shell can be destructed before this
  // `DateHelper` instance gets destructed.
  if (Shell::HasInstance()) {
    Shell::Get()->locale_update_controller()->AddObserver(this);
  }
}

DateHelper::~DateHelper() {
  if (Shell::HasInstance()) {
    Shell::Get()->locale_update_controller()->RemoveObserver(this);
  }
}

void DateHelper::ResetFormatters() {
  twelve_hour_clock_interval_formatter_ = CreateDateIntervalFormatter("hm");
  twenty_four_hour_clock_interval_formatter_ =
      CreateDateIntervalFormatter("Hm");
}

void DateHelper::ResetForTesting() {
  ResetFormatters();
  CalculateLocalWeekTitles();
  gregorian_calendar_->setTimeZone(
      system::TimezoneSettings::GetInstance()->GetTimezone());
}

void DateHelper::CalculateLocalWeekTitles() {
  week_titles_.clear();

  // To avoid the DST difference, use a certain date here to calculate the week
  // titles, since there are no daylight saving starts/ends in June worldwide.
  // If the `DCHECK` fails, use `Now()`.
  base::Time start_date = base::Time::Now();
  bool result = base::Time::FromString("15 Jun 2021 10:00 GMT", &start_date);
  DCHECK(result);
  start_date = GetLocalMidnight(start_date);

  // Explode the local midnight start_date to determine its local weekday.
  base::Time::Exploded exploded;
  (start_date + GetTimeDifference(start_date)).UTCExplode(&exploded);

  // Both exploded.day_of_week (0-based: 0 = Sunday, 1 = Monday, etc.) and
  // IcuBridge::Calendar::Weekday (1 = Sunday, 2 = Monday, etc.) use the same
  // standard day mapping. We convert the exploded day of week to 1-based
  // to match.
  const int current_day_of_week = exploded.day_of_week + 1;
  const int first_day_of_week =
      static_cast<int>(base::i18n::IcuBridge::GetInstance()
                           .calendar()
                           .GetWeekInformation()
                           .first_weekday);

  // Calculate the difference in days to reach the first day of the week,
  // and shift our start_date accordingly.
  const int diff_days = (first_day_of_week - current_day_of_week + 7) % 7;
  start_date += base::Days(diff_days);

  for (int i = 0; i < calendar_utils::kDateInOneWeek; ++i) {
    week_titles_.push_back(
        base::i18n::IcuBridge::GetInstance().date_time_formatter().Format(
            start_date, base::i18n::datetime_options::E::Short()));
    start_date += base::Days(1);
  }
}

void DateHelper::TimezoneChanged(const icu::TimeZone& timezone) {
  ResetFormatters();
  gregorian_calendar_->setTimeZone(
      system::TimezoneSettings::GetInstance()->GetTimezone());
  Shell::Get()->system_tray_model()->calendar_model()->RedistributeEvents();
  Shell::Get()->system_tray_model()->clock()->NotifyRefreshClock();
}

void DateHelper::OnLocaleChanged() {
  ResetFormatters();
  CalculateLocalWeekTitles();
}

}  // namespace ash
