// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// ignore_for_file: constant_identifier_names

/// Outputs year as four digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989), [yyyy]);
///     // => 1989
/// ```
const String yyyy = 'yyyy';

/// Outputs year as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989), [yy]);
///     // => 89
/// ```
const String yy = 'yy';

/// Outputs month as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 11), [mm]);
///     // => 11
///     formatDate(DateTime(1989, 5), [mm]);
///     // => 05
/// ```
const String mm = 'mm';

/// Outputs month compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 11), [mm]);
///     // => 11
///     formatDate(DateTime(1989, 5), [m]);
///     // => 5
/// ```
const String m = 'm';

/// Outputs day as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 2, 21), [dd]);
///     // => 21
///     formatDate(DateTime(1989, 2, 5), [dd]);
///     // => 05
/// ```
const String dd = 'dd';

/// Outputs day compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 2, 21), [d]);
///     // => 21
///     formatDate(DateTime(1989, 2, 5), [d]);
///     // => 5
/// ```
const String d = 'd';

/// Outputs week in month
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 2, 21), [w]);
///     // => 4
/// ```
const String w = 'w';

/// Outputs week in year as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 12, 31), [W]);
///     // => 53
///     formatDate(DateTime(1989, 2, 21), [W]);
///     // => 08
/// ```
const String WW = 'WW';

/// Outputs week in year compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 2, 21), [W]);
///     // => 8
/// ```
const String W = 'W';

/// Outputs hour (0 - 11) as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15), [hh]);
///     // => 03
/// ```
const String hh = 'hh';

/// Outputs hour (0 - 11) compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15), [h]);
///     // => 3
/// ```
const String h = 'h';

/// Outputs hour (0 to 23) as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15), [HH]);
///     // => 15
/// ```
const String HH = 'HH';

/// Outputs hour (0 to 23) compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 5), [H]);
///     // => 5
/// ```
const String H = 'H';

/// Outputs minute as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40), [nn]);
///     // => 40
///     formatDate(DateTime(1989, 02, 1, 15, 4), [nn]);
///     // => 04
/// ```
const String nn = 'nn';

/// Outputs minute compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 4), [n]);
///     // => 4
/// ```
const String n = 'n';

/// Outputs second as two digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10), [ss]);
///     // => 10
///     formatDate(DateTime(1989, 02, 1, 15, 40, 5), [ss]);
///     // => 05
/// ```
const String ss = 'ss';

/// Outputs second compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40, 5), [s]);
///     // => 5
/// ```
const String s = 's';

/// Outputs millisecond as three digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 999), [SSS]);
///     // => 999
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 99), [SS]);
///     // => 099
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 0), [SS]);
///     // => 009
/// ```
const String SSS = 'SSS';

/// Outputs millisecond compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 999), [SSS]);
///     // => 999
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 99), [SS]);
///     // => 99
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 9), [SS]);
///     // => 9
/// ```
const String S = 'S';

/// Outputs microsecond as three digits
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 0, 999), [uuu]);
///     // => 999
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 0, 99), [uuu]);
///     // => 099
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 0, 9), [uuu]);
///     // => 009
/// ```
const String uuu = 'uuu';

/// Outputs millisecond compactly
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 0, 999), [u]);
///     // => 999
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 0, 99), [u]);
///     // => 99
///     formatDate(DateTime(1989, 02, 1, 15, 40, 10, 0, 9), [u]);
///     // => 9
/// ```
const String u = 'u';

/// Outputs timezone as time offset
const String z = 'z';
const String Z = 'Z';

/// Escape delimiters to be used as normal string.
///
/// Example:
/// ```
///     formatDate(DateTime(1989, 02, 1, 15, 40), [HH,'\\h',nn]);
///     // => 15h40
///```
const String escape = r'\';

extension DateTimeExtension on DateTime {
  /// {@template unix}
  /// Number of seconds since epoch time / A.K.A Unix timestamp
  ///
  /// The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the
  /// number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT),
  /// not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z).
  /// Literally speaking the epoch is Unix time 0 (midnight 1/1/1970).
  /// {@endtemplate}
  static DateTime fromSecondsSinceEpoch(
    int secondsSinceEpoch, {
    bool isUtc = false,
  }) =>
      DateTime.fromMillisecondsSinceEpoch(
        secondsSinceEpoch * 1000,
        isUtc: isUtc,
      );

  /// {@macro unix}
  int get secondsSinceEpoch => millisecondsSinceEpoch ~/ 1000;

  /// Get the numer of milliseconds since epoch
  int get timestamp => millisecondsSinceEpoch;

  /// Tomorrow at same hour / minute / second than now
  static DateTime get tomorrow => DateTime.now().nextDay;

  /// Yesterday at same hour / minute / second than now
  static DateTime get yesterday => DateTime.now().previousDay;

  /// Current date (Same as [DateTime.now])
  static DateTime get today => DateTime.now();

  /// Subtracts a [Duration] from this [DateTime]
  DateTime sub(Duration duration) => add(Duration.zero - duration);

  /// Add a certain amount of milliseconds to this date
  DateTime addMilliseconds(int amount) => add(Duration(milliseconds: amount));

  /// Add a certain amount of microseconds to this date
  DateTime addMicroseconds(int amount) => add(Duration(microseconds: amount));

  /// Add a certain amount of minutes to this date
  DateTime addMinutes(int amount, {bool ignoreDaylightSavings = false}) =>
      ignoreDaylightSavings
          ? DateTime(
              year,
              month,
              day,
              hour,
              minute + amount,
              second,
              millisecond,
              microsecond,
            )
          : add(Duration(minutes: amount));

  /// Add a certain amount of hours to this date
  DateTime addHours(int amount, {bool ignoreDaylightSavings = false}) =>
      ignoreDaylightSavings
          ? DateTime(
              year,
              month,
              day,
              hour + amount,
              minute,
              second,
              millisecond,
              microsecond,
            )
          : add(Duration(hours: amount));

  /// Add a certain amount of days to this date
  DateTime addDays(int amount, {bool ignoreDaylightSavings = false}) =>
      ignoreDaylightSavings
          ? DateTime(
              year,
              month,
              day + amount,
              hour,
              minute,
              second,
              millisecond,
              microsecond,
            )
          : add(Duration(days: amount));

  /// The day after this [DateTime]
  DateTime get nextDay => addDays(1);

  /// The day previous this [DateTime]
  DateTime get previousDay => addDays(-1);

  /// The month after this [DateTime]
  DateTime get nextMonth => setMonth(month + 1);

  /// The month previous this [DateTime]
  DateTime get previousMonth => setMonth(month - 1);

  /// The year after this [DateTime]
  DateTime get nextYear => setYear(year + 1);

  /// The year previous this [DateTime]
  DateTime get previousYear => setYear(year - 1);

  /// The week after this [DateTime]
  DateTime get nextWeek => addDays(7);

  /// The week previous this [DateTime]
  DateTime get previousWeek => addDays(-7);

  /// Return true if this date [isAfter] [DateTime.now]
  bool get isFuture => isAfter(DateTime.now());

  /// Return true if this date [isBefore] [DateTime.now]
  bool get isPast => isBefore(DateTime.now());

  /// Is the given date in the leap year?
  bool get isLeapYear => year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);

  /// Change [year] of this date
  DateTime setYear(
    int year, [
    int? month,
    int? day,
    int? hour,
    int? minute,
    int? second,
    int? millisecond,
    int? microsecond,
  ]) =>
      DateTime(
        year,
        month ?? this.month,
        day ?? this.day,
        hour ?? this.hour,
        minute ?? this.minute,
        second ?? this.second,
        millisecond ?? this.millisecond,
        microsecond ?? this.microsecond,
      );

  /// Change [month] of this date
  DateTime setMonth(
    int month, [
    int? day,
    int? hour,
    int? minute,
    int? second,
    int? millisecond,
    int? microsecond,
  ]) =>
      DateTime(
        year,
        month,
        day ?? this.day,
        hour ?? this.hour,
        minute ?? this.minute,
        second ?? this.second,
        millisecond ?? this.millisecond,
        microsecond ?? this.microsecond,
      );

  /// Change [day] of this date
  DateTime setDay(
    int day, [
    int? hour,
    int? minute,
    int? second,
    int? millisecond,
    int? microsecond,
  ]) =>
      DateTime(
        year,
        month,
        day,
        hour ?? this.hour,
        minute ?? this.minute,
        second ?? this.second,
        millisecond ?? this.millisecond,
        microsecond ?? this.microsecond,
      );

  /// Change [hour] of this date
  DateTime setHour(
    int hour, [
    int? minute,
    int? second,
    int? millisecond,
    int? microsecond,
  ]) =>
      DateTime(
        year,
        month,
        day,
        hour,
        minute ?? this.minute,
        second ?? this.second,
        millisecond ?? this.millisecond,
        microsecond ?? this.microsecond,
      );

  /// Change [minute] of this date
  DateTime setMinute(
    int minute, [
    int? second,
    int? millisecond,
    int? microsecond,
  ]) =>
      DateTime(
        year,
        month,
        day,
        hour,
        minute,
        second ?? this.second,
        millisecond ?? this.millisecond,
        microsecond ?? this.microsecond,
      );

  /// Change [second] of this date
  DateTime setSecond(
    int second, [
    int? millisecond,
    int? microsecond,
  ]) =>
      DateTime(
        year,
        month,
        day,
        hour,
        minute,
        second,
        millisecond ?? this.millisecond,
        microsecond ?? this.microsecond,
      );

  /// Change [millisecond] of this date
  DateTime setMillisecond(
    int millisecond, [
    int? microsecond,
  ]) =>
      DateTime(
        year,
        month,
        day,
        hour,
        minute,
        second,
        millisecond,
        microsecond ?? this.microsecond,
      );

  /// Change [microsecond] of this date
  DateTime setMicrosecond(
    int microsecond,
  ) =>
      DateTime(
        year,
        month,
        day,
        hour,
        minute,
        second,
        millisecond,
        microsecond,
      );

  String format(List<String> formats) {
    String digits(int value, int length) {
      String ret = '$value';
      if (ret.length < length) {
        ret = '0' * (length - ret.length) + ret;
      }
      return ret;
    }

    int dayInYear(DateTime date) => date.difference(DateTime(date.year)).inDays;

    final sb = StringBuffer();

    for (String format in formats) {
      if (format.startsWith(escape)) {
        format = format.substring(1);
        sb.write(format);
      } else if (format == yyyy) {
        sb.write(digits(year, 4));
      } else if (format == yy) {
        sb.write(digits(year % 100, 2));
      } else if (format == mm) {
        sb.write(digits(month, 2));
      } else if (format == m) {
        sb.write(month);
      } else if (format == dd) {
        sb.write(digits(day, 2));
      } else if (format == d) {
        sb.write(day);
      } else if (format == w) {
        sb.write((day + 7) ~/ 7);
      } else if (format == W) {
        sb.write((dayInYear(this) + 7) ~/ 7);
      } else if (format == WW) {
        sb.write(digits((dayInYear(this) + 7) ~/ 7, 2));
      } else if (format == HH) {
        sb.write(digits(hour, 2));
      } else if (format == H) {
        sb.write(hour);
      } else if (format == hh) {
        int h = hour % 12;
        if (h == 0) {
          h = 12;
        }
        sb.write(digits(h, 2));
      } else if (format == h) {
        int h = hour % 12;
        if (h == 0) {
          h = 12;
        }
        sb.write(h);
      } else if (format == nn) {
        sb.write(digits(minute, 2));
      } else if (format == n) {
        sb.write(minute);
      } else if (format == ss) {
        sb.write(digits(second, 2));
      } else if (format == s) {
        sb.write(second);
      } else if (format == SSS) {
        sb.write(digits(millisecond, 3));
      } else if (format == S) {
        sb.write(millisecond);
      } else if (format == uuu) {
        sb.write(digits(microsecond, 3));
      } else if (format == u) {
        sb.write(microsecond);
      } else if (format == z) {
        if (timeZoneOffset.inMinutes == 0) {
          sb.write('Z');
        } else {
          if (timeZoneOffset.isNegative) {
            sb
              ..write('-')
              ..write(digits((-timeZoneOffset.inHours) % 24, 2))
              ..write(digits((-timeZoneOffset.inMinutes) % 60, 2));
          } else {
            sb
              ..write('+')
              ..write(digits(timeZoneOffset.inHours % 24, 2))
              ..write(digits(timeZoneOffset.inMinutes % 60, 2));
          }
        }
      } else if (format == Z) {
        sb.write(timeZoneName);
      } else {
        sb.write(format);
      }
    }

    return sb.toString();
  }

  bool operator <(DateTime other) => isBefore(other);

  bool operator <=(DateTime other) =>
      isBefore(other) || isAtSameMomentAs(other);

  bool operator >(DateTime other) => isAfter(other);

  bool operator >=(DateTime other) => isAfter(other) || isAtSameMomentAs(other);
}
