/*
 * This class implements all the methods necessary for the U of A Limited
 *   Calendric System.  
 * <p>
 * This calendric system has 3 epocs: 
 *     January 1, 0001 00:00:00 - August 21, 2000 06:59:59 -> ADGregorian Calendar
 *     August 21, 2000 07:00:00 - August 11, 2004 20:59:59 -> U of A Calendar
 *     August 11, 2004 21:00:00 - extentOf(ADGregorian Calendar) -> ADGregorian Calendar
 * <p> 
 * This class has the two methods castGregHourToUofAHour() and 
 * castUofAHourToGregHour().  With these two methods, the lattice will be able 
 * to do calculations between the two calendars in this calendric system.
 *
 * Last Updated: 2003/2/28 by Jessica Miller
 */

public class UofALimitedCalendricSystem
{
  /******************************************************************************/
  /* Constants                                                                  */
  /******************************************************************************/
  private static final long NUM_MINS_IN_DAY        = 60;
  
  private static final long NUM_HOURS_IN_GREG_DAY  = 24;
  private static final long NUM_HOURS_IN_UOFA_DAY  = 14;

  private static final long NUM_DAYS_IN_GREG_WEEK  = 7;
  private static final long NUM_DAYS_IN_UOFA_WEEK  = 5;

  private static final long NUM_HOURS_IN_GREG_WEEK = NUM_HOURS_IN_GREG_DAY *
                                                     NUM_DAYS_IN_GREG_WEEK;
  private static final long NUM_HOURS_IN_UOFA_WEEK = NUM_HOURS_IN_UOFA_DAY *
                                                     NUM_DAYS_IN_UOFA_WEEK;

  private static final long NUM_INVALID_GREG_HOURS_IN_UOFA_WEEK = 98;
  private static final long NUM_INVALID_GREG_HOURS_IN_UOFA_DAY  = 10;

  private static final long GREG_HOUR_ANCHOR       = 17528455;

  // i.e. 9pm Friday - 7am Monday is weekend and hours in between are invalid
  private static final long FIRST_WKEND_GREG_HOUR = 110;

  // i.e. if first hour (0th hour) of day is 7am, the 14th hour (or 9pm is the
  //   first invalid Gregorian calendar in the uofa day)
  private static final long FIRST_INVALID_GREG_HOUR_IN_UOFA_DAY = 14;

  private static final int INVALID_ARG             = -1;

  private static final int BREAK_START             = 0;
  private static final int BREAK_END               = 1;

  // the first row is the row that has the hour of start of a break; the second
  //   row is the hour of the end of the break; therefore, if there is a
  //   Gregorian hour between (and including) these hours, it is an invalid
  //   academic hour
  private static final long[][] HOURS_OF_BREAKS = {
    { // start dates (first hour) of August breaks
      ADGregorianCalendar.sumMinutes(2001, 8, 8, 21, 0) / NUM_MINS_IN_DAY,
      ADGregorianCalendar.sumMinutes(2002, 8, 8, 21, 0) / NUM_MINS_IN_DAY,
      ADGregorianCalendar.sumMinutes(2003, 8, 13, 21, 0) / NUM_MINS_IN_DAY
    },
    { // end dates (last hour) of August breaks
      ADGregorianCalendar.sumMinutes(2001, 8, 20, 6, 0) / NUM_MINS_IN_DAY,
      ADGregorianCalendar.sumMinutes(2002, 8, 26, 6, 0) / NUM_MINS_IN_DAY,
      ADGregorianCalendar.sumMinutes(2003, 8, 25, 6, 0) / NUM_MINS_IN_DAY
    }
  };

  private static final int GREG_HOUR_AFTER_BREAK  = 0;
  private static final int GREG_HOURS_IN_BREAK    = 1;  
  private static final int UOFA_HOUR_AFTER_BREAK  = 0;  
  private static final int UOFA_HOURS_IN_BREAK    = 1;
  private static final int UOFA_DAYS_NOT_COUNTED  = 2;  
  
  // this array's first row is the first valid Gregorian hour after a break; the
  //   second row is the number of uofa hours that occurred during the break
  private static final long[][] UOFA_HOURS_IN_BREAKS = {
    { // first Gregorian hour after August breaks
      ADGregorianCalendar.sumMinutes(2001, 8, 20, 7, 0) / NUM_MINS_IN_DAY,
      ADGregorianCalendar.sumMinutes(2002, 8, 26, 7, 0) / NUM_MINS_IN_DAY,
      ADGregorianCalendar.sumMinutes(2003, 8, 25, 7, 0) / NUM_MINS_IN_DAY
    },
    { // number of uofa calendar hours that occurred during the break
      98,   // # invalid uofa hours between August 8, 2001 9pm - August 20, 2001 7am
      154,  // # invalid uofa hours between August 8, 2002 9pm - August 26, 2002 7am
      98    // # invalid uofa hours between August 13, 2003 9pm - August 25, 2003 7am
    }
  };

  // this array's first row is the first valid uofa hour after a break; the
  //   second row is the number of Gregorian hours that occurred during the break
  private static final long[][] GREG_HOURS_IN_BREAKS = {
    { // first UofA hour after August breaks
      castGregHourToUofAHour(ADGregorianCalendar.sumMinutes(2001, 8, 20, 7, 0) / NUM_MINS_IN_DAY),
      castGregHourToUofAHour(ADGregorianCalendar.sumMinutes(2002, 8, 26, 7, 0) / NUM_MINS_IN_DAY),
      castGregHourToUofAHour(ADGregorianCalendar.sumMinutes(2003, 8, 25, 7, 0) / NUM_MINS_IN_DAY)
    },
    { // number of Gregorian calendar hours that occurred during the break
      9 * 24,   // Gregorian hours not counted between August 11 - 19, 2001
      16 * 24,  // Gregorian hours not counted between August 10 - 25, 2002
      9 * 24    // Gregorian hours not counted between August 16 - 24, 2003
    },
    {
      2, 
      1,
      2
    }
  };


  /***************************************************************************/
  /* Helper methods                                                          */
  /***************************************************************************/


  /*
   * Returns whether or not the given Gregorian hour is a Gregorian hour that
   *   takes place during a break during the academic year.  The breaks of
   *   academic years typically take place during the 2nd/3rd week of August.
   *
   *   @param gregHour number of Gregorian hour granules
   *   @return         whether or not the hour granule occurs during a break
   */
  private static boolean isInBreak(long gregHour)
  {
    for (int i = 0; i < HOURS_OF_BREAKS[BREAK_START].length; i++)
    {
      if (HOURS_OF_BREAKS[BREAK_START][i] <= gregHour  &&  gregHour <= HOURS_OF_BREAKS[BREAK_END][i])
        return true;
    }

    return false;
  }
  
  
  /*
   * Returns how many uofa calendar hours have occurred during all breaks up
   *   to this Gregorian hour.
   *
   *   @param gregHour number of Gregorian hour granules
   *   @return         number of uofa calendar hours that have occurred in 
   *                     breaks
   */
  private static long numUofABreakHours(long gregHour)
  {
    long uofaHoursInBreaks = 0;

    for (int i = 0; i < UOFA_HOURS_IN_BREAKS[GREG_HOUR_AFTER_BREAK].length; i++)
    {
      if (UOFA_HOURS_IN_BREAKS[GREG_HOUR_AFTER_BREAK][i] <= gregHour)
        uofaHoursInBreaks += UOFA_HOURS_IN_BREAKS[UOFA_HOURS_IN_BREAK][i];
      else
        break;
    }
    
    return uofaHoursInBreaks;
  }  
  

  /***************************************************************************/
  /* Methods that will be used for the irregular mappings in this calendar   */
  /***************************************************************************/

  public static long castGregHourToUofAHour(Long gregHourL){
      return castGregHourToUofAHour(gregHourL.longValue());      
  }

  /*
   * Covert the given amount of Gregorian hours to UofA hours using the
   *   cast operation.
   * <p>
   * Algorithm: (1) check if given Gregorian hour is a valid UofA hour by 
   *                checking if it occurs during
   *                - a break,
   *                - a weekend,
   *                - or, invalid hours of the day (between 9p - 7a)
   *            (2) if it is a valid uofa hour, calculate which uofa hour the
   *                Gregorian calendar corresponds to by subtracting the 
   *                following from the Gregorian hour
   *                - the Gregorian anchor
   *                - the number of hours in every Gregorian week that aren't
   *                  in a UofA week
   *                - the number of hours in the remainding Gregorian days that
   *                  aren't in a UofA day
   *                - the number of Gregorian hours in the breaks
   *
   *   @param gregHour number of Gregorian hour granules to cast into UofA hour
   *                   granularity
   *   @return         number of UofA hour granules
   */
  public static long castGregHourToUofAHour(long gregHour)
  {
    long hourInWeek, hourInDay, weeks, days;
    
    if (isInBreak(gregHour))
      return INVALID_ARG;
    
    hourInWeek = (gregHour - GREG_HOUR_ANCHOR) % NUM_HOURS_IN_GREG_WEEK;

    if (hourInWeek >= FIRST_WKEND_GREG_HOUR)
      return INVALID_ARG;

    hourInDay = hourInWeek % NUM_HOURS_IN_GREG_DAY;

    if (hourInDay >= FIRST_INVALID_GREG_HOUR_IN_UOFA_DAY)
      return INVALID_ARG;

    weeks = (gregHour - GREG_HOUR_ANCHOR) / NUM_HOURS_IN_GREG_WEEK;
    days  = hourInWeek / NUM_HOURS_IN_GREG_DAY;

    return gregHour
           - GREG_HOUR_ANCHOR
           - weeks * NUM_INVALID_GREG_HOURS_IN_UOFA_WEEK
           - days  * NUM_INVALID_GREG_HOURS_IN_UOFA_DAY
           - numUofABreakHours(gregHour);
  }


  public static long castUofAHourToGregHour(Long uofaHourL){
      return castUofAHourToGregHour(uofaHourL.longValue());
  }


  /*
   * Covert the given amount of UofA hours to Gregorian hours using the
   *   cast operation.
   * <p>
   * Algorithm: (1) figure out how many irregular UofA weeks there are (ones 
   *                that don't count all five days), how many UofA days aren't
   *                counted in these weeks, and how many Gregorian hours are in
   *                the breaks
   *            (2) get the number of full UofA weeks have been completed during 
   *                the uofaHour
   *            (3) get the number of remainding days that have been completed 
   *                this uofaHour
   *            (4) calculate the number of Gregorian hours that have passed in
   *                every irregular week by
   *                - counting the number of Gregorian hours that have occurred
   *                  in non-UofA counted days in this week
   *                - plus the number of Gregorian hours that have occurred in
   *                  UofA days (that wouldn't be counted by UofA hours)
   *                - plus the number of Gregorian hours that pass during the 
   *                  breaks
   *            (5) calculate the total number of Gregorian hours to this UofA
   *                hour by adding
   *                - number of hours in the UofA hour
   *                - number of hours in the Gregorian anchor
   *                - number of Gregorian hours that aren't counted in the UofA
   *                  weeks
   *                - number of Gregorian hours that aren't counted in the 
   *                  remaining UofA days
   *                - number of Gregorian hours that aren't counted in the 
   *                  irregular UofA weeks and breaks
   *
   *   @param uofaHour number of UofA hour granules to cast into Gregorian hour
   *                   granularity
   *   @return         number of Gregorian hour granules
   */
  public static long castUofAHourToGregHour(long uofaHour)
  {
    long weeks, days;
    long irregularUofAWeeks = 0, uofaDaysNotCounted = 0, gregBreakHours = 0;
    long gregHoursInIrregWeeks = 0, uofaHoursNotCounted = 0;
    
    for (int i = 0; i < GREG_HOURS_IN_BREAKS[UOFA_HOUR_AFTER_BREAK].length; i++)
    {
      if (GREG_HOURS_IN_BREAKS[UOFA_HOUR_AFTER_BREAK][i] <= uofaHour)
      {
        irregularUofAWeeks++;
        gregBreakHours += GREG_HOURS_IN_BREAKS[GREG_HOURS_IN_BREAK][i];
        uofaDaysNotCounted += GREG_HOURS_IN_BREAKS[UOFA_DAYS_NOT_COUNTED][i];
      }
      else
        break;
    }     
    
    uofaHoursNotCounted = uofaDaysNotCounted * NUM_HOURS_IN_UOFA_DAY;
    
    weeks = (uofaHour + uofaHoursNotCounted) / NUM_HOURS_IN_UOFA_WEEK - irregularUofAWeeks;
    days  = ((uofaHour + uofaHoursNotCounted) % NUM_HOURS_IN_UOFA_WEEK) / NUM_HOURS_IN_UOFA_DAY;

    gregHoursInIrregWeeks = (NUM_DAYS_IN_UOFA_WEEK * irregularUofAWeeks - uofaDaysNotCounted) * NUM_INVALID_GREG_HOURS_IN_UOFA_DAY
                            + uofaDaysNotCounted * NUM_HOURS_IN_GREG_DAY
                            + gregBreakHours;

    return uofaHour 
           + GREG_HOUR_ANCHOR
           + weeks * NUM_INVALID_GREG_HOURS_IN_UOFA_WEEK
           + days  * NUM_INVALID_GREG_HOURS_IN_UOFA_DAY
           + gregHoursInIrregWeeks;
  }

}
