<?php
/**
 * @file
 * Term Group Fields (tgf) is a custom widget for collecting data from a large
 * taxonomy by grouping the terms by their term parent.
 */

/**
 * Implements hook_form_alter().
 */
function tgf_form_alter(&$form, &$form_state, $form_id) {
  switch ($form_id) {
    case 'field_ui_field_edit_form':
      // Add options if the widget selected is ours.
      if ($form['#instance']['widget']['type'] == 'tgf_options_buttons') {
        $settings = tgf_load_field_settings($form['#instance']);
        $form_state['#tgf_new'] = empty($settings['id']);

        $tgf = array(
          '#type' => 'fieldset',
          '#title' => t('Display Settings Settings'),
          '#collapsible' => TRUE,
          '#tree' => TRUE,
        );
        $tgf['id'] = array(
          '#type' => 'value',
          '#value' => $form['#instance']['id'],
        );

        // Collection Settings
        $tgf['display_title'] = array(
          '#type' => 'checkbox',
          '#title' => t('Display Field Title'),
          '#description' => t('Display the field title on the edit form.  May be useful if wrapping this field in a fieldset.'),
          '#default_value' => $settings['display_title'],
          '#required' => FALSE,
        );
        $tgf['display_counter'] = array(
          '#type' => 'checkbox',
          '#title' => t('Display Counter'),
          '#description' => t('If the cardinality is not 1 or unlimited, show a message with the limit and lock the user out of selecting more than the allowed amount.'),
          '#default_value' => $settings['display_counter'],
          '#required' => FALSE,
        );
        $tgf['parent_selectable'] = array(
          '#type' => 'radios',
          '#title' => t('Parent Behavior'),
          '#description' => t('How should the parent term behave?'),
          '#options' => array(
            0 => t('Use parent as label'),
            1 => t('Parent may be selected alone'),
            2 => t('Parent includes all children'),
          ),
          '#default_value' => $settings['parent_selectable'],
          '#required' => FALSE,
        );

        // Append changes to the form
        $form['instance']['tgf'] = $tgf;
        $form['#submit'][] = 'tgf_field_ui_field_edit_form_submit';
        $form['#attached']['js'][] = drupal_get_path('module', 'tgf') . '/tgf.js';
      }
      break;
  }
}

/**
 * Implements hook_field_widget_info().
 */
function tgf_field_widget_info() {
  return array(
    'tgf_options_buttons' => array(
      'label' => t('Grouped Checkboxes/Radios'),
      'field types' => array('taxonomy_term_reference'),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function tgf_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  // We want to use our own function to generate allowed options, but only
  // when this widget is selected, not for all widgets used with this field.
  // Is there a better way to set this per instance?
  $field['settings']['options_list_callback'] = 'tgf_allowed_values';

  $type = str_replace('options_', '', $instance['widget']['type']);
  $value_key = key($field['columns']);
  $required = $element['#required'];
  $has_value = isset($items[0][$value_key]);
  $multiple = ($field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED);
  $properties = _options_properties($type, $multiple, $required, $has_value);
  $entity_type = $element['#entity_type'];
  $entity = $element['#entity'];

  // Load the custom settings for this field instance.
  $element['#tgf_settings'] = tgf_load_field_settings((array) $instance);

  // Get the terms of this vocabulary so we can nest them into a tree for the
  // processor.
  $vocab = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
  $terms = (is_object($vocab) ? taxonomy_get_tree($vocab->vid) : NULL);

  $options = tgf_get_options($field, $instance, $properties, $entity_type, $entity);

  switch ($instance['widget']['type']) {
    case 'tgf_options_buttons':
      $element += array(
        '#type' => ($multiple ? 'checkboxes' : 'radios'),
        '#default_value' => _options_storage_to_form($items, $options, $value_key, $properties),
        '#options' => $options,
        '#term_tree' => ($terms ? _tgf_tree_terms($terms) : NULL),
        '#multiple' => $multiple,
        '#cardinality' => $field['cardinality'],
        '#process' => array('tgf_process_checkbox_group'),
        '#value_key' => $value_key,
        '#properties' => $properties,
        '#element_validate' => array('options_field_widget_validate'),
        '#attached' => array('css' => array('file' => drupal_get_path('module', 'tgf') . '/tgf.css')),
      );
      break;
  }

  return $element;
}

/**
 * Implements hook_theme().
 */
function tgf_theme($existing, $type, $theme, $path) {
  return array(
    'tgf_form_option_element' => array(
      'render element' => 'element',
      'file' => 'tgf.themes.inc',
    ),
    'tgf_selection_counter' => array(
      'variables' => array('cardinality' => 0),
      'file' => 'tgf.themes.inc',
    )
  );
}

/**
 * Invokes tgf_allowed_values to return an array of options for the element.
 */
function tgf_get_options($field, $instance, $properties, $entity_type, $entity) {
  $options = (array) module_invoke($field['module'], 'options_list', $field, $instance, $entity_type, $entity);
  return $options;
}

/**
 * Returns the set of valid terms for a taxonomy field.
 *
 * @param $field
 *   The field definition.
 * @return
 *   The array of valid terms for this field, keyed by term id.
 */
function tgf_allowed_values($field) {
  $return = array();

  foreach ($field['settings']['allowed_values'] as $tree) {
    if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
      if ($terms = taxonomy_get_tree($vocabulary->vid, $tree['parent'])) {
        //$terms = _tgf_tree_terms($terms);
        foreach ($terms as $term) {
          $return[$term->tid] = $term->name;
        }
      }
    }
  }

  return $return;
}

/**
 * Process the group of options into a submittable form.
 *
 * In order to pass validation by Drupal and Taxonomy, the elements cannot be
 * in a higher tree branch, thus the creative markup rather than nesting groups
 * of elements.  There must be a way around this limitation, but I haven't found
 * it yet.
 */
function tgf_process_checkbox_group($element) {
  $element['#tree'] = TRUE;

  // If we have a vocabulary to work from, adjust the element accordingly.
  if (is_array($element['#term_tree']) && !empty($element['#term_tree'])) {
    // Hide the title if selected
    if (!$element['#tgf_settings']['display_title']) {
      $element['#title_display'] = 'invisible';
    }

    // Add a counter
    if ($element['#tgf_settings']['display_counter'] && $element['#cardinality'] > 1) {
      $element['#field_prefix'] = theme('tgf_selection_counter', array('cardinality' => $element['#cardinality']));
    }
  }
  else {
    return $element;
  }

  $groups = array_keys($element['#term_tree']);
  $value = is_array($element['#value']) ? $element['#value'] : array();

  if (count($element['#options']) > 0) {
    if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
      $element['#default_value'] = array();
    }
    $weight = 0;

    $i = 0;
    $group_key = 0;
    foreach ($element['#options'] as $key => $choice) {
      // Skip empty groups
      if (in_array($key, $groups) && count($element['#term_tree'][$key]->children) == 0) {
        continue;
      }

      // Integer 0 is not a valid #return_value, so use '0' instead.
      // @see form_type_checkbox_value().
      // @todo For Drupal 8, cast all integer keys to strings for consistency
      //   with form_process_radios().
      if ($key === 0) {
        $key = '0';
      }
      // Maintain order of options as defined in #options, in case the element
      // defines custom option sub-elements, but does not define all option
      // sub-elements.
      $weight += 0.01;

      // If this is a grouping term, create some clever markup to group the
      // form items. Otherwise, create the proper form item.
      if (in_array($key, $groups)) {
        $group_key = $key;
        $element += array($key => array());
        $element[$key] += array(
          '#prefix' => ($i ? '</div>' : '') . '<div class="tgf-group tgf-group-' . $group_key . '">',
          '#type' => ($element['#tgf_settings']['parent_selectable'] ? ($element['#multiple'] ? 'checkbox' : 'radio') : 'markup'),
          '#title' => $choice,
          '#return_value' => $key,
          '#default_value' => in_array($key, $value) ? $key : NULL,
          '#attributes' => $element['#attributes'],
          '#wrapper_attributes' => array('class' => array('tgf-group-parent')),
          '#weight' => $weight,
          '#theme_wrappers' => array('tgf_form_option_element'),
        );

        // Add an attribute to use as a select-all key.
        if ($element['#tgf_settings']['parent_selectable'] == 2) {
          $element[$key]['#attributes'] = array_merge_recursive((array) $element[$key]['#attributes'], array('tgf-selector' => $group_key));
        }

        // Add JS behaviors only if they are checkboxes.
        if ($element['#multiple']) {
          $element[$key]['#attached']['js']['file'][] = drupal_get_path('module', 'tgf') . '/tgf.js';
        }
      }
      else {
        $element += array($key => array());
        $element[$key] += array(
          '#type' => ($element['#multiple'] ? 'checkbox' : 'radio'),
          '#title' => $choice,
          '#return_value' => $key,
          '#default_value' => in_array($key, $value) ? $key : NULL,
          '#attributes' => array_merge_recursive($element['#attributes'], array('tgf-group' => $group_key)),
          '#wrapper_attributes' => array('class' => array('tgf-group-child')),
          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
          '#weight' => $weight,
          '#theme_wrappers' => array('tgf_form_option_element'),
        );
      }

      // Configure for radios.
      if (!$element['#multiple']) {
        $element[$key]['#id'] = drupal_html_id('edit-' . implode('-', array_merge($element['#parents'], array($key))));
        $element[$key]['#parents'] = $element['#parents'];
      }

      $i++;
    }
    $element['close-markup'] = array('#type' => 'markup', '#markup' => '</div>', '#weight' => 9999);
  }

  return $element;
}

/**
 * Create a nested tree of taxonomy terms ordered by weight then name.
 */
function _tgf_tree_terms($terms) {
  $sort = array();
  $tree = array();

  // Start by grouping all terms by depth.
  foreach ($terms as $term) {
    $term->children = array();
    $sort[$term->depth][$term->tid] = $term;
  }
  krsort($sort);

  // Starting from the deepest level, begin compiling tree branches
  $tree = $sort;
  foreach ($sort as $depth => $_terms) {
    if ($depth > 0) {
      foreach ($_terms as $_tid => $_term) {
        // This term must have a parent exactly one level less than its own level.
        foreach ($_term->parents as $pid) {
          if (isset($tree[$depth - 1][$pid])) {
            $tree[$depth - 1][$pid]->children[$_term->tid] = $_term;
          }
        }
        unset($tree[$depth][$_tid]);
      }
    }
  }

  return $tree[0];
}

/**
 * Take a full array of terms and return a flat array of tid=>name values.
 */
function _tgf_flatten_terms($terms) {
  $return = array();

  foreach ($terms as $term) {
    $return[$term->tid] = str_repeat("&mdash;", ($term->depth - 1)) . $term->name;
  }

  return $return;
}

/**
 * Fetch all children, recursing through generations.
 */
function _tgf_fetch_children_recursive($term) {
  $children = array();

  foreach ($term->children as $child) {
    if (count($child->children) > 0) {
      $children += _tgf_child_count_recursive($child->children);
    }
    else {
      $children += $term->children;
    }
  }

  return $children;
}

/**
 * Fetch the custom settings from the DB.
 */
function tgf_load_field_settings($instance) {
  $settings = db_select('tgf_field_settings', 'tgf')
            ->fields('tgf')
            ->condition('tgf.id', $instance['id'])
            ->execute()
            ->fetchAssoc();

  // If settings have not been saved yet, return the defualt values.
  $defaults = array(
    'id' => NULL,
    'display_title' => 1,
    'display_counter' => 0,
    'parent_selectable' => 1,
  );

  return (is_array($settings) ? $settings : $defaults);
}

/**
 * Store the additional field settings.
 */
function tgf_field_ui_field_edit_form_submit($form, &$form_state) {
  $vals = $form_state['values']['instance']['tgf'];

  if ($form_state['#tgf_new']) {
    drupal_write_record('tgf_field_settings', $vals);
  }
  else {
    drupal_write_record('tgf_field_settings', $vals, array('id'));
  }
}

/**
 * Implements hook_field_delete_instance().
 */
function tgf_field_delete_instance($instance) {
  // Any time a field instance is deleted, ensure any data that may have been
  // stored by TGF is deleted.
  db_delete('tgf_field_settings')->condition('id', $instance['id'])->execute();
}
