/*
  This file is part of KOrganizer.

  Copyright (c) 2003 Jonathan Singer <jsinger@leeta.net>
  Copyright (C) 2007 Loïc Corbasson <loic.corbasson@gmail.com>
  Calendar routines from Hebrew Calendar by Frank Yellin.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along
  with this program; if not, write to the Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

#include "converter.h"

HebrewDate::HebrewDate(const DateResult &d)
    : mYear(d.year)
    , mMonth(d.month)
    , mDay(d.day)
    , mDayOfWeek(d.day_of_week)
    , mHebrewMonthLength(d.hebrew_month_length)
    , mSecularMonthLength(d.secular_month_length)
    , mOnHebrewLeapYear(d.hebrew_leap_year_p)
    , mOnSecularLeapYear(d.secular_leap_year_p)
    , mKvia(d.kvia)
    , mHebrewDayNumber(d.hebrew_day_number)
{
}

HebrewDate::~HebrewDate()
{
}

HebrewDate HebrewDate::fromSecular(int year, int month, int day)
{
    DateResult result;
    Converter::secularToHebrewConversion(year, month, day, &result);
    return HebrewDate(result);
}

HebrewDate HebrewDate::fromHebrew(int year, int month, int day)
{
    DateResult result;
    Converter::hebrewToSecularConversion(year, month, day, &result);
    return HebrewDate(result);
}

int HebrewDate::year() const
{
    return mYear;
}

int HebrewDate::month() const
{
    return mMonth;
}

int HebrewDate::day() const
{
    return mDay;
}

int HebrewDate::dayOfWeek() const
{
    return mDayOfWeek;
}

int HebrewDate::hebrewMonthLength() const
{
    return mHebrewMonthLength;
}

int HebrewDate::secularMonthLength() const
{
    return mSecularMonthLength;
}

bool HebrewDate::isOnHebrewLeapYear() const
{
    return mOnHebrewLeapYear;
}

bool HebrewDate::isOnSecularLeapYear() const
{
    return mOnSecularLeapYear;
}

int HebrewDate::kvia() const
{
    return mKvia;
}

int HebrewDate::hebrewDayNumber() const
{
    return mHebrewDayNumber;
}

////////////////////////////////////////////////////////////////////////////////

long Converter::absolute_from_gregorian(int year, int month, int day)
{
    int xyear, day_number;

    xyear = year - 1;
    day_number = day + 31 * (month - 1);
    if (month > 2) {
        day_number -= (23 + (4 * month)) / 10;
        if (gregorian_leap_year_p(year)) {
            day_number++;
        }
    }
    return day_number         /* the day number within the current year */
           + 365L * xyear     /* days in prior years */
           + (xyear / 4)        /* Julian leap years */
           + (-(xyear / 100))     /* deduct century years */
           + (xyear / 400);     /* add Gregorian leap years */
}

/* Given a Hebrew date, calculate the number of days since January 0, 0001,
   Gregorian */
long Converter::absolute_from_hebrew(int year, int month, int day)
{
    long sum = day + hebrew_elapsed_days(year) - 1373429L;
    int i;

    if (month < 7) {
        int months = hebrew_months_in_year(year);
        for (i = 7; i <= months; ++i) {
            sum += hebrew_month_length(year, i);
        }
        for (i = 1; i < month; ++i) {
            sum += hebrew_month_length(year, i);
        }
    } else {
        for (i = 7; i < month; ++i) {
            sum += hebrew_month_length(year, i);
        }
    }
    return sum;
}

/* Given an absolute date, calculate the gregorian date  */
void Converter::gregorian_from_absolute(long date, int *yearp, int *monthp, int *dayp)
{
    int year, month, day;

    for (year = date / 366;
         date >= absolute_from_gregorian(year + 1, 1, 1); ++year) {
    }

    for (month = 1;
         (month <= 11)
         && (date >= absolute_from_gregorian(year, 1 + month, 1));
         ++month) {
    }

    day = 1 + date - absolute_from_gregorian(year, month, 1);
    *yearp = year;
    *monthp = month;
    *dayp = day;
}

/* Given an absolute date, calculate the Hebrew date */
void Converter::hebrew_from_absolute(long date, int *yearp, int *monthp, int *dayp)
{
    int year, month, day, gyear, gmonth, gday, months;

    gregorian_from_absolute(date, &gyear, &gmonth, &gday);
    year = gyear + 3760;
    while (date >= absolute_from_hebrew(1 + year, 7, 1)) {
        year++;
    }
    months = hebrew_months_in_year(year);
    for (month = 7;
         date > absolute_from_hebrew(year, month,
                                     hebrew_month_length(year, month));
         month = 1 + (month % months)) {
    }
    day = 1 + date - absolute_from_hebrew(year, month, 1);
    *yearp = year;
    *monthp = month;
    *dayp = day;
}

/* Number of months in a Hebrew year */
int Converter::hebrew_months_in_year(int year)
{
    if (hebrew_leap_year_p(year)) {
        return 13;
    } else {
        return 12;
    }
}

/* Number of days in a Hebrew month */
int Converter::hebrew_month_length(int year, int month)
{
    switch (month) {
    case Tishrei:
    case Shvat:
    case Nissan:
    case Sivan:
    case Ab:
        return 30;

    case Tevet:
    case Iyar:
    case Tamuz:
    case Elul:
    case AdarII:
        return 29;

    case Cheshvan:
        // 29 days, unless it's a long year.
        if ((hebrew_year_length(year) % 10) == 5) {
            return 30;
        } else {
            return 29;
        }

    case Kislev:
        // 30 days, unless it's a short year.
        if ((hebrew_year_length(year) % 10) == 3) {
            return 29;
        } else {
            return 30;
        }

    case Adar:
        // Adar (non-leap year) has 29 days. Adar I has 30 days.
        if (hebrew_leap_year_p(year)) {
            return 30;
        } else {
            return 29;
        }

    default:
        return 0;
    }
}

/* Number of days in a Julian or gregorian month */
int Converter::secular_month_length(int year, int month /*, bool julianp */)
{
    switch (month) {
    case January:
    case March:
    case May:
    case July:
    case August:
    case October:
    case December:
        return 31;

    case April:
    case June:
    case September:
    case November:
        return 30;

    case February:
        if (gregorian_leap_year_p(year)) {
            return 29;
        } else {
            return 28;
        }

    default:
        return 0;
    }
}

/* Is it a leap year in the gregorian calendar */
bool Converter::gregorian_leap_year_p(int year)
{
    if ((year % 4) != 0) {
        return 0;
    }
    if ((year % 400) == 0) {
        return 1;
    }
    if ((year % 100) == 0) {
        return 0;
    }
    return 1;
}

/* Is it a leap year in the Jewish Calendar */
bool Converter::hebrew_leap_year_p(int year)
{
    switch (year % 19) {
    case 0:
    case 3:
    case 6:
    case 8:
    case 11:
    case 14:
    case 17:
        return 1;
    default:
        return 0;
    }
}

/* Return the number of days from 1 Tishrei 0001 to the beginning of the given
   year. Since this routine gets called frequently with the same year arguments,
   we cache the most recent values. */
#define MEMORY 5
long Converter::hebrew_elapsed_days(int year)
{
    static int saved_year[MEMORY] = { -1, -1, -1, -1, -1 };
    static long saved_value[MEMORY];
    int i;

    for (i = 0; i < MEMORY; ++i) {
        if (year == saved_year[i]) {
            return saved_value[i];
        }
    }
    for (i = 0; i < MEMORY - 1; ++i) {
        saved_year[i] = saved_year[1 + i];
        saved_value[i] = saved_value[1 + i];
    }
    saved_year[MEMORY - 1] = year;
    saved_value[MEMORY - 1] = hebrew_elapsed_days2(year);
    return saved_value[MEMORY - 1];
}

/* Called by hebrew_elapsed_days to make the calculations if the result is not
   in the cache */
long Converter::hebrew_elapsed_days2(int year)
{
    long prev_year = year - 1;
    long months_elapsed = 235L * (prev_year / 19)
                          +/* months in complete cycles so far */
                          12L * (prev_year % 19)
                          +/* regular months in this cycle */
                          (((prev_year % 19) * 7 + 1) / 19);
    /* leap months in this cycle */
    long parts_elapsed = 5604 + 13753 * months_elapsed;
    long day = 1 + 29 * months_elapsed + parts_elapsed / 25920;
    long parts = parts_elapsed % 25920;
    int weekday = day % 7;
    long alt_day
        = ((parts >= 19440)
           || (weekday == 2 && (parts >= 9924) && !hebrew_leap_year_p(year))
           || (weekday == 1 && (parts >= 16789) && hebrew_leap_year_p(prev_year)))
          ? day + 1 : day;

    switch (alt_day % 7) {
    case 0:
    case 3:
    case 5:
        return 1 + alt_day;
    default:
        return alt_day;
    }
}

/* Number of days in the given Hebrew year */
int Converter::hebrew_year_length(int year)
{
    return hebrew_elapsed_days(1 + year) - hebrew_elapsed_days(year);
}

/* Fill in the DateResult structure based on the given secular date */
void Converter::secularToHebrewConversion(int syear, int smonth, int sday, struct DateResult *result)
{
    int hyear, hmonth, hday;
    long absolute;

    absolute = absolute_from_gregorian(syear, smonth, sday);

    hebrew_from_absolute(absolute, &hyear, &hmonth, &hday);

    result->year = hyear;
    result->month = hmonth;
    result->day = hday;
    finish_up(absolute, hyear, hmonth, syear, smonth, result);
}

/* Fill in the DateResult structure based on the given Hebrew date */
void Converter::hebrewToSecularConversion(int hyear, int hmonth, int hday, struct DateResult *result)
{
    int syear, smonth, sday;
    long absolute;

    absolute = absolute_from_hebrew(hyear, hmonth, hday);
    gregorian_from_absolute(absolute, &syear, &smonth, &sday);
    result->year = hyear;
    result->month = hmonth;
    result->day = hday;
    finish_up(absolute, hyear, hmonth, syear, smonth, result);
}

/* This is common code for filling up the DateResult structure */
void Converter::finish_up(long absolute, int hyear, int hmonth, int syear, int smonth, struct DateResult *result)
{
    result->hebrew_month_length = hebrew_month_length(hyear, hmonth);
    result->secular_month_length = secular_month_length(syear, smonth);
    result->hebrew_leap_year_p = hebrew_leap_year_p(hyear);
    result->secular_leap_year_p = gregorian_leap_year_p(syear);
    result->kvia = (hebrew_year_length(hyear) % 10) - 3;
    // absolute is -1 on 1/1/0001 Julian
    result->day_of_week = (7 + absolute) % 7;
    result->hebrew_day_number = absolute - absolute_from_hebrew(hyear, 7, 1) + 1;
}
