<?php

/**
 * @file
 * Import logic.
 */

/**
 * Import a description in JSON format.
 *
 * @see _archibald_import_from_json()
 *
 * @param object $node
 *    The node object to import the data on.
 * @param object $data
 *    The description data (decoded JSON).
 * @param array $files
 *    (optional) If any files were already pre-fetched, pass their references
 *    here. This is expected to be an array, where the keys are the paths in
 *    the JSON data, and the values local Drupal file IDs. Defaults to an empty
 *    array.
 *
 * @return bool
 *    True if no error has occurred, false otherwise.
 */
function _archibald_import_from_json($node, $data, $files = array()) {
  // Prepare a list of languages that are available in the data. We will use
  // this to register translations with ET.
  $available_languages = array();

  // Prepare a list of all active languages as well.
  $active_languages = array_keys(language_list());

  // Fetch the API name based on the information in
  // archibald_archibald_lomch_description_fields().
  module_load_include('inc', 'archibald', 'includes/archibald.entities');
  $entity_fields = archibald_archibald_lomch_description_fields();

  // Prepare a LomDescription object, so we can easily access field data.
  $lom = new \Educa\DSB\Client\Lom\LomDescription($data);

  // 3.4 metaMetadata.language
  // The language is the most important to get first. Many other fields depend
  // on it.
  $node->language = $lom->getField('metaMetadata.language');
  if (empty($node->language)) {
    $node->language = language_default('language');
  }

  // If the source language is not active on the system, abort early.
  if (!in_array($node->language, $active_languages)) {
    drupal_set_message(t("The source language (%lang) for description %title is not active on the system! This will result in data loss. Please make sure the language is enabled. Contact your site administrator if needed.", array(
        '%title' => $node->title,
        '%lang' => $node->language,
      )), 'error');

    return FALSE;
  }

  // Treat all LangStrings fields that are not part of a complex, sub-field
  // group, first. This includes:
  // 1.2 general.description
  // 2.1 lifeCycle.version
  // 4.6 technical.otherPlatformRequirements
  // 5.10 education.description
  foreach (array(
    'title_field',
    'lomch_general_description',
    'lomch_version',
    'lomch_platform_requirements',
    'lomch_educational_description',
  ) as $field_name) {
    if (
      isset($entity_fields[$field_name]['api_name']) ||
      $field_name == 'title_field'
    ) {
      archibald_import_langstring(
        $node,
        $lom,
        $field_name,
        // The title is special, as it isn't listed in
        // archibald_archibald_lomch_description_fields().
        $field_name == 'title_field' ?
          'general.title' :
          archibald_import_determine_path($entity_fields[$field_name]['api_name'], $lom),
        $available_languages
      );
    }
  }

  // Treat all "plain" text fields that are not part of a complex, sub-field
  // group, next. This includes:
  // 4.2 technical.size
  // 4.3 technical.location
  foreach (array(
    'lomch_size',
    'lomch_location',
  ) as $field_name) {
    if (isset($entity_fields[$field_name]['api_name'])) {
      archibald_import_plaintext(
        $node,
        $lom,
        $field_name,
        archibald_import_determine_path($entity_fields[$field_name]['api_name'], $lom)
      );

      // lomch_size is special, in that the field itself has 2 keys, "value" and
      // "unit". We have to set a "unit" value, which is "b" (Bytes) in this
      // case.
      if ($field_name == 'lomch_size' && !empty($node->{$field_name})) {
        $node->{$field_name}[LANGUAGE_NONE][0]['unit'] = 'b';
      }
    }
  }

  // Treat all vocabulary entries that are in fact Ontology data fields. These
  // include:
  // 1.8 general.aggregationLevel
  // 4.1 technical.format (special case)
  // 5.2 education.learningResourceType (special case)
  // 5.5 education.intendedEndUserRole
  // 5.6 education.context
  // 5.8 education.difficultyLevel
  // 5.9 education.typicalLearningTime.learningTime
  // 6.1 rights.cost
  foreach (array(
    'lomch_aggregation_level',
    'lomch_technical_format',
    'lomch_learning_resource_type',
    'lomch_intended_enduserrole',
    'lomch_context',
    'lomch_difficulty_level',
    'lomch_typical_learning_time',
    'lomch_rights_cost',
  ) as $field_name) {
    if (isset($entity_fields[$field_name]['api_name'])) {
      $instance = field_info_instance('node', $field_name, $node->type);

      if ($field_name == 'lomch_learning_resource_type') {
        // The Learning Resource Type, although a single vocabulary and a single
        // field, is split into 2 child categories in LOM-CH. Treat both
        // child categories here.
        foreach (array('documentary', 'pedagogical') as $child_field) {
          archibald_import_ontology_vocabulary_item(
            $node,
            $lom,
            $field_name,
            str_replace(
              '*',
              ".{$child_field}",
              archibald_import_determine_path($entity_fields[$field_name]['api_name'], $lom)
            ),
            $instance['settings']['ontology_vocabulary_id']
          );
        }
      }
      else if ($field_name == 'lomch_technical_format') {
        // The technical.format field, although treated as a vocabulary by
        // LOM-CH, is simply formatted as a list of plain text values. In order
        // to use the same import function, we pre-process these values and
        // transform them to a vocabulary format. Start by making a copy of the
        // data, so we don't tamper with the one that will be passed to
        // hook_archibald_json_import_alter().
        $tmp_data = $data;
        if (isset($tmp_data['technical']['format'])) {
          $tmp_data['technical']['format'] = array_map(function($item) {
            return array(
              'value' => $item,
            );
          }, $tmp_data['technical']['format']);

          archibald_import_ontology_vocabulary_item(
            $node,
            new \Educa\DSB\Client\Lom\LomDescription($tmp_data),
            $field_name,
            archibald_import_determine_path($entity_fields[$field_name]['api_name'], $lom),
            $instance['settings']['ontology_vocabulary_id']
          );
        }
      }
      else {
        archibald_import_ontology_vocabulary_item(
          $node,
          $lom,
          $field_name,
          archibald_import_determine_path($entity_fields[$field_name]['api_name'], $lom),
          $instance['settings']['ontology_vocabulary_id']
        );
      }
    }
  }

  // Treat all LangStrings that should, internally, be treated as vocabulary
  // items. This includes:
  // 1.5 general.keyword
  // 1.6 general.coverage
  foreach (array(
    'general.keyword' => array('archibald_lomch_keywords', 'lomch_keywords'),
    'general.coverage' => array('archibald_lomch_coverage', 'lomch_coverage'),
  ) as $key => $info) {
    $values = $lom->getField($key, []);
    if (!empty($values)) {
      list($vocabulary_machine_name, $field_name) = $info;

      archibald_import_langstring_taxonomy_term(
        $node,
        $lom,
        $field_name,
        archibald_import_determine_path($entity_fields[$field_name]['api_name'], $lom),
        $vocabulary_machine_name,
        $available_languages
      );
    }
  }

  // Now start treating individual fields.

  // 1.1 general.identifier
  archibald_import_identifier(
    $node,
    $lom,
    'lomch_general_identifier',
    'general.identifier',
    $node->language,
    $available_languages
  );

  // 1.3 general.language
  archibald_import_language_taxonomy_term(
    $node,
    $lom,
    'lomch_resource_language',
    'general.language'
  );

  // 2.3 lifeCycle.contribute
  // This is a bit special. In LOM-CH, authors, editors and publishers are all
  // mixed in lifeCycle.contribute. But in archibald_lomch_description, these
  // are separate fields. Preprocess the data, so we can split these cleanly.
  if ($contributes = $lom->getField('lifeCycle.contribute')) {
    $values = array();
    foreach ($contributes as $contribute) {
      if (!isset($values[$contribute['role']['value']])) {
        $values[$contribute['role']['value']] = array();
      }
      $values[$contribute['role']['value']] = array_merge(
        $values[$contribute['role']['value']],
        $contribute['entity']
      );
    }

    foreach (array(
      'author' => 'lomch_lifecycle_author',
      'editor' => 'lomch_lifecycle_editor',
      'publisher' => 'lomch_lifecycle_publisher',
    ) as $path => $field_name) {
      if (!empty($values[$path])) {
        archibald_import_vcard(
          $node,
          new \Educa\DSB\Client\Lom\LomDescription($values),
          $field_name,
          $path,
          LANGUAGE_NONE,
          $files
        );
      }
    }
  }

  // 4.7 technical.duration
  if ($duration = $lom->getField('technical.duration')) {
    try {
      // DateInterval throws an exception if the format is not
      // parsable.
      $date = new DateInterval($duration);

      // Get the total amount of seconds.
      $node->lomch_duration[LANGUAGE_NONE][]['value'] = (
        ($date->y * 365 * 24 * 60 * 60) +
        ($date->m * 30 * 24 * 60 * 60) +
        ($date->d * 24 * 60 * 60) +
        ($date->h * 60 * 60) +
        ($date->i * 60) +
        $date->s
      );
    }
    catch(Exception $e) { }
  }

  // 4.8 technical.previewImage
  $image = $lom->getField('technical.previewImage.image');
  if (!empty($image)) {
    if ($file = archibald_import_file($image, $files)) {
      $node->lomch_preview_image[LANGUAGE_NONE][0] = array(
        'fid' => $file->fid,
        'title' => $lom->getField('technical.previewImage.copyright'),
      );
    }
  }

  // 5.7 education.typicalAgeRange
  $path = archibald_import_determine_path(
    $entity_fields['lomch_typical_age_range']['api_name'],
    $lom
  );
  if ($ranges = $lom->getField($path)) {
    foreach ($ranges as $range) {
      // Although this should be a simple CharacterString, the standard (LOM)
      // defines it as a LangString. Simply get the first element, as it
      // usually only contains 1 language.
      $range = reset($range);

      // Although the standard is a but more flexible, in practice it is
      // (almost?) always in from-to format. Treat it like that.
      list($from, $to) = explode('-', $range);
      if (!empty($from) && !empty($to)) {
        $node->lomch_typical_age_range[LANGUAGE_NONE][] = array(
          'from' => $from,
          'to' => $to,
        );
      }
    }
  }

  // 6.3 rights.description OR 6.3 rights.copyright
  archibald_import_license_taxonomy_term(
    $node,
    $lom,
    'lomch_rights_description',
    $lom->isLegacyLOMCHFormat() ?
      'rights.description' :
      'rights.copyright'
  );

  // 7 relation
  $values = $lom->getField('relation');
  if (!empty($values)) {
    module_load_include('inc', 'entity', 'includes/entity.controller');

    foreach ($values as $group) {
      foreach ($group['resource'] as $resource) {
        $tmp_available_languages = array();
        $tmp_lom = new \Educa\DSB\Client\Lom\LomDescription($resource);

        $field_collection_values = (object) array(
          'field_name' => 'lomch_relations',
          'lomch_relations_kind' => array(
            LANGUAGE_NONE => array(array('value' => $group['kind']['value'])),
          ),
        );

        archibald_import_langstring(
          $field_collection_values,
          $tmp_lom,
          'lomch_relations_description',
          'description',
          $tmp_available_languages
        );

        archibald_import_identifier(
          $field_collection_values,
          $tmp_lom,
          'lomch_relations_identifier',
          'identifier',
          LANGUAGE_NONE,
          $tmp_available_languages
        );

        // Filter out duplicates.
        $tmp_available_languages = array_unique($tmp_available_languages);

        // We cannot have field translations at the field collection level. We
        // have to manage it at the host entity (node) level. This means that
        // all field values must be in the same language as the host's field
        // referencing it. We iterate over the field values, and set everything
        // to the same language as the host's field again. However, for each
        // language, we add a new instance to the node, in the specific
        // language.
        $field_collection_values = (array) $field_collection_values;
        foreach ($field_collection_values['lomch_relations_description'] as $langcode => $description) {
          // Make a copy.
          $store_values = $field_collection_values;

          // Update the fields. Set the current language value.
          $store_values['lomch_relations_kind'] = array(
            $langcode => $store_values['lomch_relations_kind'][LANGUAGE_NONE],
          );
          $store_values['lomch_relations_identifier'] = array(
            $langcode => $store_values['lomch_relations_identifier'][LANGUAGE_NONE],
          );
          $store_values['lomch_relations_description'] = array(
            $langcode => $description,
          );

          // Save it and link it to the node.
          $entity = entity_create('field_collection_item', $store_values);
          $entity->setHostEntity('node', $node, $langcode);
          $entity->save(TRUE);
        }

        // Merge the available languages.
        $available_languages = array_merge($available_languages, $tmp_available_languages);
      }
    }
  }

  // Done with the fields. Now, finalize the import data before saving.

  // Double-check the title. We need some kind of title.
  if (empty($node->title_field[$node->language])) {
    $node->title_field[$node->language] = array(
      array('value' => "NO TITLE!"),
    );
  }

  // Add the title properties.
  $node->title = $node->title_field[$node->language][0]['value'];

  // Register translations. Filter duplicates, and remove the source language.
  $available_languages = array_unique($available_languages);
  if (in_array($node->language, $available_languages)) {
    unset($available_languages[array_search($node->language, $available_languages)]);
  }

  // Check active languages on the system. If any languages in our import are
  // not active, we might run into issues. Remove non-active languages, if any,
  // and put up a warning message.
  foreach ($available_languages as $langcode) {
    if (!in_array($langcode, $active_languages)) {
      drupal_set_message(t("The description %title is available in a language (%lang) that is not enabled locally. Data in that language will be discarded. Please make sure you enable that language if you wish to keep all data. Contact your site administrator if needed.", array(
        '%title' => $node->title,
        '%lang' => $langcode,
      )), 'warning');

      unset($available_languages[array_search($langcode, $available_languages)]);
    }
  }

  // If there are any available languages, we need to register translations.
  if (!empty($available_languages)) {
    $handler = entity_translation_get_handler('node', $node);

    foreach ($available_languages as $langcode) {
      $translation = array(
        'translate' => 0,
        'status' => 1,
        'language' => $langcode,
        'source' => $node->language,
      );
      $handler->setTranslation($translation, $node);
    }
  }

  // We got so far, so everything is OK.
  return TRUE;
}

/**
 * Helper function to import LangStrings.
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 * @param array &$available_languages
 *    (optional) An array that will be populated with the available languages
 *    for the current field.
 */
function archibald_import_langstring($node, $lom, $field_name, $path, &$available_languages = array()) {
  $values = $lom->getField($path, []);
  if (!empty($values)) {
    if (!isset($node->{$field_name})) {
      $node->{$field_name} = array();
    }

    // Some fields can be multiple. So we treat them all as multiples. Drupal
    // fields, even single ones, have the same structure as multiple fields
    // anyway.
    if (!is_array(@reset($values))) {
      $values = array($values);
    }

    foreach ($values as $i => $value) {
      array_walk($value, function($string, $langcode) use(&$node, $field_name, $i, &$available_languages) {
        $available_languages[] = $langcode;
        $node->{$field_name}[$langcode][$i] = array('value' => $string);
      });
    }
  }
}
/**
 * Helper function to import CS (plain text elements).
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 */
function archibald_import_plaintext($node, $lom, $field_name, $path) {
  $values = $lom->getField($path, []);
  if (!empty($values)) {
    if (!isset($node->{$field_name})) {
      $node->{$field_name} = array();
    }

    // Some fields can be multiple. So we treat them all as multiples. Drupal
    // fields, even single ones, have the same structure as multiple fields
    // anyway.
    if (!is_array($values)) {
      $values = array($values);
    }

    foreach ($values as $value) {
      $node->{$field_name}[LANGUAGE_NONE][] = array('value' => $value);
    }
  }
}

/**
 * Helper function to import VC.
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 * @param string $ontology_id
 *    The ID of the handled vocabulary on the Ontology server.
 */
function archibald_import_ontology_vocabulary_item($node, $lom, $field_name, $path, $ontology_id) {
  $values = $lom->getField($path, []);
  if (!empty($values)) {
    module_load_include('inc', 'archibald', 'includes/archibald.ontology');

    if (!isset($node->{$field_name})) {
      $node->{$field_name} = array();
    }

    // Some fields can be multiple. So we treat them all as multiples. Drupal
    // fields, even single ones, have the same structure as multiple fields
    // anyway.
    if (!is_array(@reset($values))) {
      $values = array($values);
    }

    // Fetch the raw Ontology data.
    $ontology_data_raw = archibald_ontology_load($ontology_id);

    $ontology_data = array();
    foreach ($ontology_data_raw as $group_id => $items) {
      $group_id = trim($group_id);

      // Are we dealing with a multi-level vocabulary?
      if (is_array($items)) {
        foreach ($items as $item_id => $item) {
          $item_id = trim($item_id);

          // This is an edge case. For the technical_format vocabulary, the
          // values are actually {group_id}/{item_id}. Check if that's the
          // vocabulary we are dealing with, and act accordingly.
          if ($ontology_id == 'technical_format') {
            $ontology_data[] = "{$group_id}/{$item_id}";
          }
          else {
            $ontology_data[] = $item_id;
          }
        }
      }
      else {
        $ontology_data[] = $group_id;
      }
    }

    foreach ($values as $value) {
      // Deprecation mapping. Currently, the Ontology server has no method to
      // say by what value a deprecated term should be replaced. We have to
      // hard code them here, for now... This might introduce duplicates, but
      // Drupal can handle those.
      foreach (array(
        'education_context' => array(
          'pre-school' => 'compulsory education',
        ),
      ) as $depr_ontology_id => $deprecations) {
        foreach ($deprecations as $deprecated => $replacement) {
          if ($ontology_id == $depr_ontology_id) {
            if ($value['value'] == $deprecated) {
              $value['value'] = $replacement;
            }
          }
        }
      }

      // Validate the value with the Ontology server. If it doesn't exist, we
      // must ignore it.
      if (!in_array($value['value'], $ontology_data)) {
        continue;
      }

      $node->{$field_name}[LANGUAGE_NONE][] = array(
        'value' => $value['value'],
      );
    }
  }
}

/**
 * Helper function to import LangStrings that are treated as taxonomy terms.
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 * @param string $vocabulary_machine_name
 *    The name of the taxonomy vocabulary.
 * @param array &$available_languages
 *    (optional) An array that will be populated with the available languages
 *    for the current field.
 */
function archibald_import_langstring_taxonomy_term($node, $lom, $field_name, $path, $vocabulary_machine_name, &$available_languages = array()) {
  // Fetch all enabled languages. If we try to set a translation for a
  // language that doesn't exist, we will get an error.
  $languages = language_list();

  $values = $lom->getField($path, []);
  if (!empty($values)) {
    $vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_machine_name);

    if (!isset($node->{$field_name})) {
      $node->{$field_name} = array();
    }

    // Some fields can be multiple. So we treat them all as multiples. Drupal
    // fields, even single ones, have the same structure as multiple fields
    // anyway.
    if (!is_array(@reset($values))) {
      $values = array($values);
    }

    foreach ($values as $value) {
      $term = NULL;

      // Try to load a taxonomy term. If we find one, use it. Otherwise, we need
      // to create one.
      foreach ($value as $langcode => $name) {
        $tid = NULL;

        $query = new EntityFieldQuery;
        $result = $query
          ->entityCondition('entity_type', 'taxonomy_term')
          ->entityCondition('bundle', $vocabulary_machine_name)
          ->propertyCondition('name', $name, '=')
          ->execute();

        if (!empty($result['taxonomy_term'])) {
          // If we found more than one (that would be really weird), use the
          // 1st one.
          $tid = @reset(array_keys($result['taxonomy_term']));
          break;
        }
      }

      // Whether we found the term or not, we will declare the current language
      // code the source language (can be changed below).
      $source_language = $langcode;

      if (!empty($tid)) {
        // Load the term. Register it for translations if needed.
        $term = taxonomy_term_load($tid);

        // Did it have translations before? If not, register it now. This could
        // create problems, because the original term might not have been
        // translated yet, and treated as a language-independent term. In that
        // case, we could end up assigning a German term to the French language.
        // Unfortunately, there's no way around this. Entity Translation did not
        // support translating taxonomy terms prior to beta6, and thus many
        // terms we language undefined. Now that it does support it, we have to
        // start treating our terms as multi-lingual. Luckily, the chances are
        // pretty low of this actually happening.
        if (empty($term->translations)) {
          $term->translations = (object) array(
            'original' => $source_language,
          );
          taxonomy_term_save($term);
        }
        else {
          // Translations were registered before. Use the original source
          // language.
          $source_language = $term->translations->original;
        }
      }
      else {
        // We need to create an element.
        $term = (object) array(
          'vid' => $vocabulary->vid,
          'name' => $name,
          // Set a source language.
          'translations' => (object) array(
            'original' => $langcode,
          ),
        );
        taxonomy_term_save($term);
      }

      // Set the field value, using the source language. We at least know that
      // one must be set.
      $node->{$field_name}[$source_language][] = array('tid' => $term->tid);

      // Handle localization.
      foreach ($value as $langcode => $name) {
        $available_languages[] = $langcode;

        // Skip langcodes we don't know about, or the term language itself.
        if (
          empty($langcode) ||
          !isset($languages[$langcode]) ||
          $langcode == $source_language
        ) {
          continue;
        }

        $strings = array();
        $translation = array(
          'translate' => 0,
          'status' => 1,
          'language' => $langcode,
          'source' => $source_language,
        );
        $handler = entity_translation_get_handler('taxonomy_term', $term, TRUE);
        $strings['name_field'][$langcode]['0']['value'] = $name;
        $handler->setTranslation($translation, $strings);
        taxonomy_term_save($term);

        // Set the field value as well.
        $node->{$field_name}[$langcode][] = array('tid' => $term->tid);
      }
    }
  }
}

/**
 * Helper function to import language taxonomy terms.
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 */
function archibald_import_language_taxonomy_term($node, $lom, $field_name, $path) {
  $values = $lom->getField($path, []);

  if (!empty($values)) {
    $node->{$field_name} = array();

    // Load the language vocabulary.
    $vocabulary_machine_name = 'archibald_lomch_description_languages';
    $vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_machine_name);

    foreach ($values as $value) {
      $query = new EntityFieldQuery;
      $result = $query
        ->entityCondition('entity_type', 'taxonomy_term')
        ->entityCondition('bundle', $vocabulary_machine_name)
        ->fieldCondition('archibald_lang_iso_code', 'value', $value, '=')
        ->execute();

      if (!empty($result['taxonomy_term'])) {
        // If we found more than one (that would be really weird), use the
        // 1st one.
        $tid = @reset(array_keys($result['taxonomy_term']));
        $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $tid);
      }
      else {
        // We need to create an element. Does Drupal provide a human-readable
        // name for this ISO code? If so, use it. If not, simply use the ISO
        // code.
        if (empty($iso_list)) {
          include_once DRUPAL_ROOT . '/includes/iso.inc';
          $iso_list = _locale_get_predefined_list();
        }

        $term = (object) array(
          'vid' => $vocabulary->vid,
          'name' => !empty($iso_list[$value]) ?
            $iso_list[$value][0] :
            $value,
          'archibald_lang_iso_code' => array(
            LANGUAGE_NONE => array(array('value' => $value))
          ),
          'archibald_term_status' => array(
            LANGUAGE_NONE => array(array('value' => 1)),
          ),
        );

        taxonomy_term_save($term);
        $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $term->tid);
      }
    }
  }
}

/**
 * Helper function to import license taxonomy terms.
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 */
function archibald_import_license_taxonomy_term($node, $lom, $field_name, $path) {
  $values = $lom->getField($path, []);

  // Normalize the field values, so we can apply the same logic.
  $links = array();
  if ($path === 'rights.description') {
    // Make sure we strip all HTML tags.
    $values = array(array_map('strip_tags', $values));
  }
  else if ($path === 'rights.copyright') {
    // Extract the identifiers.
    foreach ($values as $i => $value) {
      if (!empty($value['identifier'])) {
        $links = array_merge(
          $links,
          array_map(function($item) {
            return array('value' => $item['entry']);
          }, $value['identifier'])
        );
      }
    }

    // Extract the descriptions, making sure we strip all HTML.
    $values = array_map(function($item) {
      return array_map('strip_tags', $item['description']);
    }, $values);
  }

  if (!empty($values)) {
    if (!isset($node->{$field_name})) {
      $node->{$field_name} = array();
    }

    // Fetch all enabled languages. If we try to set a translation for a
    // language that doesn't exist, we will get an error.
    $languages = language_list();

    // Load the license vocabulary.
    $vocabulary_machine_name = 'archibald_lomch_description_licenses';
    $vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_machine_name);

    foreach ($values as $i => $value) {
      $query = new EntityFieldQuery;
      $result = $query
        ->entityCondition('entity_type', 'taxonomy_term')
        ->entityCondition('bundle', $vocabulary_machine_name)
        ->propertyCondition('name', array_values($value), 'IN')
        ->execute();

      if (!empty($result['taxonomy_term'])) {
        // If we found more than one (that would be really weird), use the
        // 1st one.
        $tid = @reset(array_keys($result['taxonomy_term']));
        $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $tid);
      }
      else {
        // We need to create an element. Fetch a name.
        if (class_exists('\Educa\DSB\Client\Utils::getVCName')) {
          $name = \Educa\DSB\Client\Utils::getLSValue(
            $value,
            // Fetch the default language first, and fallback to the same list
            // as defined for the site.
            array_merge(
              array(language_default('language')),
              archibald_language_fallback_list()
            )
          );
        }
        else {
          // Try to fetch the default language value.
          $name = @$value[language_default('language')];
        }

        // Check if we got a good result.
        if (is_array($name) || empty($name)) {
          // No luck. Simply get the first one.
          $name = reset($value);
        }

        $term = (object) array(
          'vid' => $vocabulary->vid,
          'name' => $name,
          // Set a source language. Use the default language.
          'translations' => (object) array(
            'original' => language_default('language'),
          ),
          'archibald_term_status' => array(
            LANGUAGE_NONE => array(array('value' => 1)),
          ),
          'archibald_license_url' => array(
            LANGUAGE_NONE => !empty($links) ? $links : array(),
          ),
        );
        taxonomy_term_save($term);

        //$wrapper = entity_metadata_wrapper('taxonomy_term', $term);
        //$wrapper->save();

        // Handle localization, otherwise it will be all messed up.
        foreach ($value as $langcode => $string) {
          // Skip langcodes we don't know about, or the term language itself.
          if (
            empty($langcode) ||
            !isset($languages[$langcode]) ||
            $langcode == language_default('language')
          ) {
            continue;
          }

          $strings = array();
          $translation = array(
            'translate' => 0,
            'status' => 1,
            'language' => $langcode,
            'source' => language_default('language'),
          );
          $handler = entity_translation_get_handler('taxonomy_term', $term, TRUE);
          $strings['name_field'][$langcode]['0']['value'] = $string;
          $handler->setTranslation($translation, $strings);
          taxonomy_term_save($term);
        }

        // Register it with our node.
        $node->{$field_name}[LANGUAGE_NONE][] = array('tid' => $term->tid);
      }
    }
  }
}

/**
 * Helper function to import identifiers.
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 * @param string $default_language
 *    The default language to import on.
 * @param array &$available_languages
 *    (optional) An array that will be populated with the available languages
 *    for the current field.
 */
function archibald_import_identifier($node, $lom, $field_name, $path, $default_language, &$available_languages = array()) {
  $values = $lom->getField($path, []);
  if (!empty($values)) {
    if (!isset($node->{$field_name})) {
      $node->{$field_name} = array();
    }

    // Some fields can be multiple. So we treat them all as multiples. Drupal
    // fields, even single ones, have the same structure as multiple fields
    // anyway.
    if (!is_array(@reset($values))) {
      $values = array($values);
    }

    foreach ($values as $i => $value) {
      // The identifier fields are a little bit special. Normally, only the
      // "title" key is translatable. But, being a single field, all keys
      // are duplicated. If there's no title, we can simply set it as the
      // default language ($default_language). If there is, however, we base our
      // import on the title value's language keys.
      if (!empty($value['title'])) {
        $langcodes = array_keys($value['title']);

        array_walk($value['title'], function($string, $langcode) use(&$node, $field_name, $i, &$available_languages) {
          $available_languages[] = $langcode;
          $node->{$field_name}[$langcode][$i]['title'] = $string;
        });
      }
      else {
        $langcodes = array($default_language);
      }

      // Now, treat the other values. Duplicate them if need across all
      // languages.
      foreach ($langcodes as $langcode) {
        $node->{$field_name}[$langcode][$i]['type'] = preg_match('/^REL_/', $value['catalog']) ?
          str_replace('REL_', '', $value['catalog']) :
          $value['catalog'];
        $node->{$field_name}[$langcode][$i]['value'] = $value['entry'];
      }
    }
  }
}

/**
 * Helper function to import VCards.
 *
 * @param object $node
 *    The node object to import the data on to.
 * @param Educa\DSB\Client\Lom\LomDescription $lom
 *    The LomDescription object to access the field data.
 * @param string $field_name
 *    The field name in which the data must be imported.
 * @param string $path
 *    The path to the data in the LomDescription object.
 * @param string $default_language
 *    The default language to import on.
 * @param array &$files
 *    An array of already imported files. This array is passed by reference, and
 *    is updated with newly downloaded files. See archibald_import_file().
 */
function archibald_import_vcard($node, $lom, $field_name, $path, $default_language, &$files) {
  $values = $lom->getField($path, []);

  if (!empty($values)) {
    if (!isset($node->{$field_name})) {
      $node->{$field_name} = array();
    }

    // Some fields can be multiple. So we treat them all as multiples. Drupal
    // fields, even single ones, have the same structure as multiple fields
    // anyway.
    if (!is_array($values)) {
      $values = array($values);
    }

    foreach ($values as $i => $value) {
      if ($value) {
        $parser = new JeroenDesloovere\VCard\VCardParser($value);
        try {
          $card = $parser->getCardAtIndex(0);

          // Check if we have at least a firstname and lastname, or an
          // organization. If not, we cannot even import. Don't bother.
          if (
            (!empty($card->firstname) && !empty($card->lastname)) ||
            !empty($card->organization)
          ) {
            if (!empty($card->firstname) && !empty($card->lastname)) {
              $name = "{$card->lastname} {$card->firstname}";
              if (!empty($card->organization)) {
                $card->organization = trim($card->organization, ';');
                $name .= " ({$card->organization})";
              }
            }
            else {
              $name = trim($card->organization, ';');
            }

            // First, try to see if a VCard already exists. VCards are usually in
            // the form "firstname lastname (organization)", or,
            // "firstname lastname" if the organization is not provided. If we
            // find such a match, expect it to be the same VCard. If not, create
            // a new one.
            $query = new EntityFieldQuery();
            $result = $query->entityCondition('entity_type', 'archibald_vcard')
                            ->propertyCondition('name', $name, '=')
                            ->execute();

            if (!empty($result['archibald_vcard'])) {
              $node->{$field_name}[$default_language][]['target_id'] = @reset(array_keys($result['archibald_vcard']));
            }
            else {
              // Create a new VCard.
              $entity_values = array(
                'name' => $name,
                'type' => 'archibald_vcard',
              );

              foreach (array(
                'firstname' => 'vcard_first_name',
                'lastname' => 'vcard_last_name',
                'organization' => 'vcard_organization',
                'email' => 'vcard_email',
                'phone' => 'vcard_phone',
                'url' => 'vcard_url',
              ) as $key => $field) {
                if (!empty($card->{$key})) {
                  $entity_values[$field] = array(
                    LANGUAGE_NONE => array(array(
                      // Is this a multiple value item?
                      'value' => is_array($card->{$key}) ?
                        // We take the first in the list.
                        @reset(@reset($card->{$key})) :
                        $card->{$key}
                    )),
                  );
                }
              }

              // Address fields.
              if (!empty($card->address)) {
                // We take the first in the list.
                $address = @reset(@reset($card->address));

                foreach (array(
                  'street' => 'vcard_address',
                  'extended' => 'vcard_add_address',
                  'city' => 'vcard_city',
                  'zip' => 'vcard_zip',
                  'country' => 'vcard_country',
                ) as $key => $field) {
                  if (!empty($address->{$key})) {
                    $entity_values[$field] = array(
                      LANGUAGE_NONE => array(array(
                        'value' => $address->{$key},
                      )),
                    );
                  }
                }
              }

              // Handle the logo, if any.
              if (!empty($card->logo)) {
                if ($file = archibald_import_file($card->logo, $files)) {
                  $entity_values['vcard_logo'][LANGUAGE_NONE][0] = array(
                    'fid' => $file->fid,
                  );
                }
              }

              $entity = entity_create('archibald_vcard', $entity_values);
              $entity->save();
              $node->{$field_name}[$default_language][]['target_id'] = $entity->id;
            }
          }
        }
        catch(Exception $e) {
          // @todo Display an error?
        }
      }
    }
  }
}

/**
 * Helper function to import files.
 *
 * Check if the file is already available. If it is, return the same file
 * object. If not, attempt to download the file data, and store it locally
 * again.
 *
 * @param string $filepath
 *    The path of the file to fetch.
 * @param array &$files
 *    An array of already imported files. This array is passed by reference, and
 *    is updated with newly downloaded files.
 *
 * @return object|null
 *    A file object, or null.
 */
function archibald_import_file($filepath, &$files) {
  if (!empty($files[$filepath])) {
    $file = file_load($files[$filepath]);
    // Make sure the file is a permanent file.
    if (!$file->status) {
      $file->status = FILE_STATUS_PERMANENT;
      file_save($file);
    }

    return $file;
  }
  else {
    // Download the file, and store it locally. Suppress warnings for files that
    // could not be found.
    $file_data = @file_get_contents($filepath);
    if ($file_data) {
      $filename = @end(explode('/', $filepath));
      // Make sure there were no GET params after the file name.
      $filename = preg_replace('/\?.+$/', '', $filename);

      // Store it locally.
      if ($file = file_save_data($file_data, file_default_scheme() . "://{$filename}")) {
        $files[$filepath] = $file->fid;
        return $file;
      }
    }
  }
}

/**
 * Helper function to fetch the correct path property.
 *
 * LOM-CHv1.1 and LOM-CHv1.2+ descriptions have small differences in the data
 * structure. In order to accommodate for this, includes/archibald.entities.inc
 * either defines a single "path" (like "general.title"), or an array, keyed by
 * LOM-CH version. This function determines which path is the most appropriate.
 *
 * @param string|array $paths
 *    The paths, as defined in includes/archibald.entities.inc.
 * @param \Educa\DSB\Client\Lom\LomDescription $lom
 *    The description data.
 *
 * @return string
 *    The correct path.
 */
function archibald_import_determine_path($paths, $lom) {
  if (is_string($paths)) {
    return $paths;
  }
  $schema = $lom->getField('metaMetadata.metadataSchema');

  if (is_array($schema)) {
    foreach ($schema as $value) {
      // Return the first matching path.
      if (isset($paths[$value])) {
        return $paths[$value];
      }
    }
  }
  else {
    // If the schema doesn't exist, return the default path. If there's no
    // default path, return the first path.
    return isset($paths[$schema]) ?
      $paths[$schema] : (
        isset($paths['default']) ?
          $paths['default'] :
          reset($paths)
      );
  }
}
