<?php
// $Id$

/**
 * @file
 * Create field groups for CCK fields.
 *
 * Hooks for other modules to intervene include:
 * - hook_fieldgroup_view: Alter the group $element added to $node->content.
 * - hook_fieldgroup_form: Alter the group portion of the node form.
 * - hook_fieldgroup_types: Add additional fieldgroup group_types.
 * - hook_fieldgroup_default_settings: Add additional fieldgroup default settings.
 * - hook_fieldgroup_save: Do additional processing when a fieldgroup is saved.
 */
/**
 * Implementation of hook_init().
 */
function fieldgroup_init() {
  drupal_add_css(drupal_get_path('module', 'fieldgroup') .'/fieldgroup.css');
}

/**
 * Implementation of hook_ctools_plugin_directory().
 */
function fieldgroup_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools' && $plugin == 'content_types') {
    return 'panels/' . $plugin;
  }
}

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

  // Make sure this doesn't fire until content_types is working,
  // needed to avoid errors on initial installation.
  if (!defined('MAINTENANCE_MODE')) {
    foreach (node_get_types() as $type) {
      $type_name = $type->type;
      $content_type = content_types($type_name);
      $type_url_str = $content_type['url_str'];
      $items['admin/content/node-type/'. $type_url_str .'/groups/%'] = array(
        'title' => 'Edit group',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('fieldgroup_group_edit_form', $type_name, 5),
        'access arguments' => array('administer content types'),
        'type' => MENU_CALLBACK,
      );
      $items['admin/content/node-type/'. $type_url_str .'/groups/%/remove'] = array(
        'title' => 'Edit group',
        'page callback' => 'drupal_get_form',
        'page arguments' => array('fieldgroup_remove_group', $type_name, 5),
        'access arguments' => array('administer content types'),
        'type' => MENU_CALLBACK,
      );
    }
  }
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function fieldgroup_theme() {
  return array(
    'fieldgroup_simple' => array(
      'template' => 'fieldgroup-simple',
      'arguments' => array('element' => NULL),
    ),
    'fieldgroup_fieldset' => array(
      'arguments' => array('element' => NULL),
    ),
    'fieldgroup_display_overview_form' => array(
      'arguments' => array('form' => NULL),
    ),
  );
}

/**
 * Implementation of hook_elements().
 */
function fieldgroup_elements() {
  return array(
    'fieldgroup_simple' => array(),
    'fieldgroup_fieldset' => array('#collapsible' => FALSE, '#collapsed' => FALSE, '#value' => NULL,),
  );
}

/**
 * Implementation of hook_fieldapi().
 */
function fieldgroup_content_fieldapi($op, $field) {
  switch ($op) {
    case 'delete instance':
      db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE field_name = '%s' AND type_name = '%s'", $field['field_name'], $field['type_name']);
      cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
      break;
  }
}

function fieldgroup_group_edit_form(&$form_state, $type_name, $group_name) {
  $content_type = content_types($type_name);
  $groups = fieldgroup_groups($content_type['type']);

  if (!$group = $groups[$group_name]) {
    drupal_not_found();
    exit;
  }

  $form['label'] = array(
    '#type' => 'textfield',
    '#title' => t('Label'),
    '#default_value' => $group['label'],
    '#required' => TRUE,
  );

  // Set a default value for group type early in the form so it
  // can be overridden by subsequent form elements added by other modules.
  $group_type = !empty($group['group_type']) ? $group['group_type'] : 'standard';
  $form['group_type'] = array('#type' => 'hidden', '#default_value' => $group_type);

  $form['settings']['#tree'] = TRUE;
  $form['settings']['form'] = array(
    '#type' => 'fieldset',
    '#title' => t('Form settings'),
    '#description' => t('These settings apply to the group in the node editing form.'),
  );
  $form['settings']['form']['style'] = array(
    '#type' => 'radios',
    '#title' => t('Style'),
    '#default_value' => $group['settings']['form']['style'],
    '#options' => array(
      'fieldset' => t('always open'),
      'fieldset_collapsible' => t('collapsible'),
      'fieldset_collapsed' => t('collapsed'),
    )
  );
  $form['settings']['form']['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Help text'),
    '#default_value' => $group['settings']['form']['description'],
    '#rows' => 5,
    '#description' => t('Instructions to present to the user on the editing form.'),
    '#required' => FALSE,
  );
  $form['settings']['display'] = array(
    '#type' => 'fieldset',
    '#title' => t('Display settings'),
    '#description' => t('These settings apply to the group on node display.'),
  );
  $form['settings']['display']['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $group['settings']['display']['description'],
    '#rows' => 5,
    '#description' => t('A description of the group.'),
    '#required' => FALSE,
  );

  foreach (array_keys(content_build_modes()) as $key) {
    $form['settings']['display'][$key]['format'] = array('#type' => 'value', '#value' => isset($group['settings']['display'][$key]['format']) ? $group['settings']['display'][$key]['format'] : 'fieldset');
    $form['settings']['display'][$key]['exclude'] = array('#type' => 'value', '#value' => isset($group['settings']['display'][$key]['exclude']) ? $group['settings']['display'][$key]['exclude'] : 0);
  }
  $form['settings']['display']['label'] = array('#type' => 'value', '#value' => $group['settings']['display']['label']);
  $form['weight'] = array('#type' => 'hidden', '#default_value' => $group['weight']);
  $form['group_name'] = array('#type' => 'hidden', '#default_value' => $group_name);

  $form['#content_type'] = $content_type;

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
    '#weight' => 10,
  );

  return $form;
}

function fieldgroup_group_edit_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $content_type = $form['#content_type'];
  fieldgroup_save_group($content_type['type'], $form_values);
  $form_state['redirect'] = 'admin/content/node-type/'. $content_type['url_str'] .'/fields';
}

function fieldgroup_remove_group(&$form_state, $type_name, $group_name) {
  $content_type = content_types($type_name);
  $groups = fieldgroup_groups($content_type['type']);
  $group = isset($groups[$group_name]) ? $groups[$group_name] : '';

  if (empty($group)) {
    drupal_not_found();
    exit;
  }

  $form['#submit'][] = 'fieldgroup_remove_group_submit';
  $form['#content_type'] = $content_type;
  $form['#group_name'] = $group_name;

  return confirm_form($form,
                  t('Are you sure you want to remove the group %label?',
                  array('%label' => t($group['label']))),
                  'admin/content/node-type/'. $content_type['url_str'] .'/fields', t('This action cannot be undone.'),
                  t('Remove'), t('Cancel'));
}

function fieldgroup_remove_group_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $content_type = $form['#content_type'];
  $group_name = $form['#group_name'];
  fieldgroup_delete($content_type['type'], $group_name);
  drupal_set_message(t('The group %group_name has been removed.', array('%group_name' => $group_name)));
  $form_state['redirect'] = 'admin/content/node-type/'. $content_type['url_str'] .'/fields';
}

/**
 * Returns all groups for a content type
 */
function fieldgroup_groups($content_type = '', $sorted = FALSE, $reset = FALSE) {
  global $language;
  static $groups, $groups_sorted;
  if (!isset($groups) || $reset) {
    if ($cached = cache_get('fieldgroup_data:'. $language->language, content_cache_tablename())) {
      $data = $cached->data;
      $groups = $data['groups'];
      $groups_sorted = $data['groups_sorted'];
    }
    else {
      $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} ORDER BY weight, group_name");
      $groups = array();
      $groups_sorted = array();
      while ($group = db_fetch_array($result)) {
        $group['settings'] = unserialize($group['settings']);
        $group['fields'] = array();

        // Allow external modules to translate field group strings.
        $group_strings = array(
          'label' => $group['label'],
          'form_description' => $group['settings']['form']['description'],
          'display_description' => $group['settings']['display']['description'],
        );
        drupal_alter('content_fieldgroup_strings', $group_strings, $group['type_name'], $group['group_name']);
        $group['label'] = $group_strings['label'];
        $group['settings']['form']['description'] = $group_strings['form_description'];
        $group['settings']['display']['description'] = $group_strings['display_description'];

        $groups[$group['type_name']][$group['group_name']] = $group;
        $groups_sorted[$group['type_name']][] = &$groups[$group['type_name']][$group['group_name']];
      }
      //load fields
      $result = db_query("SELECT nfi.*, ng.group_name FROM {". fieldgroup_tablename() ."} ng ".
 "INNER JOIN {". fieldgroup_fields_tablename() ."} ngf ON ngf.type_name = ng.type_name AND ngf.group_name = ng.group_name ".
 "INNER JOIN {". content_instance_tablename() ."} nfi ON nfi.field_name = ngf.field_name AND nfi.type_name = ngf.type_name ".
 "WHERE nfi.widget_active = 1 ORDER BY nfi.weight");
      while ($field = db_fetch_array($result)) {
        // Allow external modules to translate field strings.
        $field_strings = array(
          'widget_label' => $field['label'],
          'widget_description' => $field['description'],
        );
        drupal_alter('content_field_strings', $field_strings, $field['type_name'], $field['field_name']);
        $field['label'] = $field_strings['widget_label'];
        $field['description'] = $field_strings['widget_description'];

        $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = $field;
      }
      cache_set('fieldgroup_data:'. $language->language, array('groups' => $groups, 'groups_sorted' => $groups_sorted), content_cache_tablename());
    }
  }
  if (empty($content_type)) {
    return $groups;
  }
  elseif (empty($groups) || empty($groups[$content_type])) {
    return array();
  }
  return $sorted ? $groups_sorted[$content_type] : $groups[$content_type];
}


function _fieldgroup_groups_label($content_type) {
  $groups = fieldgroup_groups($content_type);

  $labels[''] = '<'. t('none') .'>';
  foreach ($groups as $group_name => $group) {
    $labels[$group_name] = t($group['label']);
  }
  return $labels;
}

function _fieldgroup_field_get_group($content_type, $field_name) {
  return db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $field_name));
}

/**
 * Implementation of hook_form_alter()
 */
function fieldgroup_form_alter(&$form, $form_state, $form_id) {
  if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) {
    foreach (fieldgroup_groups($form['type']['#value']) as $group_name => $group) {
      $form[$group_name] = array(
        '#type' => 'fieldset',
        '#title' => check_plain(t($group['label'])),
        '#collapsed' => $group['settings']['form']['style'] == 'fieldset_collapsed',
        '#collapsible' => in_array($group['settings']['form']['style'], array('fieldset_collapsed', 'fieldset_collapsible')),
        '#weight' => $group['weight'],
        '#description' => content_filter_xss(t($group['settings']['form']['description'])),
        '#attributes' => array('class' => strtr($group['group_name'], '_', '-')),
      );

      $has_accessible_field = FALSE;
      foreach ($group['fields'] as $field_name => $field) {
        if (isset($form[$field_name])) {
          $form[$group_name][$field_name] = $form[$field_name];
          // Track whether this group has any accessible fields within it.
          if (!isset($form[$field_name]['#access']) || $form[$field_name]['#access'] !== FALSE) {
            $has_accessible_field = TRUE;
          }
          unset($form[$field_name]);
        }
      }
      if (!empty($group['fields']) && !element_children($form[$group_name])) {
        //hide the fieldgroup, because the fields are hidden too
        unset($form[$group_name]);
      }

      if (!$has_accessible_field) {
        // Hide the fieldgroup, because the fields are inaccessible.
        $form[$group_name]['#access'] = FALSE;
      }

      // Allow other modules to alter the form.
      // Can't use module_invoke_all because we want
      // to be able to use a reference to $form and $form_state.
      foreach (module_implements('fieldgroup_form') as $module) {
        $function = $module .'_fieldgroup_form';
        $function($form, $form_state, $form_id, $group);
      }

    }

  }
  // The group is only added here so it will appear in the export
  // when using Content Copy.
  elseif ($form_id == 'content_field_edit_form' && isset($form['widget'])) {
    $content_type = content_types($form['type_name']['#value']);
    $form['widget']['group'] = array(
      '#type' => 'value',
      '#value' => _fieldgroup_field_get_group($content_type['type'], $form['field_name']['#value']),
    );
  }
  elseif ($form_id == 'content_field_overview_form') {
    $form['#validate'][] = 'fieldgroup_field_overview_form_validate';
    $form['#submit'][] = 'fieldgroup_field_overview_form_submit';
  }
  elseif ($form_id == 'content_display_overview_form' && !empty($form['#groups'])) {
    $form['#submit'][] = 'fieldgroup_display_overview_form_submit';
    if (!isset($form['submit'])) {
      $form['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#weight' => 10);
    }
  }
  elseif ($form_id == 'content_field_remove_form') {
    $form['#submit'][] = 'fieldgroup_field_remove_form_submit';
  }
}

/**
 * API for group name validation.
 *
 * Pulled into separate function to be re-usable.
 */
function fieldgroup_validate_name($group, $type_name) {
  $errors = array();

  // No label.
  if (!$group['label']) {
    $errors['label'][] = t('You need to provide a label.');
  }

  // No group name.
  if (!$group['group_name']) {
    $errors['group_name'][] = t('You need to provide a group name.');
  }
  // Group name validation.
  else {
    $group_name = $group['group_name'];
    $group['group_type'] = !empty($group['group_type']) ? $group['group_type'] : 'standard';

    // Add the 'group_' prefix.
    if (substr($group_name, 0, 6) != 'group_') {
      $group_name = 'group_'. $group_name;
    }

    // Invalid field name.
    if (!preg_match('!^group_[a-z0-9_]+$!', $group_name)) {
      $errors['group_name'][] = t('The group name %group_name is invalid. The name must include only lowercase unaccentuated letters, numbers, and underscores.', array('%group_name' => $group_name));
    }
    if (strlen($group_name) > 32) {
      $errors['group_name'][] = t('The group name %group_name is too long. The name is limited to 32 characters, including the \'group_\' prefix.', array('%group_name' => $group_name));
    }

    // Group name already exists.
    $groups = fieldgroup_groups($type_name);
    if (isset($groups[$group_name])) {
      $errors['group_name'][] = t('The group name %group_name already exists.', array('%group_name' => $group_name));
    }
    if (empty($errors['group_name'])) {
      $group['group_name'] = $group_name;
    }
  }
  return array('group_name' => $group['group_name'], 'errors' => $errors);
}

function fieldgroup_field_overview_form_validate($form, &$form_state) {
  $form_values = $form_state['values'];
  $group = $form_values['_add_new_group'];

  if (array_filter(array($group['label'], $group['group_name']))) {
    $validation = fieldgroup_validate_name($group, $form['#type_name']);
    if (!empty($validation['errors'])) {
      foreach ($validation['errors'] as $type => $messages) {
        foreach ($messages as $message) {
          if ($type == 'label') {
            form_set_error('_add_new_group][label', t('Add new group:') .' '. $message);
          }
          else {
            form_set_error('_add_new_group][group_name', t('Add new group:') .' '. $message);
          }
        }
      }
    }
    $group_name = $validation['group_name'];
    form_set_value($form['_add_new_group']['group_name'], $group_name, $form_state);
  }
  else {
    // Fail validation if attempt to nest fields under a new group without the
    // proper information. Not raising an error would cause the nested fields
    // to get weights the user doesn't expect.

    foreach ($form_values as $key => $values) {
      if ($values['parent'] == '_add_new_group') {
        form_set_error('_add_new_group][label', t('Add new group: you need to provide a label.'));
        form_set_error('_add_new_group][group_name', t('Add new group: you need to provide a group name.'));
        break;
      }
    }
  }
}

function fieldgroup_field_overview_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $type_name = $form['#type_name'];

  // Create new group if needed.
  if (!empty($form_values['_add_new_group']['label'])) {
    $group = $form_values['_add_new_group'];
    $group['settings'] = field_group_default_settings($group['group_type']);
    fieldgroup_save_group($type_name, $group);
    $new_group_name = $group['group_name'];
  }

  // Parse incoming rows.
  $add_field_rows = array('_add_new_field', '_add_existing_field');
  $field_rows = array_merge($form['#fields'], $add_field_rows);
  foreach ($form_values as $key => $values) {
    // If 'field' row: update field parenting.
    if (in_array($key, $field_rows)) {
      // If newly added fields were added to a group:
      if (in_array($key, $add_field_rows)) {
        // We replace the '_add_*_field' key with the actual name of
        // the field that got added.
        // content_field_overview_form_submit() placed those
        // in $form_state['fields_added'] for us.
        if (isset($form_state['fields_added'][$key])) {
          $key = $form_state['fields_added'][$key];
        }
        else {
          // No field was actually created : skip to next row.
          continue;
        }
      }
      // If the field was added to the newly created group, replace the
      // '_add_new_group' value with the actual name of the group.
      $parent = ($values['parent'] == '_add_new_group' && isset($new_group_name)) ? $new_group_name : $values['parent'];
      // TODO: check the parent group does exist ?
      fieldgroup_update_fields(array('field_name' => $key, 'group' => $parent, 'type_name' => $type_name));
    }

    // If 'group' row:  update groups weights
    // (possible newly created group has already been taken care of).
    elseif (in_array($key, $form['#groups'])) {
      db_query("UPDATE {". fieldgroup_tablename() ."} SET weight = %d WHERE type_name = '%s' AND group_name = '%s'",
        $values['weight'], $type_name, $key);
    }
  }

  cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
}

function field_group_default_settings($group_type) {
  $settings = array(
    'form' => array('style' => 'fieldset', 'description' => ''),
    'display' => array('description' => '', 'label' => 'above'),
  );
  module_load_include('inc', 'content', 'includes/content.admin');
  foreach (array_keys(content_build_modes()) as $key) {
    $settings['display'][$key]['format'] = 'fieldset';
    $settings['display'][$key]['exclude'] = 0;
  }
  // Allow other modules to add new default settings.
  $settings = array_merge($settings, module_invoke_all('fieldgroup_default_settings', $group_type));
  return $settings;
}

function fieldgroup_display_overview_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $groups = fieldgroup_groups($form['#type_name']);
  foreach ($form_values as $key => $values) {
    if (in_array($key, $form['#groups'])) {
      $group = $groups[$key];
      // We have some numeric keys here, so we can't use array_merge.
      $group['settings']['display'] = $values + $group['settings']['display'];
      fieldgroup_save_group($form['#type_name'], $group);
    }
  }
}

function fieldgroup_field_remove_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  // TODO:
  // - when a (non last) field is removed from a group, a 'ghost row' remains in the fields overview
  // - when the last field is removed, the group disappears
  // seems to be fixed when emptying the cache.
  db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']);
}

/**
 * Implementation of hook_nodeapi().
 */
function fieldgroup_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {
    case 'view':
      // Prevent against invalid 'nodes' built by broken 3rd party code.
      if (isset($node->type)) {
        // Build the node content element needed to render each fieldgroup.
        foreach (fieldgroup_groups($node->type) as $group) {
          fieldgroup_build_content($group, $node, $teaser, $page);
        }
      }
      break;
  }
}

/**
 * Build the node content element needed to render a fieldgroup.
 *
 * @param $group
 *   The field group definition.
 * @param $node
 *   The node containing the field group to display. Can be a 'pseudo-node',
 *   containing at least 'type', 'nid', 'vid', and the content for fields
 *   required for the group.
 * @param $teaser
 * @param $page
 *   Similar to hook_nodeapi('view').
 *
 * @see fieldgroup_nodeapi()
 * @see fieldgroup_view_group()
 */
function fieldgroup_build_content($group, &$node, $teaser, $page) {
  // NODE_BUILD_NORMAL is 0, and ('whatever' == 0) is TRUE, so we need a ===.
  if ($node->build_mode === NODE_BUILD_NORMAL || $node->build_mode == NODE_BUILD_PREVIEW) {
    $context = $teaser ? 'teaser' : 'full';
  }
  else {
    $context = $node->build_mode;
  }

  $group_name = $group['group_name'];

  // Do not include group labels when indexing content.
  if ($context == NODE_BUILD_SEARCH_INDEX) {
    $group['settings']['display']['label'] = 'hidden';
  }
  $label = $group['settings']['display']['label'] == 'above';
  $element = array(
    '#title' => $label ? check_plain(t($group['label'])) : '',
    '#description' => $label ? content_filter_xss(t($group['settings']['display']['description'])) : '',
  );
  $format = isset($group['settings']['display'][$context]['format']) ? $group['settings']['display'][$context]['format'] : 'fieldset';

  switch ($format) {
    case 'simple':
      $element['#type'] = 'fieldgroup_simple';
      $element['#group_name'] = $group_name;
      $element['#node'] = $node;
      break;
    case 'hidden':
      $element['#access'] = FALSE;
      break;
    case 'fieldset_collapsed':
      $element['#collapsed'] = TRUE;
    case 'fieldset_collapsible':
      $element['#collapsible'] = TRUE;
    case 'fieldset':
      $element['#type'] = 'fieldgroup_fieldset';
      $element['#attributes'] = array('class' => 'fieldgroup '. strtr($group['group_name'], '_', '-'));
      break;
  }
  foreach ($group['fields'] as $field_name => $field) {
    if (isset($node->content[$field_name])) {
      $element[$field_name] = $node->content[$field_name];
    }
  }

  // Allow other modules to alter the group view.
  // Can't use module_invoke_all because we want
  // to be able to use a reference to $node and $element.
  foreach (module_implements('fieldgroup_view') as $module) {
    $function = $module .'_fieldgroup_view';
    $function($node, $element, $group, $context);
  }

  // Unset the original field values now that we've moved them.
  foreach ($group['fields'] as $field_name => $field) {
    if (isset($node->content[$field_name])) {
      unset($node->content[$field_name]);
    }
  }

  // The wrapper lets us get the themed output for the group
  // to populate the $GROUP_NAME_rendered variable for node templates,
  // and hide it from the $content variable if needed.
  // See fieldgroup_preprocess_node(), theme_fieldgroup_wrapper().
  $wrapper = array(
    'group' => $element,
    '#weight' => $group['weight'],
    '#post_render' => array('fieldgroup_wrapper_post_render'),
    '#group_name' => $group_name,
    '#type_name' => $node->type,
    '#context' => $context,
  );

  $node->content[$group_name] = $wrapper;
}

/**
 * Render a single field group, fully themed with label.
 *
 * To be used by third-party code (Panels, ...) that needs to output an
 * isolated field group. Do *not* use inside node templates, use the
 * $GROUP_NAME_rendered variables instead. You can also use the 'simple'
 * style format and override the template fieldgroup-simple.tpl.php.
 *
 * By default, the field group is displayed using the settings defined for the
 * 'full node' or 'teaser' contexts (depending on the value of the $teaser param).
 * Set $node->build_mode to a different value to use a different context.
 *
 * Different settings can be specified by adjusting $group['settings']['display'].
 *
 * @param $group
 *   The field group definition.
 * @param $node
 *   The node containing the field group to display. Can be a 'pseudo-node',
 *   containing at least 'type', 'nid', 'vid', and the field data required
 *   for the group.
 * @param $teaser
 * @param $page
 *   Similar to hook_nodeapi('view').
 * @return
 *   The themed output for the field group.
 *
 * @see content_view_field()
 */
function fieldgroup_view_group($group, &$node, $teaser = FALSE, $page = FALSE) {
  $group_name = $group['group_name'];
  $field_types = _content_field_types();

  // Clone the node to prevent from altering the original.
  $node_copy = drupal_clone($node);

  // Use 'full'/'teaser' if not specified otherwise.
  $node_copy->build_mode = isset($node_copy->build_mode) ? $node_copy->build_mode : NODE_BUILD_NORMAL;

  // Build the content element for individual fields in the field group.
  if (!isset($node_copy->content)) {
    $node_copy->content = array();
  }
  foreach (array_keys($group['fields']) as $field_name) {
    $field = content_fields($field_name, $node_copy->type);

    if (isset($node_copy->{$field_name})) {
      $items = $node_copy->{$field_name};

      // One-field equivalent to _content_field_invoke('sanitize').
      $module = $field_types[$field['type']]['module'];
      $function = $module .'_field';
      if (function_exists($function)) {
        $function('sanitize', $node_copy, $field, $items, $teaser, $page);
        $node_copy->{$field_name} = $items;
      }

      $field_view = content_field('view', $node_copy, $field, $items, $teaser, $page);
      // content_field('view') adds a wrapper to handle variables and 'excluded'
      // fields for node templates. We bypass it and get the actual field.
      $node_copy->content[$field_name] = $field_view[$field_name];
    }
  }

  // Build the content element of the field group itself.
  fieldgroup_build_content($group, $node_copy, $teaser, $page);

  // fieldgroup_build_content() adds a wrapper to handle variables and 'excluded'
  // groups for node templates. We bypass it and render the actual field group.
  $output = drupal_render($node_copy->content[$group_name]['group']);

  return $output;
}

/**
 * Hide specified fields from the $content variable in node templates.
 */
function fieldgroup_wrapper_post_render($content, $element) {
  $groups = fieldgroup_groups($element['#type_name']);
  $group = $groups[$element['#group_name']];

  // The display settings are not in quite the same place in the
  // group and the field, so create the value the theme will expect.
  $group['display_settings'] = $group['settings']['display'];
  if (theme('content_exclude', $content, $group, $element['#context'])) {
    return '';
  }
  return $content;
}

/*
 * Get the group name for a field.
 * If the field isn't in a group, FALSE will be returned.
 * @return The name of the group, or FALSE.
 */
function fieldgroup_get_group($content_type, $field_name) {
  foreach (fieldgroup_groups($content_type) as $group_name => $group) {
    if (in_array($field_name, array_keys($group['fields']))) {
      return $group_name;
    }
  }
  return FALSE;
}

/**
 *  Implementation of hook_node_type()
 *  React to change in node types
 */
function fieldgroup_node_type($op, $info) {
  if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {
    // update the tables
    db_query("UPDATE {". fieldgroup_tablename() ."} SET type_name='%s' WHERE type_name='%s'", array($info->type, $info->old_type));
    db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET type_name='%s' WHERE type_name='%s'", array($info->type, $info->old_type));
    cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
  }
  elseif ($op == 'delete') {
    db_query("DELETE FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s'", $info->type);
    db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s'", $info->type);
  }
}

function fieldgroup_types() {
  $types = array('standard' => t('Standard group'));
  // Allow other modules to add new group_types.
  $types = array_merge($types, module_invoke_all('fieldgroup_types'));
  return $types;
}

function fieldgroup_tablename($version = NULL) {
  if (is_null($version)) {
    $version = variable_get('fieldgroup_schema_version', 0);
  }
  return $version < 6000 ? 'node_group' : 'content_group';
}

function fieldgroup_fields_tablename($version = NULL) {
  if (is_null($version)) {
    $version = variable_get('fieldgroup_schema_version', 0);
  }
  return $version < 6000 ? 'node_group_fields' : 'content_group_fields';
}

/**
 * CRUD API for fieldgroup module.
 *
 * @todo
 * Make this into more of a real API for groups.
 */
/*
 * Saves the given group for this content-type
 */
function fieldgroup_save_group($type_name, $group) {
  $groups = fieldgroup_groups($type_name);

  // Allow other modules to intervene when the group is saved.
  foreach (module_implements('fieldgroup_save_group') as $module) {
    $function = $module .'_fieldgroup_save_group';
    $function($group);
  }

  if (!isset($groups[$group['group_name']])) {
    // Accept group name from programmed submissions if valid.
    db_query("INSERT INTO {". fieldgroup_tablename() ."} (group_type, type_name, group_name, label, settings, weight)".
      " VALUES ('%s', '%s', '%s', '%s', '%s', %d)", $group['group_type'], $type_name, $group['group_name'], $group['label'], serialize($group['settings']), $group['weight']);
    cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
    return SAVED_NEW;
  }
  else {
    db_query("UPDATE {". fieldgroup_tablename() ."} SET group_type = '%s', label = '%s', settings = '%s', weight = %d ".
             "WHERE type_name = '%s' AND group_name = '%s'",
             $group['group_type'], $group['label'], serialize($group['settings']), $group['weight'], $type_name, $group['group_name']);
    cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
    return SAVED_UPDATED;
  }
}

function fieldgroup_update_fields($form_values) {
  $default = _fieldgroup_field_get_group($form_values['type_name'], $form_values['field_name']);

  if ($default != $form_values['group']) {
    if ($form_values['group'] && !$default) {
      db_query("INSERT INTO {". fieldgroup_fields_tablename() ."} (type_name, group_name, field_name) VALUES ('%s', '%s', '%s')", $form_values['type_name'], $form_values['group'], $form_values['field_name']);
    }
    elseif ($form_values['group']) {
      db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET group_name = '%s' WHERE type_name = '%s' AND field_name = '%s'", $form_values['group'], $form_values['type_name'], $form_values['field_name']);
    }
    else {
      db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $form_values['type_name'], $form_values['field_name']);
    }
    cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
  }
}

function fieldgroup_delete($content_type, $group_name) {
  db_query("DELETE FROM {". fieldgroup_tablename() ."} WHERE  type_name = '%s' AND group_name = '%s'", $content_type, $group_name);
  db_query("DELETE FROM {". fieldgroup_fields_tablename() ."} WHERE  type_name = '%s' AND group_name = '%s'", $content_type, $group_name);
  cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
}

/**
 * Format a fieldgroup using a 'fieldset'.
 *
 * Derived from core's theme_fieldset, with no output if the content is empty.
 */
function theme_fieldgroup_fieldset($element) {
  if (empty($element['#children']) && empty($element['#value'])) {
    return '';
  }

  if ($element['#collapsible']) {
    drupal_add_js('misc/collapse.js');

    if (!isset($element['#attributes']['class'])) {
      $element['#attributes']['class'] = '';
    }

    $element['#attributes']['class'] .= ' collapsible';
    if ($element['#collapsed']) {
      $element['#attributes']['class'] .= ' collapsed';
    }
  }
  return '<fieldset'. drupal_attributes($element['#attributes']) .'>'. ($element['#title'] ? '<legend>'. $element['#title'] .'</legend>' : '') . (isset($element['#description']) && $element['#description'] ? '<div class="description">'. $element['#description'] .'</div>' : '') . (!empty($element['#children']) ? $element['#children'] : '') . (isset($element['#value']) ? $element['#value'] : '') ."</fieldset>\n";
}


/**
 * Process variables for fieldgroup.tpl.php.
 *
 * The $variables array contains the following arguments:
 * - $group_name
 * - $group_name_css
 * - $label
 * - $description
 * - $content
 *
 * @see fieldgroup-simple.tpl.php
 */
function template_preprocess_fieldgroup_simple(&$vars) {
  $element = $vars['element'];

  $vars['group_name'] = $element['#group_name'];
  $vars['group_name_css'] = strtr($element['#group_name'], '_', '-');
  $vars['label'] = isset($element['#title']) ? $element['#title'] : '';;
  $vars['description'] = isset($element['#description']) ? $element['#description'] : '';;
  $vars['content'] = isset($element['#children']) ? $element['#children'] : '';
  $vars['template_files'] = array(
    'fieldgroup-simple-',
    'fieldgroup-simple-'. $element['#group_name'],
    'fieldgroup-simple-'. $element['#node']->type,
    'fieldgroup-simple-'. $element['#group_name'] .'-'. $element['#node']->type,
  );
}

/**
 * Theme preprocess function for node.
 *
 * Adds $GROUP_NAME_rendered variables,
 * containing the themed output for the whole group.
 */
function fieldgroup_preprocess_node(&$vars) {
  $node = $vars['node'];

  foreach (fieldgroup_groups($node->type) as $group_name => $group) {
    // '#chilren' might not be set if the group is empty.
    $vars[$group_name .'_rendered'] = isset($node->content[$group_name]['#children']) ? $node->content[$group_name]['#children'] : '';
  }
}
