/*
  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"

#include <KLocale>

HebrewDate::HebrewDate( struct 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 )
{
  struct DateResult result;
  Converter::secularToHebrewConversion( year, month, day, &result );
  return HebrewDate( result );
}

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

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

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

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

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

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

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

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

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

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

int HebrewDate::hebrewDayNumber()
{
  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;
}

