<?php
// $Id: matrix.module,v 1.2.4.8.2.21.2.1 2011/01/18 10:10:05 aaron1234nz Exp $

/**
 * @file
 * Defines simple matrix field types.
 */

include 'migrate.inc';

 /**
 * Implementation of hook_menu()
 */
function matrix_menu() {
  $items['matrix/calculation'] = array(
    'title' => 'Matrix custom calculation callback',
    'description' => 'Custom calculations will be done using this callback',
    'page callback' => 'matrix_custom_calculation_callback',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implementation of hook_theme()
 */
function matrix_theme() {
  $theme['matrix_preview'] = array(
    'render element' => 'form',
  );
  $theme['matrix_table'] = array(
    'render element' => 'form',
  );
  $theme['matrix_output'] = array(
    'variables' => array('header' => NULL, 'rows' => NULL),
  );
  
  return $theme;
}

/**
 * Implementation of hook_views_api()
 */
function matrix_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'matrix') .'/views',
  );
}

/**
 * Implements hook_field_info().
 */
function matrix_field_info() {
  return array(
    'matrix_text' => array(
      'label' => t('Matrix (textfield)'),
      'description' => t('Creates a grid of text fields.'),
      'settings' => array('rows_count' => '', 'cols_count' => '', 'size' => '', 'spreadsheet_style' => ''),
      'default_widget' => 'matrix_grid',
      'default_formatter' => 'matrix_default',
      // Support hook_entity_property_info() from contrib "Entity API".
      'property_type' => 'field_item_matrix',
      'property_callbacks' => array('matrix_field_property_info_callback'),
    ),
    'matrix_custom' => array(
      'label' => t('Matrix (custom)'),
      'description' => t('Creates a grid of any field type.'),
      'settings' => array('rows_count' => '', 'cols_count' => '', 'settings' => ''),
      'default_widget' => 'matrix_grid',
      'default_formatter' => 'matrix_default',
      // Support hook_entity_property_info() from contrib "Entity API".
      'property_type' => 'field_item_matrix',
      'property_callbacks' => array('matrix_field_property_info_callback'),
    ),
  );
}

/**
 * List of all matrix field types
 */
function matrix_field_types() {
  return array(
    'full' => array(
      'none' => t('None'),
      'textfield' => t('Textfield'),
      'select' => t('Select list'),
      'radios' => t('Radio buttons'),
      'checkboxes' => t('Check boxes'),
      'calculation' => t('Calculation'),
      'custom' => t('Custom'),
    ),
    'limited' => array(
      'none' => t('None'),
      'calculation' => t('Calculation'),
      'custom' => t('Custom'),
    ),
    'none' => array(
      'none' => t('None'),
    ),
  );
}

/**
 * Implements hook_field_settings_form().
 */
function matrix_field_settings_form($field, $instance, $has_data) {
  module_load_include('inc', 'matrix', 'matrix.admin');

  if ($field['type'] == 'matrix_text') {
    return matrix_field_settings_form_text($field, $instance, $has_data);
  }
  elseif($field['type'] == 'matrix_custom') {
    return matrix_field_settings_form_custom($field, $instance, $has_data);
  }
}

/**
 * Implementation of hook_content_is_empty().
 */
function matrix_field_is_empty($item, $field) {
  if (is_array($item) && array_key_exists('value', $item)) {       
    if (empty($item['value']) && (string) $item['value'] !== '0') {
      return TRUE;
    }
    return FALSE;
  }
  return TRUE; 
}

/**
 * Implements hook_field_widget_info().
 */
function matrix_field_widget_info() {
  return array(
    'matrix_text' => array(
      'label' => t('Text Matrix'),
      'description' => t('A grid of textfields'),
      'field types' => array('matrix_text'),
      'settings' => array(),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
        'default value' => FIELD_BEHAVIOR_NONE,
      ),
    ),
    'matrix_custom' => array(
      'label' => t('Custom Matrix'),
      'description' => t('A grid of form elements'),
      'field types' => array('matrix_custom'),
      'settings' => array(),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
        'default value' => FIELD_BEHAVIOR_NONE,
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function matrix_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $field_name = $field['field_name'];
  
  $cols_count = $field['settings']['cols_count'];
  $rows_count = $field['settings']['rows_count'];
      
  $default_values = array();
  foreach ($items as $delta => $item) {
    if($item && !empty($item['row']))
    $default_values[$item['row']][$item['col']][] = $item['value'];
  }

  switch ($field['type']) {
    case 'matrix_text':
      $element['#matrix_rows'] = $rows_count;
      $element['#matrix_cols'] = $cols_count;
      $element['#matrix_more_rows'] = FALSE;
      $element['#matrix_more_cols'] = FALSE;
      $size = $field['settings']['size'];
      $spreadsheet_style = $field['settings']['spreadsheet_style'];

      if ($spreadsheet_style) {
        for ($row = 1; $row <= $rows_count; $row++) {
          $row_labels[$row] = $row;
        }
        for ($col = 1; $col <= $cols_count; $col++) {
          $column_labels[$col] = matrix_make_letter($col);
        }
      }
      else {
        $row_labels = array();
        $column_labels = array();
      }
      
      for ($row = 1; $row <= $rows_count; $row++) {
        for ($col = 1; $col <= $cols_count; $col++) {
          $element['grid'][$row .'-'. $col] = array(
            '#type' => 'textfield',
            '#default_value' => isset($default_values[$row][$col]) ? $default_values[$row][$col] : '',
            '#size' => $size,
          );
        }
      }
      break;
      
    case 'matrix_custom':
      $settings = unserialize($field['settings']['settings']);
      $define = $field['settings']['define'];
      
      if ($rows_count == -1) {
        if (count($default_values) > 3) { //check to see how big the grid should be  based on previously submitted data
          $rows_count = count($default_values);
        }
        else {
          $rows_count = 3;
        }
        
        if (isset($form_state['storage'][$field_name]['rows'])) {
          $rows_count += $form_state['storage'][$field_name]['rows'];
        }

        $element['#matrix_rows'] = $rows_count;
        $element['#matrix_more_rows'] = TRUE;
        $row_labels = array();
      }
      else {
        $element['#matrix_rows'] = $rows_count;
        $element['#matrix_more_rows'] = FALSE;
        for ($row = 1; $row <= $rows_count; $row++) {
          $row_labels[$row] = isset($settings['rows'][$row]['title']) ? $settings['rows'][$row]['title']: '';
        }
      }
      
      if ($cols_count == -1) {
        if (isset($default_values[1]) && count($default_values[1]) > 3) { //check to see how big the grid should be  based on previously submitted data
          $cols_count = count($default_values[1]);
        }
        else {
          $cols_count = 3;
        }
        if (isset($form_state['storage'][$field_name]['cols'])) {
          $cols_count += $form_state['storage'][$field_name]['cols'];
        }
        
        $element['#matrix_cols'] = $cols_count;
        $element['#matrix_more_cols'] = TRUE;
        $column_labels = array_fill(0, $cols_count, '&nbsp;');
      }
      else {
        $element['#matrix_cols'] = $cols_count;
        $element['#matrix_more_cols'] = FALSE;          
        for ($col = 1; $col <= $cols_count; $col++) {
          $column_labels[$col] = isset($settings['cols'][$col]['title']) ? $settings['cols'][$col]['title'] : '';
        }
      }
      
      $js = '';
      
      for ($row = 1; $row <= $rows_count; $row++) {
        for ($col = 1; $col <= $cols_count; $col++) {
          //determine which field type should be shown for this cell
          //if define is cols then if a row is set to calculation or custom other than 'none' then it takes precidence (and visa versa)
          if ($define == 'cols') {
            if (isset($settings['rows'][$row]['field_type']) && in_array($settings['rows'][$row]['field_type'], array('calculation', 'custom'))) {
              $field_settings = isset($settings['rows'][$row]) ? $settings['rows'][$row] : array('field_type' => 'none');
              $calculation_class = 'matrix-calc-col-'. $col;
            }
            else {
              $field_settings = isset($settings['cols'][$col]) ? $settings['cols'][$col] : array('field_type' => 'none');
              $calculation_class = 'matrix-calc-row-'. $row;
            }
          }
          elseif ($define == 'rows') {
            if (isset($settings['cols'][$col]['field_type']) && in_array($settings['cols'][$col]['field_type'], array('calculation', 'custom'))) {
              $field_settings = isset($settings['cols'][$col]) ? $settings['cols'][$col] : array('field_type' => 'none');
              $calculation_class = 'matrix-calc-row-'. $row;
            }
            else {
              $field_settings = isset($settings['rows'][$row]) ? $settings['rows'][$row] : array('field_type' => 'none');
              $calculation_class = 'matrix-calc-col-'. $col;
            }
          }
          
          //now render the element
          $field_type = $field_settings['field_type'];
          if (isset($default_values[$row][$col])) {
            $default_value = $default_values[$row][$col];
          }
          else {
            $default_value[0] = '';
          }
          if (isset($field_settings[$field_type]['required']) && $field_settings[$field_type]['required'] == TRUE) {
            $required = TRUE;
            $required_marker = theme('form_required_marker', array());
          }
          else {
            $required = FALSE;
            $required_marker = '';
          }
          switch ($field_type) {
            case 'none':
              $element['grid'][$row .'-'. $col] = array(
                '#matrix_row' => $row,
                '#matrix_column' => $col,
                '#field_name' => $element['#title'],
                '#type' => 'markup',
                '#markup' => $default_value[0],
                '#attributes' => array('class' => array('matrix-col-'. $col, 'matrix-row-'. $row, 'matrix-cell-'. $row .'-'. $col)),
              );
              break;
              
            case 'textfield':
              $element['grid'][$row .'-'. $col] = array(
                '#type' => 'textfield',
                '#matrix_row' => $row,
                '#matrix_column' => $col,
                '#field_name' => $element['#title'],
                '#field_prefix' => $field_settings['textfield']['prefix'],
                '#field_suffix' => $field_settings['textfield']['suffix'] . $required_marker,
                '#default_value' => $default_value[0],
                '#size' => $field_settings['textfield']['size'],
                '#matrix_required' => $required,
                '#element_validate' => array('matrix_required_validate'),
                '#attributes' => array('class' => array('matrix-calc-col-'. $col, 'matrix-calc-row-'. $row, 'matrix-col-'. $col, 'matrix-row-'. $row, 'matrix-cell-'. $row .'-'. $col)),
              );
              break;
              
            case 'select':
              $element['grid'][$row .'-'. $col] = array(
                '#type' => 'select',
                '#matrix_row' => $row,
                '#matrix_column' => $col,
                '#field_name' => $element['#title'],
                '#default_value' => $default_value[0],
                '#matrix_required' => $required,
                '#element_validate' => array('matrix_required_validate'),
                '#field_suffix' => $required_marker,
                '#options' => matrix_allowed_values($field, $field_settings, 'select'),
                '#attributes' => array('class' => array('matrix-calc-col-'. $col, 'matrix-calc-row-'. $row, 'matrix-col-'. $col, 'matrix-row-'. $row, 'matrix-cell-'. $row .'-'. $col)),
              );
              break;
              
            case 'radios':
              $element['grid'][$row .'-'. $col] = array(
                '#type' => 'radios',
                '#matrix_row' => $row,
                '#matrix_column' => $col,
                '#field_name' => $element['#title'],
                '#default_value' => $default_value[0],
                '#matrix_required' => $required,
                '#element_validate' => array('matrix_required_validate'),
                '#prefix' => $required_marker,
                '#options' => matrix_allowed_values($field, $field_settings, 'radios'),
                '#attributes' => array('class' => array('matrix-calc-col-'. $col, 'matrix-calc-row-'. $row, 'matrix-col-'. $col, 'matrix-row-'. $row, 'matrix-cell-'. $row .'-'. $col)),
              );
              break;
              
            case 'checkboxes':
              $element['grid'][$row .'-'. $col] = array(
                '#type' => 'checkboxes',
                '#matrix_row' => $row,
                '#matrix_column' => $col,
                '#field_name' => $element['#title'],
                '#default_value' => $default_value,
                '#matrix_required' => $required,
                '#element_validate' => array('matrix_required_validate'),
                '#prefix' => $required_marker,
                '#options' => matrix_allowed_values($field, $field_settings, 'checkboxes'),
                '#attributes' => array('class' => array('matrix-calc-col-'. $col, 'matrix-calc-row-'. $row, 'matrix-col-'. $col, 'matrix-row-'. $row, 'matrix-cell-'. $row .'-'. $col)),
              );
              break;
              
            case 'calculation':
              $element['grid'][$row .'-'. $col] = array(
                '#type' => 'hidden',
                '#matrix_row' => $row,
                '#matrix_column' => $col,
                '#field_name' => $element['#title'],
                '#suffix' => '<div id="matrix-result-'. $field_name .'-'. $row .'-'. $col .'">&nbsp;</div>',
                '#attributes' => array('class' => array('matrix-col-'. $col, 'matrix-row-'. $row, 'matrix-cell-'. $row .'-'. $col)),
              );
              $opperation = $field_settings['calculation']['calculation'];
              $prefix = $field_settings['calculation']['prefix'];
              $suffix = $field_settings['calculation']['suffix'];
              $rounding = $field_settings['calculation']['rounding'];
              $js .= "jQuery('.$calculation_class').change(function(){window.matrix.$opperation('$calculation_class', '$row-$col', '$field_name', '$prefix', '$suffix', '$rounding')});\n
                      window.matrix.$opperation('$calculation_class', '$row-$col', '$field_name', '$prefix', '$suffix', '$rounding');\n";
              break;
              
            case 'custom':
              $callback = $field_settings['custom']['custom'];
              $element['grid'][$row .'-'. $col] = array(
                '#type' => 'hidden',
                '#matrix_row' => $row,
                '#matrix_column' => $col,
                '#suffix' => '<div id="matrix-result-'. $field_name .'-'. $row .'-'. $col .'">&nbsp;</div>',
                '#attributes' => array('class' => array('matrix-col-'. $col, 'matrix-row-'. $row, 'matrix-cell-'. $row .'-'. $col)),
              );
              $js .= "jQuery('.$calculation_class').change(function() {window.matrix.custom('$callback', '$calculation_class', '$row-$col', '$field_name')});\n
                      window.matrix.custom('$callback', '$calculation_class', '$row-$col', '$field_name');\n";
              break;
          }
        }
      }
      break;
  }

  if (!empty($js)) {
    drupal_add_js(drupal_get_path('module', 'matrix') .'/matrix.js');
    drupal_add_js('jQuery(document).ready(function () {'. $js .'});', array('type' => 'inline', 'scope' => 'footer'));
  }
  
  $element['more_rows'] = array(
    '#type' => 'button',
    '#value' => t('More rows'),
    '#name' => 'matrix-button-'. $field_name,
    '#ajax' => array(
      'callback' => 'matrix_field_ajax_callback',
      'wrapper' => 'matrix-field-'. $field_name,
    ),
    '#limit_validation_errors' => array(),
  );
  
  $element['more_cols'] = array(
    '#type' => 'button',
    '#value' => t('More columns'),
    '#name' => 'matrix-button-'. $field_name,
    '#ajax' => array(
      'callback' => 'matrix_field_ajax_callback',
      'wrapper' => 'matrix-field-'. $field_name,
    ),
    '#limit_validation_errors' => array(),

  );
  
  $element['#theme'] = 'matrix_table';
  $element['#row_labels'] = $row_labels;
  $element['#column_labels'] = $column_labels;
  $element['#element_validate'] = array('matrix_field_widget_validate');
  
  $form_state['matrix'] = $element['#field_name']; //used to identify this element in the ajax callback.
  
  return $element;
}

/**
 * AJAX callback
 * Returns the field grid after the more rows or more cols button has been pressed
 */
function matrix_field_ajax_callback($form, &$form_state) {
  list($element_name, $value) = matrix_op($form_state);
 
  if (isset($element_name)) {
    return $form[$element_name];
  }
}

/**
 * Element validate callback; check that the entered values are valid.
 */
function matrix_allowed_values_setting_validate($element, &$form_state) {
  return;
}

/**
 * Submit handler for settings form
 * Clears the cache which is used for this field
 */
function matrix_clear_cache_submit(&$form, &$form_state) {
  global $user;
  cache_clear_all('matrix-cache-'. $user->uid, 'cache'); //TODO run this off the form token
}

/**
 * Generate the list of allowed values from a set of field settings
 * This will check first if there is a function for the allowed values and if not will use the allowd_values field
 *
 * @param $field
 *     The field array
 * @param $field_settings
 *     Array of settings for this field
 * @param $element_type
 *     The type of element the settings are for (eg select, radios, checkboxes).
 * @return
 *     Array of values suiteble for use in a #options form element
 */
function matrix_allowed_values($field, $field_settings, $element_type) {
  if (!empty($field_settings[$element_type]['allowed_values_function']) && function_exists($field_settings[$element_type]['allowed_values_function'])) {
    $values = call_user_func($field_settings[$element_type]['allowed_values_function'], $field);
  }
  else {
    $list = explode("\n", $field_settings[$element_type]['allowed_values']);
    $list = array_map('trim', $list);
    $list = array_filter($list, 'strlen');
    foreach ($list as $value) {
      if (strpos($value, '|') !== FALSE) {
        list($key, $value) = explode('|', $value);
        $values[$key] = $value;
      }
      else {
        $values[$value] = $value;
      }
    }
  }
  return $values;
}

/**
 * Theme a set of form elements into a table
 */
function theme_matrix_table($variables) {
  $form = $variables['form'];
  $rows_count = $form['#matrix_rows'];
  $cols_count = $form['#matrix_cols'];
  $more_cols = $form['#matrix_more_cols'];
  $more_rows = $form['#matrix_more_rows'];
  $column_labels = $form['#column_labels'];
  $row_labels = $form['#row_labels'];
  $field_name = $form['#field_name'];

  $table_rows = array();
  for ($row = 1; $row <= $rows_count; $row++) {
    for ($col = 1; $col <= $cols_count; $col++) {
      $table_rows[$row - 1][$col - 1] = drupal_render($form['grid'][$row .'-'. $col]);
    }
  }
  
  //lables
  if (!empty($row_labels)) {
    array_unshift($column_labels, ''); //The first column will contain the label
    foreach($row_labels as $id => $label) {
      array_unshift($table_rows[$id - 1], $label);
    }
  }
  
  //if there should be a "more rows" button, render a new row with just the button
  if ($more_rows) {
    $table_rows[$rows_count][1] = drupal_render($form['more_rows']);
    for ($col = 2; $col <= $cols_count; $col++) {
      $table_rows[$rows_count][$col] = '&nbsp;';
    }
  }

  //if there should be a "more columns" button, render a new column with just the button
  if ($more_cols) {
    $column_labels[] = drupal_render($form['more_cols']);
    for ($row = 1; $row <= $rows_count; $row++) {
      $table_rows[$row - 1][$cols_count + 1] = '&nbsp;';
    }
  }

  $output  = '<div id="matrix-field-'. $field_name .'">';
  $output .= theme('form_element_label', (array('element' => $form)));
  $output .= theme('table', array('header' => $column_labels, 'rows' => $table_rows));
  $output .= filter_xss_admin($form['#description']);
  $output .= '</div>';
  return $output;
}

/**
 * Theme a set of elements into a table
 */
function theme_matrix_output($variables) {
  return theme('table', array('header' => $variables['header'], 'rows' => $variables['rows']));
}

/**
 * Validate the required attribute of a cell
 * standard #requied cannot be used because:
 * - It should not be invoked during ajax calls
 * - No message is returned to the user (error set without message or cell highlighted)
 */
function matrix_required_validate($element, &$form_state) {
  $error = FALSE;
  list($e, $value) = matrix_op($form_state);
  if (!in_array($value, array(t('More rows'), t('More columns')))) {
    if ($element['#matrix_required'] == TRUE) {
      if (is_array($element['#value']) && empty($element['#value'])) {
         $error = TRUE;
      }
      elseif($element['#value'] == '') {
        $error = TRUE;    
      }
      
      if ($error) {
        form_error($element, t('The cell at row %row, column %column in the %field_name is required', array('%row' => $element['#matrix_row'], '%column' => $element['#matrix_column'], '%field_name' => $element['#field_name'])));
      }
    }
  }
}

/**
 * Because the name attribute of the add more rows and add more columns is used
 * $form_state['values']['op'] is not available.  This function basically finds the op
 * equivilent in $form_state
 *
 * @param $form_state
 *     The form_state array
 * @return
 *     single array entry ofwith first element the name of the field and second element the value of the button
 */
function matrix_op(&$form_state) {
  foreach($form_state['values'] as $key => $value){
    if (is_string($value) && strpos($key, 'matrix-button-') !== FALSE) {
      return array(substr($key, 14),  $value);
    }
  }
  return array('', ''); //fallback case
}

/**
 * Form element validation handler.
 * Respond to ajax requests and
 * Reorder the data so that it is in the correct form for the fields api to save the data
 * Each record gets its own delta.  For multi-valued elements each value gets its own delta.
 */
function matrix_field_widget_validate($element, &$form_state) {
  list($field_name, $value) = matrix_op($form_state);
  
  if (isset($form_state['values']['op'])) {
    $rows_count = $element['#matrix_rows'];
    $cols_count = $element['#matrix_cols'];
    $delta = 0;
    
    for ($row = 1; $row <= $rows_count; $row++) {
      for ($col = 1; $col <= $cols_count; $col++) {
        if (!isset($element['grid'][$row .'-'. $col]['#value'])) { //if value is not set
          $value = '';
        }
        elseif(is_array($element['grid'][$row .'-'. $col]['#value']) && empty($element['grid'][$row .'-'. $col]['#value'])) { //if array and value not set
          $value = '';
        }
        else {
          $value = $element['grid'][$row .'-'. $col]['#value']; //value set
        }
      
        if(is_array($value)) {
          foreach ($value as $v) {
            $items[$delta] = array(
              'row' => $row,
              'col' => $col,
              'value' => $v,
            );
            $delta++;
          }
        }
        else {
          $items[$delta] = array(
            'row' => $row,
            'col' => $col,
            'value' => $value,
          );
          $delta++;
        }
      }
    }

    form_set_value($element, $items, $form_state);
  }
  elseif ($field_name == $element['#field_name']) { // filter to this matrix (if there is more than 1 marix per form)
    if ($value == t('More columns')) {
      if (isset($form_state['storage'][$field_name]['cols'])) {
        $form_state['storage'][$field_name]['cols']++;
      }
      else {
        $form_state['storage'][$field_name]['cols'] = 1;
      }
    }
    
    if ($value == t('More rows')) {
      if (isset($form_state['storage'][$field_name]['rows'])) {
        $form_state['storage'][$field_name]['rows']++;
      }
      else {
        $form_state['storage'][$field_name]['rows'] = 1;
      }
    }
  }
}

/**
 * Implements hook_field_formatter_info().
 */
function matrix_field_formatter_info() {
  return array(
    'matrix_default' => array(
      'label' => t('Default'),
      'field types' => array('matrix_text', 'matrix_custom'),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function matrix_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $raw_items, $display) {
  $element = array();

  if (!$raw_items) { //If there are no items to show (eg. no content saved for this field on this node) then hide the field
    return;
  }
  
  switch ($display['type']) {
    case 'matrix_default':
      if (count($raw_items[0]) == 1) { //if a single value is passed in from views
        $element[0] = array(
          '#markup' => field_filter_xss($raw_items[0]['value']), 
        );
      }
      
      //transpose the $items array into a [$row][$col] = value arangement
      foreach ($raw_items as $delta => $item) {
        $items[$item['row']][$item['col']][] = matrix_cell_value($item, $field);
      }

      ksort($items);
      foreach ($items as $row_id => $row) {
        ksort($items[$row_id]);
      }

      // work out how many rows and columns to show, which is the number of rows
      // with at least some data in the row.
      // TODO this logic is wrong.
      foreach ($items as $row_id => $row) {
        foreach ($row as $col_id => $value) {
          if ($value != '') {
            $rows_count = $row_id;
            // if this row has a value, we don't need to keep looking at the rest
            // of the columns.
            break;
          }
        }
      }

      // TODO this is inconsistent with the row count logic.
      if ($field['settings']['cols_count'] > 0) {
        $cols_count = $field['settings']['cols_count'];
      }
      else {
        foreach ($items as $row_id => $row) {
          foreach ($row as $col_id => $value) {
            if (!empty($value)) {
              $max_cols[$col_id] = $col_id;
            }
          }
        }
        $cols_count = max($max_cols);
      }

      //populate the data part of the table rows
      $rows = array();
      for ($row = 1; $row <= $rows_count; $row++) {
        for ($col = 1; $col <= $cols_count; $col++) {
          $value = array('');
          if (isset($items[$row][$col])) {
            $value = $items[$row][$col];
          }
          $rows[$row][$col] = $value;
        }
      }
      
      //collapse any multi-valued results into a list
      foreach ($rows as $row_id => $row) {
        foreach ($row as $col_id => $values) {
          if (count($values) > 1) {
            $rows[$row_id][$col_id] = array('data' => theme('item_list', array('items' => $values)), 'class' => 'matrix-col-'. $col_id .' matrix-row-'. $row_id .' matrix-cell-'. $row_id .'-'. $col_id);
          }
          else {
            $rows[$row_id][$col_id] = array('data' => $values[0], 'class' => 'matrix-col-'. $col_id .' matrix-row-'. $row_id .' matrix-cell-'. $row_id .'-'. $col_id);
          }
        }
      }

      if ($field['type'] == 'matrix_text' && $field['settings']['spreadsheet_style'] == TRUE) {
        for ($col = 1; $col <= $cols_count; $col++) {
          $headers[$col] = matrix_make_letter($col);
        }
        array_unshift($headers, ''); //cell 0,0   
        
        for ($row = 1; $row <= $rows_count; $row++) {
          $cell = array('data' => $row, 'class' => 'matrix-col-header');
          array_unshift($rows[$row], $cell);
        }     
      }
      elseif ($field['type'] == 'matrix_custom') {
        $headers = array();
        $headers = array_fill(1, $cols_count, '&nbsp;'); //initialize the headers with spaces (in case headers have not title set)
        $settings = unserialize($field['settings']['settings']);

        if (isset($settings['cols'])) {
          foreach ($settings['cols'] as $col_id => $col) {
            $headers[$col_id] = $col['title'];
          }
        }

        $have_row_labels = FALSE;

        if (!empty($settings['rows'])) {
          foreach ($settings['rows'] as $row_id => $row) {
            if (!empty($row['title'])) {
              $have_row_labels = TRUE;
            }

            $cell = array('data' => $row['title'], 'class' => 'matrix-col-header');

            if (isset($rows[$row_id])) {
              array_unshift($rows[$row_id], $cell);
            }
          }
        }

        if ($have_row_labels) { //had left lables so translate the headers one to the right
          array_unshift($headers, ''); //cell 0,0
        }
      }
      else {
        $headers = NULL;
      }

      // Make headers translatable.
      foreach($headers as $key => $value){
        $headers[$key] = t($value);
      }

      $element[0] = array(
        '#theme' => 'matrix_output', 
        '#header' => $headers, 
        '#rows' => $rows,
      );
    break;
  }

  return $element;
}

/**
 * Find a value of a cell for either a string or a select list
 *
 * @param $item
 *    array representing the cell
 * @param $field
 *     array containing the field data
 * @return
 *     value to display
 */
function matrix_cell_value($item, $field) {
  $prefix = '';
  $suffix = '';

  if ($field['type'] == 'matrix_text') { //selection lists cannot apply to text fields
    $value = $item['value'];
  }
  elseif ($field['type'] == 'matrix_custom') { //see if the cell contains a selection list
    if (isset($field['settings']['settings'])) {
      $settings = unserialize($field['settings']['settings']);
    }
    else {
      $settings = array();
    }
    
    $define = $field['settings']['define']; //work out which settings should be used
    if ($define == 'rows') {
      $field_settings = $settings['rows'][$item['row']];
    }
    else {
      $field_settings = $settings['cols'][$item['col']];
    }

    //determine the value to return
    $field_type = $field_settings['field_type'];
    if (!in_array($field_type, array('select', 'radios', 'checkboxes'))) {
      $value = $item['value'];
      $prefix = $field_settings[$field_type]['prefix'];
      $suffix = $field_settings[$field_type]['suffix'];
    }
    else {
      $allowed_values = matrix_allowed_values($field, $field_settings, $field_type);
      $value = $allowed_values[$item['value']];
    }
  }
  
  return check_plain($prefix . $value . $suffix);
}

/**
 * Make a letter (like with a spreadsheet A, B, C, AA, AB etc)
 *
 * @param $int
 *     The number to convert to a string
 * @return string
 */
function matrix_make_letter($int) {
  $string = '';
  $first = chr((int)($int / 26) + 64);
  $second = chr(($int % 26) + 64);
  if ($first != '@') { //@ comes before 'A'
    $string .= $first;
  }
  $string .= $second;
  return $string;
}

/**
 * Menu callback for matrix custom calculations
 * @return string.
 */
function matrix_custom_calculation_callback() {
  $callback = $_POST['callback'];
  $data = explode(",", $_POST['data']);
  $functions = module_invoke_all('matrix_functions');

  //ensure the callback is allowed
  if (!in_array($callback, array_keys($functions['calculation']))) {
    drupal_json_output(array('error' => t('Calcuation callback function not available')));
    exit();
  }
  
  //ensure the data is safe
  foreach ($data as $id => $d) {
    $data[$id] = check_plain($d);
  }
  
  $result = call_user_func($callback, $data);
  drupal_json_output(array('data' => check_plain($result)));
  exit();
}

/**
 * Example Implementation of hook_matrix_functions().
 * Register functions that should be used with the matrix module
 * These are used for the "allowed values functions" and for "custom calculation functions"
 *
 * @return array of allowed functions
 */
function matrix_matrix_functions() {
  return array(
    'allowed_values' => array(
      'matrix_allowed_values_example' => t('Custom example (10\'s)')
    ),
    'calculation' => array(
      'matrix_custom_processing_example' => t('Custom example (sum of squares)')
    ),
  );
}

/**
 * Example function for providing a list of allowed values for a select list
 *
 * @param $field
 *     The field array
 * @return 
 *     An array of key-value pairs
 */
function matrix_allowed_values_example($field) {
  return array(
   10 => t('Ten'),
   20 => t('Twenty'),
   30 => t('Thirty'),
   40 => t('Forty'),
   50 => t('Fifty'),
  );
}

/**
 * Example function for performing a custom calculation on some data
 *
 * @param $data
 *     Array of data to process
 * @return 
 *     A string to store
 */
function matrix_custom_processing_example($data) {
  return implode(',', $data);
}

/**
 * Additional callback to adapt the property info of matrix fields.
 *
 * @see entity_metadata_field_entity_property_info()
 */
function matrix_field_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  // Define a data structure so it's possible to deal with the row, column and
  // value.
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';

  // Auto-create the field item as soon as a property is set.
  $property['auto creation'] = 'matrix_field_item_create';

  $property['property info'] = matrix_field_item_property_info();
  unset($property['query callback']);
}

/**
 * Callback for creating a new, empty matrix field item.
 *
 * @see matrix_field_property_info_callback()
 */
function matrix_field_item_create() {
  return array('row' => NULL, 'col' => NULL, 'value' => NULL);
}

/**
 * Defines info for the properties of the matrix-field item data structure.
 */
function matrix_field_item_property_info() {
  $properties['row'] = array(
    'type' => 'integer',
    'label' => t('The row of the item'),
    'setter callback' => 'entity_property_verbatim_set',
  );
  $properties['col'] = array(
    'type' => 'integer',
    'label' => t('The column of the item.'),
    'setter callback' => 'entity_property_verbatim_set',
  );
  $properties['value'] = array(
    'type' => 'text',
    'label' => t('The value of the item.'),
    'setter callback' => 'entity_property_verbatim_set',
  );
  return $properties;
}
