/*
 * Copyright 2020 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

// Portions of the code in this file are based on code from ICU.
// Original licensing can be found in the NOTICE file in the root directory of this source tree.

import {AnyCalendarDate, Calendar, CalendarIdentifier} from '../types';
import {CalendarDate} from '../CalendarDate';

const CIVIL_EPOC = 1948440; // CE 622 July 16 Friday (Julian calendar) / CE 622 July 19 (Gregorian calendar)
const ASTRONOMICAL_EPOC = 1948439; // CE 622 July 15 Thursday (Julian calendar)
const UMALQURA_YEAR_START = 1300;
const UMALQURA_YEAR_END = 1600;
const UMALQURA_START_DAYS = 460322;

function islamicToJulianDay(epoch: number, year: number, month: number, day: number): number {
  return (
    day +
    Math.ceil(29.5 * (month - 1)) +
    (year - 1) * 354 +
    Math.floor((3 + 11 * year) / 30) +
    epoch -
    1
  );
}

function julianDayToIslamic(calendar: Calendar, epoch: number, jd: number) {
  let year = Math.floor((30 * (jd - epoch) + 10646) / 10631);
  let month = Math.min(
    12,
    Math.ceil((jd - (29 + islamicToJulianDay(epoch, year, 1, 1))) / 29.5) + 1
  );
  let day = jd - islamicToJulianDay(epoch, year, month, 1) + 1;

  return new CalendarDate(calendar, year, month, day);
}

function isLeapYear(year: number): boolean {
  return (14 + 11 * year) % 30 < 11;
}

/**
 * The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab
 * world. The civil variant uses simple arithmetic rules rather than astronomical calculations to
 * approximate the traditional calendar, which is based on sighting of the crescent moon. It uses
 * Friday, July 16 622 CE (Julian) as the epoch. Each year has 12 months, with either 354 or 355
 * days depending on whether it is a leap year. Learn more about the available Islamic calendars
 * [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
 */
export class IslamicCivilCalendar implements Calendar {
  identifier: CalendarIdentifier = 'islamic-civil';

  fromJulianDay(jd: number): CalendarDate {
    return julianDayToIslamic(this, CIVIL_EPOC, jd);
  }

  toJulianDay(date: AnyCalendarDate): number {
    return islamicToJulianDay(CIVIL_EPOC, date.year, date.month, date.day);
  }

  getDaysInMonth(date: AnyCalendarDate): number {
    let length = 29 + (date.month % 2);
    if (date.month === 12 && isLeapYear(date.year)) {
      length++;
    }

    return length;
  }

  getMonthsInYear(): number {
    return 12;
  }

  getDaysInYear(date: AnyCalendarDate): number {
    return isLeapYear(date.year) ? 355 : 354;
  }

  getMaximumMonthsInYear(): number {
    return 12;
  }

  getMaximumDaysInMonth(): number {
    return 30;
  }

  getYearsInEra(): number {
    // 9999 gregorian
    return 9665;
  }

  getEras(): string[] {
    return ['AH'];
  }
}

/**
 * The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab
 * world. The tabular variant uses simple arithmetic rules rather than astronomical calculations to
 * approximate the traditional calendar, which is based on sighting of the crescent moon. It uses
 * Thursday, July 15 622 CE (Julian) as the epoch. Each year has 12 months, with either 354 or 355
 * days depending on whether it is a leap year. Learn more about the available Islamic calendars
 * [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
 */
export class IslamicTabularCalendar extends IslamicCivilCalendar {
  identifier: CalendarIdentifier = 'islamic-tbla';

  fromJulianDay(jd: number): CalendarDate {
    return julianDayToIslamic(this, ASTRONOMICAL_EPOC, jd);
  }

  toJulianDay(date: AnyCalendarDate): number {
    return islamicToJulianDay(ASTRONOMICAL_EPOC, date.year, date.month, date.day);
  }
}

// Generated by scripts/generate-umalqura.js
const UMALQURA_DATA =
  'qgpUDckO1AbqBmwDrQpVBakGkgepC9QF2gpcBS0NlQZKB1QLagutBa4ETwoXBYsGpQbVCtYCWwmdBE0KJg2VDawFtgm6AlsKKwWVCsoG6Qr0AnYJtgJWCcoKpAvSC9kF3AJtCU0FpQpSC6ULtAW2CVcFlwJLBaMGUgdlC2oFqworBZUMSg2lDcoF1gpXCasESwmlClILagt1BXYCtwhbBFUFqQW0BdoJ3QRuAjYJqgpUDbIN1QXaAlsJqwRVCkkLZAtxC7QFtQpVCiUNkg7JDtQG6QprCasEkwpJDaQNsg25CroEWworBZUKKgtVC1wFvQQ9Ah0JlQpKC1oLbQW2AjsJmwRVBqkGVAdqC2wFrQpVBSkLkgupC9QF2gpaBasKlQVJB2QHqgu1BbYCVgpNDiULUgtqC60FrgIvCZcESwalBqwG1gpdBZ0ETQoWDZUNqgW1BdoCWwmtBJUFygbkBuoK9QS2AlYJqgpUC9IL2QXqAm0JrQSVCkoLpQuyBbUJ1gSXCkcFkwZJB1ULagVrCisFiwpGDaMNygXWCtsEawJLCaUKUgtpC3UFdgG3CFsCKwVlBbQF2gntBG0BtgimClINqQ3UBdoKWwmrBFMGKQdiB6kLsgW1ClUFJQuSDckO0gbpCmsFqwRVCikNVA2qDbUJugQ7CpsETQqqCtUK2gJdCV4ELgqaDFUNsga5BroEXQotBZUKUguoC7QLuQXaAloJSgukDdEO6AZqC20FNQWVBkoNqA3UDdoGWwWdAisGFQtKC5ULqgWuCi4JjwwnBZUGqgbWCl0FnQI=';
let UMALQURA_MONTHLENGTH: Uint16Array;
let UMALQURA_YEAR_START_TABLE: Uint32Array;

function umalquraYearStart(year: number): number {
  return UMALQURA_START_DAYS + UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START];
}

function umalquraMonthLength(year: number, month: number): number {
  let idx = year - UMALQURA_YEAR_START;
  let mask = 0x01 << (11 - (month - 1));
  if ((UMALQURA_MONTHLENGTH[idx] & mask) === 0) {
    return 29;
  } else {
    return 30;
  }
}

function umalquraMonthStart(year: number, month: number): number {
  let day = umalquraYearStart(year);
  for (let i = 1; i < month; i++) {
    day += umalquraMonthLength(year, i);
  }
  return day;
}

function umalquraYearLength(year: number): number {
  return (
    UMALQURA_YEAR_START_TABLE[year + 1 - UMALQURA_YEAR_START] -
    UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START]
  );
}

/**
 * The Islamic calendar, also known as the "Hijri" calendar, is used throughout much of the Arab
 * world. The Umalqura variant is primarily used in Saudi Arabia. It is a lunar calendar, based on
 * astronomical calculations that predict the sighting of a crescent moon. Month and year lengths
 * vary between years depending on these calculations. Learn more about the available Islamic
 * calendars
 * [here](https://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types).
 */
export class IslamicUmalquraCalendar extends IslamicCivilCalendar {
  identifier: CalendarIdentifier = 'islamic-umalqura';

  constructor() {
    super();
    if (!UMALQURA_MONTHLENGTH) {
      UMALQURA_MONTHLENGTH = new Uint16Array(
        Uint8Array.from(atob(UMALQURA_DATA), c => c.charCodeAt(0)).buffer
      );
    }

    if (!UMALQURA_YEAR_START_TABLE) {
      UMALQURA_YEAR_START_TABLE = new Uint32Array(UMALQURA_YEAR_END - UMALQURA_YEAR_START + 1);

      let yearStart = 0;
      for (let year = UMALQURA_YEAR_START; year <= UMALQURA_YEAR_END; year++) {
        UMALQURA_YEAR_START_TABLE[year - UMALQURA_YEAR_START] = yearStart;
        for (let i = 1; i <= 12; i++) {
          yearStart += umalquraMonthLength(year, i);
        }
      }
    }
  }

  fromJulianDay(jd: number): CalendarDate {
    let days = jd - CIVIL_EPOC;
    let startDays = umalquraYearStart(UMALQURA_YEAR_START);
    let endDays = umalquraYearStart(UMALQURA_YEAR_END);
    if (days < startDays || days > endDays) {
      return super.fromJulianDay(jd);
    } else {
      let y = UMALQURA_YEAR_START - 1;
      let m = 1;
      let d = 1;
      while (d > 0) {
        y++;
        d = days - umalquraYearStart(y) + 1;
        let yearLength = umalquraYearLength(y);
        if (d === yearLength) {
          m = 12;
          break;
        } else if (d < yearLength) {
          let monthLength = umalquraMonthLength(y, m);
          m = 1;
          while (d > monthLength) {
            d -= monthLength;
            m++;
            monthLength = umalquraMonthLength(y, m);
          }
          break;
        }
      }

      return new CalendarDate(this, y, m, days - umalquraMonthStart(y, m) + 1);
    }
  }

  toJulianDay(date: AnyCalendarDate): number {
    if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) {
      return super.toJulianDay(date);
    }

    return CIVIL_EPOC + umalquraMonthStart(date.year, date.month) + (date.day - 1);
  }

  getDaysInMonth(date: AnyCalendarDate): number {
    if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) {
      return super.getDaysInMonth(date);
    }

    return umalquraMonthLength(date.year, date.month);
  }

  getDaysInYear(date: AnyCalendarDate): number {
    if (date.year < UMALQURA_YEAR_START || date.year > UMALQURA_YEAR_END) {
      return super.getDaysInYear(date);
    }

    return umalquraYearLength(date.year);
  }
}
