import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/number/number_utils.dart';
import 'package:intl/intl.dart';

/// Created by @Haoyi on 2020/5/20
///
///

enum TimeUnit { milliseconds, seconds, minutes, hour, day, month, year, infinite }

/// Signature for a function that creates a widget for a given `day`.
typedef DayBuilder = Widget? Function(BuildContext context, DateTime day);

/// Signature for a function that creates a widget for a given `day`.
/// Additionally, contains the currently focused day.
typedef FocusedDayBuilder = Widget? Function(
    BuildContext context, DateTime day, DateTime focusedDay);

/// Signature for a function returning text that can be localized and formatted with `DateFormat`.
typedef TextFormatter = String Function(DateTime date, dynamic locale);

/// Gestures available for the calendar.
enum AvailableGestures { none, verticalSwipe, horizontalSwipe, all }

/// Formats that the calendar can display.
enum CalendarFormat { month, twoWeeks, week }

/// Days of the week that the calendar can start with.
enum DayOfWeek {
  monday,
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday,
}

final daysFormat = RegExp(r"#d{([^#]*)}");
final hoursFormat = RegExp(r"#h{([^#]*)}");
final minutesFormat = RegExp(r"#m{([^#]*)}");
final secondsFormat = RegExp(r"#s{([^#]*)}");
final millisecondsFormat = RegExp(r"#ms{([^#]*)}");

/// Days in a month. This array uses 1-based month numbers, i.e. January is
/// the 1-st element in the array, not the 0-th.
const _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

class DateTimeUtils {
  static final yyyyMMddDateFormat = DateFormat("yyyyMMdd");
  static final yyMMddDateFormat = DateFormat("yyMMdd");
  static final yyyyMMNormalDateFormat = DateFormat("yyyy-MM");
  static final yyyyMMddNormalDateFormat = DateFormat("yyyy-MM-dd");
  static final MMddNormalDateFormat = DateFormat("MM.dd");
  static final standardDateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");

  static final yyyyMMDateFormat = DateFormat("yyyyMM");
  static final MMMDateFormat = DateFormat("MMM");
  static final monthAbbreviatedFormat = DateFormat("MMM");

  static final hamanDateFormat = DateFormat.yMd().add_jm();

  static final standardTimeFormat = DateFormat("HH:mm:ss");

  static const secondInMillis = 1000;
  static const minuteInMillis = secondInMillis * 60;
  static const hourInMillis = secondInMillis * 3600;
  static const sixHourInMillis = hourInMillis * 6;
  static const quarterOfHourInMillis = minuteInMillis * 15;
  static const halfHourInMillis = minuteInMillis * 30;
  static const dayInMillis = hourInMillis * 24;
  static const weekInMillis = dayInMillis * 7;

  static const minuteInSecond = 60;
  static const hourInSecond = 3600;
  static const quarterOfHourInSecond = 900;
  static const halfHourInSecond = 1800;

  static String get yyyyMMdd => yyyyMMddDateFormat.format(DateTime.now());

  static String get yyMMdd => yyMMddDateFormat.format(DateTime.now());

  static String get yyyyMM => yyyyMMDateFormat.format(DateTime.now());

  static String get MMM => MMMDateFormat.format(DateTime.now());

  static String get humanDatetime => hamanDateFormat.format(DateTime.now());

  static String get humanTime => standardTimeFormat.format(DateTime.now());

  static int get yyyyMMddUtcNum {
    final now = DateTime.now().toUtc();
    return now.year * 10000 + now.month * 100 + now.day;
  }

  static int get yearMonthNum {
    final now = DateTime.now();
    return generateYearMonthNum(now);
  }

  static String yyyyMMddBuild(DateTime dateTime) {
    // return yyyyMMddDateFormat.format(dateTime);
    return "${dateTime.year}${NumberUtils.twoDigits(dateTime.month)}${NumberUtils.twoDigits(
        dateTime.day)}";
  }

  static String yyyyMMBuild(DateTime dateTime) {
    return "${dateTime.year}${NumberUtils.twoDigits(dateTime.month)}";
    // return yyyyMMDateFormat.format(dateTime);
  }

  static int generateYearMonthNum(DateTime dateTime) {
    return dateTime.year * 100 + dateTime.month;
  }

  static int generateYearMonthDayNum(DateTime dateTime) {
    return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
  }

  static int yyyyMMddStr2Num(String yyyyMMdd) {
    return int.parse(yyyyMMdd);
  }

  static String yyyyMMddStr2yyyyMMStr(String yyyyMMdd) {
    return yyyyMMdd.substring(0, 6);
  }

  static int yyyyMMddStr2yyyyMMNum(String yyyyMMdd) {
    return int.parse(yyyyMMddStr2yyyyMMStr(yyyyMMdd));
  }

  static String formatMMdd(DateTime dateTime) {
    return "${NumberUtils.twoDigits(dateTime.month)}${NumberUtils.twoDigits(dateTime.day)}";
    // return yyyyMMDateFormat.format(dateTime);
  }

  // static String yyyyMMddNormalBuild(DateTime dateTime) {
  //   return yyyyMMddNormalDateFormat.format(dateTime);
  // }

  static int millis2second(int millis) {
    return millis ~/ Duration.millisecondsPerSecond;
  }

  static int minutes2millis(int minutes) {
    return minutes * minuteInMillis;
  }

  static int minutes2seconds(int minutes) {
    return minutes * minuteInSecond;
  }

  static int hour2millis(int hours) {
    return hours * hourInMillis;
  }

  static int hour2seconds(int hours) {
    return hours * hourInSecond;
  }

  static DateTime createDateTimeFromDateNum(int yyyyMMdd) {
    final year = yyyyMMdd ~/ 10000;
    final month = (yyyyMMdd - (year * 10000)) ~/ 100;
    final day = yyyyMMdd - (year * 10000 + month * 100);
    return DateTime(year, month, day);
  }

  static DateTime createDateTimeFromYearMonth(int yyyyMM) {
    final year = yyyyMM ~/ 100;
    final month = yyyyMM - (year * 100);
    return DateTime(year, month, 1);
  }

  static List<DateTime> buildDailyDateTimeGroup() {
    final now = DateTime.now();
    final result = <DateTime>[];
    for (int year = now.year; year >= 2020; year--) {
      int month = (year != now.year) ? 12 : now.month;
      int minMonth = year == 2020 ? 5 : 1;
      for (; month >= minMonth; month--) {
        result.add(DateTime(year, month));
      }
    }
    return result;
  }

  static int currentTimeInSecond() {
    return DateTime
        .now()
        .millisecondsSinceEpoch
        .toDouble() ~/ Duration.millisecondsPerSecond;
  }

  static int currentTimeInMillis() {
    return DateTime
        .now()
        .millisecondsSinceEpoch;
  }

  static int currentTimeInMicros() {
    return DateTime
        .now()
        .microsecondsSinceEpoch;
  }

  static int toSecondSinceEpoch(DateTime dateTime) {
    return dateTime.millisecondsSinceEpoch ~/ 1000;
  }


  static String abbrevDayOfWeek(DateTime dateTime) {
    switch (dateTime.weekday) {
      case DateTime.monday:
        return "M";
      case DateTime.tuesday:
      case DateTime.thursday:
        return "T";
      case DateTime.wednesday:
        return "W";
      case DateTime.friday:
        return "F";
      default:
        return "S";
    }
  }

  static Duration getDuration(DateTime start, DateTime to) {
    return Duration(milliseconds: to.millisecondsSinceEpoch - start.millisecondsSinceEpoch);
  }

  static Duration remainingDuration() {
    final now = DateTime.now();
    final nextDay = now.add(Duration(days: 1));
    return getDuration(now, DateTime(nextDay.year, nextDay.month, nextDay.day));
  }

  static Duration getDurationForMinutes(int startMinutes, int endMinutes) {
    return Duration(minutes: endMinutes - startMinutes);
  }

  static Duration getDurationForMillis(int start, int end) {
    return Duration(milliseconds: end - start);
  }

  static String formatMilliseconds(int milliseconds) {
    String twoDigitHour = "";
    String twoDigitMinutes = NumberUtils.twoDigits(
        (milliseconds ~/ Duration.millisecondsPerMinute).remainder(Duration.minutesPerHour));
    String twoDigitSeconds = NumberUtils.twoDigits(
        (milliseconds ~/ Duration.millisecondsPerSecond).remainder(Duration.secondsPerMinute));
    if (milliseconds > DateTimeUtils.hourInMillis) {
      twoDigitHour = NumberUtils.twoDigits((milliseconds ~/ Duration.millisecondsPerHour));
      return "$twoDigitHour:$twoDigitMinutes:$twoDigitSeconds";
    }
    return "$twoDigitMinutes:$twoDigitSeconds";
  }

  static String formatDurationShort(int milliseconds) {
    final int hour = (milliseconds ~/ Duration.microsecondsPerHour);
    int minute =
    (milliseconds ~/ Duration.millisecondsPerMinute).remainder(Duration.minutesPerHour);
    int second =
    (milliseconds ~/ Duration.millisecondsPerSecond).remainder(Duration.secondsPerMinute);
    int millis = milliseconds.remainder(Duration.millisecondsPerSecond);

    if (hour > 0) {
      return "${hour.format(2)}:${minute.format(2)}:${second.format(2)}.${millis.format(3)}";
    } else if (minute > 0) {
      return "${minute.format(2)} m ${second.format(2)}.${millis.format(3)} s";
    } else {
      return "${second.format(2)}.${millis.format(3)} s";
    }
  }

  static String formatSeconds(int seconds) {
    return formatMilliseconds(seconds * 1000);
  }

  static String formatMillisecondsToStandard(int milliseconds) {
    return milliseconds == 0
        ? "00:00:00"
        : standardDateFormat.format(DateTime.fromMillisecondsSinceEpoch(milliseconds));
  }

  static String formatLooseDuration(Duration? duration, {TimeUnit minUnit = TimeUnit.day}) {
    if (duration == null) {
      return "-- : -- : --";
    }

    if (duration.inMicroseconds < 1000) {
      return "00 : 00 : 00";
    }
    String twoDigitMinutes =
    NumberUtils.twoDigits(duration.inMinutes.remainder(Duration.minutesPerHour));
    String twoDigitSeconds =
    NumberUtils.twoDigits(duration.inSeconds.remainder(Duration.secondsPerMinute));
    if (duration.inHours <= 0) {
      if (minUnit.index >= TimeUnit.hour.index) {
        return "00 : $twoDigitMinutes : $twoDigitSeconds";
      } else {
        return "$twoDigitMinutes : $twoDigitSeconds";
      }
    } else {
      return "${NumberUtils.twoDigits(duration.inHours)} : $twoDigitMinutes : $twoDigitSeconds";
    }
  }

  static String formatDuration(Duration? duration,
      {bool verbose = false, TimeUnit minUnit = TimeUnit.day}) {
    if (duration == null) {
      return "--:--:--";
    }

    if (duration.inMicroseconds < 1000) {
      if (verbose != true) {
        return "00:00:00";
      } else {
        return "00 hr 00 min 00 sec";
      }
    }
    if (duration.inDays > 1) {
      String twoDigitHour = NumberUtils.twoDigits(duration.inHours.remainder(Duration.hoursPerDay));
      return "${duration.inDays}d ${twoDigitHour}h";
    }

    String twoDigitMinutes =
    NumberUtils.twoDigits(duration.inMinutes.remainder(Duration.minutesPerHour));
    String twoDigitSeconds =
    NumberUtils.twoDigits(duration.inSeconds.remainder(Duration.secondsPerMinute));
    if (verbose != true) {
      if (duration.inHours <= 0) {
        if (minUnit.index >= TimeUnit.hour.index) {
          return "00:$twoDigitMinutes:$twoDigitSeconds";
        } else {
          return "$twoDigitMinutes:$twoDigitSeconds";
        }
      } else {
        return "${NumberUtils.twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
      }
    } else {
      if (duration.inHours <= 0) {
        return "$twoDigitMinutes min $twoDigitSeconds sec";
      } else if (duration.inHours < 10) {
        return "${NumberUtils.twoDigits(
            duration.inHours)} hr $twoDigitMinutes min $twoDigitSeconds sec";
      } else {
        return "${duration.inHours} hr $twoDigitMinutes min $twoDigitSeconds sec";
      }
    }
  }

  // {dd} : inDays
  // {hh} : inHours
  // {mm} : inMinutes
  // {ss} : inSeconds
  // {ms} : inMillis remainder per seconds
  static String formatDurationV2(Duration? duration,
      {String format = "", bool eliminateRedundancy = true}) {
    if (duration == null || duration.inMilliseconds < 1000) {
      return "---";
    }

    int days = duration.inDays > 0 ? duration.inDays : 0;
    int hours = duration.inHours.remainder(Duration.hoursPerDay);
    int minutes = duration.inMinutes.remainder(Duration.minutesPerHour);
    int seconds = duration.inSeconds.remainder(Duration.secondsPerMinute);
    int millis = duration.inMilliseconds.remainder(Duration.millisecondsPerSecond);

    bool canIgnore = eliminateRedundancy;
    String result = format;
    final daysMatches = daysFormat.allMatches(result);
    if (daysMatches.isNotEmpty) {
      for (var match in daysMatches) {
        final pattern = match.group(0);
        if (pattern != null) {
          final group = match.groupCount > 0 ? match.group(1) : "";
          if (days > 0) {
            result = result.replaceAll(pattern, "$days$group");
            canIgnore = false;
          } else if (canIgnore) {
            result = result.replaceAll(pattern, "");
          } else {
            result = result.replaceAll(pattern, "00$group");
          }
        }
      }
    } else {
      hours += days * Duration.hoursPerDay;
    }

    final hoursMatches = hoursFormat.allMatches(result);
    if (hoursMatches.isNotEmpty) {
      for (var match in hoursMatches) {
        final pattern = match.group(0);
        if (pattern != null) {
          final group = match.groupCount > 0 ? match.group(1) : "";
          if (hours > 0) {
            result = result.replaceAll(pattern, "${hours.format(2)}$group");
            canIgnore = false;
          } else if (canIgnore) {
            result = result.replaceAll(pattern, "");
          } else {
            result = result.replaceAll(pattern, "00$group");
          }
        }
      }
    } else {
      minutes += hours * Duration.minutesPerHour;
    }

    final minutesMatches = minutesFormat.allMatches(result);
    if (minutesMatches.isNotEmpty) {
      for (var match in minutesMatches) {
        final pattern = match.group(0);
        if (pattern != null) {
          final group = match.groupCount > 0 ? match.group(1) : "";
          if (minutes > 0) {
            result = result.replaceAll(pattern, "${minutes.format(2)}$group");
            canIgnore = false;
          } else if (canIgnore) {
            result = result.replaceAll(pattern, "");
          } else {
            result = result.replaceAll(pattern, "00$group");
          }
        }
      }
    } else {
      seconds += minutes * Duration.secondsPerMinute;
    }

    final secondsMatches = secondsFormat.allMatches(result);
    if (secondsMatches.isNotEmpty) {
      for (var match in secondsMatches) {
        final pattern = match.group(0);
        if (pattern != null) {
          final group = match.groupCount > 0 ? match.group(1) : "";
          if (seconds > 0) {
            result = result.replaceAll(pattern, "${seconds.format(2)}$group");
            canIgnore = false;
          } else if (canIgnore) {
            result = result.replaceAll(pattern, "");
          } else {
            result = result.replaceAll(pattern, "00$group");
          }
        }
      }
    } else {
      millis += minutes * Duration.secondsPerMinute;
    }

    final millisecondsMatches = millisecondsFormat.allMatches(result);
    if (millisecondsMatches.isNotEmpty) {
      for (var match in millisecondsMatches) {
        final pattern = match.group(0);
        if (pattern != null) {
          final group = match.groupCount > 0 ? match.group(1) : "";
          if (millis > 0) {
            result = result.replaceAll(pattern, "${millis.format(3)}$group");
            canIgnore = false;
          } else if (canIgnore) {
            result = result.replaceAll(pattern, "");
          } else {
            result = result.replaceAll(pattern, "00$group");
          }
        }
      }
    } else {
      millis += minutes * Duration.secondsPerMinute;
    }

    return result;
  }

  static List<int> getYearList(int start, [int? end]) {
    final now = DateTime.now();
    final endYear = (end == null || end < start) ? now.year : end;
    final startYear = min(start, endYear);

    return List.generate(endYear - startYear + 1, (year) => year + start);
  }

  static List<DateTime> generateDaysInMonth(int year, int month) {
    final count = daysInMonth(year, month);
    final List<DateTime> result = [];
    for (int day = 1; day <= count; ++day) {
      result.add(DateTime(year, month, day));
    }
    return result;
  }

  static Week currentDaysInWeek() {
    return Week.current();
  }

  static String twoDigits(int n) {
    if (n >= 10) return "$n";
    return "0$n";
  }

  static int getDayCountOfMonth(DateTime dateTime) {
    final year = dateTime.year;
    final month = dateTime.month;
    return DateTime(year, month + 1, 0).day;
  }

  /// Returns a numerical value associated with given `weekday`.
  ///
  /// Returns 1 for `DayOfWeek.monday`, all the way to 7 for `DayOfWeek.sunday`.
  static int getWeekdayNumber(DayOfWeek weekday) {
    return DayOfWeek.values.indexOf(weekday) + 1;
  }

  /// Returns `date` in UTC format, without its time part.
  static DateTime normalizeDate(DateTime date) {
    return DateTime.utc(date.year, date.month, date.day);
  }

  /// Checks if two DateTime objects are the same day.
  /// Returns `false` if either of them is null.
  static bool isSameDay(DateTime? a, DateTime? b) {
    if (a == null || b == null) {
      return false;
    }

    return a.year == b.year && a.month == b.month && a.day == b.day;
  }

  static bool isSameMonth(DateTime? a, DateTime? b) {
    if (a == null || b == null) {
      return false;
    }
    return a.year == b.year && a.month == b.month;
  }

  static int getHashCode(DateTime key) {
    return key.day * 1000000 + key.month * 10000 + key.year;
  }

  static int getRowCount(DateTime focusedDay) {
    final first = _firstDayOfMonth(focusedDay);
    final daysBefore = getDaysBefore(first);
    final firstToDisplay = first.subtract(Duration(days: daysBefore));

    final last = lastDayOfMonth(focusedDay);
    final daysAfter = getDaysAfter(last);
    final lastToDisplay = last.add(Duration(days: daysAfter));

    return (lastToDisplay
        .difference(firstToDisplay)
        .inDays + 1) ~/ 7;
  }

  static DateTime _firstDayOfMonth(DateTime month) {
    return DateTime.utc(month.year, month.month, 1);
  }

  static int dayOfYear(DateTime dateTime) {
    final _first =
    dateTime.isUtc ? DateTime.utc(dateTime.year, 1, 1) : DateTime(dateTime.year, 1, 1);
    return dateTime
        .difference(_first)
        .inDays;
  }

  static int getDaysBefore(DateTime firstDay) {
    return (firstDay.weekday + 7 - getWeekdayNumber(DayOfWeek.sunday)) % 7;
  }

  static DateTime lastDayOfMonth(DateTime month) {
    final date = month.month < 12
        ? DateTime.utc(month.year, month.month + 1, 1)
        : DateTime.utc(month.year + 1, 1, 1);
    return date.subtract(const Duration(days: 1));
  }

  static int getDaysAfter(DateTime lastDay) {
    int invertedStartingWeekday = 8 - getWeekdayNumber(DayOfWeek.sunday);

    int daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7);
    if (daysAfter == 7) {
      daysAfter = 0;
    }

    return daysAfter;
  }

  /// Returns the number of days in the specified month.
  ///
  /// This function assumes the use of the Gregorian calendar or the proleptic
  /// Gregorian calendar.
  static int daysInMonth(int year, int month) =>
      (month == DateTime.february && isLeapYear(year)) ? 29 : _daysInMonth[month];

  /// Returns true if [year] is a leap year.
  ///
  /// This implements the Gregorian calendar leap year rules wherein a year is
  /// considered to be a leap year if it is divisible by 4, excepting years
  /// divisible by 100, but including years divisible by 400.
  ///
  /// This function assumes the use of the Gregorian calendar or the proleptic
  /// Gregorian calendar.
  static bool isLeapYear(int year) => (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));

  /// Takes a [date] that may be outside the allowed range of dates for a given
  /// [month] in a given [year] and returns the closest date that is within the
  /// allowed range.
  ///
  /// For example:
  ///
  /// February 31, 2013 => February 28, 2013
  ///
  /// When jumping from month to month or from leap year to common year we may
  /// end up in a month that has fewer days than the month we are jumping from.
  /// In that case it is impossible to preserve the exact date. So we "clamp" the
  /// date value to fit within the month. For example, jumping from March 31 one
  /// month back takes us to February 28 (or 29 during a leap year), as February
  /// doesn't have 31-st date.
  static int clampDayOfMonth({
    required int year,
    required int month,
    required int day,
  }) =>
      day.clamp(1, daysInMonth(year, month));
}

class DailyMonthGroup {
  final DateTime dateTime;
  final int year;
  final int month;

  DailyMonthGroup(this.dateTime, this.year, this.month);
}

class Week {
  final List<DateTime> date;

  Week.begin(DateTime first)
      : date =
  List.generate(7, (index) => first.add(Duration(days: index))).toList(growable: false);

  Week.end(DateTime first)
      : date = List.generate(7, (index) => first.subtract(Duration(days: index)))
      .toList(growable: false);

  static Week current({int offset = 0}) {
    final now = DateTime.now();
    final first = now.subtract(Duration(days: now.weekday - 1 + offset));
    return Week.begin(first);
  }
}
