<?php

/**
 * @file
 * Defines a field for attaching country-specific addresses to entities.
 */

/**
 * Implements hook_ctools_plugin_directory().
 */
function addressfield_ctools_plugin_directory($module, $plugin) {
  if ($module == 'addressfield') {
    return 'plugins/' . $plugin;
  }
}

/**
 * Implements hook_ctools_plugin_type().
 */
function addressfield_ctools_plugin_type() {
  $plugins['format'] = array(
    'load themes' => TRUE,
  );
  return $plugins;
}

/**
 * Implements hook_views_api().
 */
function addressfield_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'addressfield') . '/views',
  );
}

/**
 * Get the list of format plugins.
 */
function addressfield_format_plugins() {
  ctools_include('plugins');
  $plugins = ctools_get_plugins('addressfield', 'format');
  uasort($plugins, 'ctools_plugin_sort');

  return $plugins;
}

/**
 * Get the list of format plugins in a format suitable for #options.
 */
function addressfield_format_plugins_options() {
  $options = array();
  foreach (addressfield_format_plugins() as $widget => $info) {
    $options[$widget] = check_plain($info['title']);
  }
  return $options;
}

/**
 * @defgroup addressfield_format Address format API
 * @{
 * API for generating address forms and display formats.
 *
 * Addresses forms and display formats are collaboratively generated by one or
 * more format handler plugins. An address with a name and a company, for example,
 * will be generated by three handlers:
 *   - 'address' that will generate the country, locality, street blocks
 *   - 'organisation' that will add the organisation block to the address
 *   - 'name-full' that will add a first name and last name block to the address
 *
 * A format handler is a CTools plugin of type 'addressfield' / 'format'. Each
 * handler is passed the format in turn, and can add to or modify the format.
 *
 * The format itself is a renderable array stub. This stub will be transformed
 * into either a Form API array suitable for use as part of a form or into a
 * renderable array suitable for use with drupal_render(). The following
 * modifications are done:
 *   - when rendering as a form, every element which name (its key in the array)
 *     is a valid addressfield column (see addressfield_field_schema()), will
 *     be transformed into a form element, either using a type explicitly
 *     defined in '#widget_type' or using 'select' if '#options' is set or
 *     'textfield' if it is not. In addition, the '#default_value' of every
 *     field will be populated from the address being edited.
 *   - when rendering as a formatter, every element which name (its key in the array)
 *     is a valid addressfield column (see addressfield_field_schema()), will
 *     be transformed into a renderable element, either using a type explicitly
 *     defined in '#render_type' or else using 'addressfield_container'. When
 *     the type is 'addressfield_container' the element will be rendered as
 *     an HTML element set by '#tag' (default: span).
 */

/**
 * Generate a format for a given address.
 *
 * @param $address
 *   The address format being generated.
 * @param $handlers
 *   The format handlers to use to generate the format.
 * @param $context
 *   An associative array of context information pertaining to how the address
 *   format should be generated. If no mode is given, it will initialize to the
 *   default value. The remaining context keys should only be present when the
 *   address format is being generated for a field:
 *   - mode: either 'form' or 'render'; defaults to 'render'.
 *   - field: the field info array.
 *   - instance: the field instance array.
 *   - langcode: the langcode of the language the field is being rendered in.
 *   - delta: the delta value of the given address.
 *
 * @return
 *   A renderable array suitable for use as part of a form (if 'mode' is 'form')
 *   or for formatted address output when passed to drupal_render().
 */
function addressfield_generate($address, array $handlers, array $context = array()) {
  // If no mode is given in the context array, default it to 'render'.
  if (empty($context['mode'])) {
    $context['mode'] = 'render';
  }

  ctools_include('plugins');
  $format = array();
  $format['#handlers'] = $handlers;
  foreach ($format['#handlers'] as $handler) {
    if ($callback = ctools_plugin_load_function('addressfield', 'format', $handler, 'format callback')) {
      $callback($format, $address, $context);
    }
  }

  // Store the address in the format, for processing.
  $format['#address'] = $address;

  // Post-process the format stub, depending on the rendering mode.
  if ($context['mode'] == 'form') {
    $format['#addressfield'] = TRUE;
    $format['#process'][] = 'addressfield_process_format_form';
    $format['#required'] = FALSE;
  }
  elseif ($context['mode'] == 'render') {
    $format['#pre_render'][] = 'addressfield_render_address';
  }

  return $format;
}

/**
 * Generate a full-fledged form from a format snippet, as returned by addressfield_formats().
 */
function addressfield_process_format_form($format, &$form_state, $complete_form) {
  // Make sure to load all the plugins that participated in this format.
  ctools_include('plugins');
  foreach ($format['#handlers'] as $handler) {
    ctools_plugin_load_function('addressfield', 'format', $handler, 'format callback');
  }

  _addressfield_process_format_form($format, $format['#address'], $format['#required']);
  return $format;
}

function _addressfield_process_format_form(&$format, $address, $required) {
  foreach (element_children($format) as $key) {
    $child = &$format[$key];

    // Automatically expand elements that matches one of the field of the
    // address structure.
    if (in_array($key, array('name_line', 'first_name', 'last_name', 'organisation_name', 'country', 'administrative_area', 'sub_administrative_area', 'locality', 'dependent_locality', 'postal_code', 'thoroughfare', 'premise', 'sub_premise'))) {
      // Set the type.
      if (isset($child['#widget_type'])) {
        $child['#type'] = $child['#widget_type'];
      }
      else {
        if (isset($child['#options'])) {
          $child['#type'] = 'select';
          $child['#size'] = 0;
        }
        else {
          $child['#type'] = 'textfield';
        }
      }
      if (!$required) {
        unset($child['#required']);
      }

      $child['#default_value'] = $address[$key];
    }

    // Recurse through the child.
    _addressfield_process_format_form($child, $address, $required);
  }
}

/**
 * Render an address in a given format.
 */
function addressfield_render_address($format) {
  _addressfield_render_address($format, $format['#address']);
  return $format;
}

function _addressfield_render_address(&$format, $address) {
  foreach (element_children($format) as $key) {
    $child = &$format[$key];

    // Automatically expand elements that matches one of the field of the
    // address structure.
    if (in_array($key, array('name_line', 'first_name', 'last_name', 'organisation_name', 'country', 'administrative_area', 'sub_administrative_area', 'locality', 'dependent_locality', 'postal_code', 'thoroughfare', 'premise', 'sub_premise'), TRUE)) {
      if (isset($child['#render_type'])) {
        $child['#type'] = $child['#render_type'];
      }
      else {
        $child['#type'] = 'addressfield_container';
        if (!isset($child['#tag'])) {
          $child['#tag'] = 'span';
        }
      }

      // If the element instructs us to render the option value instead of the
      // raw address element value and its #options array has a matching key,
      // swap it out for the option value now.
      if (!empty($child['#render_option_value']) && isset($child['#options'][$address[$key]])) {
        $child['#children'] = check_plain($child['#options'][$address[$key]]);
      }
      else {
        $child['#children'] = check_plain($address[$key]);
      }

      // Skip empty elements.
      if ((string) $child['#children'] === '') {
        $child['#access'] = FALSE;
      }

      // Add #field_prefix and #field_suffix to the prefixes and suffixes.
      if (isset($child['#field_prefix'])) {
        $child['#prefix'] = (isset($child['#prefix']) ? $child['#prefix'] : '') . $child['#field_prefix'];
      }
      if (isset($child['#field_suffix'])) {
        $child['#suffix'] = (isset($child['#suffix']) ? $child['#suffix'] : '') . $child['#field_suffix'];
      }
    }

    // Recurse through the child.
    _addressfield_render_address($child, $address);
  }
}

/**
 * @} End of "ingroup addressfield_format"
 */

/**
 * Implementation of hook_theme().
 */
function addressfield_theme() {
  $hooks['addressfield_container'] = array(
    'render element' => 'element',
  );
  return $hooks;
}

/**
 * Render a container for a set of address fields.
 */
function theme_addressfield_container($variables) {
  $element = $variables['element'];

  $element['#children'] = trim($element['#children']);
  if (strlen($element['#children']) > 0) {
    $output = '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . '>';
    $output .= $element['#children'];
    $output .= '</' . $element['#tag'] . ">";
    return $output;
  }
  else {
    return '';
  }
}

/**
 * Implementation of hook_element_info().
 */
function addressfield_element_info() {
  $types['addressfield_container'] = array(
    '#theme_wrappers' => array('addressfield_container'),
    '#process' => array('addressfield_widget_process'),
    '#attributes' => array(),
    '#tag' => 'div',
  );
  return $types;
}

/**
 * Form API process function: set the #parents of the children of this element so they appear at the same level as the parent.
 */
function addressfield_widget_process($element) {
  foreach (element_children($element) as $key) {
    $element[$key]['#parents'] = $element['#parents'];
    $element[$key]['#parents'][count($element[$key]['#parents']) - 1] = $key;
  }

  return $element;
}

/**
 * Implements hook_field_info()
 */
function addressfield_field_info() {
  $fields = array();

  $fields['addressfield'] = array(
    'label' => t('Postal address'),
    'description' => t('A field type used for storing postal addresses according the xNAL standard.'),
    'settings' => array(),
    'instance_settings' => array(),
    'default_widget' => 'addressfield_standard',
    'default_formatter' => 'addressfield_default',
    'property_type' => 'addressfield',
    'property_callbacks' => array('addressfield_property_info_callback'),
  );

  return $fields;
}

/**
 * Returns an array of default values for the addressfield form elements.
 */
function addressfield_default_values($available_countries = NULL) {
  if (!isset($available_countries)) {
    $available_countries = _addressfield_country_options_list();
  }

  // Use the default country of the site if possible.
  $default_country = variable_get('site_default_country', NULL);

  // If the default country is undefined or not in the list of available countries,
  // just fallback to the first country in the list.
  if (!$default_country || !isset($available_countries[$default_country])) {
    $default_country = key($available_countries);
  }

  return array(
    'country' => $default_country,
    'name_line' => '',
    'first_name' => '',
    'last_name' => '',
    'organisation_name' => '',
    'administrative_area' => '',
    'sub_administrative_area' => '',
    'locality' => '',
    'dependent_locality' => '',
    'postal_code' => '',
    'thoroughfare' => '',
    'premise' => '',
    'sub_premise' => '',
    'data' => '',
  );
}

/**
 * Implements hook_field_is_empty().
 */
function addressfield_field_is_empty($item, $field) {
  // Every address field must have at least a country value or it is considered
  // empty, even if it has name information.
  return empty($item['country']);
}

/**
 * Implements hook_field_widget_info()
 */
function addressfield_field_widget_info() {
  $widgets = array();

  $widgets['addressfield_standard'] = array(
    'label' => t('Dynamic address form'),
    'field types' => array('addressfield'),
    'settings' => array(
      'available_countries' => array(),
      'format_handlers' => array('address'),
    ),
  );

  return $widgets;
}

/**
 * Implements hook_field_widget_settings_form()
 */
function addressfield_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $defaults = field_info_widget_settings($widget['type']);
  $settings = array_merge($defaults, $widget['settings']);
  $form = array();

  if ($widget['type'] == 'addressfield_standard') {
    $form['available_countries'] = array(
      '#type' => 'select',
      '#multiple' => TRUE,
      '#title' => t('Available countries'),
      '#description' => t('If no countries are selected, all countries will be available.'),
      '#options' => _addressfield_country_options_list(),
      '#default_value' => $settings['available_countries'],
    );

    $form['format_handlers'] = array(
      '#type' => 'checkboxes',
      '#title' => t('Format handlers'),
      '#options' => addressfield_format_plugins_options(),
      '#default_value' => $settings['format_handlers'],
    );
  }

  return $form;
}

/**
 * Implements hook_field_widget_form()
 */
function addressfield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $settings = $instance['widget']['settings'];

  // Generate a specific key used to identify this element to restore a default
  // value upon AJAX submission regardless of where this element is in the
  // $form array.
  $element_key = implode('|', array($element['#entity_type'], $element['#bundle'], $element['#field_name'], $element['#language'], $element['#delta']));

  // Store the key in the element array as a value so it can be easily retrieved
  // in context in the $form_state['values'] array in the element validator.
  $element['element_key'] = array(
    '#type' => 'value',
    '#value' => $element_key,
  );

  // Get the default address used to build the widget form elements, looking
  // first in the form state, then in the stored value for the field, and then
  // in the default values of the instance.
  $address = array();

  if (!empty($form_state['addressfield'][$element_key])) {
    // Use the value from the form_state if available.
    $address = $form_state['addressfield'][$element_key];
  }
  elseif (!empty($items[$delta]['country'])) {
    // Else use the saved value for the field.
    $address = $items[$delta];
  }
  else {
    // Otherwise use the instance default.
    $address = (array) $instance['default_value'][0];
  }

  // Determine the list of available countries, and if the currently selected
  // country is not in it, unset it so it can be reset to the default country.
  $countries = _addressfield_country_options_list($field, $instance);
  if (!empty($address['country']) && !isset($countries[$address['country']])) {
    unset($address['country']);
  }

  // Merge in default values to provide a value for every expected array key.
  $address += addressfield_default_values($countries);

  // Add the form elements for the standard widget, which includes a country
  // select list at the top that reloads the available address elements when the
  // country is changed.
  if ($instance['widget']['type'] == 'addressfield_standard') {
    // Wrap everything in a fieldset. This is not the best looking element,
    // but it's the only wrapper available in Drupal we can properly use
    // in that context, and it is overridable if necessary.
    $element['#type'] = 'fieldset';

    // Generate the address form.
    $context = array(
      'mode' => 'form',
      'field' => $field,
      'instance' => $instance,
      'langcode' => $langcode,
      'delta' => $delta,
    );
    $element += addressfield_generate($address, $settings['format_handlers'], $context);

    // Mark the form element as required if necessary.
    $element['#required'] = $delta == 0 && $instance['required'];
  }

  return $element;
}

/**
 * Element validate callback: rebuilds the form on country change and stores the
 * current address value in the $form_state for retrieval on rebuild.
 */
function addressfield_standard_country_validate($element, &$form_state) {
  // If the country was changed, rebuild the form.
  if ($element['#default_value'] != $element['#value']) {
    $form_state['rebuild'] = TRUE;
  }

  $parents = $element['#parents'];
  array_pop($parents);

  // Search through the form values to find the current address.
  $address = drupal_array_get_nested_value($form_state['values'], $parents);

  // Store the present address values in the form state for retrieval by the
  // widget form regardless of where the widget sits in the $form array.
  $form_state['addressfield'][$address['element_key']] = array_diff_key($address, array('element_key' => ''));
}

/**
 * Ajax callback in response to a change of country in an address field.
 *
 * The only thing we have to do is to find the proper element to render.
 */
function addressfield_standard_widget_refresh($form, $form_state) {
  // The target element is one element below the triggering country selector.
  $array_parents = $form_state['triggering_element']['#array_parents'];
  array_pop($array_parents);

  // Iterate over the form parents to find the element.
  $element = $form;
  foreach ($array_parents as $name) {
    $element = &$element[$name];
    if (!empty($element['#addressfield'])) {
      break;
    }
  }

  // Return the address block, but remove the '_weight' element inserted
  // by the field API.
  unset($element['_weight']);

  // Replace the address field widget with the updated widget and focus on the
  // new country select list.
  $commands[] = ajax_command_replace(NULL, render($element));
  $commands[] = ajax_command_invoke('#' . $element['country']['#id'], 'focus');

  // Allow other modules to add arbitrary AJAX commands on the refresh.
  drupal_alter('addressfield_standard_widget_refresh', $commands, $form, $form_state);

  return array('#type' => 'ajax', '#commands' => $commands);
}

/**
 * Implements hook_field_formatter_info().
 */
function addressfield_field_formatter_info() {
  return array(
    'addressfield_default' => array(
      'label' => t('Default'),
      'field types' => array('addressfield'),
      'settings' => array(
        'use_widget_handlers' => 1,
        'format_handlers' => array('address'),
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function addressfield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  $element['use_widget_handlers'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use the same configuration as the widget.'),
    '#default_value' => !empty($settings['use_widget_handlers']),
  );

  $element['format_handlers'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Format handlers'),
    '#options' => addressfield_format_plugins_options(),
    '#default_value' => $settings['format_handlers'],
    '#process' => array('form_process_checkboxes', '_addressfield_field_formatter_settings_form_process_add_state'),
    '#element_validate' => array('_addressfield_field_formatter_settings_form_validate')
  );

  return $element;
}

/**
 * Helper function: set the proper #states to the use widget handlers checkbox.
 */
function _addressfield_field_formatter_settings_form_process_add_state($element, $form_state) {
  // Build a #parents based on the current checkbox.
  $target_parents = array_slice($element['#parents'], 0, -1);
  $target_parents[] = 'use_widget_handlers';
  $target_parents = array_shift($target_parents) . ($target_parents ? '[' . implode('][', $target_parents) . ']' : '');

  $element['#states']['visible'] = array(
    ':input[name="' . $target_parents . '"]' => array('checked' => FALSE),
  );

  return $element;
}

/**
 * Helper function: filter the results of the checkboxes form element.
 */
function _addressfield_field_formatter_settings_form_validate($element, &$element_state) {
  form_set_value($element, array_filter($element['#value']), $element_state);
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function addressfield_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];

  $summary = '';

  if ($settings['use_widget_handlers']) {
    return t('Use widget configuration');
  }
  else {
    $summary = array();
    $plugins = addressfield_format_plugins();
    foreach ($settings['format_handlers'] as $handler) {
      $summary[] = $plugins[$handler]['title'];
    }
    return $summary ? implode(', ', $summary) : t('No handler');
  }
}

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

  switch ($display['type']) {
    case 'addressfield_default':
      if (!empty($settings['use_widget_handlers'])) {
        $handlers = $instance['widget']['settings']['format_handlers'];
      }
      else {
        $handlers = $settings['format_handlers'];
      }
      foreach ($items as $delta => $address) {
        // Generate the address format.
        $context = array(
          'mode' => 'render',
          'field' => $field,
          'instance' => $instance,
          'langcode' => $langcode,
          'delta' => $delta,
        );
        $element[$delta] = addressfield_generate($address, $handlers, $context);
      }
      break;
  }

  return $element;
}

/**
 * Callback to alter the property info of address fields.
 *
 * @see addressfield_field_info().
 */
function addressfield_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $name = $field['field_name'];
  $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];

  $property['type'] = ($field['cardinality'] != 1) ? 'list<addressfield>' : 'addressfield';
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';
  $property['auto creation'] = 'addressfield_auto_creation';
  $property['property info'] = addressfield_data_property_info();

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

/**
 * Auto creation callback for an addressfield value array.
 *
 * @see addressfield_property_info_callback()
 */
function addressfield_auto_creation() {
  // We can't call addressfield_default_values() directly, because it has an
  // optional array argument that will receive an invalid value when Entity API
  // tries to call it directly with its usual $property_name and $context
  // arguments.
  return addressfield_default_values();
}

/**
 * Defines info for the properties of the address field data structure.
 */
function addressfield_data_property_info($name = NULL) {
  // Build an array of basic property information for the address field.
  $properties = array(
    'country' => array(
      'label' => t('Country'),
      'options list' => '_addressfield_country_options_list',
    ),
    'name_line' => array(
      'label' => t('Full name'),
    ),
    'first_name' => array(
      'label' => t('First name'),
    ),
    'last_name' => array(
      'label' => t('Last name'),
    ),
    'organisation_name' => array(
      'label' => t('Company'),
    ),
    'administrative_area' => array(
      'label' => t('Administrative area (i.e. State / Province)'),
    ),
    'sub_administrative_area' => array(
      'label' => t('Sub administrative area'),
    ),
    'locality' => array(
      'label' => t('Locality (i.e. City)'),
    ),
    'dependent_locality' => array(
      'label' => t('Dependent locality'),
    ),
    'postal_code' => array(
      'label' => t('Postal code'),
    ),
    'thoroughfare' => array(
      'label' => t('Thoroughfare (i.e. Street address)'),
    ),
    'premise' => array(
      'label' => t('Premise (i.e. Apartment / Suite number)'),
    ),
  );

  // Add the default values for each of the address field properties.
  foreach ($properties as $key => &$value) {
    $value += array(
      'description' => !empty($name) ? t('!label of field %name', array('!label' => $value['label'], '%name' => $name)) : '',
      'type' => 'text',
      'getter callback' => 'entity_property_verbatim_get',
      'setter callback' => 'entity_property_verbatim_set',
    );
  }

  return $properties;
}

/**
 * Wraps country_get_list() for use as an Entity API options list.
 */
function _addressfield_country_options_list($field = NULL, $instance = NULL) {
  // Necessary for country_get_list().
  require_once DRUPAL_ROOT . '/includes/locale.inc';

  $countries = country_get_list();

  if (isset($field)) {
    // If the instance is not specified, loop against all the instances of the field.
    if (!isset($instance)) {
      $instances = array();
      foreach ($field['bundles'] as $entity_type => $bundles) {
        foreach ($bundles as $bundle_name) {
          $instances[] = field_info_instance($entity_type, $field['field_name'], $bundle_name);
        }
      }
    }
    else {
      $instances = array($instance);
    }

    foreach ($instances as $instance) {
      if (!empty($instance['widget']['settings']['available_countries'])) {
        $countries = array_intersect_key($countries, $instance['widget']['settings']['available_countries']);
      }
      else {
        // This instance allow all the countries.
        $countries = country_get_list();
        break;
      }
    }
  }

  return $countries;
}
