<?php
/**
 * Utility class to generate a calendar for a given year.
 * @author ljacqu
 */
class CalendarGenerator {

  
/** @var int[] The number of days a month has. */
  
private $monthDays = [
    
=> 31,  => 28,  => 31,
    
=> 30,  => 31,  => 30,
    
=> 31,  => 31,  => 30,
   
10 => 3111 => 3012 => 31
  
];

  
/** @var int[] The name of the months to use when displaying a year calendar. */
  
private $monthNames = [
    
=> 'January',    => 'February',
    
=> 'March',      => 'April',
    
=> 'May',        => 'June',
    
=> 'July',       => 'August',
    
=> 'September'10 => 'October',
   
11 => 'November',  12 => 'December'
  
];

  
/** @var string[] The names of the week days to show in table headers, starting with Monday. */
  
private $weekDayNames = [
    
'Mon''Tue''Wed''Thu''Fri''Sat''Sun'
  
];

  
/** @var bool Whether the week should start with Sunday or not. */
  
private $sundayFirst false;


  
// ------------------------
  // Matrix
  // ------------------------
  /**
   * Generates the info for the cells in a calendar table as 2-dimensional array, the first dim.
   * being the rows, the second dim. the cells for each row.
   * @param int $date The start date in unix time
   * @param int $numberOfDays The number of days starting from $startDate to compute
   * @return int[][] Array with the row entries, the values being the date day
   */
  
function createMatrix($date$numberOfDays) {
    
$startInfo $this->getDateInfo($date);
    if (
$startInfo['weekday'] === 0) {
      
$rows = [];
      
$rowIndex = -1// compensate the ++
    
} else {
      
$rows = [
        
=> array_fill(0$startInfo['weekday'], '')
      ];
      
$rowIndex 0;
    }

    while (
$numberOfDays--) {
      
$dateInfo $this->getDateInfo($date);
      if (
$dateInfo['weekday'] === 0) {
        
$rows[++$rowIndex] = [];
      }
      
$rows[$rowIndex][ $dateInfo['weekday'] ] = $dateInfo['day'];
      
$date += 86400;
    }
    
$rows[$rowIndex] = array_pad($rows[$rowIndex], 7'');
    return 
$rows;
  }

  
/**
   * Creates the matrix data for an entire year, resulting in a 3-dimensional int array. The first
   * dimension representing the months, containing each matrix data as described in createMatrix().
   * @param int $year The year to create a matrix for
   * @return int[][][] The matrix data for the year
   */
  
function createYearMatrix($year) {
    
$months = [];
    for (
$month 1$month <= 12; ++$month) {
      
$firstOfMonth $this->makeDate($year$month1);
      
$numberOfDays $this->monthDays[$month];
      if (
$month === && date('L'$firstOfMonth)) {
        ++
$numberOfDays;
      }
      
$months[] = $this->createMatrix($firstOfMonth$numberOfDays);
    }
    return 
$months;
  }


  
// ------------------------
  // HTML Output
  // ------------------------
  /**
   * Creates a HTML table with a given matrix. The output contains CSS classes for easy styling.
   * @param int[][] $matrix The matrix data to generate a HTML table from
   * @return string The generated HTML output
   */
  
function generateHtmlTable($matrix) {
    
// before, between, after arguments
    
$cell   = ["\n" '<tr><td class="day">''</td><td class="day">''</td></tr>'];
    
$header = ["\n" '<tr><th>',             '</th><th>',             '</th></tr>'];

    
$output '<table class="calendar">' $this->generatePlainTable($matrixtrue$cell[0],
      
$cell[1], $cell[2], $header[0], $header[1], $header[2]) . '</table>';
    return 
str_replace('<td class="day"></td>''<td class="day empty"></td>'$output);
  }

  
/**
   * Creates a HTML table for each month. The output contains CSS classes to allow easy styling.
   * @param int $year The year to generate a HTML calendar for
   * @return string The generated HTML output
   */
  
function generateYearHtml($year) {
    
$yearData $this->createYearMatrix($year);
    
$output '<h1 class="year">' $year '</h1>';
    
$i 1;
    foreach (
$yearData as $monthMatrix) {
      
$output .= '<h2 class="month">' htmlspecialchars($this->monthNames[$i]) . '</h2>';
      
$output .= $this->generateHtmlTable($monthMatrix);
      ++
$i;
    }
    return 
$output;
  }


  
// ------------------------
  // Plain-text output
  // ------------------------
  /**
   * Generates output for given matrix data. The matrix is the only mandatory parameter; the others
   * fall back on a default value if they are missing.
   * @param int[][] $matrix The matrix data to output
   * @param bool $htmlEntities Whether week days should be HTML-escaped
   * @param string $beforeCell Text to add before the beginning of a row
   * @param string $betweenCell Text to add between two cells in a row
   * @param string $afterCell Text to add at the end of a row
   * @param string $beforeHeader Text to add before the week day header
   * @param string $betweenHeader Text to add between the cells of the week day header
   * @param string $afterHeader Text to add at the end of the header row
   * @return string The generated output
   */
  
function generatePlainTable($matrix$htmlEntities=true$beforeCell="\n"$betweenCell="\t",
                           
$afterCell=""$beforeHeader=""$betweenHeader="\t"$afterHeader="") {
    if (
$htmlEntities) {
      
$weekDayNames array_map('htmlspecialchars'$this->weekDayNames);
    } else {
      
$weekDayNames $this->weekDayNames;
    }
    if (
$this->sundayFirst) {
      
// Move the entry for Sunday from the end to the start of the array, i.e. to index 0
      
array_unshift($weekDayNamesarray_pop($weekDayNames));
    }
    
$output $beforeHeader implode($betweenHeader$weekDayNames) . $afterHeader;

    foreach (
$matrix as $week) {
      
$output .= $beforeCell implode($betweenCell$week) . $afterCell;
    }
    return 
$output;
  }

  
/**
   * Outputs a given year's calendar with the specified texts. Aside from $year, all other
   * parameters may be omitted and a default value will be used instead.
   * @param int $year The year to generate a calendar for
   * @param bool $htmlEntities Whether week days and month names should be HTML-escaped
   * @param string $beforeCell Text to add before the beginning of a row
   * @param string $betweenCell Text to add between two cells in a row
   * @param string $afterCell Text to add at the end of a row
   * @param string $beforeHeader Text to add before the week day header
   * @param string $betweenHeader Text to add between the cells of the week day header
   * @param string $afterHeader Text to add at the end of the header row
   * @param string $beforeMonth Text to add before the month name is output
   * @param string $afterMonth Text to add after a month name's output
   * @return string The generated output for the given year
   */
  
function generateYearPlainOutput($year$htmlEntities=true$beforeCell="\n"$betweenCell="\t",
                                   
$afterCell=""$beforeHeader=""$betweenHeader="\t",
                                   
$afterHeader=""$beforeMonth="\n\n"$afterMonth="\n========") {
    
$yearData $this->createYearMatrix($year);
    
$output '';
    
$i 1;
    foreach (
$yearData as $monthMatrix) {
      
$output .= $beforeMonth
        
. ($htmlEntities htmlspecialchars($this->monthNames[$i]) : $this->monthNames[$i])
        . 
$afterMonth $beforeCell
        
$this->generatePlainTable($monthMatrix$htmlEntities$beforeCell$betweenCell,
             
$afterCell$beforeHeader$betweenHeader$afterHeader);
      ++
$i;
    }
    return 
$output;
  }


  
// ------------------------
  // Settings
  // ------------------------
  /**
   * Sets whether the week should start with Sunday or not.
   * @param bool $sundayFirst True if the week should start with Sunday, false otherwise
   */
  
function setSundayFirst($sundayFirst) {
    
$this->sundayFirst = (bool) $sundayFirst;
  }
  function 
getSundayFirst() { return $this->sundayFirst; }

  
/**
   * Sets the week day names to use.
   * @param string[] $weekDayNames The names of the week days to use, where 0 is Monday and 6 is
   *  Sunday. Keys 0 through 6 must be present in the array.
   * @throws Exception if a week day number is not present in the array
   */
  
function setWeekDayNames($weekDayNames) {
    
// Copy the entries over to a new array so we don't have any other entries. This also ensures
    // that our entries are in the right order (for implode() and friends).
    // In PHP 5.6, array_filter can be used on array keys, allowing for a more elegant way...
    
$newWeekDayNames = [];
    for (
$i 0$i 7; ++$i) {
      if (!isset(
$weekDayNames[$i])) {
        throw new 
Exception('Could not set week day names: no name present for day ' $i);
      }
      
$newWeekDayNames[$i] = $weekDayNames[$i];
    }
    
$this->weekDayNames $newWeekDayNames;
  }
  function 
getWeekDayNames() { return $this->weekDayNames; }

  
/**
   * Sets the month names to display for the year calendar. Must be an array with keys 1 through 12.
   * @param string[] $monthNames A list of month names to use
   * @throws Exception if a month number is not present in the array
   */
  
function setMonthNames($monthNames) {
    
$newMonthNames = [];
    for (
$i 1$i <= 12; ++$i) {
      if (!isset(
$monthNames[$i])) {
        throw new 
Exception('Could not set month names: no name present for month ' $i);
      }
      
$newMonthNames[$i] = $monthNames[$i];
    }
    
$this->monthNames $newMonthNames;
  }
  function 
getMonthNames() { return $this->monthNames; }


  
// ------------------------
  // Helpers
  // ------------------------
  /**
   * Gets the date information for a given timestamp. If "sundayFirst" is false (default), a date on
   * Monday is returned as 0 and Sunday is 6. If the option is set to true, the week day will behave
   * like PHP's date('w'), where 0 is Sunday and 6 is Monday.
   * @param int $timestamp The timestamp to process
   * @return string[string] The week day and the date's day returned as an array
   */
  
private function getDateInfo($timestamp) {
    
$weekDay = (int) date('w'$timestamp);
    if (
$this->sundayFirst !== true) {
      
$weekDay += ($weekDay === 0) ? : -1;
    }
    return [
      
'weekday' => $weekDay,
      
'day' => date('j'$timestamp)
    ];
  }

  
/**
   * Creates a timestamp for the given date.
   * @param int $year The year of the date
   * @param int $month The month of the date
   * @param int $day The day of the date
   * @return int The resulting timestamp.
   */
  
private function makeDate($year$month$day) {
    
// Don't use 00:00 as time or you might get the same date twice because of daylight savings
    
return mktime(1200$month$day$year);
  }
}