<?php
/**
 * @file
 * Define dependencies between fields based on their states and values.
 *
 * Conditional Fields for Drupal 7 is basically an user interface for the States
 * API, plus the ability to hide fields on certain conditions when viewing
 * content.
 */

/**
 * Dependency is triggered if the dependee has a certain value.
 */
define('CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET', 1);

/**
 * Dependency is triggered if the dependee has all values.
 */
define('CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND', 2);

/**
 * Dependency is triggered if the dependee has any of the values.
 */
define('CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR', 3);

/**
 * Dependency is triggered if the dependee has only one of the values.
 */
define('CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR', 4);

/**
 * Dependency is triggered if the dependee does not have any of the values.
 */
define('CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT', 5);

/**
 * Field view setting. Dependent is shown only if the dependency is triggered.
 */
define('CONDITIONAL_FIELDS_FIELD_VIEW_EVALUATE', 1);

/**
 * Field view setting. Dependent is shown only if the dependee is shown as well.
 */
define('CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_ORPHAN', 2);

/**
 * Field view setting. Dependent is highlighted if the dependency is not
 * triggered.
 */
define('CONDITIONAL_FIELDS_FIELD_VIEW_HIGHLIGHT', 3);

/**
 * Field view setting. Dependent has a textual description of the dependency.
 */
define('CONDITIONAL_FIELDS_FIELD_VIEW_DESCRIBE', 4);

/**
 * Field view setting. Dependent is shown only if the dependee is shown as well
 * and the dependency evaluates to TRUE.
 */
define('CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_UNTRIGGERED_ORPHAN', 5);

/**
 * Field edit setting. Dependent is shown only if the dependee is shown as well.
 */
define('CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_ORPHAN', 1);

/**
 * Field edit setting. Dependent is shown only if the dependee is shown as well
 * and the dependency evaluates to TRUE.
 */
define('CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_UNTRIGGERED_ORPHAN', 2);

/**
 * Field edit setting. Dependent is reset to its default values if the
 * dependency was not triggered when the form is submitted.
 */
define('CONDITIONAL_FIELDS_FIELD_EDIT_RESET_UNTRIGGERED', 3);

/* TODO: Grouping
define('CONDITIONAL_FIELDS_GROUPING_AND', 0);
define('CONDITIONAL_FIELDS_GROUPING_OR',  1);
define('CONDITIONAL_FIELDS_GROUPING_XOR', 2);
 */

/**
 * Implements hook_permission().
 */
function conditional_fields_permission() {
  return array(
    'administer dependencies' => array(
      'title' => t('Administer dependencies'),
      'description' => t('View, edit and delete field dependencies.'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function conditional_fields_menu() {
  $items = array();

  // Ensure the following is not executed until field_bundles is working and
  // tables are updated. Needed to avoid errors on initial installation.
  if (defined('MAINTENANCE_MODE')) {
    return $items;
  }

  $items['admin/structure/dependencies'] = array(
    'title' => 'Field dependencies',
    'description' =>  'Administer field dependencies for the site.',
    'page callback' => 'conditional_fields_dependencies_overview_page',
    'access arguments' => array('administer dependencies'),
    'file' => 'includes/conditional_fields.admin.inc',
  );

  $items['admin/structure/dependencies/overview'] = array(
    'title' => 'Overview',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 1,
  );

  $items['admin/structure/dependencies/edit/%'] = array(
    'title' => 'Edit dependency',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('conditional_fields_dependency_edit_form', 4),
    'access arguments' => array('administer dependencies'),
    'file' => 'includes/conditional_fields.admin.inc',
  );

  $items['admin/structure/dependencies/delete/%'] = array(
    'title' => 'Delete dependency',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('conditional_fields_dependency_delete_form', 4),
    'access arguments' => array('administer dependencies'),
    'file' => 'includes/conditional_fields.admin.inc',
  );

  // Some of the following code is copied from field_ui_menu().

  // Create tabs for all possible bundles.
  foreach (entity_get_info() as $entity_type => $entity_info) {
    if ($entity_info['fieldable']) {
      $items["admin/structure/dependencies/$entity_type"] = array(
        'title' => $entity_info['label'],
        'page arguments' => array(NULL, 3),
        'access arguments' => array('administer dependencies'),
        'type' => MENU_LOCAL_TASK,
        'weight' => 2,
      );

      foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
        if (module_exists('field_ui') && isset($bundle_info['admin'])) {
          // Extract path information from the bundle and replace any "magic"
          // wildcard with a normal one.
          $path = preg_replace('/(%[a-z0-9_]*)/', '%', $bundle_info['admin']['path']);

          if (isset($bundle_info['admin']['bundle argument'])) {
            $bundle_pos = $bundle_info['admin']['bundle argument'];
          }
          else {
            $bundle_pos = $bundle_name;
          }

          $items["$path/dependencies"] = array(
            'title' => $entity_type == 'comment' ? 'Comment dependencies' : 'Manage dependencies',
            'page callback' => 'conditional_fields_dependencies_overview_page',
            'page arguments' => array($bundle_pos, $entity_type),
            'type' => MENU_LOCAL_TASK,
            'weight' => $entity_type == 'comment' ? 4 : 2,
            'file' => 'includes/conditional_fields.admin.inc',
            'access arguments' => array('administer dependencies'),
          );
        }
      }
    }
  }

  return $items;
}

/**
 * Implements hook_forms().
 *
 * Maps all dependency add forms to the same callback.
 */
function conditional_fields_forms() {
  foreach (entity_get_info() as $entity_type => $entity_info) {
    if ($entity_info['fieldable']) {
      foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
        $forms['conditional_fields_dependency_add_form_' . $entity_type . '_' . $bundle_name] = array(
          'callback' => 'conditional_fields_dependency_add_form',
          'callback arguments' => array($entity_type, $bundle_name),
        );
      }
    }
  }

  return $forms;
}

/**
 * Implements hook_js_alter().
 *
 * Overrides core states API with a patched version that allows multiple
 * conditions and OR/XOR logic. Ongoing development at:
 * http://drupal.org/node/735528
 */
function conditional_fields_js_alter(&$javascript) {
  if (isset($javascript['misc/states.js'])) {
    $javascript['misc/states.js']['data'] = drupal_get_path('module', 'conditional_fields') . '/js/states.js';
  }
}

/**
 * Implements hook_element_info_alter().
 * Adds an #after_build function to all form elements.
 */
function conditional_fields_element_info_alter(&$types) {
  foreach ($types as $type => $info) {
    $types[$type]['#after_build'][] = 'conditional_fields_element_after_build';
  }
}

/**
 * Processes form elements with dependencies.
 *
 * Just adds a #conditional_fields property to the form with the needed
 * data, which is used later in conditional_fields_form_after_build():
 * - The fields #parents property.
 * - Field dependencies data.
 */
function conditional_fields_element_after_build($element, &$form_state) {
  // Ensure that the form being built is an entity.
  if (!isset($form_state['complete form']['#entity_type'], $form_state['complete form']['#bundle'])) {
    return $element;
  }

  // Ensure that the element is a field.
  if (isset($element['#field_name'])) {
    $field = $element;
  }
  elseif (isset($element['#language'], $element[$element['#language']], $element[$element['#language']]['#field_name'])) {
    // Some fields are wrapped in containers before processing.
    $field = $element[$element['#language']];
  }
  else {
    return $element;
  }

  // Some fields do not have entity type and bundle properties. In this case we
  // try to use the properties from the form. This is not an optimal solution,
  // since in case of fields in entities within entities they won't correspond,
  // and their dependencies will not be loaded.
  if (!isset($field['#entity_type'], $field['#bundle'])) {
    $entity_type = $form_state['complete form']['#entity_type'];
    $bundle = $form_state['complete form']['#bundle'];
  }
  else {
    $entity_type = $field['#entity_type'];
    $bundle = $field['#bundle'];    
  }

  $dependencies = conditional_fields_load_dependencies($entity_type, $bundle);

  if (!$dependencies) {
    return $element;
  }

  // Attach dependent.
  if (isset($dependencies['dependents'][$field['#field_name']])) {
    foreach ($dependencies['dependents'][$field['#field_name']] as $id => $dependency) {
      if (!isset($form_state['complete form']['#conditional_fields'][$field['#field_name']]['dependees'][$id])) {
        conditional_fields_attach_dependency($form_state['complete form'], array('#field_name' => $dependency['dependee']), $field, $dependency['options'], $id);
      }
    }
  }

  // Attach dependee.
  if (isset($dependencies['dependees'][$field['#field_name']])) {
    foreach ($dependencies['dependees'][$field['#field_name']] as $id => $dependency) {
      if (!isset($form_state['complete form']['#conditional_fields'][$field['#field_name']]['dependents'][$id])) {
        conditional_fields_attach_dependency($form_state['complete form'], $field, array('#field_name' => $dependency['dependent']), $dependency['options'], $id);
      }
    }
  }

  return $element;
}

/**
 * Attaches a single dependency to a form.
 *
 * Call this function when defining or altering a form to create dependencies
 * dynamically.
 *
 * @param $form
 *   The form where the dependency is attached.
 * @param $dependee
 *   The dependee field form element. Either a string identifying the element
 *   key in the form, or a fully built field array. Actually used properties of
 *   the array are #field_name and #parents.
 * @param $dependent
 *   The dependent field form element. Either a string identifying the element
 *   key in the form, or a fully built field array. Actually used properties of
 *   the array are #field_name and #parents.
 * @param $options
 *   An array of dependency options with the following key/value pairs:
 *   - state: The state applied to the dependent when the dependency is
 *     triggered. See conditional_fields_states() for available states.
 *   - condition: The condition for the dependency to be triggered. See
 *     conditional_fields_conditions() for available conditions.
 *   - values_set: One of the following constants:
 *     - CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET: Dependency is
 *       triggered if the dependee has a certain value defined in 'value'.
 *     - CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND: Dependency is triggered if
 *       the dependee has all the values defined in 'values'.
 *     - CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR: Dependency is triggered if the
 *       dependee has any of the values defined in 'values'.
 *     - CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR: Dependency is triggered if
 *       the dependee has only one of the values defined in 'values'.
 *     - CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT: Dependency is triggered if
 *       the dependee does not have any of the values defined in 'values'.
 *   - value: The value to be tested when 'values_set' is
 *     CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET. An associative array with
 *     the same structure of the dependee field values as found in
 *     $form_states['values] when the form is submitted. You can use
 *     field_default_extract_form_values() to extract this array.
 *   - values: The array of values to be tested when 'values_set' is not
 *     CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET.
 *   - value_form: An associative array with the same structure of the dependee
 *     field values as found in $form_state['input']['value']['field'] when the
 *     form is submitted.
 *   - effect: The jQuery effect associated to the state change. See
 *     conditional_fields_effects() for available effects and options.
 *   - effect_options: The options for the active effect.
 *   - element_view: An associative array of field view behaviors with 
 *     CONDITIONAL_FIELDS_FIELD_VIEW_* constants as keys and the same constants
 *     as values for enabled behaviors and 0 for disabled behaviors.
 *     See conditional_fields_behaviors() for descriptions.
 *   - element_view_per_role: Set to 1 to activate field view settings per role.
 *   - element_view_roles: An associative array of field view settings per role
 *     where the keys are role ids and the values are arrays with the same
 *     structure of 'element_view'.
 *   - element_edit: An associative array of field edit behaviors with 
 *     CONDITIONAL_FIELDS_FIELD_EDIT_* constants as keys and the same constants
 *     as values for enabled behaviors and 0 for disabled behaviors.
 *     See conditional_fields_behaviors() for descriptions.
 *   - element_edit_per_role: Set to 1 to activate field edit settings per role.
 *   - element_edit_roles: An associative array of field edit settings per role
 *     where the keys are role ids and the values are arrays with the same
 *     structure of 'element_edit'.
 *   - selector: (optional) Custom jQuery selector for the dependee.
 * @param $id
 *   (internal use) The identifier for the dependency. Omit this parameter when
 *   attaching a custom dependency.
 *
 *   Note that you don't need to manually set all these options, since default
 *   settings are always provided.
 */
function conditional_fields_attach_dependency(&$form, $dependee, $dependent, $options, $id = 0) {
  $options += conditional_fields_dependency_default_options();

  // The absence of the $id parameter identifies a custom dependency.
  if (!$id) {
    // String values are accepted to simplify usage of this function with custom
    // forms.
    if (is_string($dependee) && is_string($dependent)) {
      $dependee = array(
        '#field_name' => $dependee,
        '#parents'    => array($dependee),
      );
      $dependent = array(
        '#field_name' => $dependent,
        '#parents'    => array($dependent),
      );

      // Custom dependencies have automatically assigned a progressive id.
      static $current_id;
      if (!$current_id) {
        $current_id = 1;
      }
      $id = $current_id;
      $current_id++;
    }
  }

  // Attach dependee.
  if (isset($dependee['#parents'])) {
    $form['#conditional_fields'][$dependee['#field_name']]['parents'] = $dependee['#parents'];
    $form['#conditional_fields'][$dependee['#field_name']]['dependents'][$id] = array(
      'dependent' => $dependent['#field_name'],
      'options'   => $options,
    );
  }

  // Attach dependent.
  if (isset($dependent['#parents'])) {
    $form['#conditional_fields'][$dependent['#field_name']]['parents'] = $dependent['#parents'];
    $form['#conditional_fields'][$dependent['#field_name']]['dependees'][$id] = array(
      'dependee' => $dependee['#field_name'],
      'options'  => $options,
    );
  }

  // Actual processing is done in conditional_fields_form_after_build().
  _conditional_fields_element_add_property($form, '#after_build', 'conditional_fields_form_after_build');
}

/**
 * Builds and attaches #states properties to dependent fields, processes
 * required dependent fields and adds additional visual effects handling to
 * the States API.
 */
function conditional_fields_form_after_build($form, $form_state) {
  // Dependencies data is attached in conditional_fields_element_after_build().
  if (!isset($form['#conditional_fields'])) {
    return $form;
  }

  $state_handlers = conditional_fields_states_handlers();

  // Cycle all dependents.
  foreach ($form['#conditional_fields'] as $dependent => $dependent_info) {
    $states = array();

    if (!isset($dependent_info['dependees'])) {
      continue;
    }

    $dependent_form_field = drupal_array_get_nested_value($form, $dependent_info['parents']);

    foreach ($dependent_info['dependees'] as $dependency) {

      $dependee = $dependency['dependee'];
      
      if (!isset($form['#conditional_fields'][$dependee])) {
        continue;
      }
      
      $dependee_info = $form['#conditional_fields'][$dependee];
      $dependee_form_field = drupal_array_get_nested_value($form, $dependee_info['parents']);
      $options = $dependency['options'];

      // Load field edit behaviors.
      // If this dependent has multiple dependees, only the logic of the first
      // dependency will be taken into account.
      if (!isset($behaviors)) {
        $behaviors = conditional_fields_field_behaviors('edit', $options);
      }

      // Determine if the dependee is in the form.
      if (!$dependee_form_field) {
        // Apply orphan dependent behaviors.
        if (in_array(CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_UNTRIGGERED_ORPHAN, $behaviors)) {
          // TODO
          /*
          $is_triggered = TRUE;

          if ($is_orphan && !$is_triggered) {
            $form[$dependent]['#access'] = FALSE;
          }
          */
        }
        if (in_array(CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_ORPHAN, $behaviors)) {
          $dependent_form_field['#access'] = FALSE;
        }
        continue;
      }

      unset($behaviors[CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_ORPHAN]);
      unset($behaviors[CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_UNTRIGGERED_ORPHAN]);

      // Build a jQuery selector if it was not overridden by a custom value.
      if (!$options['selector']) {
        $options['selector'] = conditional_fields_field_selector($dependee_form_field);
      }
      else {
        // Replace the language placeholder in the selector with current language.
        $options['selector'] = str_replace('%lang', $dependee_form_field['#language'], $options['selector']);
      }

      if ($options['condition'] != 'value') {
        // Conditions different than "value" are always evaluated against TRUE.
        $state = array($options['state'] => array($options['selector'] => array($options['condition'] => TRUE)));
      }
      else {
        // Build the values that trigger the dependency.
        $values = array();

        if ($options['values_set'] == CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET) {
          $values[$options['condition']] = $options['value_form'];
        }
        elseif ($options['values_set'] == CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND) {
          $values[$options['condition']] = count($options['values']) == 1 ? $options['values'][0] : $options['values'];
        }
        else {
          if ($options['values_set'] == CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR) {
            // XOR behaves like OR with added 'xor' element.
            $values[] = 'xor';
          }
          elseif ($options['values_set'] == CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT) {
            // NOT behaves like OR with switched state.
            $options['state'] = strpos($options['state'], '!') === 0 ? drupal_substr($options['state'], 1) : '!' . $options['state'];
          }

          // OR, NOT and XOR conditions are obtained with a nested array.
          foreach ($options['values'] as $value) {
            $values[] = array($options['condition'] => $value);
          }
        }

        $state = array($options['state'] => array($options['selector'] => $values));

        $dependee_form_state = isset($dependee_form_field['#field_parents'], $dependee_form_field['#field_name'], $dependee_form_field['#language']) ? field_form_get_state($dependee_form_field['#field_parents'], $dependee_form_field['#field_name'], $dependee_form_field['#language'], $form_state) : NULL;

        // Execute special handler for fields that need further processing.
        // The handler has no return value. Modify the $state parameter by
        // reference if needed.
        foreach ($state_handlers as $handler => $handler_conditions) {
          if (array_intersect_assoc($handler_conditions, $dependee_form_field) == $handler_conditions) {
            $handler($dependee_form_field, $dependee_form_state, $options, $state);
          }
        }

        // Manage required dependent fields.
        // Validation of required fields is hard coded in _form_validate(), so we
        // must mark these fields as not required if we want to do a conditional
        // validation. When the form is submitted, we will check if the dependency
        // was triggered and only then revalidate the field with the #required
        // property restored.
        if (isset($dependent_form_field['#required']) && $dependent_form_field['#required'] == TRUE) {
          $dependent_form_field['#required'] = FALSE;

          // Add validation callback to element.
          _conditional_fields_element_add_property($dependent_form_field, '#element_validate', 'conditional_fields_required_dependent_validate');

          // Add pre render callback to element.
          _conditional_fields_element_add_property($dependent_form_field, '#pre_render', 'conditional_fields_required_dependent_pre_render');
        }

        // Apply reset dependent to default if untriggered behavior.
        if (in_array(CONDITIONAL_FIELDS_FIELD_EDIT_RESET_UNTRIGGERED, $behaviors)) {
          // Add validation callback to element.
          _conditional_fields_element_add_property($dependent_form_field, '#element_validate', 'conditional_fields_behaviors_reset_default_validate');

          unset($behaviors[CONDITIONAL_FIELDS_FIELD_EDIT_RESET_UNTRIGGERED]);
        }
      }

      // Merge this dependency with the states array.
      $states = array_merge_recursive($states, $state);

      // Add effect handler for effects with options.
      if ($options['effect'] && $options['effect'] != 'show') {
        drupal_add_js(drupal_get_path('module', 'conditional_fields') . '/js/conditional_fields.effect.js');

        $selector = conditional_fields_field_selector(drupal_array_get_nested_value($form, array($dependent_info['parents'][0])));
        drupal_add_js(array('conditionalFields' => array('effects' => array($selector => array('effect' => $options['effect'], 'options' => $options['effect_options'])))), 'setting');
      }
    }

    // Execute custom behaviors.
    if (!empty($behaviors)) {
      foreach ($behaviors as $behavior) {
        // Custom behaviors are callbacks.
        $$behavior('edit', $form, $form_state, $dependent, $dependencies);
      }
    }

    unset($behaviors);

    // Save the modified field back into the form.
    drupal_array_set_nested_value($form, $dependent_info['parents'], $dependent_form_field);

    // Add the #states property to the dependent field's outermost container.
    $dependent_parents = isset($dependent_form_field['#field_parents']) ? $dependent_form_field['#field_parents'] : array();
    $dependent_container = array_slice($dependent_info['parents'], 0, count($dependent_parents) + 1);
    drupal_array_set_nested_value($form, array_merge($dependent_container, array('#states')), $states);
  }

  return $form;
}

/**
 * Helper function to add a property/value pair to a form element safely.
 * The property is assumed to be an array and the value will be prepended to any
 * pre-existing value for that property.
 */
function _conditional_fields_element_add_property(&$element, $property, $value) {
  if (isset($element[$property])) {
    if (in_array($value, $element[$property])) {
      return;      
    }
    $element[$property] = array_merge((array) $value, $element[$property]);
  }
  else {
    // Avoid overriding default element properties that might not yet be set.
    $property_default = isset($element['#type']) ? element_info_property($element['#type'], $property, array()) : array();
    $element[$property] = array_merge((array) $value, $property_default);
  }
}

/**
 * Validate conditionally required dependent fields.
 */
function conditional_fields_required_dependent_validate($elements, $form_state, $form) {
  // Check if field was left empty.
  $is_empty_multiple = (!count($elements['#value']));
  $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0);
  $is_empty_value = ($elements['#value'] === 0);
  $is_empty_single_select = ($elements['#type'] == 'select' && $elements['#multiple'] == FALSE && $elements['#value'] == '_none');
  if (!($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_single_select)) {
    return;
  }

  // Check if the dependency was triggered.
  $is_triggered = conditional_fields_evaluate_dependencies($elements['#field_name'], $form, $form_state);

  if ($is_triggered) {
    if (isset($elements['#title'])) {
      form_error($elements, t('!name field is required.', array('!name' => $elements['#title'])));
    }
    else {
      form_error($elements);
    }
  }
}

/**
 * Validate dependent fields that should be reset to their default values if the
 * dependency was not triggered.
 *
 * Works only with Lists and Text, since there is no API to convert a field's
 * data from form to storage format.
 */
function conditional_fields_behaviors_reset_default_validate(&$elements, &$form_state, $form) {
  $modules = array('text', 'number', 'options');
  $instance = field_widget_instance($elements, $form_state);
  if (!in_array($instance['widget']['module'], $modules)) {
    return;
  }

  // Check if the dependency was triggered.
  $is_triggered = conditional_fields_evaluate_dependencies($elements['#field_name'], $form, $form_state);

  if (!$is_triggered) {
    $default_value = field_get_default_value($instance['entity_type'], NULL, $elements, $instance, $elements['#language']);

    switch ($instance['widget']['module']) {
      case 'text':
      case 'number':
        $form_state['values'][$instance['field_name']][$elements['#language']] = $default_value;
        break;
      case 'options':
        $elements['#value'] = $form_state['values'][$instance['field_name']][$elements['#language']] = $default_value[0]['value'];
        break;
    }
  }
}

/**
 * Marks conditionally required dependents as required for themeing.
 */
function conditional_fields_required_dependent_pre_render($elements) {
  // Mark as required here, so it will be themed accordingly.
  // Does not affect validation.
  $elements['#required'] = TRUE;

  return $elements;
}

/**
 * Implements hook_entity_view_alter().
 *
 * Applies entity view logic to conditional fields.
 */
function conditional_fields_entity_view_alter(&$build, $type) {
  if (!$dependencies = conditional_fields_load_dependencies($build['#entity_type'], $build['#bundle'])) {
    return;
  }

  $evaluated_dependents = array();

  foreach ($dependencies['dependents'] as $dependent => $dependency) {
    if (!(isset($build[$dependent]) && $build[$dependent]['#access'])) {
      continue;
    }

    foreach ($dependency as $dependency_options) {
      $dependee = $dependency_options['dependee'];
      $options = $dependency_options['options'];

      // We can interface with the States API only through the Value condition.
      if ($options['condition'] != 'value') {
        continue;
      }

      // Evaluate dependency.
      // TODO: grouping of dependencies to allow OR. For now, the dependent is "triggered"
      // only if all its dependencies evaluate to TRUE,

      // This case is handled by the CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_ORPHAN behavior.
      if (!isset($build[$dependee])) {
        $evaluated_dependents[$dependent]['AND'][] = TRUE;
      }
      else {
        $items = $build[$dependee]['#items'];
        $evaluated_dependents[$dependent]['AND'][] = conditional_fields_evaluate_dependency('view', $items, $options);
      }

      // Determine field view behaviors.
      // If this dependent has multiple dependencies, only the logic of the
      // first dependency will be taken into account.
      if (!isset($behaviors)) {
        $behaviors = conditional_fields_field_behaviors('view', $options);
      }
    }

    if (isset($evaluated_dependents[$dependent])) {
      $is_triggered = in_array(FALSE, $evaluated_dependents[$dependent]['AND']) ? FALSE : TRUE;

      foreach ($behaviors as $behavior) {
        switch ($behavior) {
          case CONDITIONAL_FIELDS_FIELD_VIEW_EVALUATE:
            // Hide the dependent if it is not triggered.
            if (!$is_triggered) {
              $build[$dependent]['#access'] = FALSE;
            }
            break;

          case CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_ORPHAN:
            // Hide the dependent if the dependee is not viewable.
            if (!isset($build[$dependee]) || $build[$dependee]['#access'] == FALSE) {
              $build[$dependent]['#access'] = FALSE;
            }
            break;

          case CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_UNTRIGGERED_ORPHAN:
            // Hide the dependent if the dependee is not viewable and the dependency is not triggered.
            if (!$is_triggered && (!isset($build[$dependee]) || $build[$dependee]['#access'] == FALSE)) {
              $build[$dependent]['#access'] = FALSE;
            }
            break;

          case CONDITIONAL_FIELDS_FIELD_VIEW_HIGHLIGHT:
            // Show the dependent themed like an error message if it is not triggered.
            if (!$is_triggered) {
              $build[$dependent]['#prefix'] = isset($build[$dependent]['#prefix']) ? '<div class="messages error">' . $build[$dependent]['#prefix'] : '<div class="messages error">';
              $build[$dependent]['#suffix'] = isset($build[$dependent]['#suffix']) ? $build[$dependent]['#suffix'] . '</div>' : '</div>';
            }
            break;

          case CONDITIONAL_FIELDS_FIELD_VIEW_DESCRIBE:
            // Show a textual description of the dependency under the dependent field.
            if ($build[$dependent]['#access']) {
              $dependee_title = isset($build[$dependee]['#title']) ? $build[$dependee]['#title'] : $dependee;
              $dependent_title = isset($build[$dependent]['#title']) ? $build[$dependent]['#title'] : $dependent;
              $description = conditional_fields_dependency_description($dependee_title, $dependent_title, $options);
              if (isset($build[$dependent]['#suffix'])) {
                $description = $build[$dependent]['#suffix'] . $description;
              }
              $build[$dependent]['#suffix'] = $description;
            }
            break;

          default:
            // Custom behaviors are callbacks.
            $$behavior('view', $dependee, $dependent, $is_triggered, $dependencies, $build, $type);
            break;
        }
      }
    }

    unset($behaviors);
  }
}

/**
 * Evaluate a set of dependencies for a dependent field.
 */
function conditional_fields_evaluate_dependencies($dependent, $form, $form_state) {
  $dependencies = $form['#conditional_fields'][$dependent]['dependees'];
  $evaluated_dependees = array();

  foreach ($dependencies as $dependency) {
    $values = drupal_array_get_nested_value($form_state['values'], $form['#conditional_fields'][$dependency['dependee']]['parents']);

    // TODO: OR/XOR grouping of dependencies.
    $evaluated_dependees[$dependent]['AND'][] = conditional_fields_evaluate_dependency('edit', $values, $dependency['options']);
  }

  return in_array(FALSE, $evaluated_dependees[$dependent]['AND']) ? FALSE : TRUE;
}

/**
 * Evaluate if a dependency meets the requirements to be triggered.
 *
 * @param $context
 *   'edit' if $values are extracted from $form_state or 'view' if
 *   $values are extracted from an entity.
 */
function conditional_fields_evaluate_dependency($context, $values, $options) {
  if ($options['values_set'] == CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET) {
    $dependency_values = $context == 'view' ? $options['value'] : $options['value_form'];

    // Simple case: both values are strings or integers. Should never happen in
    // view context, but does no harm to check anyway.
    if (!is_array($values)) {
      // Options elements consider "_none" value same as empty.
      $values = $values === '_none' ? '' : $values;

      if (!is_array($dependency_values)) {
        // Some widgets store integers, but values saved in $dependency_values
        // are always strings. Convert them to integers because we want to do a
        // strict equality check to differentiate empty strings from '0'.
        if (is_int($values) && is_numeric($dependency_values)) {
          $dependency_values = (int) $dependency_values;
        }

        return $dependency_values === $values;
      }

      // If $values is a string and $dependency_values an array, convert $values
      // to the standard field array form format. This covers cases like single
      // value textfields.
      $values = array(array('value' => $values));
    }

    // If we are in form context, we are done here.
    if ($context == 'edit') {
      // Compare arrays recursively ignoring keys, since multiple select widgets
      // values have numeric keys in form format and string keys in storage
      // format.
      return array_values($dependency_values) == array_values($values);
    }

    // $values, when viewing fields, may contain all sort of additional
    // information, so filter out from $values the keys that are not present in
    // $dependency_values.
    // Values here are alway keyed by delta (regardless of multiple value
    // settings).
    foreach ($values as $delta => &$value) {
      if (isset($dependency_values[$delta])) {
        $value = array_intersect_key($value, $dependency_values[$delta]);

        foreach ($value as $key => &$element_value) {
          if (isset($dependency_values[$delta][$key]) && is_int($dependency_values[$delta][$key]) && is_numeric($element_value)) {
            $element_value = (int) $element_value;
          }
        }
      }
    }

    // Compare values.
    foreach ($dependency_values as $delta => $dependency_value) {
      if (!isset($values[$delta])) {
        return FALSE;
      }
      foreach ($dependency_value as $key => $dependency_element_value) {
        // Ignore keys set in the field and not in the dependency.
        if (isset($values[$delta][$key]) && $values[$delta][$key] !== $dependency_element_value) {
          return FALSE;          
        }
      }
    }

    return TRUE;
  }

  // Flatten array of values.
  $reference_values = array();
  foreach ((array) $values as $value) {
    // TODO: support multiple values.
    $reference_values[] = is_array($value) ? array_shift($value) : $value;
  }

  switch ($options['values_set']) {
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
      $diff = array_diff($options['values'], $reference_values);
      return empty($diff);

    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
      $intersect = array_intersect($options['values'], $reference_values);
      return !empty($intersect);

    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
      $intersect = array_intersect($options['values'], $reference_values);
      return count($intersect) == 1;

    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
      $intersect = array_intersect($options['values'], $reference_values);
      return empty($intersect);
  }
}

/**
 * Determine which dependency behaviors should be used in forms and content
 * display, depending on dependency options and user roles.
 *
 * @param $op
 *   'view' or 'edit'.
 * @param $options
 *    Dependency options.
 *
 * @return
 *   An array of behaviors.
 *
 */
function conditional_fields_field_behaviors($op, $options) {
  global $user;

  if ($options['element_' . $op . '_per_role']) {
    foreach ($options['element_' . $op . '_roles'] as $rid => $role_behaviors) {
      if (isset($user->roles[$rid])) {
        $behaviors = $role_behaviors;
        break;
      }
    }
  }
  else {
    $behaviors = $options['element_' . $op];
  }

  // Filter out inactive behaviors.
  $behaviors = array_filter($behaviors);

  return $behaviors;
}

/**
 * Builds a jQuery selector from the name attribute of a field.
 */
function conditional_fields_field_selector($field) {
  if (isset($field['#attributes']['name'])) {
    return '[name="' . $field['#attributes']['name'] . '"]';
  }

  if (isset($field['#name'])) {
    return '[name="' . $field['#name'] . '"]';
  }

  // Try with id if name is not found.
  if (isset($field['#attributes']['id'])) {
    return '#' . $field['#attributes']['id'];
  }

  if (isset($field['#id'])) {
    return '#' . $field['#id'];
  }

  return FALSE;
}

/**
 * Provides default options for a dependency.
 *
 * For an explanation of available options,
 * @see conditional_fields_field_attach_dependency()
 */
function conditional_fields_dependency_default_options() {
  return array(
    'state'                 => 'visible',
    'condition'             => 'value',
    'values_set'            => CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET,
    'value'                 => array(),
    'values'                => array(),
    'value_form'            => array(),
    // TODO 'grouping'      => CONDITIONAL_FIELDS_GROUPING_AND,
    'effect'                => 'show',
    'effect_options'        => array(),
    'element_view'          => array(
      CONDITIONAL_FIELDS_FIELD_VIEW_EVALUATE => CONDITIONAL_FIELDS_FIELD_VIEW_EVALUATE,
      CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_ORPHAN => CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_ORPHAN,
      CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_UNTRIGGERED_ORPHAN => 0,
      CONDITIONAL_FIELDS_FIELD_VIEW_HIGHLIGHT => 0,
      CONDITIONAL_FIELDS_FIELD_VIEW_DESCRIBE => 0,
    ),
    'element_view_per_role' => 0,
    'element_view_roles'    => array(),
    'element_edit'          => array(
      CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_ORPHAN => CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_ORPHAN,
      CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_UNTRIGGERED_ORPHAN => 0,
      CONDITIONAL_FIELDS_FIELD_EDIT_RESET_UNTRIGGERED => 0,
    ),
    'element_edit_per_role' => 0,
    'element_edit_roles'    => array(),
    'selector'              => '',
  );
}

/**
 * Loads all dependencies from the database.
 *
 * The result can be filtered by providing an entity type and a bundle name.
 */
function conditional_fields_load_dependencies($entity_type = NULL, $bundle = NULL) {
  // Use the advanced drupal_static() pattern.
  static $dependencies;
  if (!isset($dependencies)) {
    $dependencies = &drupal_static(__FUNCTION__);
  }

  if (!$dependencies) {
    $dependencies = array();
  }

  if (!isset($dependencies[$entity_type][$bundle])) {
    $dependencies[$entity_type][$bundle] = array();
    $default_options = conditional_fields_dependency_default_options();

    $select = db_select('conditional_fields', 'cf')
      ->fields('cf', array('id', 'options'))
      ->orderBy('cf.dependent');
    $fci_depende = $select->join('field_config_instance', 'fci_dependee', 'cf.dependee = fci_dependee.id');
    $fci_dependent = $select->join('field_config_instance', 'fci_dependent', 'cf.dependent = fci_dependent.id');
    $select->addField($fci_depende, 'field_name', 'dependee');
    $select->addField($fci_dependent, 'field_name', 'dependent');
    $select->addField($fci_depende, 'entity_type');
    $select->addField($fci_depende, 'bundle');

    if ($entity_type) {
      $select->condition(
        db_and()
          ->condition('fci_dependee.entity_type', $entity_type)
          ->condition('fci_dependent.entity_type', $entity_type)
      );
    }

    if ($bundle) {
      $select->condition(
        db_and()
          ->condition('fci_dependee.bundle', $bundle)
          ->condition('fci_dependent.bundle', $bundle)
      );
    }

    $result = $select->execute();

    foreach ($result as $dependency) {
      $result_entity_type = $entity_type ? $entity_type : $dependency->entity_type;
      $result_bundle = $bundle ? $bundle : $dependency->bundle;

      $options = unserialize($dependency->options);
      $options += $default_options;

      $dependencies[$result_entity_type][$result_bundle]['dependents'][$dependency->dependent][$dependency->id] = array(
        'dependee' => $dependency->dependee,
        'options'  => $options,
      );

      $dependencies[$result_entity_type][$result_bundle]['dependees'][$dependency->dependee][$dependency->id] = array(
        'dependent' => $dependency->dependent,
        'options'  => $options,
      );
    }
  }

  if ($entity_type && isset($dependencies[$entity_type])) {
    if ($bundle && isset($dependencies[$entity_type][$bundle])) {
      return $dependencies[$entity_type][$bundle];
    }

    return $dependencies[$entity_type];
  }

  if (!empty($dependencies)) {
    return $dependencies;
  }

  return FALSE;
}

/**
 * Loads a dependency from the database by id.
 */
function conditional_fields_dependency_load_by_id($id) {
  $result = db_select('conditional_fields', 'cf')
    ->fields('cf', array('id', 'dependee', 'dependent', 'options'))
    ->condition('id', $id)
    ->execute()
    ->fetchAssoc();

  if (isset($result['options'])) {
    $options = unserialize($result['options']);
    $options += conditional_fields_dependency_default_options();
    $result['options'] = $options;
  }

  return $result;
}

/**
 * Inserts a new dependency in the database.
 * For the format of $options,
 * @see conditional_fields_dependency_default_options()
 */
function conditional_fields_dependency_insert($dependee_id, $dependent_id, $options = array()) {
  $options += conditional_fields_dependency_default_options();
  $dependency = array(
    'dependee'  => $dependee_id,
    'dependent' => $dependent_id,
    'options'   => $options,
  );

  if (drupal_write_record('conditional_fields', $dependency)) {
    return $dependency['id'];
  }
}

/**
 * Updates a dependency.
 */
function conditional_fields_dependency_update($dependency) {
  drupal_write_record('conditional_fields', $dependency, 'id');
}

/**
 * Deletes dependencies.
 */
function conditional_fields_dependency_delete($dependency_ids) {
  $or = db_or();
  foreach ($dependency_ids as $id) {
    $or = $or->condition('id', $id);
  }

  return db_delete('conditional_fields')
    ->condition($or)
    ->execute();
}

/**
 * Implements hook_field_delete_instance().
 *
 * Delete any dependency associated with the deleted instance.
 */
function conditional_fields_field_delete_instance($instance) {
  db_delete('conditional_fields')
    ->condition(
      db_or()
        ->condition('dependee', $instance['id'])
        ->condition('dependent', $instance['id']))
    ->execute();
}

/**
 * Implements hook_theme().
 */
function conditional_fields_theme() {
  return array(
    'conditional_fields_table' => array(
      'render element' => 'elements',
    ),
  );
}

/**
 * Implements hook_element_info().
 */
function conditional_fields_element_info() {
  return array(
    'conditional_fields_table' => array(
      '#theme' => 'conditional_fields_table',
    ),
  );
}

/**
 * Builds a list of supported states that may be applied to a dependent field.
 */
function conditional_fields_states() {
  $states = array(
    // Supported by States API
    'visible'    => t('Visible'),
    '!visible'   => t('Invisible'),
    '!empty'     => t('Filled with a value'),
    'empty'      => t('Emptied'),
    '!disabled'  => t('Enabled'),
    'disabled'   => t('Disabled'),
    'checked'    => t('Checked'),
    '!checked'   => t('Unchecked'),
    'required'   => t('Required'),
    '!required'  => t('Optional'),
    '!collapsed' => t('Expanded'),
    'collapsed'  => t('Collapsed'),
    // Supported by Conditional Fields
    'unchanged'  => t('Unchanged (no state)'),
    // TODO: Add support to these states:
    /*
    'relevant'   => t('Relevant'),
    '!relevant'  => t('Irrelevant'),
    'valid'      => t('Valid'),
    '!valid'     => t('Invalid'),
    'touched'    => t('Touched'),
    '!touched'   => t('Untouched'),
    '!readonly'  => t('Read/Write'),
    'readonly'   => t('Read Only'),
    */
  );

  // Allow other modules to modify the states
  drupal_alter('conditional_fields_states', $states);

  return $states;
}

/**
 * Builds a list of supported effects that may be applied to a dependent field
 * when it changes from visible to invisible and viceversa. The effects may
 * have options that will be passed as Javascript settings and used by
 * conditional_fields.effect.js.
 *
 * @return
 *   An associative array of effects. Each key is an unique name for the effect.
 *   The value is an associative array:
 *   - label: The human readable name of the effect.
 *   - states: The states that can be associated with this effect.
 *   - options: An associative array of effect options names, field types,
 *     descriptions and default values.
 */
function conditional_fields_effects() {
  $effects = array(
    'show' => array(
      'label' => t('Show/Hide'),
      'states' => array('visible', '!visible'),
    ),
    'fade' => array(
      'label' => t('Fade in/Fade out'),
      'states' => array('visible', '!visible'),
      'options' => array(
        'speed' => array(
          '#type' => 'textfield',
          '#description' => t('The speed at which the animation is performed, in milliseconds.'),
          '#default_value' => 400,
        ),
      ),
    ),
    'slide' => array(
      'label' => t('Slide down/Slide up'),
      'states' => array('visible', '!visible'),
      'options' => array(
        'speed' => array(
          '#type' => 'textfield',
          '#description' => t('The speed at which the animation is performed, in milliseconds.'),
          '#default_value' => 400,
        ),
      ),
    ),
    'fill' => array(
      'label' => t('Fill field with a value'),
      'states' => array('!empty'),
      'options' => array(
        'value' => array(
          '#type' => 'textfield',
          '#description' => t('The value that should be given to the field when automatically filled.'),
          '#default_value' => '',
        ),
        'reset' => array(
          '#type' => 'checkbox',
          '#title' => t('Restore previous value when untriggered'),
          '#default_value' => 1,
        ),
      ),
    ),
    'empty' => array(
      'label' => t('Empty field'),
      'states' => array('empty'),
      'options' => array(
        'value' => array(
          '#type' => 'hidden',
          '#description' => t('The value that should be given to the field when automatically emptied.'),
          '#value' => '',
          '#default_value' => '',
        ),
        'reset' => array(
          '#type' => 'checkbox',
          '#title' => t('Restore previous value when untriggered'),
          '#default_value' => 1,
        ),
      ),
    ),
  );

  // Allow other modules to modify the effects.
  drupal_alter('conditional_fields_effects', $effects);

  return $effects;
}

/**
 * List of states of a dependee field that may be used to evaluate a condition.
 */
function conditional_fields_conditions($checkboxes = TRUE) {
  // Supported by States API
  $conditions = array(
    '!empty'     => t('Filled'),
    'empty'      => t('Empty'),
    'touched'    => t('Touched'),
    '!touched'   => t('Untouched'),
    'focused'    => t('Focused'),
    '!focused'   => t('Unfocused'),
  );

  if ($checkboxes) {
    // Relevant only if dependee is a list of checkboxes
    $conditions['checked'] = t('Checked');
    $conditions['!checked'] = t('Unchecked');
  }

  $conditions['value'] = t('Value');

  // TODO: Add support from Conditional Fields to these conditions
  /*
  '!disabled'  => t('Enabled'),
  'disabled'   => t('Disabled'),
  'required'   => t('Required'),
  '!required'  => t('Optional'),
  'relevant'   => t('Relevant'),
  '!relevant'  => t('Irrelevant'),
  'valid'      => t('Valid'),
  '!valid'     => t('Invalid'),
  '!readonly'  => t('Read/Write'),
  'readonly'   => t('Read Only'),
  '!collapsed' => t('Expanded'),
  'collapsed'  => t('Collapsed'),
  */

  // Allow other modules to modify the conditions
  drupal_alter('conditional_fields_conditions', $conditions);

  return $conditions;
}

/**
 * List of behaviors that can be applied when editing forms and viewing content
 * with dependencies.
 */
function conditional_fields_behaviors($op = NULL) {
  $behaviors = array(
    'edit' => array(
      CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_ORPHAN => t('Hide the dependent if the dependee is not in the form'),
      CONDITIONAL_FIELDS_FIELD_EDIT_RESET_UNTRIGGERED => t('Reset the dependent to its default values if the dependency is not triggered.') . '<br /><em>' . t('Note: works only with Text and Options widgets. Enabling this options with other types of widgets could lead to unexpected results.') . '</em>',
      // TODO: Implement. Settings are imported from D6 though, they just do nothing for now.
      CONDITIONAL_FIELDS_FIELD_EDIT_HIDE_UNTRIGGERED_ORPHAN => t('Hide the dependent if the dependee is not in the form and the dependency is not triggered. (Note: not implemented)'),
    ),
    'view' => array(
      CONDITIONAL_FIELDS_FIELD_VIEW_EVALUATE => t('Hide the dependent if the dependency is not triggered'),
      CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_ORPHAN => t('Hide the dependent if the dependee is not viewable by the user'),
      CONDITIONAL_FIELDS_FIELD_VIEW_HIDE_UNTRIGGERED_ORPHAN => t('Hide the dependent if the dependee is not viewable by the user and the dependency is not triggered'),
      CONDITIONAL_FIELDS_FIELD_VIEW_HIGHLIGHT => t('Theme the dependent like an error message if the dependency is not triggered'),
      CONDITIONAL_FIELDS_FIELD_VIEW_DESCRIBE => t('Show a textual description of the dependency under the dependent'),
    ),
  );

  // Allow other modules to modify the options.
  drupal_alter('conditional_fields_behaviors', $op, $behaviors);

  if (isset($behaviors[$op])) {
    return $behaviors[$op];
  }

  return $behaviors;
}

/**
 * Builds a list of special fields handlers to be executed when building the
 * #states array. The keys are handler function names and the key/value pairs
 * are field properties and their values that trigger the execution of the handler.
 *
 * The handlers themselves must accept the parameters $field, $field_info,
 * $options and $state.
 *
 * @see conditional_fields_field_attach_form()
 * @see conditional_fields_states_handler_select()
 */
function conditional_fields_states_handlers() {
  $handlers = array(
    'conditional_fields_states_handler_select' => array(
      '#type' => 'select',
      '#multiple' => FALSE,
    ),
    'conditional_fields_states_handler_checkbox' => array(
      '#type' => 'checkbox',
    ),
    'conditional_fields_states_handler_checkboxes' => array(
      '#type' => 'checkboxes',
    ),
    'conditional_fields_states_handler_text' => array(
      '#type' => 'textfield',
    ),
    'conditional_fields_states_handler_textarea' => array(
      '#type' => 'textarea',
    ),
    'conditional_fields_states_handler_date_combo' => array(
      '#type' => 'date_combo',
    ),
    'conditional_fields_states_handler_link_field' => array(
      '#type' => 'link_field',
    ),
    'conditional_fields_states_handler_link_addressfield' => array(
      '#addressfield' => 1,
    ),
  );

  // Allow other modules to modify the handlers
  drupal_alter('conditional_fields_states_handlers', $handlers);

  return $handlers;
}

/**
 * States handler for multiple select lists.
 *
 * Multiple select fields always require an array as value.
 * In addition, since our modified States API triggers a dependency only if all
 * reference values of type Array are selected, a different selector must be
 * added for each value of a set for OR, XOR and NOT evaluations.
 */
function conditional_fields_states_handler_select($field, $field_info, $options, &$state) {
  switch ($options['values_set']) {
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET:
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
      return;

    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
      $select_states[$options['state']][] = 'xor';

    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
      foreach ($options['values'] as $value) {
        $select_states[$options['state']][] = array(
          $options['selector'] => array(
            $options['condition'] => array($value),
          ),
        );
      }
      break;

    default:
      return;
  }

  $state = $select_states;
}

/**
 * States handler for single on/off checkbox.
 */
function conditional_fields_states_handler_checkbox($field, $field_info, $options, &$state) {
  switch ($options['values_set']) {
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET:
      $checked = $options['value'][0]['value'] == $field['#on_value'] ? TRUE : FALSE;
      break;
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
      // ANDing values of a single checkbox doesn't make sense: just use the first value.
      $checked = $options['values'][0] == $field['#on_value'] ? TRUE : FALSE;
      break;
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
      $checked = in_array($field['#on_value'], $options['values']) ? TRUE : FALSE;
      break;
  }

  $state[$options['state']][$options['selector']] = array('checked' => $checked);
}

/**
 * States handler for checkboxes.
 */
function conditional_fields_states_handler_checkboxes($field, $field_info, $options, &$state) {
  // Checkboxes are actually different form fields, so the #states property
  // has to include a state for each checkbox.
  $checkboxes_selectors = array();

  switch ($options['values_set']) {
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET:
      foreach ($options['value'] as $value) {
        $checkboxes_selectors[conditional_fields_field_selector($field[current($value)])] = array('checked' => TRUE);
      }
      break;
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
      foreach ($options['values'] as $value) {
        $checkboxes_selectors[conditional_fields_field_selector($field[$value])] = array('checked' => TRUE);
      }
      break;
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
      $checkboxes_selectors[] = 'xor';
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
      foreach ($options['values'] as $value) {
        $checkboxes_selectors[] = array(conditional_fields_field_selector($field[$value]) => array('checked' => TRUE));
      }
      break;
  }

  $state = array($options['state'] => $checkboxes_selectors);
}

/**
 * States handler for text fields.
 */
function conditional_fields_states_handler_text($field, $field_info, $options, &$state) {
  // Text fields values are keyed by cardinality, so we have to flatten them.
  // TODO: support multiple values.
  if ($options['values_set'] == CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET) {
    // Cast as array to handle the exception of autocomplete text fields.
    $value = (array) $state[$options['state']][$options['selector']]['value'][0];
    $state[$options['state']][$options['selector']]['value'] = array_shift($value);
  }
}

/**
 * States handler for text areas.
 */
function conditional_fields_states_handler_textarea($field, $field_info, $options, &$state) {
  conditional_fields_states_handler_text($field, $field_info, $options, $state);
}

/**
 * States handler for date combos.
 */
function conditional_fields_states_handler_date_combo($field, $field_info, $options, &$state) {
  // Date text.
  if ($field_info['instance']['widget']['type'] == 'date_text') {
    if ($options['values_set'] == CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET) {
      $state[$options['state']][$options['selector']]['value'] = $state[$options['state']][$options['selector']]['value'][0]['value']['date'];
    }
    return;
  }

  // Add a condition for each date part.
  $date_selectors = array();

  // Date popup.
  if ($field_info['instance']['widget']['type'] == 'date_popup') {
    $date_selectors[conditional_fields_field_selector($field['value']['date'])] = array(
      'value' => $options['value_form'][0]['value']['date'],
    );

    if ($field_info['field']['settings']['granularity']['hour'] || $field_info['field']['settings']['granularity']['minute'] || $field_info['field']['settings']['granularity']['second']) {
      $date_selectors[conditional_fields_field_selector($field['value']['time'])] = array(
        'value' => $options['value_form'][0]['value']['time'],
      );
    }
  }
  // Date select.
  else {
    foreach ($field_info['field']['settings']['granularity'] as $date_part) {
      if ($date_part) {
        $date_selectors[conditional_fields_field_selector($field['value'][$date_part])] = array(
          'value' => $options['value_form'][0]['value'][$date_part],
        );
      }
    }
  }

  $state = array($options['state'] => $date_selectors);
}

/**
 * States handler for links provided by the Link module.
 */
function conditional_fields_states_handler_link_field($field, $field_info, $options, &$state) {
  $link_selectors = array();

  // Add a condition for each link part (Title and URL)
  if ($field_info['instance']['settings']['title'] == 'optional' || $field_info['instance']['settings']['title'] == 'required') {
    $link_selectors[conditional_fields_field_selector($field['title'])] = array('value' => $options['value_form'][0]['title']);
  }
  $link_selectors[conditional_fields_field_selector($field['url'])] = array('value' => $options['value_form'][0]['url']);

  $state = array($options['state'] => $link_selectors);
}

/**
 * States handler for links provided by the Addressfield module.
 */
function conditional_fields_states_handler_link_addressfield($field, $field_info, $options, &$state) {
  if ($options['values_set'] != CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET) {
    return;
  }

  $keys = array();

  if ($field['#handlers']['address']) {
    $keys += array('country', 'thoroughfare', 'premise', 'postal_code', 'locality', 'administrative_area');
  }

  if ($field['#handlers']['organisation']) {
    $keys += array('organisation_name');
  }

  if ($field['#handlers']['name-oneline']) {
    $keys += array('name_line');
  }
  elseif ($field['#handlers']['name-full']) {
    $keys += array('first_name', 'last_name');
  }

  $addressfield_selectors = array();

  foreach ($keys as $key) {
    $addressfield_selectors[str_replace('%key', $key, $options['selector'])] = array('value' => $options['value'][0][$key]);
  }

  $state = array($options['state'] => $addressfield_selectors);
}

/**
 * Build a textual description of a dependency
 */
function conditional_fields_dependency_description($dependee_name, $dependent_name, $options) {
  $states = conditional_fields_states();

  if ($options['condition'] == 'value') {
    $values = implode(', ', $options['values']);

    switch ($options['values_set']) {
      case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET:
        if (count($options['value']) == 1) {
          return t('%dependent_name is !state when %dependee_name has value "@value".', array(
            '%dependent_name' => $dependent_name,
            '!state' => drupal_strtolower($states[$options['state']]),
            '%dependee_name' => $dependee_name,
            '@value' => current($options['value'][0]),
          ));
        }
        // "Single values" of multiple value fields like checkboxes are not
        // actually single. Such fields will be ANDed.
        $value_array = array();
        foreach ($options['value'] as $value) {
          $value_array[] = current($value);
        }
        $values = implode(', ', $value_array);
      case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
        return t('%dependent_name is !state when %dependee_name has all the values: @values.', array(
          '%dependent_name' => $dependent_name,
          '!state' => drupal_strtolower($states[$options['state']]),
          '%dependee_name' => $dependee_name,
          '@values' => $values,
        ));
        break;
      case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
        return t('%dependent_name is !state when %dependee_name has at least one of the values: @values.', array(
          '%dependent_name' => $dependent_name,
          '!state' => drupal_strtolower($states[$options['state']]),
          '%dependee_name' => $dependee_name,
          '@values' => $values,
        ));
        break;
      case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
        return t('%dependent_name is !state when %dependee_name has only one of the values: @values.', array(
          '%dependent_name' => $dependent_name,
          '!state' => drupal_strtolower($states[$options['state']]),
          '%dependee_name' => $dependee_name,
          '@values' => $values,
        ));
        break;
      case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
        return t('%dependent_name is !state when %dependee_name has none of the values: @values.', array(
          '%dependent_name' => $dependent_name,
          '!state' => drupal_strtolower($states[$options['state']]),
          '%dependee_name' => $dependee_name,
          '@values' => $values,
        ));
        break;
    }
  }
  else {
    $conditions = conditional_fields_conditions();

    return t('%dependent_name is !state when %dependee_name is !condition.', array(
      '%dependent_name' => $dependent_name,
      '!state' => drupal_strtolower($states[$options['state']]),
      '%dependee_name' => $dependee_name,
      '!condition' => drupal_strtolower($conditions[$options['condition']]),
    ));
  }
}

/**
 * Implements hook_features_api().
 */
function conditional_fields_features_api() {
  return array(
    'conditional_fields' => array(
      'name' => t('Conditional Fields'),
      'default_hook' => 'conditional_fields_default_fields',
      'default_file' => FEATURES_DEFAULTS_INCLUDED,
      'features_source' => TRUE,
      'file' => drupal_get_path('module', 'conditional_fields') . '/includes/conditional_fields.features.inc',
    ),
  );
}
