<?php

/**
 * @file
 * This module provides a set of fields that can be used to store
 * tabular data with a node. The implementation uses a custom CCK widget.
 */

/**
 * @todo should we create a helper function for sanitization?
 *  - we should see if it makes sense to sanitize on load as well as view
 */

/**
 * Implements hook_menu().
 */
function tablefield_menu() {
  return array(
    'tablefield/export/%/%/%/%/%' => array(
      'page callback' => 'tablefield_export_csv',
      'page arguments' => array(2, 3, 4, 5, 6),
      'title' => 'Export Table Data',
      'access arguments' => array('export tablefield'),
    ),
    'admin/config/content/tablefield' => array(
      'page callback' => 'drupal_get_form',
      'page arguments' => array('tablefield_admin_settings_form'),
      'title' => 'tablefield',
      'description' => 'Global configuration for the tablefield module',
      'access arguments' => array('configure tablefield'),
    ),
  );
}

/**
 * Menu callback to prepare administration configuration form.
 */
function tablefield_admin_settings_form() {
  $form = array();

  $form['tablefield_csv_separator'] = array(
    '#type' => 'textfield',
    '#title' => t('CSV separator'),
    '#size' => 1,
    '#maxlength' => 1,
    '#default_value' => variable_get('tablefield_csv_separator', ','),
    '#description' => t('Select the separator for the CSV import/export.'),
  );

  return system_settings_form($form);
}

/**
 * Validation hook for tablefield_admin_settings_form form.
 *
 * @param array $form
 * @param array $form_state
 */
function tablefield_admin_settings_form_validate($form, &$form_state) {
  if (drupal_strlen($form_state['values']['tablefield_csv_separator']) !== 1) {
    $message = t('Separator must be one character only!');
    form_set_error('tablefield_csv_separator', $message);
  }
}

/**
 * Implements hook_permission().
 */
function tablefield_permission() {
  return array(
    'export tablefield' => array(
      'title' => t('Export Tablefield Data as CSV'),
    ),
    'rebuild tablefield' => array(
      'title' => t('Rebuild any tablefield'),
    ),
    'configure tablefield' => array(
      'title' => t('Allow changes in the global tablefield module configuration'),
    ),
  );
}

/**
 * Menu callback to export a table as a CSV.
 *
 * @param String $entity_type
 *  The type of entity, e.g. node.
 * @param String $entity_id
 *  The id of the entity.
 * @param String $field_name
 *  The machine name of the field to load.
 * @param String $langcode
 *  The language code specified.
 * @param String $delta
 *  The field delta to load.
 */
function tablefield_export_csv($entity_type, $entity_id, $field_name, $langcode, $delta) {
  $filename = sprintf('%s_%s_%s_%s_%s.csv', $entity_type, $entity_id, $field_name, $langcode, $delta);
  $uri = 'temporary://' . $filename;

  // Attempt to load the entity.
  $ids = array($entity_id);
  $entity = entity_load($entity_type, $ids);
  $entity = array_pop($entity);

  // Ensure that the data is available and that we can load a
  // temporary file to stream the data.
  if (isset($entity->{$field_name}[$langcode][$delta]['value']) && $fp = fopen($uri, 'w+')) {
    $table = tablefield_rationalize_table(unserialize($entity->{$field_name}[$langcode][$delta]['value']));

    // Save the data as a CSV file.
    foreach ($table as $row) {
      fputcsv($fp, $row, variable_get('tablefield_csv_separator', ','));
    }

    fclose($fp);

    // Add basic HTTP headers.
    $http_headers = array(
      'Content-Type' => 'text/csv',
      'Content-Disposition' => 'attachment; filename="' . $filename . '"',
      'Content-Length' => filesize($uri),
    );

    // IE needs special headers.
    if(strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
      $http_headers['Cache-Control'] = 'must-revalidate, post-check=0, pre-check=0';
      $http_headers['Pragma'] = 'public';
    }
    else {
      $http_headers['Pragma'] = 'no-cache';
    }

    // Stream the download.
    file_transfer($uri, $http_headers);
  }

  // Something went wrong.
  drupal_add_http_header('Status', '500 Internal Server Error');
  print t('Error generating CSV.');
  drupal_exit();
}

/**
 * Implements hook_field_info().
 */
function tablefield_field_info() {
  return array(
    'tablefield' => array(
      'label' => t('Table Field'),
      'description' => t('Stores a table of text fields'),
      'default_widget' => 'tablefield',
      'default_formatter' => 'default',
    ),
  );
}

/**
 * Implements hook_field_settings_form().
 */
function tablefield_field_settings_form($field, $instance, $has_data) {
  $form = array();
  $form['export'] = array(
    '#type' => 'checkbox',
    '#title' => 'Allow users to export table data as CSV',
    '#default_value' => isset($field['settings']['export']) ? $field['settings']['export'] : FALSE,
  );
  $form['restrict_rebuild'] = array(
    '#type' => 'checkbox',
    '#title' => 'Restrict rebuilding to users with the permission "rebuild tablefield"',
    '#default_value' => isset($field['settings']['restrict_rebuild']) ? $field['settings']['restrict_rebuild'] : FALSE,
  );
  $form['lock_values'] = array(
    '#type' => 'checkbox',
    '#title' => 'Lock table field defaults from further edits during node add/edit.',
    '#default_value' => isset($field['settings']['lock_values']) ? $field['settings']['lock_values'] : FALSE,
  );
  $form['cell_processing'] = array(
    '#type' => 'radios',
    '#title' => t('Table cell processing'),
    '#default_value' => isset($field['settings']['cell_processing']) ? $field['settings']['cell_processing'] : 0,
    '#options' => array(
      t('Plain text'),
      t('Filtered text (user selects input format)')
    ),
  );
  $form['default_message'] = array(
    '#type' => 'markup',
    '#value' => t('To specify a default table, use the &quot;Default Value&quot; above. There you can specify a default number of rows/columns and values.'),
  );

  return $form;
}

/**
 * Implements hook_field_schema().
 */
function tablefield_field_schema($field) {
  return array(
    'columns' => array(
      'value' => array(
        'type' => 'text',
        'size' => 'big',
      ),
      'format' => array(
        'type' => 'varchar',
        'length' => 255,
        'default value' => '',
      ),
    ),
  );
}

/**
 * Implements hook_field_presave().
 */
function tablefield_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $delta => $table) {
    if (empty($table['value'])) {
      $tablefield = array();
      foreach ($table['tablefield'] as $key => $value) {
        $tablefield[$key] = $value;
      }
      $items[$delta]['value'] = serialize($tablefield);
    }
    elseif (empty($table['tablefield'])) {
      // Batch processing only provides the 'value'
      $items[$delta]['tablefield'] = unserialize($items[$delta]['value']);
    }
  }
}

/**
 * Implements hook_field_validate().
 */
function tablefield_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  drupal_add_css(drupal_get_path('module', 'tablefield') . '/tablefield.css');
  // Catch empty form submissions for required tablefields
  if ($instance['required'] && isset($items[0]) && tablefield_field_is_empty($items[0], $field)) {
    $message = t('@field is a required field.', array('@field' => $instance['label']));
    $errors[$field['field_name']][$langcode][0]['tablefield'][] = array(
      'error' => 'empty_required_tablefield',
      'message' => $message,
    );
  }
}

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


/**
 * Implements hook_field_load().
 */
function tablefield_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  foreach ($items as $delta => $table) {
    if (isset($table[0]['value'])) {
      $items[$delta][0]['tabledata'] = tablefield_rationalize_table(unserialize($table[0]['value']));
    }
  }
}

/**
 * Implementation of hook_field_is_empty().
 */
function tablefield_field_is_empty($item, $field) {
  // @todo, is this the best way to mark the default value form?
  // if we don't, it won't save the number of rows/cols
  // Allow the system settings form to have an emtpy table
  $arg0 = arg(0);
  if ($arg0 == 'admin') {
    return FALSE;
  }

  // Remove the preference fields to see if the table cells are all empty
  unset($item['tablefield']['rebuild']);
  unset($item['tablefield']['import']);
  if (!empty($item['tablefield'])) {
    foreach ($item['tablefield'] as $cell) {
      if (!empty($cell)) {
        return FALSE;
      }
    }
  }

  return TRUE;
}

/**
 * Implementation of hook_field_formatter_info().
 */
function tablefield_field_formatter_info() {
  return array(
    'default' => array(
      'label' => t('Tabular View'),
      'field types' => array('tablefield'),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function tablefield_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
  $settings = $display['settings'];
  $formatter = $display['type'];

  foreach ($items as $delta => $table) {

    // Rationalize the stored data
    if (!empty($table['tablefield'])) {
      $tabledata = tablefield_rationalize_table($table['tablefield']);
    }
    elseif (!empty($table['value'])) {
      $tabledata = tablefield_rationalize_table(unserialize($table['value']));
    }

    // Run the table through input filters
    if (isset($tabledata)) {
      if (!empty($tabledata)) {
        foreach ($tabledata as $row_key => $row) {
          foreach ($row as $col_key => $cell) {
            if (!empty($table['format'])) {
              $tabledata[$row_key][$col_key] = array('data' => check_markup($cell, $table['format']), 'class' => array('row_' . $row_key, 'col_' . $col_key));
            }
            else {
              $tabledata[$row_key][$col_key] = array('data' => check_plain($cell), 'class' => array('row_' . $row_key, 'col_' . $col_key));
            }
          }
        }
      }

      // Pull the header for theming
      $header_data = array_shift($tabledata);

      // Check for an empty header, if so we don't want to theme it.
      $noheader = TRUE;
      foreach ($header_data as $cell) {
        if (strlen($cell['data']) > 0) {
          $noheader = FALSE;
          break;
        }
      }

      $header = $noheader ? NULL : $header_data;

      $entity_info = entity_get_info($entity_type);
      $entity_id = !empty($entity_info['entity keys']['id']) ? $entity->{$entity_info['entity keys']['id']} : NULL;

      // Theme the table for display
      $element[$delta]['#markup'] = theme('tablefield_view', array(
        'header' => $header,
        'rows' => $tabledata,
        'delta' => $delta,
        'export' => isset($field['settings']['export']) ? $field['settings']['export'] : NULL,
        'entity_type' => $entity_type,
        'entity_id' => $entity_id,
        'field_name' => $field['field_name'],
        'langcode' => $langcode,
      ));
    }

  }
  return $element;
}

/**
 * Implements hook_field_widget_info().
 */
function tablefield_field_widget_info() {
  return array(
    'tablefield' => array(
      'label' => t('Table field'),
      'field types' => array('tablefield'),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
        'default value' => FIELD_BEHAVIOR_DEFAULT,
      ),
    ),
  );
}

/**
 * Implements hook_widget_form().
 */
function tablefield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  drupal_add_css(drupal_get_path('module', 'tablefield') . '/tablefield.css');

  $element['#type'] = 'tablefield';
  $form['#attributes']['enctype'] = 'multipart/form-data';

  // Establish a list of saved/submitted/default values
  if (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta) {
    // Rebuilding table rows/cols
    $default_value = tablefield_rationalize_table($form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']);
  }
  elseif (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield-import-button-' . $field['field_name'] . '-' . $delta) {
    // Importing CSV data
    tablefield_import_csv($form, $form_state);
    $default_value = tablefield_rationalize_table($form_state['input'][$field['field_name']][$langcode][$delta]['tablefield']);
  }
  elseif ($form_state['submitted'] && isset($items[$delta]) && isset($items[$delta]['tablefield'])) {
    // A form was submitted
    $default_value = tablefield_rationalize_table($items[$delta]['tablefield']);
  }
  elseif (isset($items[$delta]['value'])) {
    // Default from database (saved node)
    $default_value = tablefield_rationalize_table(unserialize($items[$delta]['value']));
  }
  else {
    // After the first table, we don't want to populate the values in the table
    if ($delta > 0) {
      tablefield_delete_table_values($instance['default_value'][0]['tablefield']);
    }

    // Get the widget default value
    if(!empty($form_state['input'][$field['field_name']][$langcode][$delta]['tablefield'])) {
      $default_value = tablefield_rationalize_table($form_state['input'][$field['field_name']][$langcode][$delta]['tablefield']);
    } else {
      $default_value = tablefield_rationalize_table($instance['default_value'][0]['tablefield']);
    }

    $default_count_cols = isset($items[0]['tablefield']['rebuild']['count_cols']) ? $items[0]['tablefield']['rebuild']['count_cols'] : 5;
    $default_count_rows = isset($items[0]['tablefield']['rebuild']['count_cols']) ? $items[0]['tablefield']['rebuild']['count_cols'] : 5;
  }

  if (!empty($instance['description'])) {
    $help_text = $instance['description'];
  }
  else {
    $help_text = t('The first row will appear as the table header. Leave the first row blank if you do not need a header.');
  }

  $element['tablefield'] = array(
    '#title' => $element['#title'],
    '#description' => $help_text,
    '#attributes' => array('id' => 'form-tablefield-' . $field['field_name'] . '-' . $delta, 'class' => array('form-tablefield')),
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#collapsible' => FALSE,
    '#prefix' => '<div id="tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper">',
    '#suffix' => '</div>',
  );

  // Give the fieldset the appropriate class if it is required
  if ($element['#required']) {
    $element['tablefield']['#title'] .= ' <span class="form-required" title="' . t('This field is required') . '">*</span>';
  }

  $arg0 = arg(0);
  if ($arg0 == 'admin') {
    $element['tablefield']['#description'] = t('This form defines the table field defaults, but the number of rows/columns and content can be overridden on a per-node basis. The first row will appear as the table header. Leave the first row bland if you do not need a header.');
  }

  // Determine how many rows/columns are saved in the data
  if (!empty($default_value)) {
    $count_rows = count($default_value);
    $count_cols = 0;
    foreach ($default_value as $row) {
      $temp_count = count($row);
      if ($temp_count > $count_cols) {
        $count_cols = $temp_count;
      }
    }
  }
  else {
    $count_rows = count($default_value);
    $count_cols = isset($default_count_cols) ? $default_count_cols : 0;
    $count_rows = isset($default_count_rows) ? $default_count_rows : 0;
  }

  // Override the number of rows/columns if the user rebuilds the form.
  if (isset($form_state['clicked_button']['#value']) && $form_state['clicked_button']['#name'] == 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta) {
    $count_cols = $form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']['rebuild']['count_cols'];
    $count_rows = $form_state['tablefield_rebuild'][$field['field_name']][$langcode][$delta]['tablefield']['rebuild']['count_rows'];

    drupal_set_message(t('Table structure rebuilt.'), 'status', FALSE);
  }

  // Render the form table
  $element['tablefield']['a_break'] = array(
    '#markup' => '<table>',
  );
  for ($i = 0; $i < $count_rows; $i++) {
    $zebra = $i % 2 == 0 ? 'even' : 'odd';
    $element['tablefield']['b_break' . $i] = array(
      '#markup' => '<tr class="tablefield-row-' . $i . ' ' . $zebra . '">',
    );
    for ($ii = 0; $ii < $count_cols; $ii++) {
      $instance_default = isset($instance['default_value'][$delta]['tablefield']["cell_{$i}_{$ii}"]) ? $instance['default_value'][$delta]['tablefield']["cell_{$i}_{$ii}"] : array();
      if (!empty($instance_default) && !empty($field['settings']['lock_values']) && $arg0 != 'admin') {
        // The value still needs to be send on every load in order for the
        // table to be saved correctly.
        $element['tablefield']['cell_' . $i . '_' . $ii] = array(
          '#type' => 'value',
          '#value' => $instance_default,
        );
        // Display the default value, since it's not editable.
        $element['tablefield']['cell_' . $i . '_' . $ii . '_display'] = array(
          '#type' => 'item',
          '#title' => $instance_default,
          '#prefix' => '<td style="width:' . floor(100/$count_cols) . '%">',
          '#suffix' => '</td>',
        );
      }
      else {
        $cell_default = isset($default_value[$i][$ii]) ? $default_value[$i][$ii] : '';
        $element['tablefield']['cell_' . $i . '_' . $ii] = array(
          '#type' => 'textfield',
          '#maxlength' => 2048,
          '#size' => 0,
          '#attributes' => array(
            'id' => 'tablefield_' . $delta . '_cell_' . $i . '_' . $ii,
            'class' => array('tablefield-row-' . $i, 'tablefield-col-' . $ii),
            'style' => 'width:100%'
          ),
          '#default_value' => (empty($field_value)) ? $cell_default : $field_value,
          '#prefix' => '<td style="width:' . floor(100/$count_cols) . '%">',
          '#suffix' => '</td>',
        );
      }
    }
    $element['tablefield']['c_break' . $i] = array(
      '#markup' => '</tr>',
    );
  }
  $element['tablefield']['t_break' . $i] = array(
    '#markup' => '</table>',
  );

  // If the user doesn't have rebuild perms, we pass along the data as a value.
  // Otherwise, we will provide form elements to specify the size and ajax rebuild.
  if (isset($field['settings']['restrict_rebuild']) && $field['settings']['restrict_rebuild'] && !user_access('rebuild tablefield')) {
    $element['tablefield']['rebuild'] = array (
      '#type' => 'value',
      '#tree' => TRUE,
      'count_cols' => array(
        '#type' => 'value',
        '#value' => $count_cols,
      ),
      'count_rows' => array(
        '#type' => 'value',
        '#value' => $count_rows,
      ),
      'rebuild' => array(
        '#type' => 'value',
        '#value' => t('Rebuild Table'),
      ),
    );
  }
  else {
    $element['tablefield']['rebuild'] = array(
      '#type' => 'fieldset',
      '#tree' => TRUE,
      '#title' => t('Change number of rows/columns.'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $element['tablefield']['rebuild']['count_cols'] = array(
      '#title' => t('How many Columns'),
      '#type' => 'textfield',
      '#size' => 5,
      '#prefix' => '<div class="clearfix">',
      '#suffix' => '</div>',
      '#value' => $count_cols,
    );
    $element['tablefield']['rebuild']['count_rows'] = array(
      '#title' => t('How many Rows'),
      '#type' => 'textfield',
      '#size' => 5,
      '#prefix' => '<div class="clearfix">',
      '#suffix' => '</div>',
      '#value' => $count_rows,
    );
    $element['tablefield']['rebuild']['rebuild'] = array(
      '#type' => 'button',
      '#validate' => array(),
      '#limit_validation_errors' => array(),
      '#executes_submit_callback' => TRUE,
      '#submit' => array('tablefield_rebuild_form'),
      '#value' => t('Rebuild Table'),
      '#name' => 'tablefield_rebuild_' . $field['field_name'] . '_' . $delta,
      '#attributes' => array(
        'class' => array('tablefield-rebuild'),
      ),
      '#ajax' => array(
        'callback' => 'tablefield_rebuild_form_ajax',
        'wrapper' => 'tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper',
        'effect' => 'fade',
      ),
    );
  }

  // Allow the user to import a csv file
  $element['tablefield']['import'] = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#title' => t('Import from CSV'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $element['tablefield']['import']['tablefield_csv_' . $field['field_name'] . '_' . $delta] = array(
    '#name' => 'files[' . $field['field_name'] . '_' . $delta . ']',
    '#title' => 'File upload',
    '#type' => 'file',
  );

  $element['tablefield']['import']['rebuild_' . $field['field_name'] . '_' . $delta] = array(
    '#type' => 'button',
    '#validate' => array(),
    '#limit_validation_errors' => array(),
    '#executes_submit_callback' => TRUE,
    '#submit' => array('tablefield_rebuild_form'),
    '#value' => t('Upload CSV'),
    '#name' => 'tablefield-import-button-' . $field['field_name'] . '-' . $delta,
    '#attributes' => array(
      'class' => array('tablefield-rebuild'),
      //'id' => 'tablefield-import-button-' . $field['field_name'] . '-' . $delta,
    ),
    '#ajax' => array(
      'callback' => 'tablefield_rebuild_form_ajax',
      'wrapper' => 'tablefield-' . $field['field_name'] . '-' . $delta . '-wrapper',
      'effect' => 'fade',
      'event' => 'click'
    ),
  );


  // Allow the user to select input filters
  if (!empty($field['settings']['cell_processing'])) {
    $element['#base_type'] = $element['#type'];
    $element['#type'] = 'text_format';
    $element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL;
  }

  return $element;
}

/**
 * Helper function to import data from a CSV file
 * @param array $form
 * @param array $form_state
 */
function tablefield_import_csv($form, &$form_state) {
  // Look for the field name by checking on the clicked button
  if(preg_match('/tablefield-import-button-(.*)$/', $form_state['clicked_button']['#name'], $id)) {
    // Extract the field and file name from the id of the clicked button
    $file_name = preg_replace('/\-/', '_', $id[1]);
    preg_match('/_([0-9]+)$/', $file_name, $field_delta);
    // Extract the field delta from the field name
    $delta = $field_delta[1];
    $field_name = preg_replace('/' . $field_delta[0] . '/', '', $file_name);
    $language = isset($form[$field_name]['#language']) ? $form[$field_name]['#language'] : 'und';
    $file = file_save_upload($file_name, array('file_validate_extensions' => array('csv')));

    if (is_object($file)) {
      if (($handle = fopen($file->uri, "r")) !== FALSE) {
        tablefield_delete_table_values($form_state['values'][$field_name][$language][$delta]['tablefield']);
        tablefield_delete_table_values($form_state['input'][$field_name][$language][$delta]['tablefield']);

        // Populate CSV values
        $max_col_count = 0;
        $row_count = 0;
        while (($csv = fgetcsv($handle, 0, variable_get('tablefield_csv_separator', ','))) !== FALSE) {
          $col_count = count($csv);
          foreach ($csv as $col_id => $col) {
            $form_state['input'][$field_name][$language][$delta]['tablefield']['cell_' . $row_count . '_' . $col_id] =
            $form_state['values'][$field_name][$language][$delta]['tablefield']['cell_' . $row_count . '_' . $col_id] = $col;
          }
          $max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
          $row_count++;
        }
        fclose($handle);

        $form_state['input'][$field_name][$language][$delta]['tablefield']['rebuild']['count_cols'] =
        $form_state['values'][$field_name][$language][$delta]['tablefield']['rebuild']['count_cols'] = $max_col_count;
        $form_state['input'][$field_name][$language][$delta]['tablefield']['rebuild']['count_rows'] =
        $form_state['values'][$field_name][$language][$delta]['tablefield']['rebuild']['count_rows'] = $row_count;

        drupal_set_message(t('Successfully imported @file', array('@file' => $file->filename)));
      }
      else {
        drupal_set_message(t('There was a problem importing @file.', array('@file' => $file->filename)));
      }
    }
  }
}

/**
 * Helper function to remove all values in a particular table
 * @param array $tablefield
 */
function tablefield_delete_table_values(&$tablefield) {
  // Empty out previously entered values
  foreach ($tablefield as $key => $value) {
    if (strpos($key, 'cell_') === 0) {
      $tablefield[$key] = '';
    }
  }
}

/**
 * AJAX callback to rebuild the number of rows/columns.
 * The basic idea is to descend down the list of #parent elements of the
 * clicked_button in order to locate the tablefield inside of the $form
 * array. That is the element that we need to return.
 * @param array $form
 * @param array $form_state
 */
function tablefield_rebuild_form_ajax($form, $form_state) {
  $rebuild = $form;
  $parents = $form_state['clicked_button']['#parents'];

  if ($form['#id'] == 'field-ui-field-edit-form') {
    $rebuild = $form['instance']['default_value_widget'][$parents[0]];
  }
  else {
    foreach ($parents as $parent) {
      // Locate the delta of the field - 0 deltas need to break after
      // descending to the 'rebuild' element, but deltas greater than
      // 0 need to break /before/ adding the 'rebuild' element.
      if (is_int($parent)) {
        $delta = $parent;
      }
      $tmp = $rebuild;

      if ($parent == 'rebuild' || $parent == 'import') {
        $rebuild = $delta == 0 ? $tmp[$parent] : $tmp;
        //$rebuild = $tmp[$parent];
        break;
      }
      $rebuild = $tmp[$parent];
    }
  }

  // We don't want to re-send the format/_weight options.
  unset($rebuild['format']);
  unset($rebuild['_weight']);

  // We need to avoid sending headers or the multipart form
  // will make it fail. So, we need to explicitly define the
  // whole response to ajax_deliver().
  return array(
    '#type' => 'ajax',
    '#header' => FALSE,
    '#commands' => array(
      ajax_command_insert(NULL, drupal_render($rebuild)),
      ajax_command_prepend(NULL, theme('status_messages')),
    ),
  );

}

/**
 * Helper function to rebuild the table structure w/o submitting the form.
 * @param array $form
 * @param array $form_state
 */
function tablefield_rebuild_form($form, &$form_state) {
  // Maintain the tablefield data.
  $form_state['tablefield_rebuild'] = $form_state['input'];
  $form_state['rebuild'] = true;
}

/**
 * Helper function to turn form elements into a structured array.
 *
 * @param array $tablefield
 *  The table as it appears in FAPI.
 */
function tablefield_rationalize_table($tablefield) {
  $tabledata = array();

  // Rationalize the table data
  if (!empty($tablefield)) {
    // Remove exterraneous form data
    $count_cols = $tablefield['rebuild']['count_cols'];
    $count_rows = $tablefield['rebuild']['count_rows'];
    unset($tablefield['rebuild']);
    unset($tablefield['import']);

    foreach ($tablefield as $key => $value) {
      preg_match('/cell_(.*)_(.*)/', $key, $cell);
      // $cell[1] is row count $cell[2] is col count
      if ((int) $cell[1] < $count_rows && (int) $cell['2'] < $count_cols) {
        $tabledata[$cell[1]][$cell[2]] = $value;
      }
    }
  }

  return $tabledata;
}

/**
 * Implements hook_theme().
 */
function tablefield_theme() {
  return array(
    'tablefield_view' => array(
      'variables' => array(
        'header' => NULL,
        'rows' => NULL,
        'delta' => NULL,
        'export' => NULL,
        'entity_type' => NULL,
        'entity_id' => NULL,
        'field_name' => NULL,
        'langcode' => NULL,
      ),
    ),
  );
}

/**
 * Theme function for table view
 */
function theme_tablefield_view($variables) {
  $attributes = array(
    'id' => 'tablefield-' . $variables['delta'],
    'class' => array(
      'tablefield'
    ),
  );

  // If the user has access to the csv export option, display it now.
  $export = '';
  if ($variables['export'] && user_access('export tablefield')) {
    $url = sprintf('tablefield/export/%s/%s/%s/%s/%s', $variables['entity_type'], $variables['entity_id'], $variables['field_name'], $variables['langcode'], $variables['delta']);
    $export = '<div id="tablefield-export-link-' . $variables['delta'] . '" class="tablefield-export-link">' . l(t('Export Table Data'), $url) . '</div>';
  }

  return '<div id="tablefield-wrapper-' . $variables['delta'] . '" class="tablefield-wrapper">'
    . theme('table',
      array(
        'header' => $variables['header'],
        'rows' => $variables['rows'],
        'attributes' => $attributes,
      )
    )
    . $export
    . '</div>';
}
