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

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

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

  getYear(date: Date): number {
    return moment(date).year();
  }
  getMonth(date: Date): number {
    return moment(date).month();
  }
  getDate(date: Date): number {
    return moment(date).date();
  }
  getYearName(date: Date): string {
    return moment(date).format('YYYY');
  }

  getMonthName(date: Date): string;
  getMonthName(month: number): string;
  getMonthName(dateOrMonth: Date | number): string {
    let date: moment.Moment;
    if (dateOrMonth instanceof Date) {
      date = moment(dateOrMonth);
    } else {
      date = moment().set('month', dateOrMonth);
    }
    return date.format('MMMM');
  }

  getDateName(date: Date): string {
    return this.getDate(date).toString();
  }
  addCalendarMonths(date: Date, months: number): Date {
    return moment(date).add(months, 'months').toDate();
  }
  addCalendarDays(date: Date, days: number): Date {
    return moment(date).add(days, 'days').toDate();
  }

  getFirstDateOfWeek(date: Date): Date {
    return moment(date).startOf('week').toDate();
  }

  getLastDateOfWeek(date: Date): Date {
    return moment(date).endOf('week').toDate();
  }

  getFirstDateOfMonth(date: Date) {
    return moment(date).startOf('month').toDate();
  }

  getLastDateOfMonth(date: Date) {
    return moment(date).endOf('month').toDate();
  }

  getDifferenceInDays(first: Date, second: Date) {
    return moment(first).diff(second, 'days');
  }

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

  getStartOfTheDay(date: Date) {
    return moment(date).startOf('day').toDate();
  }

  getEndOfTheDay(date: Date) {
    return moment(date).endOf('day').toDate();
  }

  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 moment(date).toDate();
  }

  today(): Date {
    return moment().startOf('day').toDate();
  }

  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);
  }

  findClosestDate(a: Date, b: Date, compareDate?: Date) {
    const date = compareDate || this.today();
    let distancea = Math.abs(date.getTime() - a.getTime());
    let distanceb = Math.abs(date.getTime() - b.getTime());
    return distancea - distanceb;
  }

  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;
    }
  }
}
