<?php

/**
 * @file
 * Adds All Day functionality to the Date field.
 *
 * This module implements a number of hooks in the Date field and Date API
 * element processing and to add an All Day checkbox to date widgets, and also
 * adds an All Day theme and All Day information to the formatting.
 *
 * Keep in mind that the process hooks are fired from the top down, first
 * date_combo, then the date_select or date_popup.
 *
 * Validation fires from the bottom up, first date_select and date_popup, then
 * date_combo.
 */

/**
 * Implements hook_theme().
 */
function date_all_day_theme() {
  $themes = array(
    'date_all_day' => array(
      'variables' => array(
        'field' => NULL,
        'instance' => NULL,
        'which' => NULL,
        'date1' => NULL,
        'date2' => NULL,
        'format' => NULL,
        'entity_type' => NULL,
        'entity' => NULL,
        'view' => NULL,
      ),
    ),
    'date_all_day_label' => array(
      'variables' => array(),
    ),
  );

  return $themes;
}

/**
 * Implements hook_date_formatter_dates_alter().
 */
function date_all_day_date_formatter_dates_alter(&$dates, $context) {
  // This allows us to alter the $dates array created by date_formatter_process.
  $field = $context['field'];
  $instance = $context['instance'];
  $format = $context['format'];
  $entity_type = $context['entity_type'];
  $entity = $context['entity'];
  $date1 = $dates['value']['local']['object'];
  $date2 = $dates['value2']['local']['object'];
  $is_all_day = date_all_day_field($field, $instance, $date1, $date2);

  $all_day1 = '';
  $all_day2 = '';
  if ($format != 'format_interval' && $is_all_day) {
    $all_day1 = theme('date_all_day', array(
      'field' => $field,
      'instance' => $instance,
      'which' => 'date1',
      'date1' => $date1,
      'date2' => $date2,
      'format' => $format,
      'entity_type' => $entity_type,
      'entity' => $entity,
    ));
    $all_day2 = theme('date_all_day', array(
      'field' => $field,
      'instance' => $instance,
      'which' => 'date2',
      'date1' => $date1,
      'date2' => $date2,
      'format' => $format,
      'entity_type' => $entity_type,
      'entity' => $entity,
    ));
    $dates['value']['formatted_time'] = theme('date_all_day_label');
    $dates['value2']['formatted_time'] = theme('date_all_day_label');
    $dates['value']['formatted'] = $all_day1;
    $dates['value2']['formatted'] = $all_day2;
  }
}

/**
 * Adjust start/end date format to account for 'all day' .
 *
 * @param array $vars
 *   Contains the following items:
 *   'field' - The field definition for this date field.
 *    'which' - Which value to return, 'date1' or 'date2'.
 *    'date1' - A date/time object for the 'start' date.
 *    'date2' - A date/time object for the 'end' date.
 *    'format' - A date/time format.
 *    'entity' - The node this date comes from (may be incomplete, always
 *       contains nid).
 *    'view' - The view this node comes from, if applicable.
 *
 * @return string|null
 *   Formatted date.
 */
function theme_date_all_day($vars) {
  $field = $vars['field'];
  $instance = $vars['instance'];
  $which = $vars['which'];
  $date1 = $vars['date1'];
  $date2 = $vars['date2'];
  $format = $vars['format'];
  $entity = $vars['entity'];
  $view = !empty($vars['view']) ? $vars['view'] : NULL;

  if (empty($date1) || !is_object($date1) || $format == 'format_interval') {
    return;
  }
  if (empty($date2)) {
    $date2 = $date1;
  }

  $suffix = '';
  if (!date_has_time($field['settings']['granularity'])) {
    $format = date_limit_format($format, array('year', 'month', 'day'));
  }
  else {
    $format_granularity = date_format_order($format);
    $format_has_time = FALSE;
    if (in_array('hour', $format_granularity)) {
      $format_has_time = TRUE;
    }
    $all_day = date_all_day_field($field, $instance, $date1, $date2);
    if ($all_day && $format_has_time) {
      $format = date_limit_format($format, array('year', 'month', 'day'));
      $suffix = ' ' . theme('date_all_day_label');
    }
  }

  return trim(date_format_date($$which, 'custom', $format) . $suffix);
}

/**
 * Theme the way an 'all day' label will look.
 */
function theme_date_all_day_label() {
  return t('(All day)', array(), array('context' => 'datetime'));
}

/**
 * Determine if a Start/End date combination qualify as 'All day'.
 *
 * @param array $field
 *   The field definition for this date field.
 * @param array $instance
 *   The field instance for this date field.
 * @param object $date1
 *   A date/time object for the 'Start' date.
 * @param object $date2
 *   A date/time object for the 'End' date.
 *
 * @return bool
 *   TRUE or FALSE.
 */
function date_all_day_field(array $field, array $instance, $date1, $date2 = NULL) {
  if (empty($date1) || !is_object($date1)) {
    return FALSE;
  }
  elseif (!date_has_time($field['settings']['granularity'])) {
    return FALSE;
  }
  // The master setting for the 'all day' functionality.
  elseif (empty($instance['widget']['settings']['display_all_day'])) {
    return FALSE;
  }

  if (empty($date2)) {
    $date2 = $date1;
  }

  // Several scenarios result in the two date values being the same, e.g.
  // date_formatter_process().
  if ($date1 == $date2) {
    // Clone the date object so that changes made to the object won't affect
    // both variables.
    $date1 = clone $date2;

    // Set the time to midnight so that the two variables can be properly
    // compared later in date_is_all_day().
    $date1->setTime(0, 0, 0);
  }

  // Use central logic for determining the granularity.
  $granularity = date_granularity_precision($field['settings']['granularity']);

  // The increment is 1 by default, but it can be overridden.
  $increment = 1;
  if (isset($instance['widget']['settings']['increment'])) {
    $increment = $instance['widget']['settings']['increment'];
  }

  return date_is_all_day(date_format($date1, DATE_FORMAT_DATETIME), date_format($date2, DATE_FORMAT_DATETIME), $granularity, $increment);
}

/**
 * Implements hook_date_combo_process_alter().
 */
function date_all_day_date_combo_process_alter(&$element, &$form_state, $context) {
  // This hook lets us make changes to the date_combo element.
  $field = $context['field'];
  $instance = $context['instance'];

  // Add the all_day checkbox to the combo element.
  if (!empty($instance['widget']['settings']['display_all_day'])) {

    $parents = $element['#parents'];
    $first_parent = array_shift($parents);
    $all_day_id = $first_parent . '[' . implode('][', $parents) . '][all_day]';;
    foreach (array('value', 'value2') as $key) {
      if (array_key_exists($key, $element)) {
        $element[$key]['#date_all_day_id'] = $all_day_id;
      }
    }

    $from = $element['#default_value']['value'];
    $to = !empty($element['#default_value']['value2']) ? $element['#default_value']['value2'] : $element['#default_value']['value'];
    $granularity = date_granularity_precision($context['field']['settings']['granularity']);
    $increment = isset($context['instance']['widget']['settings']['increment']) ? $context['instance']['widget']['settings']['increment'] : 1;
    $date_is_all_day = date_is_all_day($from, $to, $granularity, $increment);
    $all_day = !empty($form_state['values']['all_day']) || $date_is_all_day;
    $element['all_day'] = array(
      '#title' => t('All Day'),
      '#type' => 'checkbox',
      '#default_value' => $all_day,
      '#weight' => -21,
      '#prefix' => '<div class="date-float">',
      '#suffix' => '</div>',
    );
  }

  // Set all_day to 0 for all other date fields.
  else {
    $form['all_day']['#type'] = 'hidden';
    $form['all_day']['#value'] = 0;
  }
}

/**
 * Implements hook_date_text_process_alter().
 */
function date_all_day_date_text_process_alter(&$element, &$form_state, $context) {
  // This hook lets us make changes to the date_select widget.
  $all_day_id = !empty($element['#date_all_day_id']) ? $element['#date_all_day_id'] : '';
  if ($all_day_id != '') {
    // All Day handling on text dates works only if the user leaves the time
    // out of the input value. There is no element to hide or show.
  }
}

/**
 * Implements hook_date_select_process_alter().
 */
function date_all_day_date_select_process_alter(&$element, &$form_state, $context) {
  // This hook lets us make changes to the date_select widget. Hide or show
  // this element in reaction to the all_day status for this element.
  $all_day_id = !empty($element['#date_all_day_id']) ? $element['#date_all_day_id'] : '';
  if ($all_day_id != '') {
    foreach (array('hour', 'minute', 'second', 'ampm') as $field) {
      if (array_key_exists($field, $element)) {
        $element[$field]['#states'] = array(
          'visible' => array(
            'input[name="' . $all_day_id . '"]' => array('checked' => FALSE),
          ),
        );
      }
    }
  }
}

/**
 * Implements hook_date_popup_process_alter().
 */
function date_all_day_date_popup_process_alter(&$element, &$form_state, $context) {
  // This hook lets us make changes to the date_popup element. Hide or show
  // this element in reaction to the all_day status for this element.
  $all_day_id = !empty($element['#date_all_day_id']) ? $element['#date_all_day_id'] : '';
  if ($all_day_id != '' && array_key_exists('time', $element)) {
    $element['time']['#states'] = array(
      'visible' => array(
        'input[name="' . $all_day_id . '"]' => array('checked' => FALSE),
      ),
    );
  }
}

/**
 * Implements hook_date_select_pre_validate_alter().
 */
function date_all_day_date_text_pre_validate_alter(&$element, &$form_state, &$input) {
  // This hook lets us alter the element or the form_state before the rest of
  // the date_select validation gets fired. Let Date module massage the format
  // for all day values so they will pass validation. The All day flag, if
  // used, actually exists on the parent element.
  date_all_day_value($element, $form_state);
}

/**
 * Implements hook_date_select_pre_validate_alter().
 */
function date_all_day_date_select_pre_validate_alter(&$element, &$form_state, &$input) {
  // This hook lets us alter the element or the form_state before the rest of
  // the date_select validation gets fired. Let Date module massage the format
  // for all day values so they will pass validation. The All day flag, if
  // used, actually exists on the parent element.
  date_all_day_value($element, $form_state);
}

/**
 * Implements hook_date_select_pre_validate_alter().
 */
function date_all_day_date_popup_pre_validate_alter(&$element, &$form_state, &$input) {
  // This hook lets us alter the element or the form_state before the rest of
  // the date_popup validation gets fired. Let Date module massage the format
  // for all day values so they will pass validation. The All day flag, if
  // used, actually exists on the parent element.
  date_all_day_value($element, $form_state);
}

/**
 * A helper function date_all_day_value().
 */
function date_all_day_value(&$element, $form_state) {
  // To check if the all day flag is set on the parent of an element, and
  // adjust the date_format accordingly so the missing time will not cause
  // validation errors.
  if (!empty($element['#date_all_day_id'])) {
    $parents = $element['#parents'];
    array_pop($parents);
    $parent_element = drupal_array_get_nested_value($form_state['values'], $parents);
    if (!empty($parent_element) && !empty($parent_element['all_day'])) {
      $element['#date_format'] = date_part_format('date', $element['#date_format']);
      return $parent_element['all_day'];
    }
  }
  return FALSE;
}

/**
 * Implements hook_date_combo_pre_validate_alter().
 */
function date_all_day_date_combo_pre_validate_alter(&$element, &$form_state, $context) {
  // This hook lets us alter the element or the form_state before the rest of
  // the date_combo validation gets fired.
  if (!empty($context['item']['all_day'])) {
    $field = $context['field'];

    // If we have an all day flag on this date and the time is empty,
    // change the format to match the input value
    // so we don't get validation errors.
    $element['#date_is_all_day'] = TRUE;
    $element['value']['#date_format'] = date_part_format('date', $element['value']['#date_format']);
    if (!empty($field['settings']['todate'])) {
      $element['value2']['#date_format'] = date_part_format('date', $element['value2']['#date_format']);
    }
  }
}

/**
 * Implements hook_date_combo_validate_date_start_alter().
 */
function date_all_day_date_combo_validate_date_start_alter(&$date, &$form_state, $context) {
  // This hook lets us alter the local date objects created by the date_combo
  // validation before they are converted back to the database timezone and
  // stored. If this is an 'All day' value, set the time to midnight.
  if (!empty($context['element']['#date_is_all_day'])) {
    $date->setTime(0, 0, 0);
  }
}

/**
 * Implements hook_date_combo_validate_date_end_alter().
 */
function date_all_day_date_combo_validate_date_end_alter(&$date, &$form_state, $context) {
  // This hook lets us alter the local date objects created by the date_combo
  // validation before they are converted back to the database timezone and
  // stored. If this is an 'All day' value, set the time as close to midnight as
  // possible.
  if (!empty($context['element']['#date_is_all_day'])) {
    $increment = isset($context['instance']['widget']['settings']['increment']) ? $context['instance']['widget']['settings']['increment'] : 1;
    // @todo Should this reduce by an additional 1 minute if the increment is
    // more than 1? For example, if the increment is 15 should the upper value
    // be 23:45:59 or 23:44:59?
    $date->setTime(23, 60 - $increment, 59);
  }
}

/**
 * Implements hook_field_widget_info_alter().
 */
function date_all_day_field_widget_info_alter(&$info) {
  // This Field API hook lets us add a new setting to the widgets.
  // Add a setting to a widget type.
  $info['date_select']['settings'] += array(
    'display_all_day' => 0,
  );
  $info['date_text']['settings'] += array(
    'display_all_day' => 0,
  );
  if (module_exists('date_popup')) {
    $info['date_popup']['settings'] += array(
      'display_all_day' => 0,
    );
  }
}

/**
 * Implements hook_date_field_widget_settings_form_alter().
 */
function date_all_day_date_field_widget_settings_form_alter(&$form, $context) {
  // This hook lets us alter the field settings form by adding a place to set
  // the value added above.
  $field = $context['field'];
  $instance = $context['instance'];

  $settings = $instance['widget']['settings'];

  $form['display_all_day'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display all day checkbox'),
    '#default_value' => $settings['display_all_day'],
    '#description' => t("Determines whether to display the 'All Day' checkbox to the user."),
    '#weight' => 8,
    '#fieldset' => 'date_format',
  );

  // Hide the option to use the all day checkbox for dates with no time.
  if (!date_has_time($field['settings']['granularity'])) {
    $form['display_all_day']['#type'] = 'hidden';
    $form['display_all_day']['#value'] = 0;
  }
}
