<?php
// $Id: text.module,v 1.3 2009/02/10 03:16:15 webchick Exp $

/**
 * @file
 * Defines the Combo field type.
 */

/**
 * A combo field is a collection of other fields.  For example, a
 * combo field named address might be a collection of the fields
 * street, city, state, and zip.  A multi-value combo field repeats
 * each of its collected fields as a group for each delta.
 *
 * To make this work, the combo module defines both a field type and a
 * fieldable entity type.  The field type has no data storage of its
 * own (XXX but maybe it can to solve the delta alignment problem).
 * The fieldable entity type defines a bundle for each combo field
 * name, so fields can be attached to each combo field.  For example,
 * a combo field named address defines a bundle named address.  Field
 * instances for street, city, state, and zip fields can be created
 * for the bundle named address (entity type combo), and then a field
 * instance for address can be created for the node type bundle named
 * event.
 *
 * The field data for an address combo field might look like this:
 *
 * $object->address = array(
 *   'zxx' => array(
 *     0 => array(
 *       'street' => array(
 *         'zxx' => array(
 *           0 => array(
 *             'value' => '123 Main St',
 *           ),
 *           1 => array(
 *             'value' => 'Apt. 4',
 *           )
 *         ),
 *       ),
 *       'city' => array(...),
 *       // and so on
 *     ),
 *   ),
 * );
 *
 *
 * When a Field Type API operation is performed on an object with a
 * combo field, the combo field invokes the same operation on the
 * combo field as a fieldable combo entity, which performs the
 * operation on all of the fields attached to the combo field.  The id
 * of the fieldable combo object is <object_type>-<object_id> (there
 * is an existing issue to support non-numeric object ids, but
 * combofield can also maintain its own translation table to unique
 * serial ids).
 *
 * Continuing the previous example, suppose node 12 is an event node
 * with a combo field address attached.  
 *
 *   * node.module calls field_attach_load('node', $node) 
 *   * which calls combo_field_load('node', $node, $combo_field,
 *     $combo_instance)
 *   * which sets $combo_object->id = 'node-'.$node->nid 
 *   * and calls field_attach_load('combo', $combo_object)
 *   * which loads the street, city, state, and zip fields into $combo_object.
 *   * combo_field_load sets $node->address[0]['street'] =
 *     $combo_object->street and so on for city, state, and zip.
 *
 * This leads to difficulties with field data item deltas.
 *
 * Suppose street, city, state, and zip are all single-value
 * fields. When we field_attach_load('combo', $combo_object), we will
 * get back street[0], city[0], etc., and we assign all of those to
 * $node->addrress[0].  If we get back street[1], city[1], etc., we
 * assign them to $node->address[1]; this indicate that the address
 * combo field itself was multi-value.  Simple.
 *
 * Suppose that street is a 2-value field.  When we load the combo
 * object, we get back street[0] an street[1], but both of those
 * should be values of $node->address[0], whereas in the single-value
 * street field column, street[1] went to address[1].  Even worse,
 * we really don't even know for sure they both go to address[0];
 * maybe the user only entered one street value for each of two
 * address deltas, or entered no values for the first address deltas
 * and two for the second.
 *
 * One possible solution to the 2-value case, or any fixed-cardinality
 * case, is to change the deltas on the sub-fields before they are
 * saved to always be an increment above a multiple of the delta of
 * the combo item itself.  So, the street data items for combo delta 0
 * would be deltas 0 and 1, and for combo delta 1 would be deltas 2
 * and 3, regardless of whether the street field for combo delta 0 and
 * 1 actually had any items. There are two reasons this won't work:
 * First, currently, field_attach_load() does not preserve the actual
 * delta values from the database when loading, it only preserves
 * their order; secondly, for unlimited-cardinality fields, there is
 * no fixed multiple by which to increase each sub-field's deltas.
 * The first problem can be solved by deciding to preserve deltas on
 * field attach load.  The second can be solved by picking some
 * arbitrary, very large multiple (e.g. 10,000), and just asserting
 * that "unlimited" value subfields can never contain that many
 * values.
 *
 * An alternative is for the combo field to store its own data.  It
 * can have columns field_name and deltas.  For each delta, the combo
 * field stores the number of deltas for each sub-field.  In the
 * example above, if there are two combo deltas, and street has 0
 * deltas for combo[0] and 2 deltas for combo[1], the combo field
 * would store (street,0) for delta 0 and (street,2) for delta 1. On
 * load, it would then be a simple matter to collate the deltas for
 * each sub-field into the correct deltas of the combo field.  This
 * approach works equally well for unlimited value fields.
 *
 * Since the alternative approach has the advantage of working with
 * Field API as it is currently exists, I'll go with that.
 */

function combo_atomize($atom) {
  // TODO: Transaction
  $id = db_select('combo_atoms', 'a')
    ->fields('a', array('id'))
    ->condition('atom', $atom)
    ->execute()
    ->fetchField();
  if (empty($id)) {
    $id = db_insert('combo_atoms')->fields(array('atom' => $atom))->execute();
  }
  return $id;
}

/**
 * Implement hook_entity_info().
 */
function combo_entity_info() {
  $return = array(
    'combo' => array(
      'label' => t('Combo field'),
      'fieldable' => TRUE,
      'object keys' => array(
        'id' => 'id',
        'revision' => 'vid',
        'bundle' => 'bundle',
      ),
      'cacheable' => FALSE,
      'bundles' => array(),
    ),
  );

  // Every combo field name is a combo entity bundle.
  foreach (field_info_fields() as $field) {
    if ($field['type'] == 'combo') {
      $return['combo']['bundles'][$field['field_name']] = array(
        'label' => $field['field_name'],
      );
    }
  }
  return $return;
}

/**
 * Implementation of hook_field_info().
 */
function combo_field_info() {
  return array(
    'combo' => array(
      'label' => t('Combo'),
      'description' => t('Combine multiple fields into a single group.'),
      'default_widget' => 'combo_default',
      'default_formatter' => 'combo_default',
    ),
  );
}

/**
 * Implementation of hook_field_schema().
 */
function combo_field_schema($field) {
  $columns = array(
    'deltas' => array(
      'type' => 'text',
      'description' => 'A serialized array identifying, for each field attached to the combo field as an enity, how the deltas of the sub-field map to deltas of the combo field.',
    ),
  );
  return array('columns' => $columns);
}

/**
 * Implementation of hook_field_load().
 */
function combo_field_load($obj_type, $objects, $combo_field, $combo_instances, $langcode, &$combo_items, $options, $age) {
  // Load all the fields on us as an entity.
  // TODO: load_multiple
  foreach ($objects as $obj_id => $object) {
    // Our combo data items are loaded, but still serialized.
    foreach ($combo_items[$obj_id] as $combo_delta => &$combo_item) {
      $combo_item['deltas'] = unserialize($combo_item['deltas']);
    }

    // Create the combo entity for $field on $object.
    // TODO: Why do we need to pass $instance here?
    list($obj_id) = field_attach_extract_ids($obj_type, $object);
    $combo_entity = _combo_create_entity($obj_type, $object, $combo_field, $combo_instances[$obj_id]);

    // Load field data for the combo entity, which is all the fields
    // attached to the combo field.
    field_attach_load('combo', array($combo_entity->id => $combo_entity), $age, $options);

    // Collate each $combo_entity field's data into $combo_items, which is
    // what will end up on $object->{combo_field_name}.
    $entity_instances = field_info_instances($combo_field['field_name']);
    foreach ($entity_instances as $entity_instance) {
      foreach ($combo_items[$obj_id] as $combo_delta => &$combo_item) {
        // It is possible this combo item will have no data for one of
        // the sub-fields.
        // TODO: The extra $combo_deltas here is unnecessary, a
        // holdover from when I thought I was storing all sub-field
        // deltas for all combo field items in one array.
        if (isset($combo_item['deltas'][$combo_delta][$entity_instance['field_name']])) {
          for ($i=0; $i<$combo_item['deltas'][$combo_delta][$entity_instance['field_name']]; $i++) {
            $combo_item[$entity_instance['field_name']][$langcode][] = array_shift($combo_entity->{$entity_instance['field_name']}[$langcode]);
          }
        }
      }
    }
  }
}

/**
 * Implementation of hook_field_update().
 */
function combo_field_insert($obj_type, $object, $field, $instance, $langcode, &$items) {
  combo_field_update($obj_type, $object, $field, $instance, $langcode, $items);
}

/**
 * Implementation of hook_field_update().
 */
function combo_field_update($obj_type, $object, $field, $instance, $langcode, &$items) {
  // Collect all field data from the combo field into the combo
  // entity, saving info to collate the deltas later during load.
  $entity_instances = field_info_instances($field['field_name']);
  foreach ($items as $combo_delta => &$combo_item) {
    // Create the combo entity for $field/$combo_delta on $object
    $combo_entity = _combo_create_entity($obj_type, $object, $field, $instance);

    $item_deltas = array();

    foreach ($entity_instances as $entity_instance) {
      // It is possible this combo item will have no data for one of
      // the sub-fields.
      if (isset($combo_item[$entity_instance['field_name']][$langcode])) {
        // Store the number of deltas for this sub-field for this
        // combo field delta.
        // TODO: Use hook_field_is_empty()?
        $item_deltas[$combo_delta][$entity_instance['field_name']] = count($combo_item[$entity_instance['field_name']][$langcode]);
        // For each sub-field item, append it to the corresponding
        // field on the combo entity. We cannot just assign the array
        // here because we are appending sub-field items from multiple
        // combo field deltas.
        foreach ($combo_item[$entity_instance['field_name']][$langcode] as $subfield_item) {
          $combo_entity->{$entity_instance['field_name']}[$langcode][] = $subfield_item;
        }
      }
    }
    
    // Save the deltas item data so Field API will save it for us.  We
    // save the sub-field deltas for each combo field item with that
    // combo field item, though TODO: at the moment, $item_deltas is
    // indexed by $combo_delta which isn't necessary.  I'm wondering
    // if we should store all the deltas for all sub-fields in combo
    // field item 0, just to keep it all handy in one place.
    $combo_item['deltas'] = serialize($item_deltas);

    // Save all the subfield data.
    field_attach_update('combo', $combo_entity);
  }
}

/**
 * Create a combo entity whose id is based on the object the combo
 * field is attached to and whose bundle is the name of the combo
 * field.
 */
function _combo_create_entity($obj_type, $object, $field, $instance) {
  list($obj_id) = field_attach_extract_ids($obj_type, $object);
  $combo_id = _combo_get_id($obj_type, $object, $field, $instance);
  $combo_vid = _combo_get_vid($obj_type, $object, $field, $instance);
  $combo_bundle = $field['field_name'];
  $combo_entity = field_attach_create_stub_object('combo', array($combo_id, $combo_vid, $combo_bundle));
  return $combo_entity;
}

/**
 * Create or retrieve a unique id for the combo entity for a combo
 * field on an object type.
 */
function _combo_get_id($obj_type, $object, $field, $instance) {
  list($obj_id) = field_attach_extract_ids($obj_type, $object);
  return combo_atomize("{$obj_type}-id-{$obj_id}");
}

/**
 * Create or retrieve a unique revision id for the combo entity for a combo
 * field on an object type.
 */
function _combo_get_vid($obj_type, $object, $field, $instance) {
  // TODO
  return NULL;
}

/**********************************************************************
 * Field Type API: Formatters
 **********************************************************************/

/**
 * Implement hook_field_formatter_info().
 */
function combo_field_formatter_info() {
  return array(
    'combo_default' => array(
      'label' => t('Combo Field'),
      'field types' => array('combo'),
    ),
  );
}

/**
 * Implement hook_theme().
 */
function combo_theme() {
  return array(
    'field_formatter_combo_default' => array(
      'arguments' => array('element' => NULL),
    ),      
  );
}

/**
 * Theme function for 'default' text field formatter.
 */
function theme_field_formatter_combo_default($element) {
  return 'combo field';
}

/**********************************************************************
 * Field Type API: Widget
 **********************************************************************/

/**
 * Implement hook_field_widget_info().
 */
function combo_field_widget_info() {
  return array(
    'combo_default' => array(
      'label' => t('Combo Field'),
      'field types' => array('combo'),
    ),
  );
}

/**
 * Implement hook_field_widget().
 *
 * This widget displays three text fields, one each for red, green,
 * and blue.  However, the field type defines a single text column,
 * rgb, which needs an HTML color spec. Define an element validate
 * handler that converts our r, g, and b fields into a simulaed single
 * 'rgb' form element.
 */
function combo_field_widget(&$form, &$form_state, $field, $instance, $langcode, $items, $delta = 0) {
  return array();
}
