import { Inject, Injectable, LOCALE_ID } from '@angular/core';
import { isNullOrUndefined } from '@utils/common';

type Precision = 'year' | 'month' | 'day' | 'time';

@Injectable()
export class DateAdapter {
  constructor(@Inject(LOCALE_ID) public locale: string) {}

  getYear(date: Date): number {
    return date.getFullYear();
  }
  getMonth(date: Date): number {
    return date.getMonth();
  }
  getDate(date: Date): number {
    return date.getDate();
  }
  getYearName(date: Date): string {
    return this.getYear(date).toString();
  }

  getMonthName(date: Date): string;
  getMonthName(month: number): string;
  getMonthName(dateOrMonth: Date | number): string {
    let date = new Date();
    if (dateOrMonth instanceof Date) {
      date = dateOrMonth;
    } else {
      date.setMonth(dateOrMonth);
    }
    return date.toLocaleString(this.locale, { month: 'long' });
  }

  getDateName(date: Date): string {
    return this.getDate(date).toString();
  }
  addCalendarMonths(date: Date, months: number): Date {
    return this.createDate(this.getYear(date), this.getMonth(date) + months, this.getDate(date));
  }
  addCalendarDays(date: Date, days: number): Date {
    return this.createDate(this.getYear(date), this.getMonth(date), this.getDate(date) + days);
  }

  getFirstDateOfWeek(date: Date): Date {
    const clone = new Date(date);
    const day = clone.getDay();
    const diff = clone.getDate() - day + (day === 0 ? -6 : 1); // Start of Week = Monday
    return this.getStartOfTheDay(new Date(clone.setDate(diff)));
  }

  getLastDateOfWeek(date: Date): Date {
    const clone = new Date(date);
    const day = clone.getDay();
    const diff = clone.getDate() - day + (day === 0 ? 0 : 7); // End of Week = Sunday
    return this.getEndOfTheDay(new Date(clone.setDate(diff)));
  }

  getFirstDateOfMonth(date: Date) {
    return this.createDate(this.getYear(date), this.getMonth(date), 1);
  }

  getLastDateOfMonth(date: Date) {
    return this.createDate(this.getYear(date), this.getMonth(date) + 1, 0);
  }

  getDifferenceInDays(first: Date, second: Date) {
    return (first.getTime() - second.getTime()) / (1000 * 3600 * 24);
  }

  getDayOfWeek(date: Date) {
    return date.getDay();
  }

  getStartOfTheDay(date: Date) {
    return this.createDate(date?.getFullYear(), date?.getMonth(), date?.getDate());
  }

  getEndOfTheDay(date: Date) {
    const nextDate = this.createDate(date?.getFullYear(), date?.getMonth(), date?.getDate() + 1);
    return new Date(nextDate.getTime() - 1);
  }

  createDate(year: number, month: number, date: number): Date {
    const result = new Date(year, month, date);
    // abbreviations for 19xx.
    if (year >= 0 && year < 100) {
      result.setFullYear(this.getYear(result) - 1900);
    }
    return result;
  }

  parseDate(date: string) {
    return new Date(date);
  }

  today(): Date {
    return new Date(new Date().toJSON().slice(0, 10).replace(/-/g, '/'));
  }

  isValid(date: any): date is Date {
    return date instanceof Date && !isNaN(date.getDate());
  }

  getDatesAndCalendarWeeksForMonth(date: Date): { week: number; dates: Date[] }[] {
    const result: { week: number; dates: Date[] }[] = [];
    const weeks = this.getCalendarWeeksForMonth(date);

    for (const week of weeks) {
      let iWeek = week;
      if (this.getMonth(date) === 11 && week === 1) {
        iWeek = 53;
      }

      const dates = this.getDatesForCalendarWeek(this.getYear(date), iWeek);

      result.push({
        week,
        dates,
      });
    }

    return result;
  }

  getDatesForCalendarWeek(year: number, week: number): Date[] {
    const firstDateOfYear = this.createDate(year, 0, 1);
    const firstDayOfYear = firstDateOfYear.getDay();
    let firstDayOfWeek = 0;

    if (firstDayOfYear === 0) {
      firstDayOfWeek = 2;
    } else if (firstDayOfYear <= 3) {
      firstDayOfWeek = -firstDayOfYear + 2;
    } else {
      firstDayOfWeek = -firstDayOfYear + 9;
    }

    const firstDateOfWeek = this.createDate(year, 0, firstDayOfWeek);

    const dates: Date[] = [];

    for (let i = 0; i < 7; i++) {
      dates.push(
        this.createDate(this.getYear(firstDateOfWeek), this.getMonth(firstDateOfWeek), this.getDate(firstDateOfWeek) + i + (week - 1) * 7)
      );
    }
    return dates;
  }

  getCalendarWeek(date: Date): number {
    const firstDateOfYear = this.createDate(this.getYear(date), 0, 1);

    const totalDays = (date.getTime() - firstDateOfYear.getTime()) / 86400000;

    const week = Math.ceil((totalDays + 4 - (date.getDay() || 7)) / 7);

    return week > 52 ? 1 : week;
  }

  getCalendarWeeksForMonth(date: Date): number[] {
    let firstDateOfMonth = this.createDate(this.getYear(date), this.getMonth(date), 1);
    const weekStart = this.getCalendarWeek(firstDateOfMonth);
    let weekEnd = this.getCalendarWeek(this.createDate(this.getYear(firstDateOfMonth), this.getMonth(firstDateOfMonth) + 1, 0));
    let weeks: number[] = [];

    if (weekEnd === 1) {
      weekEnd = 53;
    }

    for (let index = 0; index <= weekEnd - weekStart; index++) {
      weeks.push(weekStart + index);
    }

    return weeks;
  }

  getMonthForCalendarWeek(year: number, week: number): number {
    const dates = this.getDatesForCalendarWeek(year, week);
    return this.getMonth(dates[0]);
  }

  compareDate(first: Date, second: Date) {
    if (isNullOrUndefined(first) || isNullOrUndefined(second)) {
      return null;
    }

    return (
      this.getYear(first) - this.getYear(second) ||
      this.getMonth(first) - this.getMonth(second) ||
      this.getDate(first) - this.getDate(second)
    );
  }

  compareYearAndMonth(first: Date, second: Date) {
    if (isNullOrUndefined(first) || isNullOrUndefined(second)) {
      return null;
    }
    return this.getYear(first) - this.getYear(second) || this.getMonth(first) - this.getMonth(second);
  }

  isLessThan({ first, second, precision }: { first: Date; second: Date; precision?: Precision }): boolean {
    return this.formatDate(first, precision) < this.formatDate(second, precision);
  }

  isLessEqual({ first, second, precision }: { first: Date; second: Date; precision?: Precision }): boolean {
    return this.formatDate(first, precision) <= this.formatDate(second, precision);
  }

  isGreaterThan({ first, second, precision }: { first: Date; second: Date; precision?: Precision }): boolean {
    return this.formatDate(first, precision) > this.formatDate(second, precision);
  }

  isGreaterEqual({ first, second, precision }: { first: Date; second: Date; precision?: Precision }): boolean {
    return this.formatDate(first, precision) >= this.formatDate(second, precision);
  }

  equals({ first, second, precision }: { first: Date; second: Date; precision?: Precision }): boolean {
    return this.formatDate(first, precision).getTime() === this.formatDate(second, precision).getTime();
  }

  private formatDate(date: Date, precision: Precision = 'time'): Date {
    switch (precision) {
      case 'year':
        return new Date(this.getYear(date));
      case 'month':
        return new Date(this.getYear(date), this.getMonth(date));
      case 'day':
        return new Date(this.getYear(date), this.getMonth(date), this.getDate(date));
      case 'time':
        return date;
      default:
        return date;
    }
  }
}
