<?php

/**
 * @file
 * Publishing and unpublishing logic for the Archibald module.
 *
 * This contains functions and helpers for communicating with the ReST API
 * of the national catalog of the Swiss digital school library. These functions
 * serve to validate descriptions, publish them, unpublish them, etc.
 */

/**
 * Prepare a node for conversion to a JSON string.
 *
 * @param object $node
 *    The node that must be exported to JSON.
 *
 * @return object
 *    A PHP object representation of the expected JSON.
 */
function archibald_prepare_for_json_export($node) {
  module_load_include('inc', 'archibald', 'includes/archibald.ontology');

  // Prepare the data object, and the main category fields.
  $data = new stdClass();
  $data->general = new stdClass();
  $data->lifeCycle = new stdClass();
  $data->metaMetadata = new stdClass();
  $data->technical = new stdClass();
  $data->education = array(new stdClass());
  $data->rights = new stdClass();
  $data->relation = array();
  $data->classification = array();
  $data->curriculum = array();

  // 1.1 general.identifier[]
  if (!empty($node->lomch_general_identifier)) {
    $data->general->identifier = _archibald_prepare_for_json_export_identifier($node->lomch_general_identifier);
  }

  // 1.2 general.title
  $data->general->title = _archibald_prepare_for_json_export_langstring($node->title_field);

  // 1.3 general.language
  if (!empty($node->lomch_resource_language[LANGUAGE_NONE])) {
    $data->general->language = array_map(function($item) {
      $term = taxonomy_term_load($item['tid']);
      return $term->archibald_lang_iso_code[LANGUAGE_NONE][0]['value'];
    }, $node->lomch_resource_language[LANGUAGE_NONE]);
  }

  // 1.4 general.description
  if (!empty($node->lomch_general_description)) {
    // @todo We should support multiple values.
    $data->general->description = array(
      _archibald_prepare_for_json_export_langstring($node->lomch_general_description),
    );
  }

  // 1.5 general.keyword[]
  if (!empty($node->lomch_keywords)) {
    $data->general->keyword = _archibald_prepare_for_json_export_taxonomy_terms_langstring($node->lomch_keywords);
  }

  // 1.6 general.coverage[]
  if (!empty($node->lomch_coverage)) {
    $data->general->coverage = _archibald_prepare_for_json_export_taxonomy_terms_langstring($node->lomch_coverage);
  }

  // 1.8 general.aggregationLevel
  if (!empty($node->lomch_aggregation_level[LANGUAGE_NONE])) {
    $value = $node->lomch_aggregation_level[LANGUAGE_NONE][0]['value'];
    $data->general->aggregationLevel = (object) array(
      'source' => archibald_get_ontology_context('aggregation_level', $value),
      'value' => $value,
    );
  }

  // 2.1 lifeCycle.version
  if (!empty($node->lomch_version)) {
    $data->lifeCycle->version = _archibald_prepare_for_json_export_langstring($node->lomch_version);
  }

  // 2.3 lifeCycle.contribute[]
  foreach (array(
    'author',
    'editor',
    'publisher',
  ) as $role) {
    if (!empty($node->{"lomch_lifecycle_{$role}"})) {
      if (!isset($data->lifeCycle->contribute)) {
        $data->lifeCycle->contribute = array();
      }

      $data->lifeCycle->contribute[] = _archibald_prepare_for_json_export_contribute(
        $node->{"lomch_lifecycle_{$role}"},
        $role,
        'lc_cont_role'
      );
    }
  }

  // 3.1 metaMetadata.identifier[]
  $data->metaMetadata->identifier = array(
    (object) array(
      'catalog' => 'ARCHIBALD',
      'entry' => url('node/' . $node->nid, array('absolute' => TRUE)),
    ),
  );

  // 3.2 metaMetadata.contribute[]
  // Fetch all users that helped for this node. Prepare an array that can be
  // parsed by _archibald_prepare_for_json_export_contribute().
  $data->metaMetadata->contribute = array();
  $result = db_select('node_revision', 'v')
              ->fields('v', array('uid', 'timestamp'))
              ->condition('v.nid', $node->nid)
              ->condition('v.vid', $node->vid, '<=')
              ->condition('v.uid', 0, '<>')
              ->distinct()
              ->execute();
  while ($row = $result->fetchAssoc()) {
    $account = user_load($row['uid']);
    // Does the account have a VCard? Add it to our values.
    if (!empty($account->user_vcard)) {
      $contribute = _archibald_prepare_for_json_export_contribute(array(
        array(
          $account->user_vcard[LANGUAGE_NONE][0],
        ),
      ), 'creator', 'mmd_cont_role');
      $contribute->date = format_date($row['timestamp'], 'custom', 'Y-m-d\TH:i:sP');

      $data->metaMetadata->contribute[] = $contribute;
    }
  }

  // Add the partner as well to metametaData.contribute.
  if (!empty($node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id'])) {
    $vcard = _archibald_convert_archibald_partner_to_string(
      $node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']
    );
    if (!empty($vcard)) {
      $data->metaMetadata->contribute[] = (object) array(
        'role' => (object) array(
          'source' => archibald_get_ontology_context('mmd_cont_role', 'creator'),
          'value' => 'creator',
        ),
        'entity' => array($vcard),
        'date' => format_date($node->changed, 'custom', 'Y-m-d\TH:i:sP')
      );
    }
  }

  // 3.3 metaMetadata.schema
  // This is hard coded. Archibald always supports a single version.
  $data->metaMetadata->metadataSchema = array('LOM-CHv1.3');

  // 3.4 metaMetadata.language
  $data->metaMetadata->language = _archibald_convert_language_none_to_default($node->language);

  // 4.1 technical.format[]
  if (!empty($node->lomch_technical_format)) {
    $data->technical->format = array_map(function($item) {
      return $item['value'];
    }, $node->lomch_technical_format[LANGUAGE_NONE]);
  }

  // 4.2 technical.size
  if (!empty($node->lomch_size)) {
    $value = $node->lomch_size[LANGUAGE_NONE][0]['value'];
    switch ($node->lomch_size[LANGUAGE_NONE][0]['unit']) {
      case 'gb':
        $value *= DRUPAL_KILOBYTE;
      case 'mb':
        $value *= DRUPAL_KILOBYTE;
      case 'kb':
        $value *= DRUPAL_KILOBYTE;
    }
    $data->technical->size = $value;
  }

  // 4.3 technical.location[]
  if (!empty($node->lomch_location)) {
    $data->technical->location = array_map(function($item) {
      return $item['value'];
    }, $node->lomch_location[LANGUAGE_NONE]);
  }

  // 4.6 technical.otherPlatformRequirements
  if (!empty($node->lomch_platform_requirements)) {
    $data->technical->otherPlatformRequirements = _archibald_prepare_for_json_export_langstring($node->lomch_platform_requirements);
  }

  // 4.7 technical.duration
  if (!empty($node->lomch_duration[LANGUAGE_NONE][0]['value'])) {
    $t = round($node->lomch_duration[LANGUAGE_NONE][0]['value']);
    $data->technical->duration = 'PT' . sprintf('%dH%dM%dS', ($t/3600),($t/60%60), $t%60);
  }

  // 4.8 technical.previewImage
  if (!empty($node->lomch_preview_image)) {
    // If we wish to load the preview image on the dsb API server, we don't put
    // any information in the JSON data.
    if (!variable_get('archibald_host_images_on_api', FALSE)) {
      $file = file_load($node->lomch_preview_image[LANGUAGE_NONE][0]['fid']);
      if ($file) {
        $url = file_create_url($file->uri);
        $data->technical->previewImage = (object) array(
          'image' => $url,
        );

        if (!empty($node->lomch_preview_image[LANGUAGE_NONE][0]['title'])) {
          $data->technical->previewImage->copyright = $node->lomch_preview_image[LANGUAGE_NONE][0]['title'];
        }
      }
    }
  }

  // 5.2 education[].learningResourceType
  if (!empty($node->lomch_learning_resource_type)) {
    $data->education[0]->learningResourceType = new stdClass();
    // Unfortunately, contrary to standard LOM objects, LOM-CH objects split the
    // resource types into 2 sub trees. But that's not how they are stored in
    // the node. Because of this, we need to prepare the 2 trees separately, and
    // add them to the object in turn. We load the Ontology data for this.
    $terms = archibald_ontology_load('learning_resourcetype', TRUE);

    // Prepare 2 lists of keys.
    $keys = array(
      'pedagogical' => array_keys($terms['pedagogical']['terms']),
      'documentary' => array_keys($terms['documentary']['terms']),
    );

    // Now we prepare 2 lists, grouping the values together.
    $lists = array(
      'pedagogical' => array(),
      'documentary' => array(),
    );

    foreach ($node->lomch_learning_resource_type[LANGUAGE_NONE] as $type) {
      foreach (array('pedagogical', 'documentary') as $group) {
        if (in_array($type['value'], $keys[$group])) {
          $lists[$group][] = $type;
        }
      }
    }

    // Now that the lists are complete, we can treat them.
    foreach (array(
      'pedagogical',
      'documentary',
    ) as $group) {
      if (!empty($lists[$group])) {
        $data->education[0]->learningResourceType->{$group} = _archibald_prepare_for_json_export_vocabulary(
          array($lists[$group]),
          'learning_resourcetype'
        );
      }
    }
  }

  // 5.5 education[].intendedEndUserRole[]
  if (!empty($node->lomch_intended_enduserrole)) {
    $data->education[0]->intendedEndUserRole = _archibald_prepare_for_json_export_vocabulary(
      $node->lomch_intended_enduserrole,
      'intended_enduserrole'
    );
  }

  // 5.6 education[].context[]
  if (!empty($node->lomch_context)) {
    $data->education[0]->context = _archibald_prepare_for_json_export_vocabulary(
      $node->lomch_context,
      'education_context'
    );
  }

  // 5.7 education[].typicalAgeRange[]
  if (!empty($node->lomch_typical_age_range)) {
    $data->education[0]->typicalAgeRange = array();
    array_walk($node->lomch_typical_age_range, function($item, $langcode) use(&$data) {
      $langcode = _archibald_convert_language_none_to_default($langcode);
      $data->education[0]->typicalAgeRange[] = (object) array(
        $langcode => implode('-', $item[0]),
      );
    });
  }

  // 5.8 education[].difficultyLevel
  if (!empty($node->lomch_difficulty_level)) {
    // There's only one entry; get the first one only.
    $data->education[0]->difficultyLevel = @reset(_archibald_prepare_for_json_export_vocabulary(
      $node->lomch_difficulty_level,
      'difficulty'
    ));
  }

  // 5.9 education[].typicalLearningTime
  if (!empty($node->lomch_typical_learning_time)) {
    $values = _archibald_prepare_for_json_export_vocabulary(
      $node->lomch_typical_learning_time,
      'typical_learning_time'
    );
    // This object is a bit special, in that it has a sub-object that doesn't
    // make much sense... But that's the standard.
    $data->education[0]->typicalLearningTime = (object) array(
      // There's only 1 item. If there are more, we ignore them.
      'learningTime' => $values[0],
    );
  }

  // 5.10 education[].description
  if (!empty($node->lomch_educational_description)) {
    // @todo We should support multiple values.
    $data->education[0]->description = array(
      _archibald_prepare_for_json_export_langstring($node->lomch_educational_description),
    );
  }

  // 6.1 rights.cost
  if (!empty($node->lomch_rights_cost)) {
    $values = _archibald_prepare_for_json_export_vocabulary(
      $node->lomch_rights_cost,
      'rights_cost'
    );
    // There's only 1 item. If there are more, we ignore them.
    $data->rights->cost = $values[0];
  }

  // 6.3 rights.description
  if (!empty($node->lomch_rights_description)) {
    $data->rights->copyright = array();
    foreach ($node->lomch_rights_description as $langcode => $values) {
      foreach ($values as $value) {
        // Fetch the LangString.
        $description = array(
          'description' => _archibald_taxonomy_term_load_localized_names(
            $value['tid'],
            _archibald_convert_language_none_to_default($langcode)
          ),
        );

        // Does it have any URLs?
        $term = taxonomy_term_load($value['tid']);
        if (!empty($term->archibald_license_url)) {
          $description['identifier'] = array();
          // Add all URLs to the identifier key.
          foreach ($term->archibald_license_url as $url_values) {
            foreach ($url_values as $url_value) {
              if (empty($url_value['value'])) {
                continue;
              }

              $description['identifier'][] = array(
                'catalog' => 'URL',
                'entry' => $url_value['value'],
              );
            }
          }
        }

        $data->rights->copyright[] = $description;
      }
    }
  }

  // 7 relation[]
  if (!empty($node->lomch_relations)) {
    $relations = _archibald_prepare_for_json_export_relations($node->lomch_relations);
    if (!empty($relations)) {
      $data->relation += $relations;
    }
  }

  return $data;
}

/**
 * Helper function for treating LangStrings.
 *
 * @param array $values
 *    An array of field values to be extracted. This expects the values to be
 *    stored in the standard Drupal entity field format, keyed by language.
 * @param string $value_key
 *   (optional) The key at which the value is found. Defaults to "value".
 *
 * @return object
 *    A LangString object, ready for export.
 */
function _archibald_prepare_for_json_export_langstring($values, $value_key = 'value') {
  $data = array();
  foreach ($values as $langcode => $item) {
    $langcode = _archibald_convert_language_none_to_default($langcode);
    $data[$langcode] = $item[0][$value_key];
  };
  return (object) $data;
}

/**
 * Helper function for treating vocabulary items.
 *
 * @param array $values
 *    An array of field values to be extracted. This expects the values to be
 *    stored in the standard Drupal entity field format, keyed by language.
 * @param string $vocabulary_id
 *    The identifier of the vocabulary.
 * @param string $value_key
 *    (optional) The key at which the value is found. Defaults to "value".
 *
 * @return object
 *    An array of vocabulary objects, ready for export.
 */
function _archibald_prepare_for_json_export_vocabulary($values, $vocabulary_id, $value_key = 'value') {
  module_load_include('inc', 'archibald', 'includes/archibald.ontology');
  $data = array();
  foreach ($values as $subvalues) {
    foreach($subvalues as $item) {
      $data[] = (object) array(
        'source' => archibald_get_ontology_context($vocabulary_id, $item[$value_key]),
        'value' => $item[$value_key],
      );
    }
  }
  return $data;
}

/**
 * Helper function for treating LangStrings stored as taxonomy terms.
 *
 * @param array $values
 *    An array of field values to be extracted. This expects the values to be
 *    stored in the standard Drupal entity field format, keyed by language.
 * @param string $value_key
 *   (optional) The key at which the value is found. Defaults to "tid".
 *
 * @return array
 *    A list of LangString objects, ready for export.
 */
function _archibald_prepare_for_json_export_taxonomy_terms_langstring($values, $value_key = 'tid') {
  $data = array();
  foreach($values as $langcode => $item) {
    $langcode = _archibald_convert_language_none_to_default($langcode);
    foreach ($item as $value) {
      $term_values = _archibald_taxonomy_term_load_localized_names($value[$value_key], $langcode);
      if (!empty($term_values)) {
        $data[] = (object) $term_values;
      }
    }
  }
  return $data;
}

/**
 * Helper function for treating identifier objects.
 *
 * Identifiers are special, in that they constitute an array of arrays. But, it
 * is fully possible that 2 items in different languages actually point to the
 * same resource. To solve this, we combine items based on their value and type.
 * If 2 entries match, the titles are combined in a LangString.
 *
 * @param array $values
 *    An array of field values to be extracted. This expects the values to be
 *    stored in the standard Drupal entity field format, keyed by language.
 *
 * @return array
 *    A list of identifier objects, ready for export.
 */
function _archibald_prepare_for_json_export_identifier($values) {
  $data = array();
  foreach ($values as $langcode => $lang_values) {
    foreach($lang_values as $item) {
      $langcode = _archibald_convert_language_none_to_default($langcode);
      $key = "{$item['type']}:{$item['value']}";
      if (!isset($data[$key])) {
        $data[$key] = (object) array(
          'catalog' => $item['type'],
          'entry' => $item['value'],
        );
      }
      if (!empty($item['title'])) {
        if (!isset($data[$key]->title)) {
          $data[$key]->title = new stdClass();
        }
        $data[$key]->title->{$langcode} = $item['title'];
      }
    }
  }
  return array_values($data);
}

/**
 * Helper function for treating contribute lists.
 *
 * @param array $values
 *    An array of field values to be extracted. This expects the values to be
 *    stored in the standard Drupal entity field format, keyed by language.
 * @param string $role
 *    The role type. Must be a valid LOMv1.0 key, like "author", "editor" or
 *    "publisher". If it is part of another vocabulary, it must be specified
 *    with the $role_src parameter.
 * @param string $vocabulary_id
 *   (optional) The vocabulary the role is taken from. Defaults to "LOMv1.0".
 * @param string $value_key
 *   (optional) The key at which the value is found. Defaults to "target_id".
 *
 * @return object
 *    A contribute object, ready for export.
 */
function _archibald_prepare_for_json_export_contribute($values, $role, $vocabulary_id, $value_key = 'target_id') {
  $data = (object) array(
    'role' => (object) array(
      'source' => archibald_get_ontology_context($vocabulary_id, $role),
      'value' => $role,
    ),
    'entity' => array(),
  );
  foreach($values as $item) {
    foreach ($item as $value) {
      $vcard = _archibald_convert_archibald_vcard_to_string($value[$value_key]);
      if (!empty($vcard)) {
        $data->entity[] = $vcard;
      }
    }
  }
  return $data;
}

/**
 * Helper function for treating relationship lists.
 *
 * Relations are special, and a bit tricky to handle. The reason is the form
 * fields, and the limitations of Field Collection with Entity Translation.
 * We have to treat the whole relations field collection as translatable, where
 * in fact, only 1 field is actually translatable. The other ones are simply
 * shared. This creates issues, as we need to merge data, which could very well
 * lead to conflicts. If we see that the values in different languages differ,
 * we treat them as separate entities. If they match, we can safely merge them.
 *
 * Furthermore, we need to merge values based on the relation type. All
 * relationships of a given type should be combined under the same object in the
 * final JSON.
 *
 * @param array $values
 *    An array of field values to be extracted. This expects the values to be
 *    stored in the standard Drupal entity field format, keyed by language.
 *
 * @return object
 *    A list of relationship objects, ready for export.
 */
function _archibald_prepare_for_json_export_relations($values, $vocabulary_src = 'LOMv1.0') {
  $data = array();
  $types = array();
  $merged_by_kind = array();
  $merged_by_value = array();

  foreach($values as $langcode => $items) {
    // Because of a bug in Field Collection with Entity Translation, it is
    // possible that we see occurrences of LANGUAGE_NONE items, although there
    // should only be fields of a specific language. In order to prevent this,
    // we do not treat LANGUAGE_NONE items, but we simply ignore them.
    // @todo This requires more thorough investigation!!
    // @todo This might be solved with the patch from
    //       https://www.drupal.org/node/1344672
    if ($langcode == LANGUAGE_NONE) {
      continue;
    }

    foreach ($items as $value) {
      $relations = _archibald_load_relation_object($value['value']);
      if (!empty($relations)) {
        foreach ($relations as $relation) {
          // Cache the parsed kind, so we can reuse it later.
          $kinds[$relation['kind']->value] = $relation['kind'];

          // If this is the first time we encounter this kind, prepare the list.
          if (!isset($merged_by_kind[$relation['kind']->value])) {
            $merged_by_kind[$relation['kind']->value] = array();
          }

          $merged_by_kind[$relation['kind']->value][] = $relation;
        }
      }
    }
  }

  // Now treat each element, and try to merge the languages together. We do this
  // based on the identifier.entry. If 2 elements have the same entry, we try
  // to merge them, unless they have the same language.
  foreach ($merged_by_kind as $kind => $items) {
    $merged_by_value[$kind] = array();

    foreach ($items as $item) {
      if (!isset($merged_by_value[$kind][$item['identifier']['entry']])) {
        $merged_by_value[$kind][$item['identifier']['entry']] = (object) array(
          'identifier' => (object) $item['identifier'],
        );
      }

      if (!empty($item['description'])) {
        if (!isset($merged_by_value[$kind][$item['identifier']['entry']]->description)) {
          $merged_by_value[$kind][$item['identifier']['entry']]->description = new stdClass();
        }

        // Merge the descriptions.
        $merged_by_value[$kind][$item['identifier']['entry']]->description->{$item['language']} = $item['description'];
      }
    }
  }

  // Finally, we combine the combined values with the cached kinds, and return
  // the result.
  foreach($merged_by_value as $kind => $items) {
    $data[] = (object) array(
      'kind' => $kinds[$kind],
      'resource' => array_values($items),
    );
  }

  return $data;
}

/**
 * Helper function to load and localize taxonomy terms.
 *
 * Helps with the loading of taxonomy terms, as well handling the localization
 * based on the language passed.
 *
 * @param int $tid
 *    The taxonomy term ID.
 * @param string $default_langcode
 *    If the taxonomy term is not localized, the language as which the term
 *    should be treated for.
 *
 * @return array
 *    A list of localized values for the taxonomy term, keyed by language.
 */
function _archibald_taxonomy_term_load_localized_names($tid, $default_langcode) {
  // The static cache is not actually necessary... But it simplifies unit tests
  // A LOT. See ArchibaldNodeJsonExportHelpersUnitTestCase.
  $terms = &drupal_static(__FUNCTION__, array());

  if (!isset($terms[$tid])) {
    $term = taxonomy_term_load($tid);
    if (!empty($term->name_field)) {
      $values = array();
      array_walk($term->name_field, function($item, $langcode) use(&$values, $default_langcode) {
        if ($langcode !== LANGUAGE_NONE) {
          $values[$langcode] = $item[0]['value'];
        }
        else if (!isset($values[$default_langcode])) {
          $values[$default_langcode] = $item[0]['value'];
        }
      });
      $terms[$tid] = $values;
    }
    else {
      $terms[$tid] = array(
        $default_langcode => $term->name,
      );
    }
  }

  return $terms[$tid];
}

/**
 * Helper function for translating "und" strings to the default language.
 *
 * Drupal fields can have a "und" (LANGUAGE_NONE) key. But this is not usable
 * by the API. If we encounter such a key, we substitute it for the default
 * language of the site.
 *
 * @param string $langcode
 *    The language code to be checked.
 *
 * @return string
 *    The same language code if it is not "und", or the default language code
 *    otherwise.
 */
function _archibald_convert_language_none_to_default($langcode) {
  if ($langcode == LANGUAGE_NONE) {
    return language_default('language');
  }
  return $langcode;
}

/**
 * Helper function to transform an archibald_vcard entity to a string.
 *
 * @see ArchibaldVcardExporter
 *
 * @param int $vcard_id
 *    The archibald_vcard entity ID.
 *
 * @return string|null
 *    A VCard encoded string, or null if the VCard could not be loaded.
 */
function _archibald_convert_archibald_vcard_to_string($vcard_id) {
  // The static cache is not actually necessary... But it simplifies unit tests
  // A LOT. See ArchibaldNodeJsonExportHelpersUnitTestCase.
  $vcards = &drupal_static(__FUNCTION__, array());

  if (!isset($vcards[$vcard_id])) {
    $vcard = archibald_vcard_load($vcard_id);
    if (empty($vcard)) {
      return NULL;
    }

    $object = new ArchibaldVcardExporter();

    if (
      !empty($vcard->vcard_first_name[LANGUAGE_NONE][0]['value']) &&
      !empty($vcard->vcard_last_name[LANGUAGE_NONE][0]['value'])
    ) {
      $object->addName(
        $vcard->vcard_first_name[LANGUAGE_NONE][0]['value'],
        $vcard->vcard_last_name[LANGUAGE_NONE][0]['value']
      );
    }

    // For the address, we require at least a country or a city.
    if (
      !empty($vcard->vcard_country[LANGUAGE_NONE][0]['value']) ||
      !empty($vcard->vcard_city[LANGUAGE_NONE][0]['value'])
    ) {
      if (
        !empty($vcard->vcard_first_name[LANGUAGE_NONE][0]['value']) &&
        !empty($vcard->vcard_last_name[LANGUAGE_NONE][0]['value'])
      ) {
        $name = $vcard->vcard_first_name[LANGUAGE_NONE][0]['value'] . ' ' . $vcard->vcard_last_name[LANGUAGE_NONE][0]['value'];
        $extended = !empty($vcard->vcard_organization[LANGUAGE_NONE][0]['value']) ?
          $vcard->vcard_organization[LANGUAGE_NONE][0]['value'] :
          '';
      }
      else {
        $name = $vcard->vcard_organization[LANGUAGE_NONE][0]['value'];
        $extended = '';
      }

      $street = !empty($vcard->vcard_address[LANGUAGE_NONE][0]['value']) ?
        $vcard->vcard_address[LANGUAGE_NONE][0]['value'] :
        '';
      $street .= !empty($vcard->vcard_add_address[LANGUAGE_NONE][0]['value']) ?
        ' ' . $vcard->vcard_add_address[LANGUAGE_NONE][0]['value'] :
        '';
      $city = !empty($vcard->vcard_city[LANGUAGE_NONE][0]['value']) ?
        $vcard->vcard_city[LANGUAGE_NONE][0]['value'] :
        '';
      $zip = !empty($vcard->vcard_zip[LANGUAGE_NONE][0]['value']) ?
        $vcard->vcard_zip[LANGUAGE_NONE][0]['value'] :
        '';
      $country = !empty($vcard->vcard_country[LANGUAGE_NONE][0]['value']) ?
        $vcard->vcard_country[LANGUAGE_NONE][0]['value'] :
        '';

      $object->addAddress(
        $name,
        $extended,
        $street,
        $city,
        '',
        $zip,
        $country,
        'WORK;POSTAL'
      );
    }

    if (!empty($vcard->vcard_organization[LANGUAGE_NONE][0]['value'])) {
      $object->addCompany($vcard->vcard_organization[LANGUAGE_NONE][0]['value']);
    }

    if (!empty($vcard->vcard_email[LANGUAGE_NONE][0]['email'])) {
      $object->addEmail($vcard->vcard_email[LANGUAGE_NONE][0]['email'], 'WORK');
    }

    if (!empty($vcard->vcard_phone[LANGUAGE_NONE][0]['value'])) {
      $object->addPhoneNumber($vcard->vcard_phone[LANGUAGE_NONE][0]['value'], 'WORK');
    }

    if (!empty($vcard->vcard_url[LANGUAGE_NONE][0]['value'])) {
      $object->addURL($vcard->vcard_url[LANGUAGE_NONE][0]['value'], 'WORK');
    }

    // This is still pending a PR on Github, hence the method_exists() check.
    if (method_exists($object, 'addLogo') && !empty($vcard->vcard_logo[LANGUAGE_NONE][0]['fid'])) {
      // If we wish to upload images to the dsb API server, we use a little
      // trick. We put the local full realpath, instead of a full URL.
      // This will allow us, in archibald_archibald_publish_description(), to
      // hijack the data before it's being sent to the server, and upload the
      // logos as required.
      try {
        $object->addLogo(
          variable_get('archibald_host_images_on_api', FALSE) ?
            drupal_realpath($vcard->vcard_logo[LANGUAGE_NONE][0]['uri']) :
            file_create_url($vcard->vcard_logo[LANGUAGE_NONE][0]['uri']),
          false
        );
      }
      catch(Exception $e) {
        // From time to time, an import will not fetch any file extension for
        // a VCard logo. This is fine most of the time, as browsers and other
        // application still know it's an image. However, the
        // JeroenDesloovere\VCard\VCard library will throw an exception if it
        // doesn't know how to deal with a file whose MIME type is not
        // deductible . Catch it here, but simply ignore it; there's nothing we
        // can do. Put up a warning message, though.
        drupal_set_message(t("The VCard %name contains a logo, but it couldn't be exported to a valid VCard string. It has been left out for now. Please check it's logo, or try re-uploading it.", array(
          '%name' => $vcard->name,
        )), 'warning');
      }
    }

    $vcards[$vcard_id] = $object->buildVCardRev($vcard->changed);
  }

  return $vcards[$vcard_id];
}

/**
 * Helper function to transform an archibald_partner entity to a string.
 *
 * @see ArchibaldVcardExporter
 *
 * @param int $partner_id
 *    The archibald_partner entity ID.
 *
 * @return string|null
 *    A VCard encoded string, or null if the VCard could not be loaded.
 */
function _archibald_convert_archibald_partner_to_string($partner_id) {
  // The static cache is not actually necessary... But it simplifies unit tests
  // A LOT. See ArchibaldNodeJsonExportHelpersUnitTestCase.
  $partners = &drupal_static(__FUNCTION__, array());

  if (!isset($partners[$partner_id])) {
    $partner = archibald_partner_load($partner_id);
    if (empty($partner)) {
      return NULL;
    }

    $object = new ArchibaldVcardExporter();

    $object->addName(
      !empty($partner->partner_display_name[LANGUAGE_NONE][0]['value']) ?
        $partner->partner_display_name[LANGUAGE_NONE][0]['value'] :
        $partner->name
    );

    // For the address, we require at least a country or a city.
    if (
      !empty($partner->partner_country[LANGUAGE_NONE][0]['value']) ||
      !empty($partner->partner_city[LANGUAGE_NONE][0]['value'])
    ) {
      $name = $partner->partner_display_name[LANGUAGE_NONE][0]['value'] . ' (' . $partner->partner_username[LANGUAGE_NONE][0]['value'] . ')';
      $extended = !empty($partner->partner_organization[LANGUAGE_NONE][0]['value']) ?
        $partner->partner_organization[LANGUAGE_NONE][0]['value'] :
        '';

      $street = !empty($partner->partner_address[LANGUAGE_NONE][0]['value']) ?
        $partner->partner_address[LANGUAGE_NONE][0]['value'] :
        '';
      $street .= !empty($partner->partner_add_address[LANGUAGE_NONE][0]['value']) ?
        ' ' . $partner->partner_add_address[LANGUAGE_NONE][0]['value'] :
        '';
      $city = !empty($partner->partner_city[LANGUAGE_NONE][0]['value']) ?
        $partner->partner_city[LANGUAGE_NONE][0]['value'] :
        '';
      $zip = !empty($partner->partner_zip[LANGUAGE_NONE][0]['value']) ?
        $partner->partner_zip[LANGUAGE_NONE][0]['value'] :
        '';
      $country = !empty($partner->partner_country[LANGUAGE_NONE][0]['value']) ?
        $partner->partner_country[LANGUAGE_NONE][0]['value'] :
        '';

      $object->addAddress(
        $name,
        $extended,
        $street,
        $city,
        '',
        $zip,
        $country,
        'WORK;POSTAL'
      );
    }

    if (!empty($partner->partner_organization[LANGUAGE_NONE][0]['value'])) {
      $object->addCompany($partner->partner_organization[LANGUAGE_NONE][0]['value']);
    }

    if (!empty($partner->partner_email[LANGUAGE_NONE][0]['email'])) {
      $object->addEmail($partner->partner_email[LANGUAGE_NONE][0]['email'], 'WORK');
    }

    if (!empty($partner->partner_url[LANGUAGE_NONE][0]['value'])) {
      $object->addURL($partner->partner_url[LANGUAGE_NONE][0]['value'], 'WORK');
    }

    // This is still pending a PR on Github, hence the method_exists() check.
    if (method_exists($object, 'addLogo') && !empty($partner->partner_logo[LANGUAGE_NONE][0]['fid'])) {
      // If we wish to upload images to the dsb API server, we use a little
      // trick. We put the local full realpath, instead of a full URL.
      // This will allow us, in archibald_archibald_publish_description(), to
      // hijack the data before it's being sent to the server, and upload the
      // logos as required.
      try {
        $object->addLogo(
          variable_get('archibald_host_images_on_api', FALSE) ?
            drupal_realpath($partner->partner_logo[LANGUAGE_NONE][0]['uri']) :
            file_create_url($partner->partner_logo[LANGUAGE_NONE][0]['uri']),
          false
        );
      }
      catch(Exception $e) {
        // From time to time, an import will not fetch any file extension for
        // a VCard logo. This is fine most of the time, as browsers and other
        // application still know it's an image. However, the
        // JeroenDesloovere\VCard\VCard library will throw an exception if it
        // doesn't know how to deal with a file whose MIME type is not
        // deductible . Catch it here, but simply ignore it; there's nothing we
        // can do. Put up a warning message, though.
        drupal_set_message(t("The Partner %name contains a logo, but it couldn't be exported to a valid VCard string. It has been left out for now. Please check it's logo, or try re-uploading it.", array(
          '%name' => $partner->name,
        )), 'warning');
      }
    }

    $partners[$partner_id] = $object->buildVCardRev($partner->changed);
  }

  return $partners[$partner_id];
}

/**
 * Helper function to load a relation object.
 *
 * @param int $relation_id
 *    The ID of the relation field collection entity.
 *
 * @return array|null
 *    If not found, this will return null. Otherwise, it will return an array
 *    containing the following keys:
 *    - kind: The kind vocabulary object, already parsed.
 *    - identifier: The identifier data, which contains 2 keys:
 *      - catalog: The catalog type.
 *      - entry: The identifier value.
 *    - description: The description, in string format.
 */
function _archibald_load_relation_object($relation_id) {
  // The static cache is not actually necessary... But it simplifies unit tests
  // A LOT. See ArchibaldNodeJsonExportHelpersUnitTestCase.
  $relations = &drupal_static(__FUNCTION__, array());

  if (!isset($relations[$relation_id])) {
    // Load the field collection entity.
    $field_collections = entity_load('field_collection_item', array($relation_id));
    $field_collection = reset($field_collections);

    // Does it exist?
    if (!empty($field_collection)) {
      // Start by parsing the "kind" key.
      $tmp_kinds = _archibald_prepare_for_json_export_vocabulary(
        $field_collection->lomch_relations_kind,
        'rel_kind'
      );

      // If we parsed the kind correctly, store the whole shebang.
      if (!empty($tmp_kinds)) {
        foreach ($field_collection->lomch_relations_identifier as $langcode => $identifier) {
          $relation = array(
            'kind' => $tmp_kinds[0],
            'identifier' => array(
              'catalog' => $identifier[0]['type'],
              'entry' => $identifier[0]['value'],
            ),
            'language' => $langcode,
          );

          // The description is not required. Check the same language as the
          // identifier, as they are always linked anyway.
          if (!empty($field_collection->lomch_relations_description[$langcode][0]['value'])) {
            $relation['description'] = $field_collection->lomch_relations_description[$langcode][0]['value'];
          }

          $relations[$relation_id][] = $relation;
        }
      }
    }
  }

  return isset($relations[$relation_id]) ? $relations[$relation_id] : NULL;
}

/**
 * Publish a description.
 *
 * @param object $node
 *    The description node to publish.
 *
 * @return bool
 *    True if the publishing succeeded. False otherwise.
 */
function archibald_publish_description($node) {
  watchdog('archibald:publish', "About to publish %title.", array(
    '%title' => $node->title,
  ), WATCHDOG_DEBUG);

  // First, try to validate.
  $result = archibald_validate_description($node);

  if (!empty($result['valid'])) {
    $client = archibald_get_client_based_on_config(
      $node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']
    );

    $data = archibald_json_export($node);

    // Prepare the catalog data.
    $catalogs = array_map(function($item) {
      return $item['value'];
    }, $node->archibald_publication_catalogs[LANGUAGE_NONE]);

    // Allow other modules to interact with the publishing procedure.
    // IMPORTANT: This is where we upload images, if needed! See
    // archibald_archibald_publish_description() for more information.
    module_invoke_all('archibald_publish_description', 'pre publish', $data, $node);

    try {
      // Convert to JSON.
      $json = json_encode($data, JSON_HEX_TAG | JSON_HEX_QUOT);

      // Is this an update?
      if (isset($node->lom_id)) {
        $result = $client->putDescription($node->lom_id, $json, $catalogs);
        watchdog('archibald:publish', "Made a successful PUT request when publishing %title. Result: %result", array(
          '%title' => $node->title,
          '%result' => json_encode($result),
        ), WATCHDOG_DEBUG);

        // Make sure the resource is indeed published. If it was unpublished
        // before, we can still update it, but this will not change its
        // publication state.
        if (method_exists($client, 'postDescriptionAction')) {
          $result = $client->postDescriptionAction($node->lom_id, 'publish_description');
          watchdog('archibald:publish', "Successfully POSTed a publish action request for %title.", array(
            '%title' => $node->title,
          ), WATCHDOG_DEBUG);
        }
      }
      else {
        // This node was never published.
        $result = $client->postDescription($json, $catalogs);
        watchdog('archibald:publish', "Made a successful POST request when publishing %title. Result: %result", array(
          '%title' => $node->title,
          '%result' => json_encode($result),
        ), WATCHDOG_DEBUG);

        db_insert('archibald_lom_id')
          ->fields(array(
            'nid' => $node->nid,
            'lom_id' => $result['lomId'],
          ))
          ->execute();
      }

      module_invoke_all('archibald_publish_description', 'post publish', $data, $node);
    }
    catch(Exception $e) {
      module_invoke_all('archibald_publish_description', 'publish error', $data, $node);

      watchdog('archibald:publish', "Could not publish %title. Error: %e", array(
        '%title' => $node->title,
        '%e' => $e->getMessage(),
      ), WATCHDOG_ERROR);
      watchdog_exception('archibald:publish', $e);

      // Something went wrong with the publishing. Add a task to the queue so
      // we can try again later.
      archibald_add_to_queue(
        "node:{$node->nid}:publish",
        'archibald_publish_description',
        array($node),
        'includes/archibald.publish'
      );

      return FALSE;
    }

    // Update the hashes.
    archibald_set_hash($node, 'published');
    archibald_set_hash($node, 'current');

    return TRUE;
  }
  else {
    watchdog('archibald:publish', "Publishing failed for %title.", array(
      '%title' => $node->title,
    ), WATCHDOG_ERROR);

    return FALSE;
  }
}

/**
 * Unpublish a description.
 *
 * @param object $node
 *    The description node to publish.
 *
 * @return bool
 *    True if the unpublishing succeeded. False otherwise.
 */
function archibald_unpublish_description($node) {
  watchdog('archibald:unpublish', "About to unpublish %title.", array(
    '%title' => $node->title,
  ), WATCHDOG_DEBUG);

  module_invoke_all('archibald_unpublish_description', 'pre unpublish', $node);

  try {
    $client = archibald_get_client_based_on_config(
      $node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']
    );
    if (method_exists($client, 'postDescriptionAction')) {
      $result = $client->authenticate()->postDescriptionAction($node->lom_id, 'unpublish_description');
      watchdog('archibald:unpublish', "Successfully POSTed an unpublish action request for %title.", array(
        '%title' => $node->title,
      ), WATCHDOG_DEBUG);
    }
    else {
      $result = $client->authenticate()->deleteDescription($node->lom_id);
      watchdog('archibald:unpublish', "Made a successful DELETE request for %title.", array(
        '%title' => $node->title,
      ), WATCHDOG_DEBUG);
    }

    module_invoke_all('archibald_unpublish_description', 'post unpublish', $node);

    return TRUE;
  }
  catch(Exception $e) {
    module_invoke_all('archibald_unpublish_description', 'unpublish error', $node);

    watchdog('archibald:unpublish', "Could not unpublish node %title (Lom ID: %lomid). Error: %e", array(
      '%title' => $node->title,
      '%lomid' => $node->lom_id,
      '%e' => $e->getMessage(),
    ), WATCHDOG_ERROR);

    // Something went wrong with the unpublishing. Add a task to the queue so
    // we can try again later.
    archibald_add_to_queue(
      "node:{$node->nid}:unpublish",
      'archibald_unpublish_description',
      array($node),
      'includes/archibald.publish'
    );

    return FALSE;
  }
}

/**
 * Delete a description.
 *
 * @param object $node
 *    The description node to publish.
 *
 * @return bool
 *    True if the deletion succeeded. False otherwise.
 */
function archibald_delete_description($node) {
  watchdog('archibald:delete', "About to delete %title.", array(
    '%title' => $node->title,
  ), WATCHDOG_DEBUG);

  $client = archibald_get_client_based_on_config(
    $node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']
  );

  module_invoke_all('archibald_delete_description', 'pre delete', $node);

  try {
    $result = $client->authenticate()->deleteDescription($node->lom_id);
    watchdog('archibald:unpublish', "Made a successful DELETE request for %title.", array(
      '%title' => $node->title,
    ), WATCHDOG_DEBUG);

    module_invoke_all('archibald_delete_description', 'post delete', $node);

    db_delete('archibald_lom_id')
      ->condition('nid', $node->nid)
      ->execute();

    return TRUE;
  }
  catch(Exception $e) {
    module_invoke_all('archibald_delete_description', 'delete error', $node);

    watchdog('archibald:delete', "Could not delete node %title (Lom ID: %lomid). Error: %e", array(
      '%title' => $node->title,
      '%lomid' => $node->lom_id,
      '%e' => $e->getMessage(),
    ), WATCHDOG_ERROR);

    // Something went wrong with the deleting. Add a task to the queue so
    // we can try again later.
    archibald_add_to_queue(
      "node:{$node->nid}:delete",
      'archibald_delete_description',
      array($node),
      'includes/archibald.publish'
    );

    return FALSE;
  }
}

/**
 * Validate a description.
 *
 * @param object $node
 *    The description node to validate.
 *
 * @return array|false
 *    The result of the ReST API, or false if the client could not contact or
 *    authenticate with the API endpoint. Check the ReST API's documentation for
 *    more information.
 */
function archibald_validate_description($node) {
  watchdog('archibald:validate', "About to validate %title.", array(
    '%title' => $node->title,
  ), WATCHDOG_DEBUG);

  $json = json_encode(
    archibald_json_export($node)
  );

  try {
    $client = archibald_get_client_based_on_config(
      $node->archibald_publication_partner[LANGUAGE_NONE][0]['target_id']
    );
    $result = $client->validateDescription($json);
    watchdog('archibald:validate', "Validated %title. Result: %result.", array(
      '%title' => $node->title,
      '%result' => json_encode($result),
    ), WATCHDOG_DEBUG);
    return $result;
  }
  catch(Exception $e) {
    watchdog('archibald:validate', "Could not validate %title. Error: %e", array(
      '%title' => $node->title,
      '%e' => $e->getMessage(),
    ), WATCHDOG_ERROR);
    return FALSE;
  }
}
