<?php
/**
 * @file
 * Provides a set of fields that can be used to store tabular data with a node.
 *
 * @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);
}

/**
 * Form validation handler for tablefield_admin_settings_form().
 *
 * @see tablefield_admin_settings_form()
 */
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' => 'tablefield_default',
      'property_type' => 'tablefield',
      'property_callbacks' => array('tablefield_property_info_callback'),
    ),
  );
}

/**
 * Defines info for the properties of the tablefield field data structure.
 */
function tablefield_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';
  unset($property['query callback']);
}

/**
 * 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 header so default values cannot be changed.',
    '#default_value' => isset($field['settings']['lock_values']) ? $field['settings']['lock_values'] : FALSE,
  );
  $form['hide_headers'] = array(
    '#type' => 'checkbox',
    '#title' => 'Hide table header row.',
    '#default_value' => isset($field['settings']['hide_headers']) ? $field['settings']['hide_headers'] : 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_prepare_view().
 */
function tablefield_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  $instance = reset($instances); // This isn't used, hack here is okay.
  foreach ($entities as $id => $entity) {
    $entity_items = &$items[$id];
    tablefield_field_presave($entity_type, $entity, $field, $instance, $langcode, $entity_items);
  }
}

/**
 * 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();
      if (!empty($table['tablefield'])) {
        $rows_max = $table['tablefield']['rebuild']['count_rows'];
        $cols_max = $table['tablefield']['rebuild']['count_cols'];
        $count_rows = 0;
        $count_cols = 0;

        // Build an array we can sort by weight out of the data.
        $meta = array();
        $tablified_data = array();
        foreach ($table['tablefield'] as $key => $value) {
          if (substr($key, 0, 5) !== 'cell_') {
            // Save these for later.
            $meta[$key] = $value;
          }
          else {
            if (strstr($key, '_weight')) {
              // To use this for sorting, we call it's key "weight".
              $tablified_data[$count_rows]['weight'] = $value;
            }
            else {
              if ($count_cols == $cols_max) {
                $count_rows++;
                $count_cols = 0;
              }
              $tablified_data[$count_rows]['data'][$count_cols] = $value;
              $count_cols++;
            }
          }
        }

        // Sort by weight.
        uasort($tablified_data, 'drupal_sort_weight');

        // Put the data in the desired order before saving.
        $row_counter = $col_counter = 0;
        foreach ($tablified_data as $row) {
          foreach ($row['data'] as $cell) {
            $tablefield['cell_' . $row_counter . '_' . $col_counter] = $cell;
            $col_counter++;
          }
          $row_counter++;
          $col_counter = 0;
        }

        // Add the non-value data back in before we save.
        $tablefield = array_merge($tablefield, $meta);
      }
      $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) {
  // 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) {
    foreach ($table as $id => $field){
      if (isset($field['value'])) {
        $items[$delta][$id]['tabledata'] = tablefield_rationalize_table(unserialize($field['value']));
      }
    }
  }
}

/**
 * Implements 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;
  }

  // Check for already serialized data. This is the case for other language
  // versions of this field.
  if (!empty($item['tabledata'])) {
    return FALSE;
  }

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

  return TRUE;
}

/**
 * Implements hook_field_formatter_info().
 */
function tablefield_field_formatter_info() {
  return array(
    'tablefield_default' => array(
      'label' => t('Tabular View'),
      'field types' => array('tablefield'),
      'settings' => array(
        'trim_trailing_rows' => FALSE,
        'trim_trailing_cols' => FALSE,
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function tablefield_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  if ($display['type'] === 'tablefield_default') {
    $trim_trailing_rows = empty($settings['trim_trailing_rows']) ? t('No') : t('Yes');
    $trim_trailing_cols = empty($settings['trim_trailing_cols']) ? t('No') : t('Yes');
    return t('Trim empty trailing rows: %tr', array('%tr' => $trim_trailing_rows)) . '<br>' . t('Trim empty trailing columns: %tr', array('%tr' => $trim_trailing_cols));
  }
  return '';
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function tablefield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();
  if ($display['type'] === 'tablefield_default') {
    $element['trim_trailing_rows'] = array(
      '#title' => t('Trim empty trailing rows'),
      '#type' => 'checkbox',
      '#default_value' => $settings['trim_trailing_rows'],
    );
    $element['trim_trailing_cols'] = array(
      '#title' => t('Trim empty trailing columns'),
      '#type' => 'checkbox',
      '#default_value' => $settings['trim_trailing_cols'],
    );
  }
  return $element;
}

/**
 * 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) {
    // Check for table caption.
    $raw = unserialize($table['value']);
    $caption = isset($raw['caption'])? check_plain($raw['caption']): '';

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

    // Run the table through input filters.
    if (isset($tabledata)) {
      if (!empty($tabledata)) {

        if (!empty($settings['trim_trailing_rows'])) {
          $tabledata = tablefield_rtrim_rows($tabledata);
        }

        if (!empty($settings['trim_trailing_cols'])) {
          $tabledata = tablefield_rtrim_cols($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),
              );
            }
          }
        }
      }

      // Check for an empty header, if so we don't want to theme it.
      $noheader = TRUE;
      if (empty($field['settings']['hide_headers'])) {
        $header_data = array_shift($tabledata);
        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] = array(
        '#theme' => 'tablefield_view',
        '#attributes' => array(
          'id' => 'tablefield-' . $delta,
          'class' => array('tablefield'),
        ),
        '#caption' => $caption,
        '#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,
        '#formatter' => $formatter,
      );
    }

  }
  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';

  /* Tablefield is sometimes embedded within another form by other modules, such
   * as Field Collection. Because of that, we cannot rely on field_name and
   * $delta to provide a unique ID for this element. Instead we use a
   * concatenation of the field parents along with the current field name,
   * language, delta and tablefield key.
   */
  $tablefield_parents = isset($element['#field_parents']) ? $element['#field_parents'] : array();
  array_push($tablefield_parents,
    $element['#field_name'],
    $element['#language'],
    $element['#delta'],
    'tablefield'
  );
  $id = drupal_clean_css_identifier(implode('-', $tablefield_parents));

  // IDs to use for various buttons/wrapper for this element. When processing
  // an AJAX request, these IDs are used to build the field stack so we know
  // where the value we're adjusting is in the FormAPI array.
  $ajax_wrapper_id = "$id-wrapper";
  $rebuild_id = "$id-rebuild";
  $import_id = "$id-import";
  $pasted_id = "$id-pasted";

  // Default table size.
  $default_size['count_cols'] = isset($instance['default_value'][0]['tablefield']['rebuild']['count_cols']) ? $instance['default_value'][0]['tablefield']['rebuild']['count_cols'] : 5;
  $default_size['count_rows'] = isset($instance['default_value'][0]['tablefield']['rebuild']['count_rows']) ? $instance['default_value'][0]['tablefield']['rebuild']['count_rows'] : 5;

  // Default table cell values.
  $default_value = NULL;

  // If there's a triggering element get the values from form state.
  if (isset($form_state['triggering_element'])) {
    if ($form_state['triggering_element']['#name'] == $rebuild_id) {
      // Rebuilding table rows/cols.
      $default_value = drupal_array_get_nested_value($form_state['tablefield_rebuild'], $tablefield_parents);
      drupal_set_message(t('Table structure rebuilt.'), 'status', FALSE);
    }
    elseif ($form_state['triggering_element']['#name'] == $import_id) {
      // Importing CSV data.
      tablefield_import_csv($form, $form_state, $langcode, $import_id, $tablefield_parents);
      $default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
    }
    elseif ($form_state['triggering_element']['#name'] == $pasted_id) {
      // Importing pasted data
      tablefield_import_pasted($form, $form_state, $langcode, $pasted_id, $tablefield_parents);
      $default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
      if (empty($default_value['rebuild'])) {
        $default_value['rebuild'] = $default_size;
      }
    }
    else {
      // The triggering element is neither a rebuild nor an import
      // e.g. a file upload.
      $default_value = drupal_array_get_nested_value($form_state['input'], $tablefield_parents);
    }
  }

  // If no values by now, get tablefield item value stored in database, if any.
  if (!$default_value) {
    // This could be set e.g. when using paragraphs module.
    if (isset($items[$delta]['tablefield'])) {
      $default_value = $items[$delta]['tablefield'];
    }
    elseif (isset($items[$delta]['value'])) {
      $default_value = unserialize($items[$delta]['value']);
    }
    // If still nothing then get the instance default, but only for delta 0.
    elseif ($delta === 0 && isset($instance['default_value'][0]['tablefield'])) {
      $default_value = $instance['default_value'][0]['tablefield'];
    }
  }

  if (empty($default_value['rebuild'])) {
    $default_value['rebuild'] = $default_size;
  }
  else {
    $default_size = $default_value['rebuild'];
  }
  $default_value = tablefield_rationalize_table($default_value);

  $count_rows = $default_size['count_rows'];
  $count_cols = $default_size['count_cols'];

  // Now we can build the widget.
  if (!empty($instance['description'])) {
    $help_text = $instance['description'];
  }
  else {
    if ($field['settings']['hide_headers'] == TRUE) {
      $help_text = t('This table will not have a header.');
    }
    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' => filter_xss_admin($help_text),
    '#attributes' => array(
      'id' => $id,
      'class' => array('form-tablefield'),
    ),
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#collapsible' => FALSE,
    '#prefix' => '<div id="' . $ajax_wrapper_id . '">',
    '#suffix' => '</div>',
  );
  if (!empty($field['settings']['hide_headers'])) {
    $element['tablefield']['#attributes']['class'][] = 'table-no-headers';
  }

  // Give the fieldset the appropriate class if it is required.
  if ($element['#required']) {
    $element['tablefield']['#title'] .= ' <span class="form-required" title="';
    $element['tablefield']['#title'] .= t('This field is required');
    $element['tablefield']['#title'] .= '">*</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.');
    if (!$field['settings']['hide_headers']) {
      $element['tablefield']['#description'] .=  ' ' . t('The first row will appear as the table header. Leave the first row blank if you do not need a header.');
    }
  }

  // Render the form table.
  $element['tablefield']['a_break'] = array(
    '#markup' => '<table id="tablefield-editor">',
  );

  // Loop over all the rows.
  for ($i = 0; $i < $count_rows; $i++) {
    $zebra = $i % 2 == 0 ? 'even' : 'odd';
    $element['tablefield']['b_break' . $i] = array(
      '#markup' => '<tr class="draggable tablefield-row-' . $i . ' ' . $zebra . '"><td class="tablefield-row-count">' . ($i+1) . '</td>',
    );

    // Loop over all the columns.
    for ($ii = 0; $ii < $count_cols; $ii++) {
      $instance_default = array();
      if (isset($instance['default_value'][0]['tablefield']["cell_{$i}_{$ii}"])) {
        $instance_default =  $instance['default_value'][0]['tablefield']["cell_{$i}_{$ii}"];
      }
      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>',
          '#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' => $id . '-cell-' . $i . '-' . $ii,
            'class' => array('tablefield-row-' . $i, 'tablefield-col-' . $ii),
            'style' => 'min-width:100%',
          ),
          '#default_value' => $cell_default,
          '#prefix' => '<td>',
          '#suffix' => '</td>',
        );
      }
    }

    // Add an extra column for the weight.
    $row_weight_default = isset($default_value[$i]['weight']) ? $default_value[$i]['weight'] : ($i+1);
    $element['tablefield']['cell_' . $i . '_weight'] = array(
      '#type' => 'weight',
      '#title' => t('Weight'),
      '#default_value' => $row_weight_default,
      '#delta' => $count_rows,
      '#attributes' => array('class' => array('tablefield-weight')),
      '#prefix' => '<td class="tabledrag-hide">',
      '#suffix' => '</td>',
    );

    $element['tablefield']['c_break' . $i] = array(
      '#markup' => '</tr>',
    );
  }

  $element['tablefield']['t_break' . $i] = array(
    '#markup' => '</table>',
  );
  drupal_add_tabledrag('tablefield-editor', 'order', 'sibling', 'tablefield-weight');

  // Provide caption field to describe the data contained in the table.
  $element['tablefield']['caption'] = array(
    '#type' => 'textfield',
    '#title' => t('Table description'),
    '#default_value' => '',
    '#description' => t('This brief caption will be associated with the table and will help Assitive Technology, like screen readers, better describe the content within.'),
  );
  if (isset($items[$delta]['value'])) {
    $raw = unserialize($items[$delta]['value']);
    if (isset($raw['caption'])) {
      $element['tablefield']['caption']['#default_value'] = check_plain($raw['caption']);
    }
  }

  // If the user doesn't have rebuild perms, we pass along the data as a value.
  // Otherwise we 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' => $rebuild_id,
      '#attributes' => array(
        'class' => array('tablefield-rebuild'),
      ),
      '#ajax' => array(
        'callback' => 'tablefield_rebuild_form_ajax',
        'wrapper' => $ajax_wrapper_id,
        'effect' => 'fade',
      ),
    );
  }

  // Allow the user to import a csv file.
  $element['tablefield']['import'] = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#title' => t('Upload CSV file'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $element['tablefield']['import']['file'] = array(
    '#name' => 'files[' . $import_id . ']',
    '#title' => t('File upload'),
    '#type' => 'file',
  );
  $element['tablefield']['import']['import'] = array(
    '#type' => 'button',
    '#validate' => array(),
    '#limit_validation_errors' => array(),
    '#executes_submit_callback' => TRUE,
    '#submit' => array('tablefield_rebuild_form'),
    '#value' => t('Upload CSV'),
    '#name' => $import_id,
    '#attributes' => array(
      'class' => array('tablefield-rebuild'),
    ),
    '#ajax' => array(
      'callback' => 'tablefield_rebuild_form_ajax',
      'wrapper' => $ajax_wrapper_id,
      'effect' => 'fade',
    ),
  );

  // Allow user to paste data (e.g. from Excel)
  $element['tablefield']['paste'] = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#title' => t('Copy & Paste'),
    '#attributes' => array('class' => array('tablefield-extra tablefield-paste')),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $delimiters = array(
    'TAB' => 'TAB',
    ',' => 'Comma ,',
    ';' => 'Semicolon ;',
    '|' => 'Pipe |',
    '+' => 'Plus +',
    ':' => 'Colon :',
  );
  $element['tablefield']['paste']['paste_delimiter'] = array(
    '#type' => 'select',
    '#tree' => TRUE,
    '#title' => t('Column separator'),
    '#name' => 'delimiter[' . $pasted_id . ']',
    '#options' => $delimiters,
    '#description' => t('Data copied from Excel will use TAB.'),
  );
  $element['tablefield']['paste']['data'] = array(
    '#type' => 'textarea',
    '#tree' => TRUE,
    '#name' => 'data[' . $pasted_id . ']',
    '#title' => t('Paste table data here:'),
  );
  $element['tablefield']['paste']['paste_import'] = array(
    '#type' => 'button',
    '#validate' => array(),
    '#limit_validation_errors' => array(),
    '#executes_submit_callback' => TRUE,
    '#submit' => array('tablefield_rebuild_form'),
    '#value' => t('Import & Rebuild'),
    '#name' => $pasted_id,
    '#ajax' => array(
      'callback' => 'tablefield_rebuild_form_ajax',
      'wrapper' => $ajax_wrapper_id,
      'effect' => 'fade',
    ),
  );

  // 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;
    if (module_exists('wysiwyg')) {
      $element['#wysiwyg'] = FALSE;
    }
  }

  return $element;
}

/**
 * Helper function to import data from a CSV file.
 *
 * @param array $form
 *   The form structure.
 * @param array $form_state
 *   The current state of the form.
 * @param string $langcode
 *   The language associated with the form items.
 * @param string $file_form_field_name
 *   The key of the upload form element in the form array.
 * @param array $tablefield_parents
 *   The parents of the tablefield element.
 */
function tablefield_import_csv($form, &$form_state, $langcode, $file_form_field_name, $tablefield_parents) {
  // Extract the field and file name from the id of the clicked button.
  $file = file_save_upload($file_form_field_name, array('file_validate_extensions' => array('csv')));

  if (is_object($file)) {
    // Assure Mac/Linux EOL markers work with fgetcsv().
    $auto_detect_line_endings = ini_get('auto_detect_line_endings');
    ini_set('auto_detect_line_endings', TRUE);
    if (($handle = fopen($file->uri, "r")) !== FALSE) {
      tablefield_delete_table_values(drupal_array_get_nested_value($form_state['values'], $tablefield_parents));
      tablefield_delete_table_values(drupal_array_get_nested_value($form_state['input'], $tablefield_parents));

      // Populate CSV values.
      $max_col_count = 0;
      $row_count = 0;
      $imported_tablefield = array();
      while (($csv = fgetcsv($handle, 0, variable_get('tablefield_csv_separator', ','))) !== FALSE) {
        $col_count = count($csv);
        foreach ($csv as $col_id => $col) {
          $imported_tablefield['cell_' . $row_count . '_' . $col_id] = $col;
        }
        $max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
        $row_count++;
      }
      fclose($handle);
      ini_set('auto_detect_line_endings', $auto_detect_line_endings);
      $imported_tablefield['rebuild'] = array('count_cols' => $max_col_count, 'count_rows' => $row_count);
      drupal_array_set_nested_value($form_state['values'], $tablefield_parents, $imported_tablefield);
      drupal_array_set_nested_value($form_state['input'], $tablefield_parents, $imported_tablefield);

      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 import pasted data
 * @param array $form
 * @param array $form_state
 * @param string $langcode
 * @param string $file_form_field_name
 * @param array $tablefield_parents
 */
function tablefield_import_pasted($form, &$form_state, $langcode, $pasted_form_field_name, $tablefield_parents) {
  $data = $form_state['input']['data'][$pasted_form_field_name];
  if (!empty($data)) {
    // Empty the current table.
    tablefield_delete_table_values(drupal_array_get_nested_value($form_state['values'], $tablefield_parents));
    tablefield_delete_table_values(drupal_array_get_nested_value($form_state['input'], $tablefield_parents));

    // Get the delimiter.
    $col_delimiter = $form_state['input']['delimiter'][$pasted_form_field_name];
    if ($col_delimiter == 'TAB') {
      $col_delimiter = "\t";
    }

    // Populate table with pasted values.
    $max_col_count = 0;
    $row_count = 0;
    $imported_tablefield = array();
    $rows = explode(PHP_EOL, $data);
    foreach ($rows as $row_id => $row) {
      // Explode the current row into columns:
      $cols = explode($col_delimiter, $row);
      $col_count = count($cols);
      if ($col_count > 0) {
        foreach ($cols as $col_id => $col) {
          $imported_tablefield['cell_' . $row_count . '_' . $col_id] = $col;
        }
        $max_col_count = $col_count > $max_col_count ? $col_count : $max_col_count;
        $row_count++;
      }
    }

    // Rebuild the table if necessary, and save.
    $imported_tablefield['rebuild'] = array('count_cols' => $max_col_count, 'count_rows' => $row_count);
    drupal_array_set_nested_value($form_state['values'], $tablefield_parents, $imported_tablefield);
    drupal_array_set_nested_value($form_state['input'], $tablefield_parents, $imported_tablefield);

    drupal_set_message(t('Successfully imported pasted data.'));
  }
  else {
    drupal_set_message(t('There was a problem importing pasted data.'));
  }
}

/**
 * Helper function to remove all values in a particular table.
 *
 * @param array $tablefield
 *   The table as it appears in FAPI.
 */
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.
 */
function tablefield_rebuild_form_ajax($form, $form_state) {
  $parents = $form_state['triggering_element']['#array_parents'];

  // We do not want to go as deep as rebuild/rebuild or import/import,
  // i.e. the triggering buttons.
  array_pop($parents);
  array_pop($parents);

  $rebuild = drupal_array_get_nested_value($form, $parents);

  // 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 without submitting the form.
 */
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['caption']);
    unset($tablefield['rebuild']);
    unset($tablefield['import']);
    unset($tablefield['paste']);

    foreach ($tablefield as $key => $value) {
      if (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(
        'caption' => NULL,
        'header' => NULL,
        'rows' => NULL,
        'export' => NULL,
        'delta' => NULL,
        'entity_type' => NULL,
        'entity_id' => NULL,
        'field_name' => NULL,
        'langcode' => NULL,
        'formatter' => NULL,
        'attributes' => array(),
      ),
    ),
  );
}

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

  // Apply scope property to headers for accessibility.
  if (is_array($variables['header'])) {
    foreach ($variables['header'] as &$header) {
      $header['scope'] = 'col';
    }
  }

  // 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>';
  }

  // Prepare variables for theme_table().
  $theme_variables = array(
    'header' => $variables['header'],
    'rows' => $variables['rows'],
    'attributes' => $attributes,
  );
  if ($variables['caption']) {
    $theme_variables['caption'] = $variables['caption'];
  }

  return '<div id="tablefield-wrapper-' . $variables['delta'] . '" class="tablefield-wrapper">'
    . theme('table__tablefield', $theme_variables)
    . $export
    . '</div>';
}

/**
 * Helper function to fill in locked values from instance defaults.
 *
 * @param array $tablefield
 *   The table as it appears in FAPI.
 * @param array $instance_default
 *   Instance default tablefield.
 *
 * @return array
 *   Filled tablefield.
 */
function tablefield_fill_locked_values($tablefield, $instance_default) {
  if (!empty($tablefield['rebuild'])) {
    $count_cols = $tablefield['rebuild']['count_cols'];
    $count_rows = $tablefield['rebuild']['count_rows'];

    for ($i = 0; $i < $count_rows; $i++) {
      for ($ii = 0; $ii < $count_cols; $ii++) {
        if (!isset($tablefield["cell_${i}_${ii}"])) {
          $tablefield["cell_${i}_${ii}"] = isset($instance_default["cell_${i}_${ii}"]) ? $instance_default["cell_${i}_${ii}"] : '';
        }
      }
    }
  }
  return $tablefield;
}

/**
 * Trim trailing empty rows.
 *
 * @param array $tabledata
 *   The rationalized tablefield.
 */
function tablefield_rtrim_rows($tabledata) {
  $row_num = count($tabledata);
  while ($row_num) {
    $row_num--;
    foreach ($tabledata[$row_num] as $value) {
      if (!empty($value)) {
        // Stop traversing at the first non empty value.
        break 2;
      }
    }
    unset($tabledata[$row_num]);
  }
  return $tabledata;
}

/**
 * Trim trailing empty columns.
 *
 * @param array $tabledata
 *   The rationalized tablefield.
 */
function tablefield_rtrim_cols($tabledata) {
  $row_num = count($tabledata);

  if (!$row_num) {
    return $tabledata;
  }

  // Transpose the array.
  $tabledata = tablefield_transpose($tabledata);

  // Trim trailing empty rows.
  $tabledata = tablefield_rtrim_rows($tabledata);

  // Transpose back.
  $tabledata = tablefield_transpose($tabledata);

  return $tabledata;
}

/**
 * Helper function to transpose table data arrays.
 */
function tablefield_transpose($array) {
  $transposed = array();
  foreach ($array as $key => $row) {
    foreach ($row as $subkey => $value) {
      $transposed[$subkey][$key] = $value;
    }
  }
  return $transposed;
}
