<?php
/**
 *
 * Project: PHP Calendar class
 * Description: A PHP-5 calendar class based on locale settings
 *
 * File:    fm_calendar.class.php
 *
 * ********************************************************************************
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * @see http://www.gnu.org/licenses/lgpl.html
 * ********************************************************************************
 *
 * @link http://code.google.com/p/intl-php-calendar/
 * @copyright 2008 Constantin Bejenaru (www.frozenminds.com)
 * @author Constantin Bejenaru (www.frozenminds.com)
 * @version 0.1 (release)
 */

class fm_calendar
{
   /**
    * Holds an array with todays date
    * keys are: "year", "month", "day", "timestamp"
    * Access example: $this->now->timestamp
    *
    * @var array
    */
   public $now;

   /**
    * Hold dates
    * We hold extra dates information to skip same calculations
    *
    * @var object
    */
   protected $dates;

   /**
    * Hold UNIX timestamps
    * We hold extra dates information to skip same calculations
    *
    * @var object
    */
   protected $timestamps;

   /**
    * Associative array to hold month names
    * ie: key[month number] => value[month name]
    *
    * @var array
    */
   private $_daysOfWeek = array();

   /**
    * ISO-8601 numeric representation of the first day of the week
    * 1 (for Monday) through 7 (for Sunday)
    *
    * @var unknown_type
    */
   private $firstDay = 1;

   /**
    * Locale settings used before the class was initialized
    * Store to allow later reverting back to initial setup
    *
    * @var string
    */
   private $_defaultLocale = null;

   /**
    * Locale settings used in the calendar class
    *
    * @var string
    */
   private $_locale = null;

   /**
    * Hold overloading properties
    * http://www.php.net/manual/en/language.oop5.overloading.php
    *
    * @var array
    */
   private $_overload = array();


   /**
    * Constructor
    */
   public function __construct()
   {
      //Get current locale settings,
      //we'll revert back in destructor
      $this->_defaultLocale = setlocale (LC_TIME, 0);

      //Initialize dates holder
      $this->dates = new stdClass();
      $this->timestamps = new stdClass();

      //Set todays date
      $this->now = new stdClass(); //Create standard class
      $this->now->timestamp = time(); //UNIX timestamp
      $this->now->year  = date ('Y', $this->now->timestamp);
      $this->now->month = date ('n', $this->now->timestamp);
      $this->now->day   = date ('j', $this->now->timestamp);

      //Initialize event holder
      $this->events = new stdClass();

      //Build days of week array
      $this->_daysOfWeek = $this->days_of_week();

      //Add here required functions that might stop everything working
      try
      {
         //$action = $this->do_work();
      }
      catch (Exception $e)
      {
         //Language pack was not initialized
         $this->__destruct();
      }
   }

   /**
    * Destructor
    */
   public function __destruct()
   {
      //Revert to initial locale settings
      $revertLocale = setlocale (LC_ALL, $this->_defaultLocale);
   }

   /**
    * Overload a member
    * http://www.php.net/manual/language.oop5.overloading.php
    *
    * @param string $var
    * @param mixed $value
    */
   public function __set($var, $value)
   {
      $this->_overload[$var] = $value;
   }

   /**
    * Get an overloaded member
    * http://www.php.net/manual/language.oop5.overloading.php
    *
    * @param string $var
    * @return mixed
    */
   public function __get($var)
   {
      //Check if error message exists
      if (isset ($this->_overload[$var]))
      {
         //Return error message
         return $this->_overload[$var];
      }

      //Fall back to unknown error message
      return false;
   }

   /**
    * Set locale information.
    * See PHP's setlocale() function
    *
    * @param string $locale
    * @return mixed Returns the new current locale, or FALSE if the locale functionality is not implemented on your platform, the specified locale does not exist or the category name is invalid.
    */
   public function setlocale($locale)
   {
      try
      {
         //Set new locale information
         $this->_locale = setlocale (LC_ALL, $locale);

         if (false === $this->_locale)
         {
            //Error occured while trying to set new locale information
            throw new Exception('Could not save locale settings');
         }

         //Return result (should be the new current locale)
         return $this->_locale;
      }
      catch (Exception $e)
      {
         if (!empty ($this->_defaultLocale))
         {
            $this->_locale = setlocale (LC_TIME, $this->_defaultLocale);
         }
      }

      return false;
   }

   /**
    * Parse UNIX timestamp and split it
    *
    * <pre>
    * array (
    *    [year]          => yyyy
    *    [month]         => mm
    *    [day]           => dd
    *    [hour]          => hh
    *    [minute]        => mm
    *    [second]        => ss
    *    [days_in_month] => t
    *    [week_in_year]  => W
    *    [day_in_week]   => d
    * )
    * </pre>
    *
    * @param  integer $timestamp
    * @return object
    */
   public function stamp_collection($timestamp)
   {
      //Make sure timestamp is an integer
      $timestamp = (int) $timestamp;

      //Make sure timestamp was not parsed before
      if (!isset ($this->timestamps->{$timestamp}))
      {
         //Parse timestamp
         $date = date ('Y n j H i s t W w', $timestamp);

         //Split date
         $date = explode (' ', $date);

         $this->timestamps->{$timestamp}->year          = (int) reset ($date);
         $this->timestamps->{$timestamp}->month         = (int) next ($date);
         $this->timestamps->{$timestamp}->day           = (int) next ($date);
         $this->timestamps->{$timestamp}->hour          = (int) next ($date);
         $this->timestamps->{$timestamp}->minute        = (int) next ($date);
         $this->timestamps->{$timestamp}->second        = (int) next ($date);
         $this->timestamps->{$timestamp}->days_in_month = (int) next ($date);
         $this->timestamps->{$timestamp}->week_in_year  = (int) next ($date);
         $this->timestamps->{$timestamp}->day_in_week   = (int) next ($date);
         $this->timestamps->{$timestamp}->first_day     = date ('w', mktime (0, 0, 0, $this->timestamps->{$timestamp}->month, 1, $this->timestamps->{$timestamp}->year));

         //Make sure 0 (zero) is treated as 7 (seven)
         $this->timestamps->{$timestamp}->first_day     = (1 > $this->timestamps->{$timestamp}->first_day ? 7 : $this->timestamps->{$timestamp}->first_day);

         $tempDays = ($this->timestamps->{$timestamp}->first_day + $this->timestamps->{$timestamp}->days_in_month);
         $this->timestamps->{$timestamp}->weeks_in_month = ceil ($tempDays / 7);

         $this->timestamps->{$timestamp}->week = array();

         $counter = 1;
         for ($j = 0; $j < $this->timestamps->{$timestamp}->weeks_in_month; $j++)
         {
            for ($i = $this->firstDay; $i < 7 + $this->firstDay; $i++)
            {
               $counter++;
               $this->timestamps->{$timestamp}->week[$j][$i] = $counter;

               //offset the days
               $this->timestamps->{$timestamp}->week[$j][$i] -= $this->timestamps->{$timestamp}->first_day;

               /*
               if (($this->timestamps->{$timestamp}->week[$j][$i] < 1) || ($this->timestamps->{$timestamp}->week[$j][$i] > $this->timestamps->{$timestamp}->days_in_month))
               {
               $this->timestamps->{$timestamp}->week[$j][$i] = false;
               }
               */

            }
         }//for
      }

      return $this->timestamps->{$timestamp};
   }

   /**
     * Get UNIX timestamp for a given date
     *
     * @param  integer year
     * @param  integer month
     * @param  integer day
     * @param  integer hour
     * @param  integer minute
     * @param  integer second
     * @return integer Unix timestamp
     */
   public function date2stamp($year, $month, $day, $hour=0, $minute=0, $second=0)
   {
      if (!isset ($this->dates->{$year}->{$month}->{$day}->{$hour}->{$minute}->{$second}))
      {
         $this->dates->{$year}->{$month}->{$day}->{$hour}->{$minute}->{$second} = mktime ($hour, $minute, $second, $month, $day, $year);
      }

      return (int) $this->dates->{$year}->{$month}->{$day}->{$hour}->{$minute}->{$second};
   }

   /**
    * Save the current date based on UNIX timestamp
    *
    * @param integer $timestamp
    * @return object current date object
    */
   public function set_current_timestamp($timestamp)
   {
      $this->current = new stdClass();

      //Parse date
      $this->current = $this->stamp_collection($timestamp);

      //Save timestamp
      $this->current->timestamp = (int) $timestamp;

      //Calculate and save previous date
      $this->set_previous_timestamp($this->current->timestamp);
      //Calculate and save next date
      $this->set_next_timestamp($this->current->timestamp);

      return $this->current;
   }

   /**
    * Save the current date based on date
    *
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @param  integer $hour
    * @param  integer $minute
    * @param  integer $second
    * @return object current date object
    */
   public function set_current_date($year, $month=1, $day=1, $hour=0, $minute=0, $second=0)
   {
      $this->current = new stdClass();

      $day = (1 > $day ? 1 : $day);

      //Parse date
      $timestamp = $this->date2stamp($year, $month, $day, $hour, $minute, $second);
      $this->current = $this->stamp_collection($timestamp);

      //Save timestamp
      $this->current->timestamp = (int) $timestamp;

      //Calculate and save previous date
      $this->set_previous_date($this->current->year, $this->current->month, $this->current->day, $this->current->hour, $this->current->minute, $this->current->second);
      //Calculate and save next date
      $this->set_next_date($this->current->year, $this->current->month, $this->current->day, $this->current->hour, $this->current->minute, $this->current->second);

      return $this->current;
   }

   /**
    * Save the previous date based on UNIX timestamp
    *
    * @param integer $timestamp Current timestamp
    * @return object previous date object
    */
   public function set_previous_timestamp($timestamp)
   {
      $this->previous = new stdClass();

      //Parse date
      $date = date ('Y n j H i s', $timestamp);

      //Split date
      $date = explode (' ', $date);
      $date = array_map ('intval', $date);//probably useless

      //Assign variables as if they were an array
      list ($year, $month, $day, $hour, $minute, $second) = $date;

      //Calculate prious date/time items
      $this->previous->year   = $this->stamp_collection(mktime ($hour  , $minute  , $second  , $month  , $day  , $year-1));
      $this->previous->month  = $this->stamp_collection(mktime ($hour  , $minute  , $second  , $month-1, $day  , $year  ));
      $this->previous->day    = $this->stamp_collection(mktime ($hour  , $minute  , $second  , $month  , $day-1, $year  ));
      $this->previous->hour   = $this->stamp_collection(mktime ($hour-1, $minute  , $second  , $month  , $day  , $year  ));
      $this->previous->minute = $this->stamp_collection(mktime ($hour  , $minute-1, $second  , $month  , $day  , $year  ));
      $this->previous->second = $this->stamp_collection(mktime ($hour  , $minute  , $second-1, $month  , $day  , $year  ));

      return $this->previous;
   }

   /**
    * Save the previous date based on date
    *
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @param  integer $hour
    * @param  integer $minute
    * @param  integer $second
    * @return object previous date object
    */
   public function set_previous_date($year, $month=1, $day=1, $hour=0, $minute=0, $second=0)
   {
      $day = (1 > $day ? 1 : $day);

      //Parse date
      $timestamp = $this->date2stamp($year, $month, $day, $hour, $minute, $second);

      return $this->set_previous_timestamp($timestamp);
   }

   /**
    * Save the next date based on UNIX timestamp
    *
    * @param integer $timestamp Current timestamp
    * @return object next date object
    */
   public function set_next_timestamp($timestamp)
   {
      $this->next = new stdClass();

      //Parse date
      $date = date ('Y n j H i s', $timestamp);

      //Split date
      $date = explode (' ', $date);
      $date = array_map ('intval', $date);//probably useless

      //Assign variables as if they were an array
      list ($year, $month, $day, $hour, $minute, $second) = $date;

      //Calculate prious date/time items
      $this->next->year   = $this->stamp_collection(mktime ($hour  , $minute  , $second  , $month  , $day  , $year+1));
      $this->next->month  = $this->stamp_collection(mktime ($hour  , $minute  , $second  , $month+1, $day  , $year  ));
      $this->next->day    = $this->stamp_collection(mktime ($hour  , $minute  , $second  , $month  , $day+1, $year  ));
      $this->next->hour   = $this->stamp_collection(mktime ($hour+1, $minute  , $second  , $month  , $day  , $year  ));
      $this->next->minute = $this->stamp_collection(mktime ($hour  , $minute+1, $second  , $month  , $day  , $year  ));
      $this->next->second = $this->stamp_collection(mktime ($hour  , $minute  , $second+1, $month  , $day  , $year  ));

      return $this->next;
   }

   /**
    * Save the next date based on date
    *
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @param  integer $hour
    * @param  integer $minute
    * @param  integer $second
    * @return object next date object
    */
   public function set_next_date($year, $month=1, $day=1, $hour=0, $minute=0, $second=0)
   {
      $day = (1 > $day ? 1 : $day);

      //Parse date
      $timestamp = $this->date2stamp($year, $month, $day, $hour, $minute, $second);

      return $this->set_next_timestamp($timestamp);
   }

   /**
    * Add an event to a given timestamp
    *
    * @param  integer $timetamp
    * @param  mixed   $event
    */
   public function add_event($timestamp, $event)
   {
      //Check first if event on current timestamp exists
      if (!isset ($this->events->{$timestamp}))
      {
         //Create event array
         $this->events->{$timestamp} = array();
      }

      //Append event to date (& time)
      $this->events->{$timestamp}[] = $event;
   }

   /**
    * Add an event to a given date
    * Easy method to skip transforation to UNIX timestamp on your own
    *
    * @param mixed   $event
    * @param integer $year
    * @param integer $month
    * @param integer $day
    * @param integer $hour
    * @param integer $minute
    * @param integer $second
    */
   public function add_date_event($event, $year=0, $month=0, $day=0, $hour=0, $minute=0, $second=0)
   {
      $timestamp = $this->date2stamp($year, $month, $day, $hour, $minute, $second);

      $this->add_event($timestamp, $event);
   }

   /**
    * Get events for a given timestamp
    *
    * @param  integer $timetamp
    * @return array
    */
   public function get_events($timestamp)
   {
      return (isset ($this->events->{$timestamp}) ? (array)$this->events->{$timestamp} : array());
   }

   /**
    * Get events for a given date
    * Easy method to skip transforation to UNIX timestamp on your own
    *
    * @param  mixed   $event
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @param  integer $hour
    * @param  integer $minute
    * @param  integer $second
    * @return array
    */
   public function get_date_events($year=0, $month=0, $day=0, $hour=0, $minute=0, $second=0)
   {
      $timestamp = $this->date2stamp($year, $month, $day, $hour, $minute, $second);

      return $this->get_events($timestamp);
   }

   /**
    * Get all events on a specific day
    *
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @return array
    */
   public function & get_day_events($year, $month, $day)
   {
      if (!is_object ($this->day_events))
      {
         $this->day_events = new stdClass();
      }

      if (isset ($this->day_events->{$year}->{$month}->{$day}) && is_array ($this->day_events->{$year}->{$month}->{$day}))
      {
         return $this->day_events->{$year}->{$month}->{$day};
      }

      //First second on day (00:00:00)
      $startDay = mktime (0, 0, 0, $month, $day, $year);
      //Last second on day (23:59:59)
      $endDay   = mktime (0, 0, -1, $month, $day + 1, $year);

      //Get all timestamps for events
      $eventStamps = array_keys(get_object_vars($this->events));

      //Loop array of event stamps
      foreach ($eventStamps as $stampKey => & $eventStamp)
      {
         //Check if current event timestamp is on this day
         if ($eventStamp >= $startDay && $eventStamp <= $endDay)
         {
            //Loop through events
            foreach ($this->events->{$eventStamp} as $eventKey => & $event)
            {
               //Append events to this day events
               $this->day_events->{$year}->{$month}->{$day}[] = $event;
            }
         }

         //Free some memory
         unset ($eventStamps[$stampKey], $eventStamp, $stampKey);
      }

      return $this->day_events->{$year}->{$month}->{$day};
   }

   /**
    * Checks if there are events on a specific day
    *
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @return boolean
    */
   public function has_day_events($year, $month, $day)
   {
      //Get the events and count them
      $dayEvents = $this->get_day_events($year, $month, $day);

      return (1 > count ($dayEvents) ? false : true);
   }



   /**
     * The upper limit of years that we can work with
     *
     * @return integer (2037)
     */
   public function get_max_years()
   {
      return 2037;
   }

   /**
     * The lower limit of years that we can work with
     * (1970 for Windows and 1902 for all other OSs)
     *
     * @return integer
     */
   public function get_min_years()
   {
      return (false === strpos (PHP_OS, 'WIN') ? 1902 : 1970);
   }

   /**
     * The upper limit of months in a year
     *
     * @return integer
     */
   public function get_max_months()
   {
      return $max = $this->get_months_in_year();
   }

   /**
     * The lower limit of months in a year
     *
     * @return integer
     */
   public function get_min_months()
   {
      return 1;
   }

   /**
     * The number of months for a given year
     * (Always returns 12)
     *
     * @return integer
     */
   public function get_months_in_year($year=null)
   {
      return 12;
   }

   /**
     * Return the number of days in a month for a given year and month
     *
     * @param  integer $year
     * @param  integer $month
     * @return integer
     */
   public function get_days_in_month($year, $month)
   {
      $daysInMonth = cal_days_in_month (CAL_GREGORIAN, $month, $year);

      if (empty ($daysInMonth))
      {
         //Fall back to regular calculation
         $daysInMonth = date ('t', mktime (0 , 0, 0, $month, 1, $year));
      }

      return (int) $daysInMonth;
   }

   /**
    * This will generate a list of months based on current locale settings
    *
    * @return array
    */
   public function months_of_year()
   {
      //Empty array
      $daysOfWeek = array();

      //Loop from 1 to 12 and get corresponding month name
      for ($i = $this->get_min_months(); $i <= $this->get_months_in_year(); $i++)
      {
         $daysOfWeek[$i] = strftime ('%B', mktime (0, 0, 0, $i, 1, $this->now->year));
      }

      return $daysOfWeek;
   }

   /**
    * This will generate a list of days based on current locale settings
    * 1 for Monday through 7 for Sunday (ISO-8601)
    *
    * @param  integer Week starting day
    * @return array
    */
   public function days_of_week($start=3)
   {
      //Empty array
      $daysList = array();

      //Loop from start day, 7 days,  and get corresponding day name
      $k = 1;
      for ($i = $this->get_first_day(); $i < 7 + $this->get_first_day(); $i++)
      {
         $daysList[$k] = strftime ('%A', mktime (0, 0, 0, 1, $i, 2001));//January 1st, 2001, was on a Monday
         $k++;
      }

      return $daysList;
   }

   /**
    * Set the ISO-8601 numeric representation of the first day of the week
    * 1 (for Monday) through 7 (for Sunday)
    *
    * @param integer $day
    */
   public function set_first_day($day=1)
   {
      $day = (int) $day;
      $day = ($day < 1 ? 1 : $day);
      $day = ($day > 7 ? 7 : $day);

      $this->firstDay = $day;
   }

   /**
    * Set the set numeric representation of the first day of the week
    * 1 (for Monday) through 7 (for Sunday)
    *
    * @return integer
    */
   public function get_first_day()
   {
      return (!empty ($this->firstDay) ? (int) $this->firstDay : 1);
   }

   /**
    * Get the day number
    * Works with negative values for day (and month if needed).
    * Useful to display previous/next days of month in calendar
    *
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @return integer
    */
   public function get_daynum($year, $month, $day)
   {
      $year  = (int) $year;

      //Determine timestamp
      $timestamp = $this->date2stamp($year, $month, $day);

      return (int) strftime ('%d', $timestamp);
   }

   /**
    * Checks if a given date is in the current month
    * (between first second of first day in month and last second of last day in month)
    *
    * @param  integer $year
    * @param  integer $month
    * @param  integer $day
    * @return boolean
    */
   public function in_current_month($year, $month, $day)
   {
      $year  = (int) $year;

      //Determine timestamp
      $timestamp = $this->date2stamp($year, $month, $day);

      //First day in month (00:00:00)
      $firstDay = mktime (0, 0, 0, $this->current->month, 1, $this->current->year);
      //Last day in month (23:59:59)
      $lastDay  = mktime (0, 0, -1, $this->current->month, $this->current->days_in_month + 1, $this->current->year);

      if ($timestamp >= $firstDay && $timestamp <= $lastDay)
      {
         return true;
      }

      return false;
   }

   /**
    * Checks if a given timestamp is in the current month
    * (between first second of first day in month and last second of last day in month)
    *
    * @param  integer $timestamp
    * @return boolean
    */
   public function timestamp_in_current_month($timestamp)
   {
      $timestamp = (int) $timestamp;

      //First day in month (00:00:00)
      $firstDay = mktime (0, 0, 0, $this->current->month, 1, $this->current->year);
      //Last day in month (23:59:59)
      $lastDay  = mktime (0, 0, -1, $this->current->month, $this->current->days_in_month + 1, $this->current->year);

      if ($timestamp >= $firstDay && $timestamp <= $lastDay)
      {
         return true;
      }

      return false;
   }

}

?>
