import type { OldLangFn } from '../../hooks/useOldLang';
import type { TimeFormat } from '../../types';
import type { LangFn } from '../localization';

import { getServerTime } from '../serverTime';
import withCache from '../withCache';
import { getDays, getHours, getMinutes } from './units';

const WEEKDAYS_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const MONTHS_FULL = [
  'January', 'February', 'March', 'April', 'May', 'June',
  'July', 'August', 'September', 'October', 'November', 'December',
];

const MONTHS_FULL_LOWERCASE = MONTHS_FULL.map((month) => month.toLowerCase());
const MIN_SEARCH_YEAR = 2015;
const MAX_DAY_IN_MONTH = 31;
const MAX_MONTH_IN_YEAR = 12;
export const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;

export function isToday(date: Date) {
  return getDayStartAt(new Date()) === getDayStartAt(date);
}

export function getDayStart(datetime: number | Date) {
  const date = new Date(datetime);
  date.setHours(0, 0, 0, 0);
  return date;
}

export function getDayStartAt(datetime: number | Date) {
  return getDayStart(datetime).getTime();
}

export function toYearMonth(timestamp: number) {
  const date = new Date(timestamp * 1000);
  return `${date.getFullYear()}-${date.getMonth()}`;
}

function toIsoString(date: Date) {
  return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}

// @optimization `toLocaleTimeString` is avoided because of bad performance
export function formatTime(lang: OldLangFn, datetime: number | Date) {
  const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
  const timeFormat = lang.timeFormat || '24h';

  let hours = date.getHours();
  let marker = '';
  if (timeFormat === '12h') {
    marker = hours >= 12 ? '\xa0PM' : '\xa0AM'; // NBSP
    hours = hours > 12 ? hours % 12 : hours;
  }

  return `${String(hours).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}${marker}`;
}

export function formatPastTimeShort(lang: OldLangFn, datetime: number | Date, alwaysShowTime = false) {
  const date = typeof datetime === 'number' ? new Date(datetime) : datetime;

  const time = formatTime(lang, date);

  const today = getDayStart(new Date());
  if (date >= today) {
    return time;
  }

  const weekAgo = new Date(today);
  weekAgo.setDate(today.getDate() - 7);
  if (date >= weekAgo) {
    const weekday = lang(`Weekday.Short${WEEKDAYS_FULL[date.getDay()]}`);
    return alwaysShowTime ? lang('formatDateAtTime', [weekday, time]) : weekday;
  }

  const noYear = date.getFullYear() === today.getFullYear();

  const formattedDate = formatDateToString(date, lang.code, noYear);
  return alwaysShowTime ? lang('formatDateAtTime', [formattedDate, time]) : formattedDate;
}

export function formatFullDate(lang: OldLangFn | LangFn, datetime: number | Date) {
  return formatDateToString(datetime, lang.code, false, 'numeric');
}

export function formatMonthAndYear(lang: OldLangFn | LangFn, date: Date, isShort = false) {
  return formatDateToString(date, lang.code, false, isShort ? 'short' : 'long', true);
}

export function formatCountdown(
  lang: LangFn,
  secondsLeft: number,
) {
  const days = getDays(secondsLeft);
  if (secondsLeft < 0) {
    return 0;
  } else if (days < 1) {
    return formatMediaDuration(secondsLeft);
  } else if (days < 7) {
    const count = days;
    return lang('Days', { count }, { pluralValue: count });
  } else if (days < 30) {
    const count = Math.floor(days / 7);
    return lang('Weeks', { count }, { pluralValue: count });
  } else if (days < 365) {
    const count = Math.floor(days / 30);
    return lang('Months', { count }, { pluralValue: count });
  } else {
    const count = Math.floor(days / 365);
    return lang('Years', { count }, { pluralValue: count });
  }
}

export function formatCountdownDays(
  lang: LangFn,
  days: number,
) {
  if (days < 7) {
    return lang('Days', { count: days }, { pluralValue: days });
  } else if (days < 30) {
    const count = Math.floor(days / 7);
    return lang('Weeks', { count }, { pluralValue: count });
  } else if (days < 360) {
    const count = Math.floor(days / 30);
    return lang('Months', { count }, { pluralValue: count });
  } else {
    const count = Math.floor(days / 360);
    return lang('Years', { count }, { pluralValue: count });
  }
}

export function formatCountdownShort(lang: OldLangFn, msLeft: number): string {
  if (msLeft < 60 * 1000) {
    return Math.ceil(msLeft / 1000).toString();
  } else if (msLeft < 60 * 60 * 1000) {
    return Math.ceil(msLeft / (60 * 1000)).toString();
  } else if (msLeft < MILLISECONDS_IN_DAY) {
    return lang('MessageTimer.ShortHours', Math.ceil(msLeft / (60 * 60 * 1000)));
  } else {
    return lang('MessageTimer.ShortDays', Math.ceil(msLeft / MILLISECONDS_IN_DAY));
  }
}

export function formatLocationLastUpdate(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
  const seconds = currentTime - lastUpdated;
  if (seconds < 60) {
    return lang('LiveLocationUpdated.JustNow');
  } else if (seconds < 60 * 60) {
    return lang('LiveLocationUpdated.MinutesAgo', Math.floor(seconds / 60));
  } else {
    return lang('LiveLocationUpdated.TodayAt', formatTime(lang, lastUpdated));
  }
}

export function formatRelativePastTime(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
  const seconds = currentTime - lastUpdated;

  if (seconds < 60) {
    return lang('Time.JustNow');
  }

  // within an hour
  if (seconds < 60 * 60) {
    return lang('Time.MinutesAgo', Math.floor(seconds / 60));
  }

  const lastUpdatedDate = new Date(lastUpdated * 1000);
  const today = getDayStart(new Date());
  if (lastUpdatedDate >= today) {
    return lang('Time.TodayAt', formatTime(lang, lastUpdatedDate));
  }

  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  if (lastUpdatedDate > yesterday) {
    return lang('Time.YesterdayAt', formatTime(lang, lastUpdatedDate));
  }

  return lang('Time.AtDate', formatFullDate(lang, lastUpdatedDate));
}

export function formatPastDatetime(lang: LangFn, pastTime: number, currentTime = getServerTime()) {
  const seconds = currentTime - pastTime;
  const minutes = getMinutes(seconds);
  const hours = getHours(seconds);
  const days = getDays(seconds);

  if (seconds < 60) {
    return lang('JustNowAgo');
  }

  if (minutes < 60) {
    return lang('MinutesAgo', { count: minutes }, { pluralValue: minutes });
  }

  if (hours < 24) {
    return lang('HoursAgo', { count: hours }, { pluralValue: hours });
  }

  if (days < 28) {
    return lang('DaysAgo', { count: days }, { pluralValue: days });
  }

  return lang('AtDateAgo', { date: formatFullDate(lang, pastTime) });
}

type DurationType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks';

export function formatTimeDuration(lang: OldLangFn, duration: number, showLast = 2) {
  if (!duration) {
    return undefined;
  }

  const durationRecords: { duration: number; type: DurationType }[] = [];
  const labels = [
    { multiplier: 1, type: 'Seconds' },
    { multiplier: 60, type: 'Minutes' },
    { multiplier: 60, type: 'Hours' },
    { multiplier: 24, type: 'Days' },
    { multiplier: 7, type: 'Weeks' },
  ] as Array<{ multiplier: number; type: DurationType }>;
  let t = 1;
  labels.forEach((label, idx) => {
    t *= label.multiplier;

    if (duration < t) {
      return;
    }

    const modulus = labels[idx === (labels.length - 1) ? idx : idx + 1].multiplier;
    durationRecords.push({
      duration: Math.floor((duration / t) % modulus),
      type: label.type,
    });
  });

  const out = durationRecords.slice(-showLast).reverse();
  for (let i = out.length - 1; i >= 0; --i) {
    if (out[i].duration === 0) {
      out.splice(i, 1);
    }
  }

  // TODO In arabic we don't use "," as delimiter rather we use "and" each time
  return out.map((part) => lang(part.type, part.duration, 'i')).join(', ');
}

export function formatScheduledDateTime(
  scheduleDateTimestamp: number,
  lang: LangFn,
  oldLang: OldLangFn,
): string {
  const scheduleDate = new Date(scheduleDateTimestamp * 1000);

  return lang('FormatDateAtTime', {
    date: isToday(scheduleDate)
      ? lang('WeekdayToday')
      : formatHumanDate(oldLang, scheduleDateTimestamp * 1000, true, false, true),
    time: formatTime(oldLang, scheduleDateTimestamp * 1000),
  });
}

export function formatHumanDate(
  lang: OldLangFn,
  datetime: number | Date,
  isShort = false,
  noWeekdays = false,
  isUpperFirst?: boolean,
) {
  const date = typeof datetime === 'number' ? new Date(datetime) : datetime;

  const today = getDayStart(new Date());

  if (!noWeekdays) {
    if (toIsoString(date) === toIsoString(today)) {
      return (isUpperFirst || !isShort ? upperFirst : lowerFirst)(lang('Weekday.Today'));
    }

    const yesterday = new Date(today);
    yesterday.setDate(today.getDate() - 1);
    if (toIsoString(date) === toIsoString(yesterday)) {
      return (isUpperFirst || !isShort ? upperFirst : lowerFirst)(lang('Weekday.Yesterday'));
    }

    const limitBefore = new Date(today);
    limitBefore.setDate(today.getDate() - 6); // Avoid returning same weekday as today
    const limitAhead = new Date(today);
    limitAhead.setDate(today.getDate() + 6);

    if (date >= limitBefore && date <= limitAhead) {
      const weekDayString = formatWeekday(lang, date.getDay(), isShort);
      return (isUpperFirst || !isShort ? upperFirst : lowerFirst)(weekDayString);
    }
  }

  const noYear = date.getFullYear() === today.getFullYear();
  const formattedDate = formatDateToString(date, lang.code, noYear, isShort ? 'short' : 'long');

  return (isUpperFirst || !isShort ? upperFirst : lowerFirst)(formattedDate);
}

/**
 * Returns weekday name
 * @param day 0 - Sunday, 1 - Monday, ...
 */
export function formatWeekday(lang: OldLangFn, day: number, isShort = false) {
  const weekDay = WEEKDAYS_FULL[day];
  return isShort ? lang(`Weekday.Short${weekDay}`) : lang(`Weekday.${weekDay}`);
}

export function formatMediaDateTime(
  lang: OldLangFn,
  datetime: number | Date,
  isUpperFirst?: boolean,
) {
  const date = typeof datetime === 'number' ? new Date(datetime) : datetime;

  return `${formatHumanDate(lang, date, true, undefined, isUpperFirst)}, ${formatTime(lang, date)}`;
}

export function formatMediaDuration(duration: number, maxValue?: number) {
  const hours = Math.floor(duration / 3600);
  const minutes = Math.floor((duration % 3600) / 60);
  const seconds = Math.floor(duration % 3600 % 60);

  const maxHours = maxValue ? Math.floor(maxValue / 3600) : 0;
  const maxMinutes = maxValue ? Math.floor((maxValue % 3600) / 60) : 0;
  let string = '';
  if (hours > 0 || maxHours > 0) {
    string += `${String(hours).padStart(2, '0')}:`;
    string += `${String(minutes).padStart(2, '0')}:`;
  } else if (maxMinutes >= 10) {
    string += `${String(minutes).padStart(2, '0')}:`;
  } else {
    string += `${String(minutes)}:`;
  }
  string += String(seconds).padStart(2, '0');

  return string;
}

export function formatShortHoursMinutes(lang: OldLangFn, durationInSeconds: number) {
  if (durationInSeconds <= 0) {
    return lang('MessageTimer.ShortMinutes', 0);
  }

  const hours = Math.floor(durationInSeconds / 3600);
  const minutes = Math.floor((durationInSeconds % 3600) / 60);

  if (hours > 0) {
    const hoursText = lang('MessageTimer.ShortHours', hours);
    if (minutes === 0) {
      return hoursText;
    }

    const minutesText = lang('MessageTimer.ShortMinutes', minutes);
    return `${hoursText} ${minutesText}`;
  }
  return lang('MessageTimer.ShortMinutes', minutes);
}

export function formatVoiceRecordDuration(durationInMs: number) {
  const parts = [];

  let milliseconds = durationInMs % 1000;
  durationInMs -= milliseconds;
  milliseconds = Math.floor(milliseconds / 10);

  durationInMs = Math.floor(durationInMs / 1000);
  const seconds = durationInMs % 60;
  durationInMs -= seconds;

  durationInMs = Math.floor(durationInMs / 60);
  const minutes = durationInMs % 60;
  durationInMs -= minutes;

  durationInMs = Math.floor(durationInMs / 60);
  const hours = durationInMs % 60;

  if (hours > 0) {
    parts.push(String(hours).padStart(2, '0'));
  }
  parts.push(String(minutes).padStart(hours > 0 ? 2 : 1, '0'));
  parts.push(String(seconds).padStart(2, '0'));

  return `${parts.join(':')},${String(milliseconds).padStart(2, '0')}`;
}

const formatDayToStringWithCache = withCache((
  dayStartAt: number,
  locale: string,
  noYear?: boolean,
  monthFormat: 'short' | 'long' | 'numeric' = 'short',
  noDay?: boolean,
) => {
  return new Date(dayStartAt).toLocaleString(
    locale,
    {
      year: noYear ? undefined : 'numeric',
      month: monthFormat,
      day: noDay ? undefined : 'numeric',
    },
  );
});

export function formatDateToString(
  datetime: Date | number,
  locale = 'en-US',
  noYear = false,
  monthFormat: 'short' | 'long' | 'numeric' = 'short',
  noDay = false,
) {
  const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
  const dayStartAt = getDayStartAt(date);

  return formatDayToStringWithCache(dayStartAt, locale, noYear, monthFormat, noDay);
}

export function formatDateTimeToString(
  datetime: Date | number, locale = 'en-US', noSeconds?: boolean,
  timeFormat?: TimeFormat, noYear?: boolean,
) {
  const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
  return date.toLocaleString(
    locale,
    {
      year: noYear ? undefined : 'numeric',
      month: 'short',
      day: 'numeric',
      hour: 'numeric',
      minute: 'numeric',
      second: noSeconds ? undefined : 'numeric',
      hourCycle: timeFormat === '12h' ? 'h12' : 'h23',
    },
  );
}

export function formatDateAtTime(
  lang: OldLangFn,
  datetime: number | Date,
) {
  const date = typeof datetime === 'number' ? new Date(datetime) : datetime;

  const today = getDayStart(new Date());
  const time = formatTime(lang, date);

  if (toIsoString(date) === toIsoString(today)) {
    return lang('Time.TodayAt', time);
  }

  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  if (toIsoString(date) === toIsoString(yesterday)) {
    return lang('Time.YesterdayAt', time);
  }

  const noYear = date.getFullYear() === today.getFullYear();
  const formattedDate = formatDateToString(date, lang.code, noYear);

  return lang('formatDateAtTime', [formattedDate, time]);
}

export function formatShortDuration(lang: LangFn, duration: number, hoursPriority?: boolean) {
  if (duration < 0) {
    return lang('RightNow');
  }

  if (duration < 60) {
    const count = Math.ceil(duration);
    return lang('Seconds', { count }, { pluralValue: duration });
  }

  if (duration < 60 * 60) {
    const count = Math.ceil(duration / 60);
    return lang('Minutes', { count }, { pluralValue: count });
  }

  if (duration < 60 * 60 * 24 || (hoursPriority && duration <= 60 * 60 * 24 * 2)) {
    const count = Math.ceil(duration / (60 * 60));
    return lang('Hours', { count }, { pluralValue: count });
  }

  const count = Math.ceil(duration / (60 * 60 * 24));
  return lang('Days', { count }, { pluralValue: count });
}

function isValidDate(day: number, month: number, year = 2021): boolean {
  if (month > (MAX_MONTH_IN_YEAR - 1) || day > MAX_DAY_IN_MONTH) {
    return false;
  }
  const date = new Date(year, month, day);
  return !Number.isNaN(date.getTime()) && date.getDate() === day;
}

export function parseDateString(query = ''): string | undefined {
  const matchStringDate = query.match(/\d{1,2}\s[a-zA-Z]{3,}/);
  const matchEuropeStringDate = query.match(/[a-zA-Z]{3,}\s\d{1,2}/);
  const matchNumberDate = query.match(/\d{1,2}[./-]\d{1,2}([./-]\d{2,4})?/);
  if (!matchStringDate && !matchNumberDate && !matchEuropeStringDate) {
    return undefined;
  }

  if (matchNumberDate) {
    const [date, month, year] = query.split(/[./-]/).map(Number);
    return !(year && year < MIN_SEARCH_YEAR) && isValidDate(date, month - 1, year || undefined)
      ? `${year ? `${year}-` : ''}${String(month).padStart(2, '0')}-${String(date).padStart(2, '0')}`
      : undefined;
  }

  const dateParts = query.split(' ');
  const date = matchStringDate ? dateParts[0] : dateParts[1];
  const month = (matchStringDate ? dateParts[1] : dateParts[0]).toLowerCase();
  const monthIndex = MONTHS_FULL_LOWERCASE.findIndex((item) => item.startsWith(month));

  return monthIndex !== -1 && isValidDate(Number(date), monthIndex)
    ? `${String(monthIndex + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`
    : undefined;
}

export function timestampPlusDay(timestamp: number) {
  return timestamp + MILLISECONDS_IN_DAY / 1000;
}

function lowerFirst(str: string) {
  return `${str[0].toLowerCase()}${str.slice(1)}`;
}

function upperFirst(str: string) {
  return `${str[0].toUpperCase()}${str.slice(1)}`;
}

export function formatRegistrationMonth(lang: string, dateString: string) {
  const [month, year] = dateString.split('.');
  const date = new Date(`${year}-${month}`);

  return new Intl.DateTimeFormat(lang, { month: 'long', year: 'numeric' }).format(date);
}
