<?php
/**
 * @file
 * A module to enable jquery calendar multiselect picker.
 *
 * Requires the Date Popup module.
 *
 * Add a type of #date_multiselect to any date or datetime field that will
 * use this widget.
 *
 * No time elements are included in the format string, only the date
 * will be saved.
 */

define('DATE_MULTISELECT_FORMAT', 'm/d/Y');

/**
 * Implements hook_date_field_settings_form_alter().
 *
 * If we select date_multiselect as the widget, we shouldn't allow and enddate
 * field and the granularity must be ['year', 'month', 'day'].
 */
function date_multiselect_date_field_settings_form_alter(&$form, $context) {
  if ($context['instance']['widget']['type'] == 'date_multiselect') {
    // This field is meant to be used only for dates wihtout hour granularity.
    $form['granularity']['#type'] = 'value';
    $form['granularity']['#default_value'] = array('year', 'month', 'day');
    // There is a year element inside granularity that is no longer needed.
    unset($form['granularity']['year']);

    // We don't support using an end date.
    $form['enddate_get']['#type'] = 'value';
    $form['enddate_get']['#default_value'] = FALSE;
    $form['enddate_required']['#type'] = 'value';
    $form['enddate_required']['#default_value'] = FALSE;

    // Dates without hours granularity must not use any timezone handling.
    $form['tz_handling']['#type'] = 'value';
    $form['tz_handling']['#default_value'] = 'none';
  }
}

/**
 * Implements hook_field_widget_info().
 */
function date_multiselect_field_widget_info() {
  $info['date_multiselect'] = array(
    'label' => t('Multiselect calendar'),
    'field types' => array('date', 'datestamp', 'datetime'),
    'settings' => array(
      'yearRange' => '-3:+3',
      'numberOfMonths' => '2',
      'minDate' => '',
      'maxDate' => '',
    ),
    'behaviors' => array(
      'default value' => FIELD_BEHAVIOR_NONE,
      'multiple values' => FIELD_BEHAVIOR_CUSTOM,
    ),
  );

  return $info;
}

/**
 * Implements hook_field_widget_settings_form().
 */
function date_multiselect_field_widget_settings_form($field, $instance) {
  $settings = $instance['widget']['settings'];
  $form['numberOfMonths'] = array(
    '#title' => t('Number of months to display'),
    '#type' => 'textfield',
    '#default_value' => $settings['numberOfMonths'],
    '#element_validate' => array('element_validate_integer'),
  );
  $form['minDate'] = array(
    '#title' => t('Minimum date relative to the current date'),
    '#type' => 'textfield',
    '#default_value' => $settings['minDate'],
    '#element_validate' => array('element_validate_number'),
    '#description' => t('A number of days from today. For example 2 represents two days from today and -1 represents yesterday.'),
  );
  $form['maxDate'] = array(
    '#title' => t('Maximum date relative to the current date'),
    '#type' => 'textfield',
    '#default_value' => $settings['maxDate'],
    '#element_validate' => array('element_validate_number'),
    '#description' => t('A number of days from today. For example 2 represents two days from today and -1 represents yesterday.'),
  );
  $form['yearRange'] = array(
    '#type' => 'date_year_range',
    '#default_value' => $settings['yearRange'],
    '#fieldset' => 'date_format',
    '#weight' => 6,
  );

  return $form;
}

/**
 * Load needed files.
 *
 * Play nice with jQuery UI.
 */
function date_multiselect_add() {
  drupal_add_library('system', 'ui.datepicker');
  libraries_load('multidatespicker');
  drupal_add_js(drupal_get_path('module', 'date_multiselect') . '/js/date_multiselect.js');
}

/**
 * Implements hook_libraries_info().
 */
function date_multiselect_libraries_info() {
  $libraries['multidatespicker'] = array(
    'name' => 'MultiDatesPicker',
    'vendor url' => 'http://multidatespickr.sourceforge.net/',
    'download url' => 'http://sourceforge.net/projects/multidatespickr/files/MultiDatesPicker%20v1.6.3.zip/download',
    'version arguments' => array(
      'file' => 'jquery-ui.multidatespicker.js',
      'pattern' => '/MultiDatesPicker\s+v([0-9\.]+)/',
      'lines' => 5,
    ),
    'files' => array(
      'js' => array('jquery-ui.multidatespicker.js'),
    ),
  );
  return $libraries;
}

/**
 * Create a unique CSS id and output a single inline JS block for settings.
 *
 * Create a unique CSS id name and output a single inline JS block for
 * each startup function to call and settings array to pass it.  This
 * used to create a unique CSS class for each unique combination of
 * function and settings, but using classes requires a DOM traversal
 * and is much slower than an id lookup.  The new approach returns to
 * requiring a duplicate copy of the settings/code for every element
 * that uses them, but is much faster.  We could combine the logic by
 * putting the ids for each unique function/settings combo into
 * Drupal.settings and searching for each listed id.
 *
 * @param string $id
 *   The CSS class prefix to search the DOM for.
 * @param array $settings
 *   The settings array to pass to the jQuery function.
 *
 * @return string
 *   The CSS id to assign to the element.
 */
function date_multiselect_js_settings_id($id, array $settings) {
  $js_added = &drupal_static(__FUNCTION__);
  $id_count = &drupal_static(__FUNCTION__ . ':id_count', array());

  // Make sure multiselect date selector grid is in correct year.
  if (!empty($settings['yearRange'])) {
    $parts = explode(':', $settings['yearRange']);
    // Set the default date to 0 or the lowest bound.
    // Necessary for the datepicker to render and select dates correctly.
    $default_date = ($parts[0] > 0 || 0 > $parts[1]) ? $parts[0] : 0;
    $settings += array('defaultDate' => (string) $default_date . 'y');
  }

  if (!isset($js_added)) {
    date_multiselect_add();
    $js_added = TRUE;
  }

  // We use a static array to account for possible multiple form_builder()
  // calls in the same request (form instance on 'Preview').
  if (!isset($id_count[$id])) {
    $id_count[$id] = 0;
  }

  $return_id = "$id-multiselect-" . $id_count[$id]++;
  $js_settings['dateMultiselect'][$return_id] = array(
    'settings' => $settings + array(
      'altField' => 'input#' . $return_id,
      'smartphoneWidth' => variable_get('smartphone_width', 380),
    ),
  );
  drupal_add_js($js_settings, 'setting');
  return $return_id;
}

/**
 * Implode the date values array into a comma separated string of dates.
 *
 * @param mixed $items
 *   It contains all the values of the field.
 */
function date_multiselect_implode_dates($items) {
  $return = '';
  foreach ($items as $item) {
    if ($item['value']) {
      if (is_numeric($item['value'])) {
        $value = date(DATE_MULTISELECT_FORMAT, $item['value']);
      }
      else {
        $value = (new DateObject($item['value']))->format(DATE_MULTISELECT_FORMAT);
      }
      $return = $return ? ("$return, $value") : $value;
    }
  }
  return $return;
}

/**
 * Implements hook_field_widget_form().
 */
function date_multiselect_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  module_load_include('inc', 'date_api', 'date_api_elements');
  $timezone = date_default_timezone();

  // TODO see if there's a way to keep the timezone element from ever being
  // nested as array('timezone' => 'timezone' => value)). After struggling
  // with this a while, I can find no way to get it displayed in the form
  // correctly and get it to use the timezone element without ending up
  // with nesting.
  if (is_array($timezone)) {
    $timezone = $timezone['timezone'];
  }

  $element += array(
    '#type' => 'textfield',
    '#weight' => $delta,
    '#size' => 50,
    '#maxlength' => 512,
    '#date_timezone' => $timezone,
    '#element_validate' => array('date_multiselect_validate'),
    '#process' => array('date_multiselect_element_process'),
  );
  $element['#default_value'] = date_multiselect_implode_dates($items);

  if ($field['settings']['tz_handling'] == 'date') {
    $element['timezone'] = array(
      '#type' => 'date_timezone',
      '#theme_wrappers' => array('date_timezone'),
      '#delta' => $delta,
      '#default_value' => $timezone,
      '#weight' => $instance['widget']['weight'] + 1,
      '#attributes' => array('class' => array('date-no-float')),
    );
  }

  return $element;
}

/**
 * Javascript multiselect element processing.
 *
 * Add multiselect attributes to the element and include JS files. This is done
 * here instead of on the hook_field_widget_form function to avoid not including
 * the JS when showing the form from cache (like after a validation error).
 */
function date_multiselect_element_process($element, &$form_state, $form) {
  $instance = field_widget_instance($element, $form_state);
  $field = field_widget_field($element, $form_state);
  $date = NULL;
  if (isset($element['#value'])) {
    $values = array_map('trim', explode(',', $element['#value']));
    $date = new DateObject(array_shift($values), $element['#date_timezone'], DATE_MULTISELECT_FORMAT);
  }
  $range = date_range_years($instance['widget']['settings']['yearRange'], $date);
  $year_range = date_range_string($range);

  $settings = array(
    'numberOfMonths' => (int) $instance['widget']['settings']['numberOfMonths'],
    'firstDay' => intval(variable_get('date_first_day', 0)),
    'dateFormat' => date_popup_format_to_popup(DATE_MULTISELECT_FORMAT),
    'yearRange' => $year_range,
  );
  if (!empty($instance['widget']['settings']['minDate'])) {
    $settings['minDate'] = (int) $instance['widget']['settings']['minDate'];
  }
  if (!empty($instance['widget']['settings']['maxDate'])) {
    $settings['maxDate'] = (int) $instance['widget']['settings']['maxDate'];
  }
  if ($field['cardinality'] != '-1') {
    $settings['maxPicks'] = (int) $field['cardinality'];
  }

  // Create a unique id for each set of custom settings.
  $element['#id'] = date_multiselect_js_settings_id('multiselect-widget', $settings);

  return $element;
}

/**
 * Massage the input values back into a single date.
 *
 * When used as a Views widget, the validation step always gets triggered,
 * even with no form submission. Before form submission $element['#value']
 * contains a string, after submission it contains an array.
 */
function date_multiselect_validate($element, &$form_state) {
  $items = array();
  if ($element['#value']) {
    $values = array_map('trim', explode(',', $element['#value']));
    module_load_include('inc', 'date_api', 'date_api_elements');
    $field = field_info_field($element['#field_name']);
    $format = date_type_format($field['type']);
    foreach ($values as $value) {
      $date = new DateObject($value, $element['#date_timezone'], DATE_MULTISELECT_FORMAT);
      // If the date has errors, display them.
      // If something was input but there is no date, the date is invalid.
      // If the field is empty and required, set error message and return.
      if (empty($date) || !empty($date->errors)) {
        if (is_object($date) && !empty($date->errors)) {
          $message = t('The value input for field %field is invalid:', array('%field' => $element['#title']));
          $message .= '<br />' . implode('<br />', $date->errors);
          form_error($element, $message);
        }
        if (!empty($value)) {
          $message = t('The value input for field %field is invalid.', array('%field' => $element['#title']));
          form_error($element, $message);
        }
      }
      $items[] = array(
        'value' => $date->format($format),
        'timezone' => $element['#date_timezone'],
      );
    }

    if (empty($items) && $element['#required']) {
      $message = t('A valid date is required for %title.', array('%title' => $element['#title']));
      form_error($element, $message);
    }
  }

  form_set_value($element, $items, $form_state);
}

/**
 * Implements hook_field_widget_error().
 */
function date_multiselect_field_widget_error($element, $error, $form, &$form_state) {
  form_error($element, $error['message']);
}
