import { VegaInternalTranslation } from '../translation/internal-translation-controller';
import { CalendarDateTime } from './calendar-date-time';
import { getSystemTimezone } from './utils';
import { DateFormatParserRegex } from '../../constants/regex';
import { compareIsAfter, compareIsBefore, pad } from '../../utils/date';
export const WEEKDAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// TODO: Need rename calendar-date as vega-date to manage all date operation in vega project
// https://gethired.atlassian.net/browse/VD-5918
/**
 * Calendar date class
 */
export class CalendarDate {
  constructor(year, month, date) {
    this._date = date;
    this._month = month;
    this._year = year;
  }
  /**
   * Create a calendar date from a Date object
   *
   * @param {Date} currentDate - The Date object to create CalendarDate from
   * @param {string} timezone - Timezone identifier for timezone-aware date creation. If not provided, uses system timezone.
   * @returns {CalendarDate} - Return a CalendarDate object
   */
  static from(currentDate, timezone = getSystemTimezone()) {
    return CalendarDateTime.fromDate(currentDate, timezone).calendarDate;
  }
  /**
   * Converts a date string to CalendarDate with optional format and timezone parameters.
   *
   * @param {string} dateString - The string date.
   * @param {string} [format] - The format of string date
   * @param {string} [lang] - lang
   * @returns {Nullable<CalendarDate>} - CalendarDate.
   */
  static fromString(dateString, format = 'MM/DD/YYYY', lang = 'en-us') {
    if (!dateString)
      return null;
    const formatMatchers = [];
    let formatMatcher = null;
    // Analyze the format and find the corresponding positions for year, month, and day.
    // Data: formatMatchers = [['DD', index: 0, ...], ['MM', index: 3, ...], ['YYYY', index: 7, ...]]
    while ((formatMatcher = DateFormatParserRegex.exec(format)) != null) {
      if (formatMatcher[1] === '\\')
        continue;
      formatMatchers.push(formatMatcher);
    }
    if (formatMatchers.length) {
      const datePattern = {
        year: undefined,
        month: undefined,
        shortMonth: undefined,
        longMonth: undefined,
        day: undefined,
        value: '',
      };
      const firstMatcher = formatMatchers[0];
      if (firstMatcher && firstMatcher.index > 0) {
        datePattern.value += '.*?';
      }
      // According `matchers` to generate a new regex which can parse the date by corresponding year, month and day
      // Data: dateRegex = "(\d{2}).*?(\d{2}).*?(\d{4}).*?"
      //       datePattern = {day: 1, longMonth: null, month: 2, shortMonth: null, year: 3, value: "(\d{2}).*?(\d{2}).*?(\d{4}).*?"}
      for (const [k, match] of Object.entries(formatMatchers)) {
        const key = Number(k);
        if (match) {
          const { group, pattern } = this.getPatternByFormatToken(match[0], lang);
          datePattern[group] = key + 1;
          datePattern.value += pattern;
          datePattern.value += '.*?';
        }
      }
      const dateRegex = new RegExp(`^${datePattern.value}$`);
      if (dateRegex.test(dateString)) {
        // Parse the date
        // Example: parsedDate = ['08-02-2020', '08', '02', '2020']
        const parsedDate = dateRegex.exec(dateString);
        if (parsedDate &&
          datePattern.year &&
          (datePattern.month || datePattern.shortMonth || datePattern.longMonth) &&
          datePattern.day) {
          const year = Number(parsedDate[datePattern.year]);
          let month = 1;
          if (datePattern.month) {
            month = Number(parsedDate[datePattern.month]) - 1;
          }
          else if (datePattern.shortMonth) {
            month = this.shortMonths(lang).indexOf(parsedDate[datePattern.shortMonth]);
          }
          else if (datePattern.longMonth) {
            month = this.longMonths(lang).indexOf(parsedDate[datePattern.longMonth]);
          }
          const day = Number(parsedDate[datePattern.day]);
          const fullYear = Number(pad(year, 4, `${new Date().getFullYear()}`.slice(0, 2)));
          if (isNaN(Date.parse(`${fullYear}-${pad(month + 1)}-${pad(day)}`)))
            return null;
          return this.from(new Date(fullYear, month, day, 0, 0, 0, 0));
        }
      }
    }
    return this.from(new Date(dateString));
  }
  /**
   * Get regex pattern by format token.
   *
   * @param {string} token - The format token.
   * @param {string} lang - Language.
   * @returns {FormatPatternType} - regex pattern.
   */
  static getPatternByFormatToken(token, lang) {
    switch (token) {
      case 'YY':
      case 'YYYY':
        return {
          group: 'year',
          pattern: `(\\d{${token.length}})`,
        };
      case 'M':
        return {
          group: 'month',
          pattern: '(\\d{1,2})',
        };
      case 'MM':
        return {
          group: 'month',
          pattern: '(\\d{2})',
        };
      case 'MMM':
        return {
          group: 'shortMonth',
          pattern: `(${this.shortMonths(lang).join('|')})`,
        };
      case 'MMMM':
        return {
          group: 'longMonth',
          pattern: `(${this.longMonths(lang).join('|')})`,
        };
      case 'D':
        return {
          group: 'day',
          pattern: '(\\d{1,2})',
        };
      case 'DD':
        return {
          group: 'day',
          pattern: '(\\d{2})',
        };
      /* istanbul ignore next */
      default: {
        return {
          group: 'unknown',
          pattern: '(.*)',
        };
      }
    }
  }
  /**
   * Shorts months.
   *
   * @param {string} lang - Language
   * @returns {string[]} - Shorts months.
   */
  static shortMonths(lang) {
    return this.MONTHS.map((month) => new Date(2019, month).toLocaleString(lang, { month: 'short' }));
  }
  /**
   * Long months.
   *
   * @param {string} lang - Language
   * @returns {string[]} - Long months.
   */
  static longMonths(lang) {
    return this.MONTHS.map((month) => new Date(2019, month).toLocaleString(lang, { month: 'long' }));
  }
  /**
   * The year of the calendar date
   *
   * @type {number}
   */
  get year() {
    return this._year;
  }
  set year(value) {
    this._year = value;
  }
  /**
   * The month of the calendar date
   *
   * @type {number}
   */
  get month() {
    return this._month;
  }
  set month(value) {
    this._month = value;
  }
  /**
   * The date of the calendar date
   *
   * @type {number}
   */
  get date() {
    return this._date;
  }
  set date(value) {
    this._date = value;
  }
  /**
   * Converts CalendarDate to a string with an optional format specifier.
   *
   * @param {string} [format] - The format of string date
   * @param {string} [lang] - Language
   * @returns {string} The string date.
   */
  toString(format = 'MM/DD/YYYY', lang = 'en-us') {
    let result = '';
    const formatMatchers = [];
    let formatMatcher = null;
    while ((formatMatcher = DateFormatParserRegex.exec(format)) != null) {
      if (formatMatcher[1] === '\\')
        continue;
      formatMatchers.push(formatMatcher);
    }
    if (formatMatchers.length) {
      // add start line of tokens are not at the beginning
      const firstMatcher = formatMatchers[0];
      if (firstMatcher && firstMatcher.index > 0) {
        result += format.substring(0, firstMatcher.index);
      }
      for (const [k, match] of Object.entries(formatMatchers)) {
        const key = Number(k);
        if (match) {
          result += this.getDateItemByFormatToken(match[0], lang);
          const nextMatch = formatMatchers[key + 1];
          if (nextMatch) {
            result += format.substring(match.index + match[0].length, nextMatch.index);
          }
          if (key === formatMatchers.length - 1) {
            result += format.substring(match.index + match[0].length);
          }
        }
      }
    }
    return result.replace(/\\/g, '');
  }
  /**
   * Verify whether the calendar date is today
   *
   * @param {string} timezone - Timezone to consider when checking today's date. If not provided, uses system timezone.
   * @returns {boolean} - Return true if the calendar date is today. Otherwise, return false.
   */
  isToday(timezone = getSystemTimezone()) {
    if (timezone === getSystemTimezone()) {
      const today = new Date();
      return (today.getFullYear() === this.year &&
        today.getMonth() + 1 === this.month &&
        today.getDate() === this.date);
    }
    const { year, month, date } = CalendarDateTime.fromDate(new Date(), timezone).calendarDate;
    return year === this.year && month === this.month && date === this.date;
  }
  /**
   * Convert the calendar date to a Date object
   *
   * @returns {Date} - Return a Date object
   */
  toDate() {
    return new Date(this.year, this.month - 1, this.date);
  }
  /**
   * Compare with another calendar date
   *
   * @param {CalendarDate} date - The calendar date to compare with
   * @returns {DateCompareResult} - Return a DateCompareResult object
   */
  compare(date) {
    const { year, month } = date;
    const result = {
      isPreviousMonth: false,
      inMonth: false,
      isNextMonth: false,
    };
    if (this.year === year) {
      if (this.month < month) {
        result.isPreviousMonth = true;
      }
      else if (this.month > month) {
        result.isNextMonth = true;
      }
      else {
        result.inMonth = true;
      }
    }
    else if (this.year > year) {
      result.isNextMonth = true;
    }
    else {
      result.isPreviousMonth = true;
    }
    return result;
  }
  /**
   * The function "equal" compares the year, month, and date of two CalendarDate objects and returns  true if they are equal.
   *
   * @param {Nullable<CalendarDate>} calendarDate - The `equal` method in the code snippet is a function that compares the year, month, and date of the current `CalendarDate` object with another
   * `CalendarDate` object passed as a parameter. The method returns `true` if all three values (year, month, date) are equal
   * @returns {boolean} The date is same day with current
   */
  equal(calendarDate) {
    if (!calendarDate)
      return false;
    const { year, month, date } = calendarDate;
    return this._year === year && this._month === month && this._date === date;
  }
  /**
   * Get month name.
   *
   * @param {boolean} isShortName - Whether to get the short name or long name of the month
   * @returns {string} The name of the month
   */
  getMonthName(isShortName = false) {
    return VegaInternalTranslation.t(this.toDate().toLocaleDateString('en-US', {
      month: isShortName ? 'short' : 'long',
    }));
  }
  /**
   * To determine if it is larger than a given `CalendarDate`.
   *
   * @param {Nullable<CalendarDate>} calendarDate - A `CalendarDate`
   * @returns {boolean} - Compared result
   */
  isAfter(calendarDate) {
    if (!calendarDate)
      return false;
    return compareIsAfter(this.toDate(), calendarDate.toDate());
  }
  /**
   * To determine if it is less than a given `CalendarDate`.
   *
   * @param {Nullable<CalendarDate>} calendarDate - A `CalendarDate`
   * @returns {boolean} - Compared result
   */
  isBefore(calendarDate) {
    if (!calendarDate)
      return false;
    return compareIsBefore(this.toDate(), calendarDate.toDate());
  }
  /**
   * Get first day of month.
   *
   * @returns {CalendarDate} - First day of month.
   */
  firstDateOfMonth() {
    return CalendarDate.from(new Date(this.year, this.month - 1, 1));
  }
  /**
   * Get last day of month.
   *
   * @returns {CalendarDate} - Last day of month.
   */
  lastDateOfMonth() {
    return CalendarDate.from(new Date(this.year, this.month, 0));
  }
  /**
   * Adjusts the calendar date by adding the specified number of years, months, and days.
   *
   * @param {number} [years=0] - The number of years to add.
   * @param {number} [months=0] - The number of months to add.
   * @param {number} [days=0] - The number of days to add.
   * @returns {CalendarDate} - A new CalendarDate object with the adjusted date.
   */
  add(years = 0, months = 0, days = 0) {
    const currentDate = this.toDate();
    const adjustedDate = new Date(currentDate.getFullYear() + years, currentDate.getMonth() + months, currentDate.getDate() + days);
    return CalendarDate.from(adjustedDate);
  }
  /**
   * Get the short name of the week day for the calendar date.
   *
   * @returns {string} - The short name of the week day.
   */
  getWeekDayShortName() {
    const currentDate = this.toDate();
    return WEEKDAYS_SHORT[currentDate.getDay()];
  }
  /**
   * Get year, month and day value by format token.
   *
   * @param {string} token - The format token.
   * @param {string} lang - Language.
   * @returns {string} - year, month and day value.
   */
  getDateItemByFormatToken(token, lang) {
    switch (token) {
      case 'YY':
        return String(this.year).slice(-2);
      case 'YYYY':
        return String(this.year);
      case 'M':
        return String(this.month);
      case 'MM':
        return `0${this.month}`.slice(-2);
      case 'MMM':
        return CalendarDate.shortMonths(lang)[this.month - 1];
      case 'MMMM':
        return CalendarDate.longMonths(lang)[this.month - 1];
      case 'D':
        return String(this.date);
      case 'DD':
        return `0${this.date}`.slice(-2);
      default:
        return '';
    }
  }
}
CalendarDate.MONTHS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
