/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef builtin_temporal_MonthCode_h
#define builtin_temporal_MonthCode_h

#include "mozilla/Assertions.h"
#include "mozilla/EnumSet.h"

#include <compare>
#include <initializer_list>
#include <stddef.h>
#include <stdint.h>
#include <string_view>
#include <utility>

#include "jstypes.h"

#include "builtin/temporal/Calendar.h"

namespace js::temporal {

class MonthCode final {
 public:
  enum class Code {
    Invalid = 0,

    // Months 01 - M12.
    M01 = 1,
    M02,
    M03,
    M04,
    M05,
    M06,
    M07,
    M08,
    M09,
    M10,
    M11,
    M12,

    // Epagomenal month M13.
    M13,

    // Leap months M01 - M12.
    M01L,
    M02L,
    M03L,
    M04L,
    M05L,
    M06L,
    M07L,
    M08L,
    M09L,
    M10L,
    M11L,
    M12L,
  };

 private:
  static constexpr int32_t toLeapMonth =
      static_cast<int32_t>(Code::M01L) - static_cast<int32_t>(Code::M01);

  Code code_ = Code::Invalid;

 public:
  constexpr MonthCode() = default;

  constexpr explicit MonthCode(Code code) : code_(code) {}

  constexpr explicit MonthCode(int32_t month, bool isLeapMonth = false) {
    MOZ_ASSERT(1 <= month && month <= 13);
    MOZ_ASSERT_IF(isLeapMonth, 1 <= month && month <= 12);

    code_ = static_cast<Code>(month + (isLeapMonth ? toLeapMonth : 0));
  }

  constexpr auto code() const { return code_; }

  constexpr int32_t ordinal() const {
    int32_t ordinal = static_cast<int32_t>(code_);
    if (isLeapMonth()) {
      ordinal -= toLeapMonth;
    }
    return ordinal;
  }

  constexpr bool isLeapMonth() const { return code_ >= Code::M01L; }

  constexpr auto operator<=>(const MonthCode& other) const {
    if (ordinal() != other.ordinal()) {
      return ordinal() <=> other.ordinal();
    }
    return code_ <=> other.code_;
  }

  constexpr bool operator==(const MonthCode&) const = default;

  constexpr explicit operator std::string_view() const {
    constexpr const char* name =
        "M01L"
        "M02L"
        "M03L"
        "M04L"
        "M05L"
        "M06L"
        "M07L"
        "M08L"
        "M09L"
        "M10L"
        "M11L"
        "M12L"
        "M13";
    size_t index = (ordinal() - 1) * 4;
    size_t length = 3 + isLeapMonth();
    return {name + index, length};
  }

  /**
   * Returns the maximum non-leap month. This is the epagomenal month "M13".
   */
  constexpr static auto maxNonLeapMonth() { return MonthCode{Code::M13}; }

  /**
   * Returns the maximum leap month.
   */
  constexpr static auto maxLeapMonth() { return MonthCode{Code::M12L}; }
};

class MonthCodes final {
  // Common month codes supported by all calendars.
  //
  // See IsValidMonthCodeForCalendar, step 1.
  mozilla::EnumSet<MonthCode::Code> monthCodes_{
      MonthCode::Code::M01, MonthCode::Code::M02, MonthCode::Code::M03,
      MonthCode::Code::M04, MonthCode::Code::M05, MonthCode::Code::M06,
      MonthCode::Code::M07, MonthCode::Code::M08, MonthCode::Code::M09,
      MonthCode::Code::M10, MonthCode::Code::M11, MonthCode::Code::M12,
  };

 public:
  constexpr MOZ_IMPLICIT MonthCodes(std::initializer_list<MonthCode> list) {
    for (auto value : list) {
      monthCodes_ += value.code();
    }
  }

  constexpr bool contains(MonthCode monthCode) const {
    return monthCodes_.contains(monthCode.code());
  }

  constexpr bool contains(const MonthCodes& monthCodes) const {
    return monthCodes_.contains(monthCodes.monthCodes_);
  }
};

// static variables in constexpr functions requires C++23 support, so we can't
// declare the month codes directly in CalendarMonthCodes.
//
// https://tc39.es/proposal-intl-era-monthcode/#table-additional-month-codes
//
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Buddhist.html#month-codes
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Indian.html#month-codes
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Hijri.html#months-and-days
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Japanese.html#month-codes
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Persian.html#month-codes
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Roc.html#month-codes
//
// https://docs.rs/icu/latest/icu/calendar/cal/east_asian_traditional/struct.EastAsianTraditional.html#months-and-days
//
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Coptic.html#month-codes
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Ethiopian.html#month-codes
//
// https://docs.rs/icu/latest/icu/calendar/cal/struct.Hebrew.html#month-codes
namespace monthcodes {
// The ISO8601 calendar doesn't have any additional month codes.
inline constexpr MonthCodes ISO8601 = {};

// The Chinese/Dangi calendars can have a leap month inserted after every month.
inline constexpr MonthCodes ChineseOrDangi = {
    // Leap months.
    MonthCode{1, /* isLeapMonth = */ true},
    MonthCode{2, /* isLeapMonth = */ true},
    MonthCode{3, /* isLeapMonth = */ true},
    MonthCode{4, /* isLeapMonth = */ true},
    MonthCode{5, /* isLeapMonth = */ true},
    MonthCode{6, /* isLeapMonth = */ true},
    MonthCode{7, /* isLeapMonth = */ true},
    MonthCode{8, /* isLeapMonth = */ true},
    MonthCode{9, /* isLeapMonth = */ true},
    MonthCode{10, /* isLeapMonth = */ true},
    MonthCode{11, /* isLeapMonth = */ true},
    MonthCode{12, /* isLeapMonth = */ true},
};

// The Coptic/Ethiopian calendars has a thirteenth month.
inline constexpr MonthCodes CopticOrEthiopian = {
    // Short epagomenal month.
    MonthCode{13},
};

// The Hebrew calendar has a single leap month.
inline constexpr MonthCodes Hebrew = {
    // Leap month Adar I.
    MonthCode{5, /* isLeapMonth = */ true},
};
}  // namespace monthcodes

constexpr auto& CalendarMonthCodes(CalendarId id) {
  switch (id) {
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Gregorian:
    case CalendarId::Indian:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicTabular:
    case CalendarId::IslamicUmmAlQura:
    case CalendarId::Persian:
    case CalendarId::Japanese:
    case CalendarId::ROC:
      return monthcodes::ISO8601;

    case CalendarId::Chinese:
    case CalendarId::Dangi:
      return monthcodes::ChineseOrDangi;

    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem:
      return monthcodes::CopticOrEthiopian;

    case CalendarId::Hebrew:
      return monthcodes::Hebrew;
  }
  MOZ_CRASH("invalid calendar id");
}

/**
 * IsValidMonthCodeForCalendar ( calendar, monthCode )
 */
constexpr bool IsValidMonthCodeForCalendar(CalendarId id, MonthCode monthCode) {
  return CalendarMonthCodes(id).contains(monthCode);
}

constexpr bool CalendarHasLeapMonths(CalendarId id) {
  switch (id) {
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem:
    case CalendarId::Gregorian:
    case CalendarId::Indian:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicTabular:
    case CalendarId::IslamicUmmAlQura:
    case CalendarId::Japanese:
    case CalendarId::Persian:
    case CalendarId::ROC:
      return false;

    case CalendarId::Chinese:
    case CalendarId::Dangi:
    case CalendarId::Hebrew:
      return true;
  }
  MOZ_CRASH("invalid calendar id");
}

constexpr bool CalendarHasEpagomenalMonths(CalendarId id) {
  switch (id) {
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Chinese:
    case CalendarId::Dangi:
    case CalendarId::Gregorian:
    case CalendarId::Hebrew:
    case CalendarId::Indian:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicTabular:
    case CalendarId::IslamicUmmAlQura:
    case CalendarId::Japanese:
    case CalendarId::Persian:
    case CalendarId::ROC:
      return false;

    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem:
      return true;
  }
  MOZ_CRASH("invalid calendar id");
}

constexpr int32_t CalendarMonthsPerYear(CalendarId id) {
  if (CalendarHasLeapMonths(id) || CalendarHasEpagomenalMonths(id)) {
    return 13;
  }
  return 12;
}

constexpr std::pair<int32_t, int32_t> CalendarDaysInMonth(CalendarId id) {
  switch (id) {
    // ISO8601 calendar.
    // M02: 28-29 days
    // M04, M06, M09, M11: 30 days
    // M01, M03, M05, M07, M08, M10, M12: 31 days
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Gregorian:
    case CalendarId::Japanese:
    case CalendarId::ROC:
      return {28, 31};

    // Chinese/Dangi calendars have 29-30 days per month.
    //
    // Hebrew:
    // M01, M05, M07, M09, M11: 30 days.
    // M02, M03: 29-30 days.
    // M04, M06, M08, M10, M12: 29 days.
    // M05L: 30 days
    //
    // Islamic calendars have 29-30 days.
    //
    // IslamicCivil, IslamicTabular:
    // M01, M03, M05, M07, M09, M11: 30 days
    // M02, M04, M06, M08, M10: 29 days
    // M12: 29-30 days.
    case CalendarId::Chinese:
    case CalendarId::Dangi:
    case CalendarId::Hebrew:
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicTabular:
    case CalendarId::IslamicUmmAlQura:
      return {29, 30};

    // Coptic, Ethiopian, EthiopianAmeteAlem:
    // M01..M12: 30 days.
    // M13: 5-6 days.
    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem:
      return {5, 30};

    // Indian:
    // M1: 30-31 days.
    // M02..M06: 31 days
    // M07..M12: 30 days
    case CalendarId::Indian:
      return {30, 31};

    // Persian:
    // M01..M06: 31 days
    // M07..M11: 30 days
    // M12: 29-30 days
    case CalendarId::Persian:
      return {29, 31};
  }
  MOZ_CRASH("invalid calendar id");
}

// ISO8601 calendar.
// M02: 28-29 days
// M04, M06, M09, M11: 30 days
// M01, M03, M05, M07, M08, M10, M12: 31 days
constexpr std::pair<int32_t, int32_t> ISODaysInMonth(MonthCode monthCode) {
  int32_t ordinal = monthCode.ordinal();
  if (ordinal == 2) {
    return {28, 29};
  }
  if (ordinal == 4 || ordinal == 6 || ordinal == 9 || ordinal == 11) {
    return {30, 30};
  }
  return {31, 31};
}

constexpr std::pair<int32_t, int32_t> CalendarDaysInMonth(CalendarId id,
                                                          MonthCode monthCode) {
  switch (id) {
    case CalendarId::ISO8601:
    case CalendarId::Buddhist:
    case CalendarId::Gregorian:
    case CalendarId::Japanese:
    case CalendarId::ROC:
      return ISODaysInMonth(monthCode);

    // Chinese/Dangi calendars have 29-30 days per month.
    case CalendarId::Chinese:
    case CalendarId::Dangi:
      return {29, 30};

    // Coptic, Ethiopian, EthiopianAmeteAlem:
    // M01..M12: 30 days.
    // M13: 5-6 days.
    case CalendarId::Coptic:
    case CalendarId::Ethiopian:
    case CalendarId::EthiopianAmeteAlem: {
      if (monthCode.ordinal() <= 12) {
        return {30, 30};
      }
      return {5, 6};
    }

    // Hebrew:
    // M01, M05, M07, M09, M11: 30 days.
    // M02, M03: 29-30 days.
    // M04, M06, M08, M10, M12: 29 days.
    // M05L: 30 days
    case CalendarId::Hebrew: {
      int32_t ordinal = monthCode.ordinal();
      if (ordinal == 2 || ordinal == 3) {
        return {29, 30};
      }
      if ((ordinal & 1) == 1 || monthCode.isLeapMonth()) {
        return {30, 30};
      }
      return {29, 29};
    }

    // Indian:
    // M1: 30-31 days.
    // M02..M06: 31 days
    // M07..M12: 30 days
    case CalendarId::Indian: {
      int32_t ordinal = monthCode.ordinal();
      if (ordinal == 1) {
        return {30, 31};
      }
      if (ordinal <= 6) {
        return {31, 31};
      }
      return {30, 30};
    }

    // IslamicUmmAlQura calendar has 29-30 days per month.
    case CalendarId::IslamicUmmAlQura:
      return {29, 30};

    // IslamicCivil, IslamicTabular:
    // M01, M03, M05, M07, M09, M11: 30 days
    // M02, M04, M06, M08, M10: 29 days
    // M12: 29-30 days.
    case CalendarId::IslamicCivil:
    case CalendarId::IslamicTabular: {
      int32_t ordinal = monthCode.ordinal();
      if ((ordinal & 1) == 1) {
        return {30, 30};
      }
      if (ordinal < 12) {
        return {29, 29};
      }
      return {29, 30};
    }

    // Persian:
    // M01..M06: 31 days
    // M07..M11: 30 days
    // M12: 29-30 days
    case CalendarId::Persian: {
      int32_t ordinal = monthCode.ordinal();
      if (ordinal <= 6) {
        return {31, 31};
      }
      if (ordinal <= 11) {
        return {30, 30};
      }
      return {29, 30};
    }
  }
  MOZ_CRASH("invalid calendar id");
}

}  // namespace js::temporal

#endif /* builtin_temporal_MonthCode_h */
