<?php
/**
 * @file
 * Intercepts entityreference autocomplete submission validation and
 * creates a target node on the fly if it doesn't yet exist.
 *
 * Config:
 * Edit your entityreference field settings, and choose
 * 'Autocreate if not found'.
 */


/**
 * Implements hook_widget_info_alter().
 *
 * Adds settings that we need to declare to widgets we are extending.
 * Need this so that the settings we add to the widget settings form get saved.
 */
function entityreference_autocreate_field_widget_info_alter(array &$info) {
  $info['entityreference_autocomplete']['settings']['entityreference_autocreate'] = 0;
  $info['entityreference_autocomplete_tags']['settings']['entityreference_autocreate'] = 0;
}

/**
 * Adds our extra option to the field widget settings form.
 *
 * eg at
 * /admin/structure/types/manage/{CONTENT_TYPE}/fields/{ENTITYREFERENCE_FIELD}
 *
 * Implements hook_form_FORMID_alter().
 */
function entityreference_autocreate_form_field_ui_field_edit_form_alter(&$form, $form_state) {
  $supported_widgets = array(
    'entityreference_autocomplete',
    'entityreference_autocomplete_tags',
  );
  if (in_array($form['instance']['widget']['type']['#value'], $supported_widgets)) {
    // $field = $form['#field'];
    $instance = field_info_instance($form['instance']['entity_type']['#value'], $form['instance']['field_name']['#value'], $form['instance']['bundle']['#value']);
    $widget = $instance['widget'];
    $defaults = field_info_widget_settings($widget['type']);
    $settings = array_merge($defaults, $widget['settings']);
    $form['instance']['widget']['settings']['entityreference_autocreate'] = array(
      '#type' => 'fieldset',
      '#title' => t('EntityReference Autocreate settings'),
      '#collapsible' => TRUE,
    );

    $form['instance']['widget']['settings']['entityreference_autocreate']['active'] = array(
      '#type' => 'checkbox',
      '#title' => t('Autocreate target if not found'),
      '#description' => t('This will replace the normal validation that checks to see if a target item exists with code that just makes up whatever it was you asked for.'),
      '#default_value' => isset($settings['entityreference_autocreate']['active']) ? $settings['entityreference_autocreate']['active'] : FALSE,
    );

    // Use form API UI field visibility toggles to conditionally show the rest.
    // The following options only apply to 'node' entities AFAIK.
    // So hide them when inappropriate.

    $form['instance']['widget']['settings']['entityreference_autocreate']['author_current_user'] = array(
      '#type' => 'checkbox',
      '#title' => t('Author current user'),
      '#description' => t('This will set the current user as author of the new entities. Uncheck to get more options.'),
      '#default_value' => isset($settings['entityreference_autocreate']['author_current_user']) ? $settings['entityreference_autocreate']['author_current_user'] : TRUE,
      '#states' => array(
        'visible' => array(
          '#edit-instance-widget-settings-entityreference-autocreate-active' => array('checked' => TRUE),
          '#edit-field-settings-target-type' => array('value' => 'node'),
        ),
      ),
    );
    $form['instance']['widget']['settings']['entityreference_autocreate']['author'] = array(
      '#type' => 'textfield',
      '#title' => t('Authored by'),
      '#maxlength' => 60,
      '#autocomplete_path' => 'user/autocomplete',
      '#default_value' => isset($settings['entityreference_autocreate']['author']) ? $settings['entityreference_autocreate']['author'] : '',
      '#description' => t('This will set the chosen user as author of the new entities. Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
      '#states' => array(
        'visible' => array(
          '#edit-instance-widget-settings-entityreference-autocreate-author-current-user' => array('checked' => FALSE),
          '#edit-field-settings-target-type' => array('value' => 'node'),
        ),
      ),
    );
    $form['instance']['widget']['settings']['entityreference_autocreate']['status'] = array(
      '#type' => 'select',
      '#title' => t('Published status'),
      '#options' => array(
        -1 => 'Bundle default',
        1 => 'Published',
        0 => 'Unpublished',
      ),
      '#default_value' => isset($settings['entityreference_autocreate']['status']) ? $settings['entityreference_autocreate']['status'] : -1,
      '#states' => array(
        'visible' => array(
          '#edit-instance-widget-settings-entityreference-autocreate-active' => array('checked' => TRUE),
          '#edit-field-settings-target-type' => array('value' => 'node'),
        ),
      ),
    );

    $form['instance']['widget']['settings']['entityreference_autocreate']['help'] = array(
      '#markup' => t('Autocreation can only work if there is exactly one <b>Target bundle</b> selected in "<b>Entity Selection</b>" below.'),
    );
  }
}

/**
 * Adjust the behaviour of entityreference autocomplete widgets.
 *
 * Replaces the normal validation that prevents linking to imaginary entities
 * with our own, which makes it on the fly if needed.
 *
 * hook_field_widget_form_alter()
 */
function entityreference_autocreate_field_widget_form_alter(&$element, &$form_state, $context) {
  // First check if we are relevant or needed on this field widget.
  if ($context['field']['type'] != 'entityreference') {
    return;
  }
  if (empty($context['instance']['widget']['settings']['entityreference_autocreate']['active'])) {
    return;
  }

  // We are an autocomplete. What's the details?
  $target_bundles = $context['field']['settings']['handler_settings']['target_bundles'];
  // So, not all entites have bundles. 'user' doesn't. Fake it for now.
  if (empty($target_bundles)) {
    $target_bundles = array($context['field']['settings']['target_type']);
  }
  if (count($target_bundles) != 1) {
    watchdog('entityreference_autocreate', 'Can only autocreate an entity if there is exactly one target bundle.', array(), WATCHDOG_NOTICE);
    return;
  }

  $title = t('Autocreate enabled - any title put here will cause a "!bundle" to be created if no autocomplete match is found.', array('!bundle' => reset($target_bundles)));

  // So adjust the form field now.
  if ($context['instance']['widget']['type'] == 'entityreference_autocomplete') {
    // If it's autocomplete standard, there is a 'target_id'
    $element['target_id']['#attributes']['title'] = $title;
    $element['target_id']['#entityreference_autocreate_settings'] = $context['instance']['widget']['settings']['entityreference_autocreate'];
    // To bypass the normal validation, need to REPLACE it totally.
    $element['target_id']['#element_validate'] = array('entityreference_autocreate_validate');
  }

  if ($context['instance']['widget']['type'] == 'entityreference_autocomplete_tags') {
    // If it's autocomplete tags style ..
    $element['#attributes']['title'] = $title;
    $element['#entityreference_autocreate_settings'] = $context['instance']['widget']['settings']['entityreference_autocreate'];
    // To bypass the normal validation, need to REPLACE it totally.
    $element['#element_validate'] = array('entityreference_autocreate_validate_tags');
  }
}


/**
 * Make a missing target if asked for by name.
 *
 * An element_validate callback for autocomplete fields.
 * Replaces _entityreference_autocomplete_validate().
 *
 * @see _entityreference_autocomplete_validate()
 */
function entityreference_autocreate_validate($element, &$form_state, $form) {
  if (empty($element['#value'])) {
    return;
  }
  $field = field_info_field($element['#field_name']);
  $field['settings']['entityreference_autocreate'] = $element['#entityreference_autocreate_settings'];

  // Fetch an entity ID, making it on the fly if needed.
  if ($value = entityreference_autocreate_get_entity_by_title($field, $element['#value'])) {
    form_set_value($element, $value, $form_state);
    return;
  }

  // Something has failed.
  // Either could not create the target
  // (permissions or something?)
  // Or did a lookup and found two identically named targets already existing,
  // so bailed.

  $strings = array(
    '!target' => $element['#value'],
  );
  form_error($element, t('Failed to create or find a target called !target (entityreference_autocreate). This may be due to permissions, or possibly if there are two targets with identical titles already on the system.', $strings));
}

/**
 * Validate handler that makes things up on the fly if needed.
 *
 * @see _entityreference_autocomplete_tags_validate()
 */
function entityreference_autocreate_validate_tags($element, &$form_state, $form) {
  $value = array();
  // If a value was entered into the autocomplete...
  if (!empty($element['#value'])) {
    $field = field_info_field($element['#field_name']);
    $field['settings']['entityreference_autocreate'] = $element['#entityreference_autocreate_settings'];
    $entities = drupal_explode_tags($element['#value']);
    foreach ($entities as $title) {
      if ($target_id = entityreference_autocreate_get_entity_by_title($field, $title)) {
        $value[] = array(
          'target_id' => $target_id,
        );
      }
    }
  }
  // Update the values.
  form_set_value($element, $value, $form_state);
}

/**
 * Fetch the named entity for the field, create it if not found.
 *
 * @param array $field_info
 *   As loaded from field_info_field()
 * @param string $title
 *   Title to search for.
 *
 * @return object|NULL
 *   Pre-existing or new entity. is_new should be set on it if it is fresh.
 *   Returns NULL on unexpected failure. A failure should probably be caught.
 */
function entityreference_autocreate_get_entity_by_title($field_info, $title) {

  if (strstr($title, ',')) {
    // Problem?
    // dpm("Title '$title has commas - this may be a problem'");
    // dpm(debug_backtrace());
    return NULL;
  }

  $title = trim($title);
  if (empty($title)) {
    return NULL;
  }

  // Take "label (entity id)', match the id from parenthesis.
  if (preg_match("/.+\((\d+)\)/", $title, $matches)) {
    return $matches[1];
  }

  // Try to get a match from the input string when the user didn't use the
  // autocomplete but filled in a value manually.
  $handler = entityreference_get_selection_handler($field_info);

  // Search for matches (exact), limit to 2 so we can detect if there is a
  // potential conflict.
  $entities = $handler->getReferencableEntities($title, '=', 2);

  // Case where $entities looks like $entites[BUNDLE][ETID] = TITLE.
  if (is_array(reset($entities))) {
    // Extract items from results. The return is keyed by bundle.
    $target_bundles = $field_info['settings']['handler_settings']['target_bundles'];
    $tmp = array();
    foreach ($target_bundles as $bundle) {
      $tmp += $entities[$bundle];
    }

    // User entities are special - they have no bundle.
    // (or it's 'user' but not explicit about it in the entityreference options)
    // I guess there may be other entities like that also. Try to catch them,
    // by assuming that their entity type and their bundle id are the same.
    if (empty($target_bundles)) {
      $bundle = $field_info['settings']['target_type'];
      $tmp += $entities[$bundle];
    }
    $entities = $tmp;
  }

  if (count($entities) == 1) {
    // Exact match, no confusion, use that.
    return key($entities);
  }

  if (count($entities) > 1) {
    // More than one match.
    // This is a genuine form validation error I can't automate.
    return NULL;
  }

  // By now we've eliminated the options. There is no match.
  if (count($entities) == 0) {
    // Now make one of the named things.
    return entityreference_autocreate_new_entity($field_info, $title);
  }
  return NULL;
}

/**
 * Create a placeholder item of the type described in the field settings.
 */
function entityreference_autocreate_new_entity($field_info, $title) {
  // Now make one of the named things.
  $entity_type = $field_info['settings']['target_type'];
  $target_bundle = reset($field_info['settings']['handler_settings']['target_bundles']);

  // Select user depending on settings.
  if (!empty($field_info['settings']['entityreference_autocreate']['author_current_user'])) {
    global $user;
  }
  elseif (!empty($field_info['settings']['entityreference_autocreate']['author'])) {
    $user = user_load_by_name($field_info['settings']['entityreference_autocreate']['author']);
  }
  else {
    $user = user_load(0);
  }

  // Make a skeleton/minimal whatever entity. Probably a node.
  // @see entity_create_stub_entity($entity_type, $ids).

  $entity_info = entity_get_info($entity_type);
  $label_key = 'title';
  if (!empty($entity_info['entity keys']['label'])) {
    $label_key = $entity_info['entity keys']['label'];
  }
  $bundle_key = 'type';
  if (!empty($info['entity keys']['bundle'])) {
    $bundle_key = $info['entity keys']['bundle'];
  }

  $new_entity = NULL;
  // These two attributes seem common to each entity I've met so far.
  $new_entity_values = array(
    $bundle_key => $target_bundle,
    $label_key => $title,
  );

  switch ($entity_type) {
    case 'node':
      // Check the expected published status.
      $status = TRUE;
      if (isset($field_info['settings']['entityreference_autocreate']['status'])) {
        $status = $field_info['settings']['entityreference_autocreate']['status'];
        if ($status == -1) {
          // Use the bundle default.
          $node_options = variable_get('node_options_' . $target_bundle, array('status', 'promote'));
          $status = in_array('status', $node_options);
        }
      }

      $new_entity_values += array(
        'uid' => $user->uid,
        'name' => (isset($user->name) ? $user->name : ''),
        'language' => LANGUAGE_NONE,
        'status' => $status,
      );
      $new_entity = entity_create($entity_type, $new_entity_values);

      break;

    case 'taxonomy_term':
      if ($vocabulair = taxonomy_vocabulary_machine_name_load($target_bundle)) {
        $new_entity_values += array(
          'vid' => $vocabulair->vid,
        );
        $new_entity = entity_create($entity_type, $new_entity_values);
      }
      break;

    case 'user':
      // Creating users on the fly is a bit risky,
      // so they are not enabled by default.
      //
      // Entity_info did not define the label_key,
      // and users dont really have bundles.
      $label_key = 'name';
      $target_bundle = 'user';
      $new_entity_values = array(
        $bundle_key => $target_bundle,
        $label_key => $title,
      );
      $new_entity = entity_create($entity_type, $new_entity_values);

      break;

    default:
      // It's some unknown/custom entity.
      // We really can't guess what shape it is.
      // It's likely that each field listed in the infos
      // $entity_info['entity keys']
      // will be required though?
      //
      // It's probably a *little* like a node...
      // but it's a crap-shoot really.
      // Hopefully entity API will take care of the rest of the abstraction
      // and validation needed from here.
      // YMMV.
      $new_entity = entity_create($entity_type, $new_entity_values);
      break;

  }

  drupal_alter('entityreference_autocreate_new_entity', $new_entity, $field_info, $title);

  if (empty($new_entity)) {
    // The entity is unknown so don't continue.
    drupal_set_message(t("The entity that needs to be created is unknown (entityreference_autocreate)"), 'error');
    return NULL;
  }

  $saved = entity_save($entity_type, $new_entity);

  // The return from this isn't reliable, check for an ID instead.
  $target_id = entity_id($entity_type, $new_entity);
  $uri = entity_uri($entity_type, $new_entity);
  $strings = array(
    '%entity_type' => $entity_type,
    '%target_bundle' => $target_bundle,
    '!target' => l($new_entity->$label_key, $uri['path']),
    '%title' => $title,
  );
  if ($target_id) {
    drupal_set_message(t('Created a new %entity_type %target_bundle : !target (entityreference_autocreate)', $strings));
    return $target_id;
  }
  else {
    // Can't say why, but it's probably worth complaining about.
    drupal_set_message(t("Failed to created a new %target_bundle called %title, no id returned (entityreference_autocreate)", $strings), 'error');
    return NULL;
  }
}

/**
 * Load feeds support.
 *
 * Implements hook_init().
 */
function entityreference_autocreate_init() {
  // Include feeds.module integration.
  if (module_exists('feeds')) {
    module_load_include('inc', 'entityreference_autocreate', 'entityreference_autocreate.feeds');
  }
}
