<?php
// $Id: ca.module,v 1.1.2.18 2009/11/11 21:06:48 islandusurper Exp $

/**
 * @file
 * This is a demonstration module for the new conditional actions API.
 */

/**
 * The Drupal menu path to Conditional Actions pages.
 */
define('CA_UI_PATH', 'admin/store/ca');

require_once('ca.ca.inc');

/*******************************************************************************
 * Drupal Hooks
 ******************************************************************************/

/**
 * Implementation of hook_menu().
 */
function ca_menu() {
  $items = array();

  $items[CA_UI_PATH] = array(
    'title' => 'Conditional actions',
    'description' => 'Administer the predicates setup to automate your store.',
    'page callback' => 'ca_admin',
    'access arguments' => array('administer conditional actions'),
    'file' => 'ca.admin.inc',
    'weight' => 5,
  );
  $items[CA_UI_PATH .'/overview'] = array(
    'title' => 'Overview',
    'weight' => 0,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items[CA_UI_PATH .'/overview/trigger'] = array(
    'title' => 'By trigger',
    'description' => 'Administer the predicates setup to automate your store.',
    'weight' => 0,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items[CA_UI_PATH .'/overview/class'] = array(
    'title' => 'By class',
    'description' => 'Administer the predicates setup to automate your store.',
    'page callback' => 'ca_admin',
    'page arguments' => array('class'),
    'access arguments' => array('administer conditional actions'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 5,
    'file' => 'ca.admin.inc',
  );
  $items[CA_UI_PATH .'/add'] = array(
    'title' => 'Add a predicate',
    'description' => 'Allows an administrator to create a new predicate.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_predicate_meta_form', '0'),
    'access arguments' => array('administer conditional actions'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 5,
    'file' => 'ca.admin.inc',
  );
  $items[CA_UI_PATH .'/convert'] = array(
    'title' => 'Convert configurations',
    'description' => 'Convert Workflow-ng configurations into Conditional Actions predicates.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_conversion_form'),
    'access callback' => 'ca_convert_configurations_access',
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
    'file' => 'ca.admin.inc',
  );
  $items[CA_UI_PATH .'/%/edit'] = array(
    'title' => 'Edit predicate',
    'description' => "Edit a predicate's meta data, conditions, and actions.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_predicate_meta_form', 3),
    'access arguments' => array('administer conditional actions'),
    'file' => 'ca.admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items[CA_UI_PATH .'/%/edit/meta'] = array(
    'title' => 'Meta data',
    'description' => 'Edit the meta data for a predicate like title, trigger, etc.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_predicate_meta_form', 3),
    'access arguments' => array('administer conditional actions'),
    'file' => 'ca.admin.inc',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items[CA_UI_PATH .'/%/edit/conditions'] = array(
    'title' => 'Conditions',
    'description' => 'Edit the conditions for a predicate.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_conditions_form', 3),
    'access arguments' => array('administer conditional actions'),
    'file' => 'ca.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => -5,
  );
  $items[CA_UI_PATH .'/%/edit/actions'] = array(
    'title' => 'Actions',
    'description' => 'Edit the actions for a predicate.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_actions_form', 3),
    'access arguments' => array('administer conditional actions'),
    'file' => 'ca.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 0,
  );
  $items[CA_UI_PATH .'/%/reset'] = array(
    'title' => 'Reset a predicate',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_predicate_delete_form', 3),
    'access arguments' => array('administer conditional actions'),
    'file' => 'ca.admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items[CA_UI_PATH .'/%/delete'] = array(
    'title' => 'Delete a predicate',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('ca_predicate_delete_form', 3),
    'access arguments' => array('administer conditional actions'),
    'file' => 'ca.admin.inc',
    'type' => MENU_CALLBACK,
  );

  return $items;
}

/**
 * Determine access to the configuration conversion form.
 */
function ca_convert_configurations_access() {
  return user_access('administer conditional actions') && variable_get('ca_show_conversion_tab', TRUE);
}

/**
 * Implementation of hook_perm().
 */
function ca_perm() {
  return array('administer conditional actions');
}


/*******************************************************************************
 * Module and Helper Functions
 ******************************************************************************/

/**
 * Pull a trigger and evaluate any predicates associated with that trigger.
 *
 * @param ...
 *   Accepts a variable number of arguments. The first should always be the
 *   string name of the trigger to pull with any additional arguments being
 *   the arguments expected by the trigger and used for evaluation.
 * @return
 *   TRUE or FALSE indicating if at least one predicate was evaluated.
 */
function ca_pull_trigger() {
  $args = func_get_args();
  $trigger = array_shift($args);

  // Load the data for the specified trigger.
  $trigger_data = ca_load_trigger($trigger);

  // Fail if the specified trigger doesn't exist.
  if (!$trigger_data) {
    return FALSE;
  }

  // Load any predicates associated with the trigger.
  $predicates = ca_load_trigger_predicates($trigger);

  // Fail if we didn't find any predicates.
  if (!$predicates || count($predicates) == 0) {
    return FALSE;
  }

  // Prepare the arguments for evaluation.
  $arguments = ca_parse_trigger_args($trigger_data, $args);

  // Fail if we didn't receive the right type of or enough arguments.
  if (!$arguments) {
    return FALSE;
  }

  // Loop through the predicates and evaluate them one by one.
  foreach ($predicates as $pid => $predicate) {
    // If all of a predicate's conditions evaluate to TRUE...
    if (ca_evaluate_conditions($predicate, $arguments)) {
      // Then perform its actions.
      ca_perform_actions($predicate, $arguments);
    }
  }

  return TRUE;
}

/**
 * Parse the argument array into a CA friendly array for the trigger.
 *
 * @param $trigger
 *   The name of the trigger for which we are parsing the arguments.
 * @param $args
 *   An array of arguments to check against the expected arguments.
 * @return
 *   The array of arguments keyed according to the trigger's argument names.
 */
function ca_parse_trigger_args($trigger, $args) {
  // Fail if we didn't receive enough arguments for this trigger.
  if (count($args) < count($trigger['#arguments'])) {
    return FALSE;
  }

  // Load all the entity information.
  $entities = module_invoke_all('ca_entity');

  // Loop through the expected arguments.
  foreach ($trigger['#arguments'] as $key => $value) {
    // Grab for comparison the next argument passed to the trigger.
    $arg = array_shift($args);

    // Check the type and fail if it is incorrect.
    if (gettype($arg) != $entities[$value['#entity']]['#type']) {
      return FALSE;
    }

    // Add the entity to the arguments array along with its meta data.
    $arguments[$key] = array(
      '#entity' => $value['#entity'],
      '#title' => $value['#title'],
      '#data' => $arg,
    );
  }

  return $arguments;
}

/**
 * Load predicates based on the specified parameters.
 *
 * @param $trigger
 *   The name of the trigger for which to search when loading the predicates.
 * @param $all
 *   FALSE by default, specifies whether we want to load all possible predicates
 *   or only those that are active (status > 0).
 * @return
 *   An array of predicates.
 */
function ca_load_trigger_predicates($trigger, $all = FALSE) {
  // Load all the module defined predicates.
  $predicates = module_invoke_all('ca_predicate');

  // Loop through the module defined predicates to prepare the data - unsets
  // inactive predicates if $all == FALSE and adds a default weight if need be.
  foreach ($predicates as $key => $value) {
    // Unset the predicate if it doesn't use the specified trigger.
    if ($value['#trigger'] != $trigger) {
      unset($predicates[$key]);
      continue;
    }

    if (!$all && $value['#status'] <= 0) {
      unset($predicates[$key]);
    }
    elseif (!isset($value['#weight'])) {
      $predicates[$key]['#weight'] = 0;
    }
  }

  // Load and loop through the predicates from the database for this trigger.
  $result = db_query("SELECT * FROM {ca_predicates} WHERE ca_trigger = '%s'", $trigger);
  while ($data = db_fetch_array($result)) {
    // Module defined predicates have string IDs.  When a user modifies one of
    // these, we unset the module defined predicate and reconsider adding it in.
    if (!is_numeric($data['pid'])) {
      unset($predicates[$data['pid']]);
    }

    // Add predicates from the database to our return array if $all == TRUE or
    // if the predicate is active.
    if ($all || $data['status'] > 0) {
      $predicate = ca_prepare_db_predicate($data);

      // Set the actions' weight if necessary and sort actions by their weight.
      for ($i = 0; $i < count($predicate['#actions']); $i++) {
        if (!isset($predicate['#actions'][$i]['#weight'])) {
          $predicate['#actions'][$i]['#weight'] = 0;
        }
      }
      usort($predicate['#actions'], 'ca_weight_sort');

      $predicates[$data['pid']] = $predicate;
    }
  }

  uasort($predicates, 'ca_weight_sort');

  return $predicates;
}

/**
 * Prepare predicate data from the database into a full predicate array.
 *
 * @param $data
 *   An array of data representing a row in the predicates table.
 * @return
 *   A predicate array.
 */
function ca_prepare_db_predicate($data) {
  $predicate = array();

  foreach ($data as $key => $value) {
    switch ($key) {
      // Condition and action data needs to be unserialized.
      case 'conditions':
      case 'actions':
        $predicate['#'. $key] = unserialize($value);
        break;
      case 'ca_trigger':
        $predicate['#trigger'] = $value;
        break;
      default:
        $predicate['#'. $key] = $value;
        break;
    }
  }

  return $predicate;
}

/**
 * Evaluate a predicate's conditions.
 *
 * @param $predicate
 *   A fully loaded predicate array.
 * @param $arguments
 *   The array of parsed arguments for the trigger.
 * @return
 *   TRUE or FALSE indicating the success or failure of the evaluation.
 */
function ca_evaluate_conditions($predicate, $arguments) {
  // Automatically pass if there are no conditions.
  if (count($predicate['#conditions']) == 0) {
    return TRUE;
  }

  // Load the data for the conditions as defined by modules.
  $condition_data = module_invoke_all('ca_condition');

  // Recurse through the predicate's conditions for evaluation.
  $result = _ca_evaluate_conditions_tree($predicate['#conditions'], $arguments, $condition_data);

  if (is_null($result)) {
    $result = FALSE;
  }

  return $result;
}

/**
 * Recursively evaluate conditions to accommodate nested logical groups.
 */
function _ca_evaluate_conditions_tree($condition, $arguments, $condition_data) {
  if (isset($condition['#operator']) && is_array($condition['#conditions'])) {
    // Default to TRUE for cases of empty conditions arrays.
    $result = TRUE;

    foreach ($condition['#conditions'] as $sub_condition) {
      $result = _ca_evaluate_conditions_tree($sub_condition, $arguments, $condition_data);

      // Invalid conditions return NULL. Skip it and go to the next one.
      if (is_null($result)) {
        continue;
      }
      // Save the processors! Apply Boolean shortcutting if we can.
      if ($condition['#operator'] == 'OR' && $result) {
        return TRUE;
      }
      elseif ($condition['#operator'] == 'AND' && !$result) {
        return FALSE;
      }
    }
    return $result;
  }
  else {
    return _ca_evaluate_condition($condition, $arguments, $condition_data);
  }
}

/**
 * Evaluate a single condition.
 */
function _ca_evaluate_condition($condition, $arguments, $condition_data) {
  $args = array();

  // Make sure the condition tree is sane.
  if (!is_array($condition_data[$condition['#name']])) {
    return NULL;
  }

  // Get the callback function for the current condition.
  $callback = $condition_data[$condition['#name']]['#callback'];

  // Skip this condition if the function does not exist.
  if (!function_exists($callback)) {
    return NULL;
  }

  // Loop through the expected arguments for a condition.
  // Cast to an array to accommodate conditions that need no arguments.
  foreach ((array) $condition_data[$condition['#name']]['#arguments'] as $key => $value) {
    // Using the argument map for the condition on this predicate, fetch the
    // argument that was passed to the trigger that matches to the argument
    // needed to evaluate this condition.
    if (isset($condition['#argument_map'][$key])) {
      if ($value['#entity'] == 'arguments') {
        $args[] = $arguments;
      }
      else {
        $args[] = $arguments[$condition['#argument_map'][$key]]['#data'];
      }
    }
    else {
      // Skip this condition of the predicate didn't map the arguments needed.
      return NULL;
    }
  }

  // Add the condition settings to the argument list.
  $args[] = $condition['#settings'];

  // Call the condition's function with the appropriate arguments.
  $result = call_user_func_array($callback, $args);

  // If the negate operator is TRUE, then switch the result!
  if ($condition['#settings']['negate']) {
    $result = !$result;
  }

  return $result;
}

/**
 * Perform a predicate's actions in order, preserving changes to the arguments.
 *
 * @param $predicate
 *   A fully loaded predicate array.
 * @param $arguments
 *   The array of parsed arguments for the trigger.
 * @return
 *   An array of results, if any, that were returned by the actions.
 */
function ca_perform_actions($predicate, $arguments) {
  // Exit now if we don't have any actions.
  if (count($predicate['#actions']) == 0) {
    return;
  }

  $results = array();

  // Load the data for the actions as defined by modules.
  $action_data = module_invoke_all('ca_action');

  foreach ($predicate['#actions'] as $i => $action) {
    $args = array();

    // Get the callback function for the current action.
    $callback = $action_data[$action['#name']]['#callback'];

    // Do not perform the action if the function does not exist.
    if (!function_exists($callback)) {
      continue;
    }

    // Loop through the expected arguments for an action.
    foreach ($action_data[$action['#name']]['#arguments'] as $key => $value) {
      // Using the argument map for the action on this predicate, fetch the
      // argument that was passed to the trigger that matches to the argument
      // needed to perform this action.
      if (isset($action['#argument_map'][$key])) {
        // Adding the arguments as references so action functions can update the
        // arguments here when they make changes to the argument data.
        if ($value['#entity'] == 'arguments') {
          $args[] = &$arguments;
        }
        else {
          $args[] = &$arguments[$action['#argument_map'][$key]]['#data'];
        }
      }
      else {
        // Skip this action of the predicate didn't map the arguments needed.
        continue 2;
      }
    }

    // Add the condition settings to the argument list.
    $args[] = is_array($action['#settings']) ? $action['#settings'] : array();

    // Call the action's function with the appropriate arguments.
    $results[$i] = call_user_func_array($callback, $args);
  }

  return $results;
}

/**
 * Load triggers defined in modules via hook_ca_trigger().
 *
 * @param $trigger
 *   Defaults to 'all'; may instead be the name of the trigger to load.
 * @return
 *   An array of data for the specified trigger or an array of all the trigger
 *   data if 'all' was specified.  Returns FALSE if a specified trigger is
 *   non-existent.
 */
function ca_load_trigger($trigger = 'all') {
  static $triggers;

  // Load the triggers defined in enabled modules to a cached variable.
  if (empty($triggers)) {
    $triggers = module_invoke_all('ca_trigger');
  }

  // Return the whole array if the trigger specified is 'all'.
  if ($trigger === 'all') {
    return $triggers;
  }

  // If the specified trigger is non-existent, return FALSE.
  if (!isset($triggers[$trigger])) {
    return FALSE;
  }

  return $triggers[$trigger];
}

/**
 * Return a trigger's arguments formatted for select element options.
 *
 * @param $trigger
 *   The name of the trigger to load arguments for.
 * @param $entity
 *   The name of the entity type we must restrict our returned arguments to.
 * @return
 *   An array of arguments in FAPI options format.
 */
function ca_load_trigger_arguments($trigger, $entity) {
  static $arguments = array();

  // Load the arguments if we can't find a cached version.
  if (!isset($arguments[$trigger][$entity])) {
    $arguments[$trigger][$entity] = array();

    // Handle the arguments entity specially, as it should never be specified
    // in a trigger.
    if ($entity == 'arguments') {
      $arguments[$trigger][$entity]['arguments'] = t('Arguments');
    }
    else {
      // Load the trigger data.
      $trigger_data = ca_load_trigger($trigger);

      // Add the trigger's arguments to the options array if the entity matches.
      foreach ((array) $trigger_data['#arguments'] as $key => $value) {
        if ($value['#entity'] == $entity) {
          $arguments[$trigger][$entity][$key] = $value['#title'];
        }
      }
    }
  }

  return $arguments[$trigger][$entity];
}

/**
 * Load conditions defined in modules via hook_ca_condition().
 *
 * @param $condition
 *   Defaults to 'all'; may instead be the name of the condition to load.
 * @return
 *   An array of data for the specified condition or an array of all the
 *     condition data if 'all' was specified.  Returns FALSE if a specified
 *     condition is non-existent.
 */
function ca_load_condition($condition = 'all') {
  static $conditions;

  // Load the conditions defined in enabled modules to a cached variable.
  if (empty($conditions)) {
    $conditions = module_invoke_all('ca_condition');
  }

  // Return the whole array if the trigger specified is 'all'.
  if ($condition === 'all') {
    return $conditions;
  }

  // If the specified trigger is non-existent, return FALSE.
  if (!isset($conditions[$condition])) {
    return FALSE;
  }

  return $conditions[$condition];
}

/**
 * Return an array of conditions available for the specified trigger.
 *
 * @param $trigger
 *   The name of a trigger to find conditions for; if left empty, the function
 *   returns conditions for the previously specified trigger.
 * @return
 *   A nested array of names/titles for the conditions available for the
 *   trigger; in the format required by select elements in FAPI.
 */
function ca_load_trigger_conditions($trigger = '') {
  static $options = array();

  if (!empty($trigger)) {
    // Load the specified trigger.
    $trigger = ca_load_trigger($trigger);
    $trigger_entities = array();

    // Organize trigger arguments by entity.
    foreach ($trigger['#arguments'] as $argument) {
      $trigger_entities[$argument['#entity']] = $argument;
    }

    // Load and loop through all the conditions defined by modules.
    $conditions = ca_load_condition();

    foreach ($conditions as $name => $condition) {
      // Check through each argument needed for the condition.
      // Cast to an array to accommodate conditions that need no arguments.
      foreach ((array) $condition['#arguments'] as $argument) {
        $entity = $argument['#entity'];
        // If the condition requires an entity the trigger doesn't provide,
        // then skip to the next condition.
        if ($entity != 'arguments' && !$trigger_entities[$entity]) {
          continue 2;
        }
      }

      // Getting this far means that all of the condition's arguments have
      // the same entity types as the trigger's. Add it to the options,
      // and group them by category for usability.
      $options[$condition['#category']][$name] = $condition['#title'];
    }

    // Alphabetically sort the groups and their options.
    foreach ($options as $group => $conditions) {
      asort($conditions);
      $options[$group] = $conditions;
    }
    ksort($options);
  }

  return $options;
}

/**
 * Load actions defined in modules via hook_ca_action().
 *
 * @param $action
 *   Defaults to 'all'; may instead be the name of the action to load.
 * @return
 *   An array of data for the specified action or an array of all the action
 *   data if 'all' was specified.  Returns FALSE if a specified action is
 *   non-existent.
 */
function ca_load_action($action = 'all') {
  static $actions;

  // Load the actions defined in enabled modules to a cached variable.
  if (empty($actions)) {
    $actions = module_invoke_all('ca_action');
  }

  // Return the whole array if the trigger specified is 'all'.
  if ($action === 'all') {
    return $actions;
  }

  // If the specified trigger is non-existent, return FALSE.
  if (!isset($actions[$action])) {
    return FALSE;
  }

  return $actions[$action];
}

/**
 * Return an array of actions available for the specified trigger.
 *
 * @param $trigger
 *   The name of a trigger to find actions for; if left empty, the function
 *   returns conditions for the previously specified trigger.
 * @return
 *   A nested array of names/titles for the actions available for the trigger;
 *   in the format required by select elements in FAPI.
 */
function ca_load_trigger_actions($trigger = '') {
  static $options = array();

  if (!empty($trigger)) {
    // Load the specified trigger.
    $trigger = ca_load_trigger($trigger);
    $trigger_entities = array();

    // Organize trigger arguments by entity.
    foreach ($trigger['#arguments'] as $argument) {
      $trigger_entities[$argument['#entity']] = $argument;
    }

    // Load and loop through all the actions defined by modules.
    $actions = ca_load_action();

    foreach ($actions as $name => $action) {
      // Check through each argument needed for the condition.
      foreach ($action['#arguments'] as $argument) {
        $entity = $argument['#entity'];
        // If the action requires an entity the trigger doesn't provide,
        // then skip to the next action.
        if ($entity != 'arguments' && !$trigger_entities[$entity]) {
          continue 2;
        }
      }

      // Getting this far means that all of the condition's arguments have
      // the same entity types as the trigger's. Add it to the options,
      // and group them by category for usability.
      $options[$action['#category']][$name] = $action['#title'];
    }

    // Alphabetically sort the groups and their options.
    foreach ($options as $group => $actions) {
      asort($actions);
      $options[$group] = $actions;
    }
    ksort($options);
  }

  return $options;
}

/**
 * Return a default conditions array for use on new predicates or in the UI.
 */
function ca_new_conditions() {
  return array(
    '#operator' => 'AND',
    '#conditions' => array(
      array(
        '#operator' => 'AND',
        '#conditions' => array(),
      ),
    ),
  );
}

/**
 * Add a new condition group to a predicate.
 *
 * @param $pid
 *   The ID of the predicate to add the condition group to.
 * @return
 *   An array representing the full, updated predicate.
 */
function ca_add_condition_group($pid) {
  // Load the predicate.
  $predicate = ca_load_predicate($pid);

  // Add the condition group to the conditions array in the appropriate place.
  if (empty($predicate['#conditions'])) {
    $predicate['#conditions'] = ca_new_conditions();
  }
  else {
    $predicate['#conditions']['#conditions'][] = array(
      '#operator' => 'AND',
      '#conditions' => array(),
    );
  }

  // Save the changes.
  ca_save_predicate($predicate);

  return $predicate;
}

/**
 * Remove a condition group from a predicate.
 *
 * @param $pid
 *   The ID of the predicate to remove the condition group from.
 * @param $group_key
 *   The key of the condition group to remove.
 * @return
 *   An array representing the full, updated predicate.
 */
function ca_remove_condition_group($pid, $group_key) {
  // Load the predicate as it is now.
  $predicate = ca_load_predicate($pid);

  // Update, save, and return the predicate.
  unset($predicate['#conditions']['#conditions'][$group_key]);
  ca_save_predicate($predicate);

  return $predicate;
}

/**
 * Add a new condition to a predicate.
 *
 * @param $pid
 *   The ID of the predicate to add the condition to.
 * @param $name
 *   The name of the condition to add.
 * @param $group_key
 *   The key of the condition group we're adding the condition to.
 * @param $mark_expanded
 *   If set to TRUE marks the condition so that it will be expanded next time it
 *   displays in the UI so the user can adjust its settings.
 * @return
 *   An array representing the full, updated predicate.
 */
function ca_add_condition($pid, $name, $group_key, $mark_expanded = TRUE) {
  // Load the predicate.
  $predicate = ca_load_predicate($pid);

  // Load the condition we want to add to the predicate.
  $data = ca_load_condition($name);

  // If the condition exists...
  if ($data) {
    // Build the condition array.
    $condition = array(
      '#name' => $name,
      '#title' => $data['#title'],
      '#argument_map' => array(),
      '#settings' => array(),
    );

    // Mark it for expansion in the form if specified.
    if ($mark_expanded) {
      $condition['#expanded'] = TRUE;
    }

    // Add the condition to the predicate.
    $predicate['#conditions']['#conditions'][$group_key]['#conditions'][] = $condition;
  }

  ca_save_predicate($predicate);

  return $predicate;
}

/**
 * Remove a condition from a predicate.
 *
 * @param $pid
 *   The ID of the predicate to remove the condition group from.
 * @param $group_key
 *   The key of the condition group the condition is in.
 * @param $cond_key
 *   The key of the condition to remove.
 * @return
 *   An array representing the full, updated predicate.
 */
function ca_remove_condition($pid, $group_key, $cond_key) {
  // Load the predicate as it is now.
  $predicate = ca_load_predicate($pid);

  // Update, save, and return the predicate.
  unset($predicate['#conditions']['#conditions'][$group_key]['#conditions'][$cond_key]);
  ca_save_predicate($predicate);

  return $predicate;
}

/**
 * Add a new action to a predicate.
 *
 * @param $pid
 *   The ID of the predicate to add the action to.
 * @param $name
 *   The name of the action to add.
 * @return
 *   An array representing the full, updated predicate.
 */
function ca_add_action($pid, $name) {
  // Load the predicate as it is now.
  $predicate = ca_load_predicate($pid);

  // Load the action.
  $action = ca_load_action($name);

  // Abort if it did not exist.
  if (empty($action)) {
    return $predicate;
  }

  // Add the name to the action array so it's saved in the predicate.
  $action['#name'] = $name;

  // Add the action to the predicate's actions array.
  $predicate['#actions'][] = $action;

  // Save and return the updated predicate.
  ca_save_predicate($predicate);

  return $predicate;
}

/**
 * Remove an action from a predicate.
 *
 * @param $pid
 *   The ID of the predicate to remove the action from.
 * @param $index
 *   The index of the action to remove.
 * @return
 *   An array representing the full, updated predicate.
 */
function ca_remove_action($pid, $index) {
  $actions = array();

  // Load the predicate as it is now.
  $predicate = ca_load_predicate($pid);

  // Build a new actions array, leaving out the one marked for removal.
  foreach ($predicate['#actions'] as $key => $action) {
    if ($key != $index) {
      $actions[] = $action;
    }
  }

  // Update, save, and return the predicate.
  $predicate['#actions'] = $actions;
  ca_save_predicate($predicate);

  return $predicate;
}

/**
 * Load a predicate by its ID.
 *
 * @param $pid
 *   The ID of the predicate to load.
 * @return
 *   A fully loaded predicate array.
 */
function ca_load_predicate($pid) {
  $predicate = array();

  // First attempt to load the predicate from the database.
  $result = db_query("SELECT * FROM {ca_predicates} WHERE pid = '%s'", $pid);
  if ($predicate = db_fetch_array($result)) {
    $predicate = ca_prepare_db_predicate($predicate);
  }
  else {
    // Otherwise look for it in the module defined predicates.
    $predicates = module_invoke_all('ca_predicate');

    if (!empty($predicates[$pid])) {
      $predicate = $predicates[$pid];
    }
  }

  // Add the pid to the predicate so it can be resaved later.
  if (!empty($predicate)) {
    $predicate['#pid'] = $pid;
  }

  return $predicate;
}

/**
 * Save a predicate array to the database.
 *
 * @param $predicate
 *   A fully loaded predicate array.
 */
function ca_save_predicate($predicate) {
  // Check to see if the predicate has been previously saved to the database.
  $result = db_result(db_query("SELECT COUNT(*) FROM {ca_predicates} WHERE pid = '%s'", $predicate['#pid']));

  if (!$result) {
    // If not, then insert it.
    db_query("INSERT INTO {ca_predicates} (pid, title, description, class, status, weight, uid, ca_trigger, conditions, actions, created, modified) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d, '%s', '%s', '%s', %d, %d)",
      $predicate['#pid'], $predicate['#title'], $predicate['#description'], $predicate['#class'], $predicate['#status'], $predicate['#weight'], $predicate['#uid'], $predicate['#trigger'], serialize($predicate['#conditions']), serialize($predicate['#actions']), time(), time());
  }
  else {
    // Otherwise, update it.
    db_query("UPDATE {ca_predicates} SET title = '%s', description = '%s', class = '%s', status = %d, weight = %d, uid = %d, ca_trigger = '%s', conditions = '%s', actions = '%s', modified = %d WHERE pid = '%s'",
      $predicate['#title'], $predicate['#description'], $predicate['#class'], $predicate['#status'], $predicate['#weight'], $predicate['#uid'], $predicate['#trigger'], serialize($predicate['#conditions']), serialize($predicate['#actions']), time(), $predicate['#pid']);
  }
}

/**
 * Delete a predicate from the database.
 *
 * @param $pid
 *   The ID of the predicate to delete.
 */
function ca_delete_predicate($pid) {
  db_query("DELETE FROM {ca_predicates} WHERE pid = '%s'", $pid);
}

/**
 * Compare two conditional action arrays to sort them by #weight.
 */
function ca_weight_sort($a, $b) {
  if ($a['#weight'] == $b['#weight']) {
    return 0;
  }

  return ($a['#weight'] > $b['#weight']) ? 1 : -1;
}

/**
 * Configure the #parents and #id field of any child filter_forms.
 *
 * Unfortunately, FAPI doesn't give us any way to know we have a filter
 * element, so we require all filters to have an #is_format element.
 */
function filter_configure_form(&$form) {
  // Grab all the filters, and search for the formats.
  $formats = filter_formats();
  _filter_configure_form_recur($form, $formats, array());
}

/**
 * Recursive filter configuration.
 */
function _filter_configure_form_recur(&$form, &$formats, $parents) {
  // Look for our special element, then search the children.
  if (isset($form['#is_format']) ? $form['#is_format'] : FALSE) {
    foreach (element_children($form) as $key) {
      // Found a filter.
      if (array_key_exists($key, $formats)) {
        // Configure the parents and ID so FAPI is happy.
        $form[$key] = array(
          '#parents' => $parents,
          '#id' => form_clean_id('edit-'. implode('-', array_merge($parents, array($key)))),
        ) + $form[$key];

      }
    }
  }
  else {
    // Keep looking.
    foreach (element_children($form) as $key) {
      _filter_configure_form_recur($form[$key], $formats, array_merge($parents, array($key)));
    }
  }
}
