<?php

/**
 * @file
 * The class for PoolParty Taxonomy Manager.
 * Export / Import / Sync methods.
 */

class PPTaxonomyManager {
  protected $config;

  /**
   * Constructor of the PPTaxonomyManager class.
   *
   * @param object $config
   *   A PoolParty Taxonomy Manager configuration.
   */
  public function __construct($config) {
    $this->config = $config;
  }

  /**
   * Returns a list of all additional fields for a PoolParty taxonomy.
   *
   * @return array
   *   A list additional fields.
   */
  protected static function taxonomyFields() {
    $taxonomy_field_schema = array(
      'field_uri' => (object) array(
        'name' => 'field_uri',
        'type' => 'text',
        'label' => t('URI'),
        'description' => t('The URI of the concept'),
        'required' => FALSE,
        'settings' => array(
          'max_length' => 255,
        ),
        'widget' => array(
          'type' => 'text_textfield',
          'weight' => 3,
        ),
      ),
      'field_alt_labels' => (object) array(
        'name' => 'field_alt_labels',
        'type' => 'text',
        'label' => t('Alternative labels'),
        'description' => t('A list of synonyms'),
        'required' => FALSE,
        'cardinality' => -1,
        'settings' => array(
          'max_length' => 1024,
        ),
        'widget' => array(
          'type' => 'text_textfield',
          'weight' => 4,
        ),
        'property' => 'skos:altLabel',
        'pull_key' => 'altLabels',
        'push_key' => 'alternativeLabel',
        'merge_char' => ',',
      ),
      'field_hidden_labels' => (object) array(
        'name' => 'field_hidden_labels',
        'type' => 'text',
        'label' => t('Hidden labels'),
        'description' => t('A list of secondary name variants of this term'),
        'required' => FALSE,
        'cardinality' => -1,
        'settings' => array(
          'max_length' => 1024,
        ),
        'widget' => array(
          'type' => 'text_textfield',
          'weight' => 5,
        ),
        'property' => 'skos:hiddenLabel',
        'pull_key' => 'hiddenLabels',
        'push_key' => 'hiddenLabel',
        'merge_char' => ',',
      ),
      'field_scope_notes' => (object) array(
        'name' => 'field_scope_notes',
        'type' => 'text_long',
        'label' => t('Scope notes'),
        'description' => t('An information about the scope of a concept'),
        'cardinality' => -1,
        'required' => FALSE,
        'widget' => array(
          'type' => 'text_textarea',
          'weight' => 6,
        ),
        'property' => 'skos:scopeNote',
        'pull_key' => 'scopeNotes',
        'push_key' => 'scopeNote',
      ),
      'field_related_concepts' => (object) array(
        'name' => 'field_related_concepts',
        'type' => 'text',
        'label' => t('Related concepts'),
        'description' => t('URIs to related concepts'),
        'cardinality' => -1,
        'required' => FALSE,
        'settings'    => array(
          'max_length' => 1024,
        ),
        'widget' => array(
          'type' => 'text_textfield',
          'weight' => 7,
        ),
        'property' => 'skos:related',
        'pull_key' => 'relateds',
      ),
      'field_exact_match' => (object) array(
        'name' => 'field_exact_match',
        'type' => 'text',
        'label' => t('Exact matches'),
        'description' => t('URIs which show to the same concept at a different data source'),
        'cardinality' => -1,
        'required' => FALSE,
        'settings'    => array(
          'max_length' => 1024,
        ),
        'widget' => array(
          'type' => 'text_textfield',
          'weight' => 8,
        ),
        'property' => 'skos:exactMatch',
        'pull_key' => 'exactMatch',
      ),
    );

    // Add the possibility to add custom fields  via hook here.
    $custom_fields = array();
    drupal_alter('pp_taxonomy_manager_custom_attributes', $custom_fields);
    if (!empty($custom_fields)) {
      foreach ($custom_fields as $field_id => $custom_field) {
        // Check if a property is given and it is not one of the custom_fields
        if (isset($custom_field->property) && !isset($taxonomy_field_schema[$field_id]) && isset($custom_field->type) && $custom_field->type == 'text') {
          $taxonomy_field_schema[$field_id] = $custom_field;
        }
      }
    }

    return $taxonomy_field_schema;
  }

  /**
   * Returns the PoolParty Taxonomy Manager configuration.
   *
   * @return object
   *   A PoolParty Taxonomy Manager configuration object.
   */
  public function getConfig() {
    return $this->config;
  }

  /**
   * Set the correct translation mode for the Drupal taxonomy.
   *
   * @param object $taxonomy
   *   A Drupal taxonomy.
   * @param array $languages
   *   An array of languages:
   *    key = Drupal language
   *    value = PoolParty language.
   *
   * @return boolean
   *   TRUE if the translation mode had to be changed, FALSE if not.
   */
  public function setTranslationMode(&$taxonomy, $languages) {
    if (!module_exists('i18n_taxonomy')) {
      return FALSE;
    }

    $language_count = count($languages);
    $i18n_mode_old = $taxonomy->i18n_mode;

    // Make the taxonomy translatable if the translation module for taxonomies
    // is installed and more than one language is selected or the translation
    // mode is wrong.
    if ($language_count > 1 && $taxonomy->i18n_mode != I18N_MODE_TRANSLATE) {
      $taxonomy->i18n_mode = I18N_MODE_TRANSLATE;
      taxonomy_vocabulary_save($taxonomy);
    }

    return ($language_count > 1 && $i18n_mode_old == I18N_MODE_LOCALIZE);
  }

  /**
   * Changes the translation mode of a taxonomy from "Localize" to "Translate".
   *
   * @param int[] $tids
   *   The IDs of the taxonomy terms in the default language.
   * @param string $vocabulary_machine_name
   *   The machine name of the vocabulary to use
   * @param array $languages
   *   An array of languages:
   *    key = Drupal language
   *    value = PoolParty language.
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function changeTranslationModeBatch($tids, $vocabulary_machine_name, $languages, array &$context) {
    $default_language = language_default('language');
    $selected_languages = array_keys($languages);

    // Get the list of already translated tids.
    if (!isset($context['results']['translation_mode_change'])) {
      $context['results']['translation_mode_change'] = array(
        'translated_tids' => array(),
      );
    }
    $translated_tids = $context['results']['translation_mode_change']['translated_tids'];

    // Go through all taxonomy terms.
    $terms = $this->loadTermsWithParents($tids);
    foreach ($terms as $term) {
      if (isset($translated_tids[$term->tid])) {
        continue;
      }
      $translated_tids[$term->tid] = array();

      // Create a new translation set
      $translation_set = i18n_translation_set_create('taxonomy_term', $vocabulary_machine_name);

      // Go through all selected languages.
      foreach ($selected_languages as $language) {
        if ($language != $default_language) {
          $translate_term = clone $term;

          // Get its translations if exists and create new term object.
          $translations = i18n_string_translation_search('taxonomy:term:' . $term->tid . ':*', $language);
          if (!empty($translations)) {
            $translate_term->tid = NULL;
            $translate_term->parent = NULL;
            foreach ($translations as $translation) {
              $translate_term->{$translation->property} = $translation->translations[$language];
            }

            // Add the parents for the terms.
            foreach ($term->parents as $parent) {
              if (!empty($translated_tids[$parent][$language])) {
                $translate_term->parent[] = $translated_tids[$parent][$language];
              }
            }

            // Save the term.
            $translate_term->language = $language;
            taxonomy_term_save($translate_term);

            // Add the term to the translation set.
            $translation_set->add_item($translate_term, $language);

            $translated_tids[$term->tid][$language] = $translate_term->tid;
          }
        }
      }

      // Update the default term if language was not set yet.
      if ($term->language != $default_language) {
        $term->language = $default_language;
        taxonomy_term_save($term);
      }

      // Add the default term to the translation set and finally save it.
      $translation_set->add_item($term, $default_language);
      $translation_set->save();

      $context['results']['translation_mode_change_processed']++;
    }

    // Update the translated tids list.
    $context['results']['translation_mode_change']['translated_tids'] = $translated_tids;
  }

  /**
   * Creates a new Drupal taxonomy.
   *
   * @param object $root_object
   *   Either a PoolParty concept scheme or a PoolParty project.
   * @param string $taxonomy_name
   *   The name of the taxonomy to create If NULL is given the title of the
   *   concept scheme gets used (default).
   *
   * @return object
   *   The created taxonomy.
   */
  public function createTaxonomy($root_object, $taxonomy_name = '') {
    $taxonomy_name = trim(check_plain($taxonomy_name));
    if (empty($taxonomy_name)) {
      $taxonomy_name = $root_object->title;
    }

    // Check if the new taxonomy already exists.
    $machine_name = self::machineName($taxonomy_name);
    $taxonomy = taxonomy_vocabulary_machine_name_load($machine_name);

    if ($taxonomy === FALSE) {
      // Set the data for the new taxonomy.
      $taxonomy = (object) array(
        'name' => $taxonomy_name,
        'machine_name' => $machine_name,
        'description' => substr(t('Automatically created by PoolParty Taxonomy Manager.') . ' ' . (isset($root_object->description) ? $root_object->description : (isset($root_object->descriptions) && !empty($root_object->descriptions) ? $root_object->descriptions[0] : '')), 0, 128),
      );

      // Create the taxonomy.
      taxonomy_vocabulary_save($taxonomy);

      drupal_set_message(t('Vocabulary %taxonomy successfully created.', array('%taxonomy' => $taxonomy_name)));
      watchdog('PoolParty Taxonomy Manager', 'Vocabulary created: %taxonomy (VID = %vid)', array(
        '%taxonomy' => $taxonomy_name,
        '%vid' => $taxonomy->vid,
      ));
    }

    return $taxonomy;
  }

  /**
   * Adds additional fields to a specific taxonomy term if not exists.
   *
   * @param object $taxonomy
   *   A Drupal taxonomy object.
   */
  public function adaptTaxonomyFields($taxonomy) {
    $fields = self::taxonomyFields();
    foreach ($fields as $field) {
      $this->createTaxonomyTermField($field);
      $this->addFieldToTaxonomyTerm($field, $taxonomy);
    }
  }

  /**
   * Creates a new field if not exists.
   *
   * @param object $field
   *   The field that should be created.
   */
  protected function createTaxonomyTermField($field) {
    $exists = field_info_field($field->name) ? TRUE : FALSE;
    if (!$exists) {
      $new_field = array(
        'field_name' => $field->name,
        'type' => $field->type,
        'label' => $field->label,
      );
      if (isset($field->settings)) {
        $new_field['settings'] = $field->settings;
      }
      if (isset($field->cardinality)) {
        $new_field['cardinality'] = $field->cardinality;
      }
      field_create_field($new_field);
    }
  }

  /**
   * Adds a field to a specific taxonomy term if not exists.
   *
   * @param object $field
   *   The field that should be added.
   * @param object $taxonomy
   *   The taxonomy at which the field should be added.
   */
  protected function addFieldToTaxonomyTerm($field, $taxonomy) {
    $fields = field_info_instances('taxonomy_term', $taxonomy->machine_name);
    if (!isset($fields[$field->name])) {
      $instance = array(
        'field_name' => $field->name,
        'entity_type' => 'taxonomy_term',
        'bundle' => $taxonomy->machine_name,
        'label' => $field->label,
        'description' => $field->description,
        'required' => $field->required,
        'widget' => $field->widget,
      );
      field_create_instance($instance);
    }
  }

  /**
   * Creates a new concept scheme on the specified PoolParty server.
   *
   * @param object $taxonomy
   *   A Drupal taxonomy object.
   * @param string $scheme_title
   *   The title of the concept scheme to create. If NULL is given the name of
   *   the taxonomy gets used (default).
   *
   * @return string
   *   The URI of the new concept scheme.
   */
  public function createConceptScheme($taxonomy, $scheme_title = '') {
    $scheme_title = trim(check_plain($scheme_title));
    if (empty($scheme_title)) {
      $scheme_title = $taxonomy->name;
    }

    $description = 'Automatically created by Drupal. ' . $taxonomy->description;
    /* @var $ppt SemanticConnectorPPTApi */
    $ppt = $this->config->getConnection()->getAPI('PPT');
    $scheme_uri = $ppt->createConceptScheme($this->config->getProjectId(), $scheme_title, $description);
    drupal_set_message(t('Concept scheme %scheme successfully created.', array('%scheme' => $scheme_title)));
    watchdog('PoolParty Taxonomy Manager', 'Concept scheme created: %scheme (URI = %uri)', array(
      '%scheme' => $scheme_title,
      '%uri' => $scheme_uri,
    ));

    return $scheme_uri;
  }

  /**
   * Creates a new project on the specified PoolParty server.
   *
   * @param object $taxonomy
   *   A Drupal taxonomy object.
   * @param string $project_title
   *   The title of the project to create. If NULL is given the name of
   *   the taxonomy gets used (default).
   * @param string $default_language
   *   The language code of the language to use as default for the new project
   *   in PoolParty.
   * @param array $available_languages
   *   A complete array of languages available for the new PoolParty project;
   *   If this array is empty only the default language will be used.
   *
   * @return string
   *   The ID of the new project.
   */
  public function createProject($taxonomy, $project_title = '', $default_language = 'en', array $available_languages = array()) {
    $project_title = trim(check_plain($project_title));
    if (empty($project_title)) {
      $project_title = $taxonomy->name;
    }

    if (empty($available_languages)) {
      $available_languages = array($default_language);
    }

    $description = 'Automatically created by Drupal. ' . $taxonomy->description;
    /* @var $ppt SemanticConnectorPPTApi */
    $ppt = $this->config->getConnection()->getAPI('PPT');

    $project_id = $ppt->createProject($project_title, $default_language, array('Public'), array('description' => $description, 'availableLanguages' => $available_languages));
    drupal_set_message(t('Project %project successfully created.', array('%project' => $project_title)));
    watchdog('PoolParty Taxonomy Manager', 'Project created: %project (ID = %id)', array(
      '%project' => $project_title,
      '%id' => $project_id,
    ));

    return $project_id;
  }

  /**
   * Creates a batch for exporting all terms of a taxonomy.
   *
   * @param object $taxonomy
   *   A Drupal taxonomy object.
   * @param string $root_uri
   *   A concept scheme URI or project ID.
   * @param array $languages
   *   An array of languages:
   *    key = Drupal language
   *    value = PoolParty language.
   * @param int $terms_per_request
   *   Count of taxonomy terms per http request.
   * @param array $inform_user
   *   An array with email address and batch URL for the summery email.
   */
  public function exportTaxonomyTerms($taxonomy, $root_uri, $languages, $terms_per_request, $inform_user=array()) {
    $start_time = time();

    // Configure the batch data.
    $batch = array(
      'title' => t('Exporting taxonomy %name ...', array('%name' => $taxonomy->name)),
      'operations' => array(),
      'init_message' => t('Starting with the export of the taxonomy terms.'),
      'progress_message' => '',
      'finished' => 'pp_taxonomy_manager_taxonomy_export_finished',
    );

    // Get the taxonomy tree for the default language.
    $default_language = language_default('language');
    if (module_exists('i18n_taxonomy') && $taxonomy->i18n_mode == I18N_MODE_TRANSLATE) {
      $tree = i18n_taxonomy_get_tree($taxonomy->vid, $default_language, 0, NULL, TRUE);
    }
    else {
      $tree = taxonomy_get_tree($taxonomy->vid, 0, NULL, TRUE);
    }

    // Set additional data.
    $count = count($tree);
    $info = array(
      'total' => $count,
      'start_time' => $start_time,
    );

    // Set the correct translation mode for the taxonomy.
    $change_terms = $this->setTranslationMode($taxonomy, $languages);
    // If the translation mode was wrong, then change it for all terms.
    if ($change_terms) {
      // Set the taxonomy term translation mode change operations.
      for ($i = 0; $i < $count; $i += $terms_per_request) {
        $terms = array_slice($tree, $i, $terms_per_request);
        $tids = array();
        foreach ($terms as $term) {
          $tids[] = $term->tid;
        }

        $batch['operations'][] = array(
          'pp_taxonomy_manager_taxonomy_change_translation_mode',
          array(
            $this,
            $tids,
            $taxonomy->machine_name,
            $languages,
            $info
          ),
        );
      }
    }

    // Set the Drupal language to LANGUAGE_NONE if required (to save the hash properly).
    $term_default_language = $default_language;
    if (module_exists('i18n_taxonomy') && !in_array($taxonomy->i18n_mode, array(I18N_MODE_TRANSLATE, I18N_MODE_LANGUAGE))) {
      $term_default_language = LANGUAGE_NONE;
    }

    // Set the export operations.
    for ($i = 0; $i < $count; $i += $terms_per_request) {
      $terms = array_slice($tree, $i, $terms_per_request);
      $tids = array();
      foreach ($terms as $term) {
        $tids[] = $term->tid;
      }

      $batch['operations'][] = array(
        'pp_taxonomy_manager_taxonomy_export',
        array(
          $this,
          $tids,
          $term_default_language,
          $languages[$default_language],
          $root_uri,
          $info,
        ),
      );
    }

    // Set the export of related concepts operations.
    for ($i = 0; $i < $count; $i += $terms_per_request) {
      $terms = array_slice($tree, $i, $terms_per_request);
      $tids = array();
      foreach ($terms as $term) {
        $tids[] = $term->tid;
      }

      $batch['operations'][] = array(
        'pp_taxonomy_manager_taxonomy_export_relations',
        array(
          $this,
          $tids,
          $info,
        ),
      );
    }

    // Set the update hash table operations after the export of all terms.
    for ($i = 0; $i < $count; $i += $terms_per_request) {
      $terms = array_slice($tree, $i, $terms_per_request);
      $tids = array();
      foreach ($terms as $term) {
        $tids[] = $term->tid;
      }

      $batch['operations'][] = array(
        'pp_taxonomy_manager_taxonomy_update_hash',
        array(
          $this,
          $tids,
          $info
        ),
      );
    }

    // Set the export translation operations.
    unset($languages[$default_language]);
    if (!empty($languages)) {
      foreach ($languages as $drupal_lang => $pp_lang) {
        $info = array(
          'total' => $count,
          'start_time' => $start_time,
        );

        for ($i = 0; $i < $count; $i += $terms_per_request) {
          $terms = array_slice($tree, $i, $terms_per_request);
          // Since the terms have to be loaded for each of the language anyway,
          // the term IDs are sufficient.
          $tids = array();
          foreach ($terms as $term) {
            $tids[] = $term->tid;
          }

          $batch['operations'][] = array(
            'pp_taxonomy_manager_taxonomy_export_translations',
            array(
              $this,
              $tids,
              $drupal_lang,
              $pp_lang,
              $info
            ),
          );
        }
      }
    }

    // Set the log operation.
    if (!empty($inform_user)) {
      $info['type'] = 'export';
      $info['inform_user_email'] = $inform_user['user_email'];
      $info['server_name'] = $this->config->getConnection()->getTitle();
      $info['batch_url'] = $inform_user['batch_url'];
    }
    $batch['operations'][] = array(
      'pp_taxonomy_manager_taxonomy_log_save',
      array(
        $this,
        $taxonomy->vid,
        $info
      ),
    );

    // Start the batch.
    batch_set($batch);
  }

  /**
   * Batch process method for exporting taxonomy terms into a PoolParty server.
   *
   * @param int[] $tids
   *   The IDs of the taxonomy terms in the default language.
   * @param string $drupal_lang
   *   The language of the taxonomy terms that are to be exported.
   * @param string $pp_lang
   *   The language of the concept that are to be created.
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function exportBatch(array $tids, $drupal_lang, $pp_lang, array &$context) {
    $config = $this->getConfig();
    /* @var $ppt SemanticConnectorPPTApi */
    $ppt = $config->getConnection()->getAPI('PPT');

    $fields = self::taxonomyFields();
    $settings = $this->config->getConfig();
    $exported_terms = &$context['results']['exported_terms'];
    $project_id = ($settings['root_level'] == 'conceptscheme' ? $config->getProjectId() : $exported_terms[0]['uri']);

    $terms = $this->loadTermsWithParents($tids);
    foreach ($terms as $term) {
      // Create new concept.
      $created = FALSE;
      if (!isset($exported_terms[$term->tid])) {
        foreach ($term->parents as $parent) {
          if (isset($exported_terms[$parent])) {
            // Create new concept.
            if (!$created) {
              if ($settings['root_level'] == 'project' && $parent == 0) {
                $uri = $ppt->createConceptScheme($project_id, $term->name, (!empty($term->description) ? $term->description : ''));
              }
              else {
                $uri = $ppt->createConcept($project_id, $term->name, $exported_terms[$parent]['uri']);
                // Add definition, alt labels, hidden labels and custom properties
                // if required.
                if (!empty($term->description)) {
                  $raw_description = str_replace(array("\r", "\n"), '', strip_tags($term->description));
                  if (!empty($raw_description)) {
                    $ppt->addLiteral($project_id, $uri, 'definition', $raw_description, $pp_lang);
                  }
                }
                foreach ($fields as $field_id => $field_schema) {
                  if (!isset($field_schema->push_key)) {
                    continue;
                  }
                  if (!empty($term->{$field_id})) {
                    if (isset($field_schema->cardinality) && $field_schema->cardinality != 1) {
                      foreach ($term->{$field_id}[LANGUAGE_NONE] as $value) {
                        $value = trim($value['value']);
                        if (!empty($value)) {
                          $ppt->addLiteral($project_id, $uri, $field_schema->push_key, $value, $pp_lang);
                        }
                      }
                    }
                    else {
                      $char = isset($field_schema->merge_char) ? $field_schema->merge_char : ',';
                      if ($char == ' ') {
                        $values = array($term->{$field_id}[LANGUAGE_NONE][0]['value']);
                      }
                      else {
                        $values = explode($char, $term->{$field_id}[LANGUAGE_NONE][0]['value']);
                      }
                      foreach ($values as $value) {
                        $value = trim($value);
                        if (!empty($value)) {
                          $ppt->addLiteral($project_id, $uri, $field_schema->push_key, $value, $pp_lang);
                        }
                      }
                    }
                  }
                }
              }

              // Save the old URI for any relations between concepts on URI
              // basis.
              $old_uri = NULL;
              if (isset($term->field_uri) && !empty($term->field_uri)) {
                $old_uri = $term->field_uri[LANGUAGE_NONE][0]['value'];
              }

              // Update term with the new URI.
              $term->field_uri[LANGUAGE_NONE][0]['value'] = $uri;
              taxonomy_term_save($term);
              $exported_terms[$term->tid] = array(
                'uri' => $uri,
                'old_uri' => $old_uri,
                'parents' => array($parent),
                'drupalLang' => $drupal_lang,
                'ppLang' => $pp_lang,
                'hash' => FALSE,
              );
              watchdog('PoolParty Taxonomy Manager', 'Concept created: %name (URI = %uri)', array(
                '%name' => $term->name,
                '%uri' => $uri,
              ));
              $created = TRUE;
            }
            // Add additional parents.
            else {
              $relation_type = ($settings['root_level'] != 'project' || !in_array(0, $exported_terms[$parent]['parents'])) ? 'broader' : 'topConceptOf';
              $uri = $exported_terms[$term->tid]['uri'];
              $ppt->addRelation($project_id, $uri, $exported_terms[$parent]['uri'], $relation_type);
              $exported_terms[$term->tid]['parents'][] = $parent;
            }
          }
        }
      }
      // Add missing parents to the concept.
      else {
        foreach ($term->parents as $parent) {
          if (isset($exported_terms[$parent]) && !in_array($parent, $exported_terms[$term->tid]['parents'])) {
            $relation_type = ($settings['root_level'] != 'project' || !in_array(0, $exported_terms[$parent]['parents'])) ? 'broader' : 'topConceptOf';
            $uri = $exported_terms[$term->tid]['uri'];
            $ppt->addRelation($project_id, $uri, $exported_terms[$parent]['uri'], $relation_type);
            $exported_terms[$term->tid]['parents'][] = $parent;
          }
        }
      }
      $context['results']['processed']++;
    }
  }

  /**
   * Batch process method for exporting relations data.
   *
   * @param int[] $tids
   *   The IDs of the taxonomy terms in the default language.
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function exportRelationsBatch(array $tids, array &$context) {
    $config = $this->getConfig();
    /* @var $ppt SemanticConnectorPPTApi */
    $ppt = $config->getConnection()->getAPI('PPT');
    $settings = $config->getConfig();
    $exported_terms = &$context['results']['exported_terms'];
    $project_id = ($settings['root_level'] == 'conceptscheme' ? $config->getProjectId() : $exported_terms[0]['uri']);

    $terms = taxonomy_term_load_multiple($tids);
    foreach ($terms as $term) {
      if (isset($exported_terms[$term->tid]) && !empty($term->field_uri)) {
        $source = $term->field_uri[LANGUAGE_NONE][0]['value'];
        if (!empty($term->field_related_concepts)) {
          $new_relation_uris = [];
          foreach ($term->field_related_concepts[LANGUAGE_NONE] as $uri) {
            // Search for the new URI of the relation.
            foreach ($exported_terms as $exported_term) {
              if (isset($exported_term['old_uri']) && $exported_term['old_uri'] == $uri['value'] && isset($exported_term['uri'])) {
                $new_relation_uris[] = ['value' => $exported_term['uri']];
                $ppt->addRelation($project_id, $source, $exported_term['uri'], 'skos:related');
              }
            }
          }

          // Update term with the new related concept URIs.
          $term->field_related_concepts[LANGUAGE_NONE] = $new_relation_uris;
          taxonomy_term_save($term);
        }
      }
      $context['results']['related_concepts_processed']++;
    }
  }

  /**
   * Batch process function for updating the hash table after the export.
   *
   * @param int[] $tids
   *   The IDs of the taxonomy terms in the default language.
   * @param array $info
   *   An associative array of information about the batch process.
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function updateHashBatch(array $tids, array $info, array &$context) {
    $config = $this->getConfig();
    /* @var $ppt SemanticConnectorPPTApi */
    $ppt = $config->getConnection()->getAPI('PPT');

    $settings = $config->getConfig();
    $exported_terms = &$context['results']['exported_terms'];
    $project_id = ($settings['root_level'] == 'conceptscheme' ? $config->getProjectId() : $exported_terms[0]['uri']);

    $terms = $this->loadTermsWithParents($tids);
    foreach ($terms as $term) {
      if (isset($exported_terms[$term->tid]) && !$exported_terms[$term->tid]['hash']) {
        // Add hash data to the database.
        $data = $exported_terms[$term->tid];
        // The term is a concept scheme.
        if ($settings['root_level'] == 'project' && $term->parents == array(0)) {
          $concept = new stdClass();
          $schemes = $ppt->getConceptSchemes($project_id, $data['ppLang']);
          foreach ($schemes as $scheme) {
            if ($scheme->uri == $data['uri']) {
              $concept = $scheme;
            }
          }
        }
        // The term is a concept.
        else {
          $concept = $ppt->getConcept($project_id, $data['uri'], $this->skosProperties(), $data['ppLang']);
        }
        if (!empty($concept)) {
          $concept->drupalLang = $data['drupalLang'];
          $concept->ppLang = $data['ppLang'];

          $uri_lang = $this->getUri($concept);
          $hash = $this->hash($concept);
          $this->addHashData($term, $uri_lang, $hash, $info['start_time']);

          $exported_terms[$term->tid]['hash'] = TRUE;
        }
        $context['results']['hash_update_processed']++;
      }
    }
  }

  /**
   * Batch process method for exporting taxonomy terms into a PoolParty server.
   *
   * @param int[] $tids
   *   The IDs of the taxonomy terms in the default language.
   * @param string $drupal_lang
   *   The language of the taxonomy terms that are to be exported.
   * @param string $pp_lang
   *   The language of the concept that are to be created.
   * @param array $info
   *   An associative array of information about the batch process.
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function exportTranslationsBatch(array $tids, $drupal_lang, $pp_lang, array $info, array &$context) {
    // Load the terms in the correct language.
    $terms = i18n_taxonomy_translate_terms($tids, $drupal_lang);

    if (!empty($terms)) {
      $config = $this->getConfig();
      /* @var $ppt SemanticConnectorPPTApi */
      $ppt = $config->getConnection()->getAPI('PPT');

      $fields = self::taxonomyFields();

      $default_language = language_default('language');
      $settings = $this->config->getConfig();
      $exported_terms = &$context['results']['exported_terms'];
      $project_id = ($settings['root_level'] == 'conceptscheme' ? $config->getProjectId() : $exported_terms[0]['uri']);

      foreach ($terms as $term) {
        // Get the taxonomy term ID from the default language.
        $default_tid = i18n_taxonomy_translation_term_tid($term->tid, $default_language, FALSE);

        // Check if the term with the default language is already exported.
        if ($default_tid && isset($exported_terms[$default_tid])) {
          $uri = $exported_terms[$default_tid]['uri'];
          $has_parents = TRUE;
          if (count($exported_terms[$default_tid]['parents']) == 1 && $exported_terms[$default_tid]['parents'][0] == 0) {
            $has_parents = FALSE;
          }

          if ($settings['root_level'] == 'conceptscheme' || $has_parents) {
            // Add prefLabel and description.
            $ppt->addLiteral($project_id, $uri, 'preferredLabel', (!empty($term->name) ? $term->name : 'Label not set'), $pp_lang);
            if (!empty($term->description)) {
              $raw_description = str_replace(array("\r", "\n"), '', strip_tags($term->description));
              if (!empty($raw_description)) {
                $ppt->addLiteral($project_id, $uri, 'definition', $raw_description, $pp_lang);
              }
            }
            // Add all other labels and data.
            foreach ($fields as $field_id => $field_schema) {
              if (!isset($field_schema->push_key)) {
                continue;
              }
              if (!empty($term->{$field_id})) {
                if (isset($field_schema->cardinality) && $field_schema->cardinality != 1) {
                  foreach ($term->{$field_id}[LANGUAGE_NONE] as $value) {
                    $value = trim($value['value']);
                    if (!empty($value)) {
                      $ppt->addLiteral($project_id, $uri, $field_schema->push_key, $value, $pp_lang);
                    }
                  }
                }
                else {
                  $char = isset($field_schema->merge_char) ? $field_schema->merge_char : ',';
                  if ($char == ' ') {
                    $values = array($term->{$field_id}[LANGUAGE_NONE][0]['value']);
                  }
                  else {
                    $values = explode($char, $term->{$field_id}[LANGUAGE_NONE][0]['value']);
                  }
                  foreach ($values as $value) {
                    $value = trim($value);
                    if (!empty($value)) {
                      $ppt->addLiteral($project_id, $uri, $field_schema->push_key, $value, $pp_lang);
                    }
                  }
                }
              }
            }
          }

          // Update term with the new URI.
          $term->field_uri[LANGUAGE_NONE][0]['value'] = $uri;
          taxonomy_term_save($term);

          // Add hash data to the database.
          // The term is a concept scheme.
          if ($settings['root_level'] == 'conceptscheme' || $has_parents) {
            $concept = $ppt->getConcept($project_id, $uri, $this->skosProperties(), $pp_lang);
          }
          // The term is a concept.
          else {
            $concept = new stdClass();
            $schemes = $ppt->getConceptSchemes($project_id, $pp_lang);
            foreach ($schemes as $scheme) {
              if ($scheme->uri == $uri) {
                $concept = $scheme;
                break;
              }
            }
          }
          $concept->drupalLang = $drupal_lang;
          $concept->ppLang = $pp_lang;

          $uri_lang = $this->getUri($concept);
          $hash = $this->hash($concept);
          $this->addHashData($term, $uri_lang, $hash, $info['start_time']);
        }
      }
    }
    $context['results']['translation_processed']++;
  }

  /**
   * Creates a batch for updating all terms of a taxonomy.
   *
   * @param string $update_type
   *   The update type: sync, powertagging_taxonomy_update or import.
   * @param object $taxonomy
   *   A Drupal taxonomy object.
   * @param string $root_uri
   *   The URI of a concept scheme or the ID of a project
   * @param array $languages
   *   An array of languages:
   *    key = Drupal language
   *    value = PoolParty language.
   * @param array $data_properties
   *   An array of SKOS properties to be fetched.
   * @param bool $preserve_concepts
   *   TRUE if old concepts should be converted into freeterms, FALSE if they
   *   should be deleted.
   * @param int $concepts_per_request
   *   Count of concepts per http request.
   * @param array $inform_user
   *   An array with email address and batch URL for the summery email.
   *
   * @throws \Exception
   */
  public function updateTaxonomyTerms($update_type, $taxonomy, $root_uri, $languages, $data_properties, $preserve_concepts, $concepts_per_request, $inform_user = []) {
    $start_time = time();

    // Configure the batch data.
    switch ($update_type) {
      case 'import':
        $title = t('Import terms into taxonomy %name ...', array('%name' => $taxonomy->name));
        $init_message = t('Starting with the import of the taxonomy terms.');
        break;

      default:
        $title = t('Updating taxonomy %name ...', array('%name' => $taxonomy->name));
        $init_message = t('Starting with the update of the taxonomy terms.');
        break;
    }
    $batch = array(
      'title' => $title,
      'operations' => array(),
      'init_message' => $init_message,
      'progress_message' => '',
      'finished' => 'pp_taxonomy_manager_taxonomy_update_finished',
    );

    // Get all the concepts from the PoolParty server per language.
    $data_properties[] = 'skos:broader';
    $data_properties[] = 'skos:definition';
    /* @var $ppt SemanticConnectorPPTApi */
    $ppt = $this->config->getConnection()->getApi('PPT');

    $settings = $this->config->getConfig();
    $top_term_uris = array();
    $concepts = array();
    if ($settings['root_level'] == 'project') {
      foreach ($languages as $drupal_lang => $pp_lang) {
        $concept_schemes = $ppt->getConceptSchemes($root_uri, $pp_lang);
        if (is_null($concept_schemes)) {
          throw new Exception('Error while fetching the concept schemes for project "' . $root_uri . '"');
        }
        foreach ($concept_schemes as $concept_scheme) {
          // Get the "hasTopConcept" relations of the scheme.
          $top_concepts = $ppt->getTopConcepts($root_uri, $concept_scheme->uri, $data_properties, $pp_lang);
          $concept_scheme_children = array();
          if (is_null($top_concepts)) {
            throw new Exception('Error while fetching the top concepts for URI "' . $concept_scheme->uri . '"');
          }
          elseif (is_array($top_concepts)) {
            foreach ($top_concepts as $top_concept) {
              $concept_scheme_children[] = $top_concept->uri;
            }
          }

          // Add the concept scheme itself.
          $concept_scheme_object = (object) array(
            'uri' => $concept_scheme->uri,
            'prefLabel' => $concept_scheme->title,
            'narrowers' => $concept_scheme_children,
            'drupalLang' => $drupal_lang,
            'ppLang' => $pp_lang,
          );
          $concepts[$this->getUri($concept_scheme_object)] = $concept_scheme_object;

          $top_term_uris[] = $concept_scheme->uri . '@' . $pp_lang;
          // Add the top concepts and concepts.
          $tree = $ppt->getSubTree($root_uri, $concept_scheme->uri, $data_properties, $pp_lang);
          if (is_null($tree)) {
            throw new Exception('Error while fetching the subtree for URI "' . $concept_scheme->uri . '"');
          }
          $tree_list = $this->tree2list($tree, $drupal_lang, $pp_lang);

          // Add the fake broader relation to the concept scheme for all top
          // concepts.
          if (is_array($tree)) {
            foreach ($tree_list as &$concept) {
              if (in_array($concept->uri, $concept_scheme_children)) {
                $concept->broaders = array($concept_scheme->uri);
              }
              unset($top_concept);
            }
          }
          $concepts = array_merge($concepts, $tree_list);
        }
      }
    }
    else {
      foreach ($languages as $drupal_lang => $pp_lang) {
        $top_concepts = $ppt->getTopConcepts($this->config->getProjectId(), $root_uri, $data_properties, $pp_lang);
        if (is_null($top_concepts)) {
          throw new Exception('Error while fetching the top concepts for URI "' . $root_uri . '"');
        }
        elseif (is_array($top_concepts)) {
          foreach ($top_concepts as $top_concept) {
            $top_term_uris[] = $top_concept->uri . '@' . $pp_lang;
          }
        }
        // Includes top concepts and concepts.
        $tree = $ppt->getSubTree($this->config->getProjectId(), $root_uri, $data_properties, $pp_lang);
        if (is_null($tree)) {
          throw new Exception('Error while fetching the subtree for URI "' . $root_uri . '"');
        }
        $tree_list = $this->tree2list($tree, $drupal_lang, $pp_lang);
        $concepts = array_merge($concepts, $tree_list);
      }
    }

    // Set additional data.
    $count = count($concepts);
    $info = array(
      'total' => $count,
      'start_time' => $start_time,
      'top_concept_uris' => $top_term_uris,
    );

    // Set the correct translation mode for the taxonomy.
    $change_terms = $this->setTranslationMode($taxonomy, $languages);
    // If the translation mode was wrong, then change it for all terms.
    if ($change_terms) {
      $drupal_vocabulary_tree = taxonomy_get_tree($taxonomy->vid, 0, NULL, TRUE);
      // Set the taxonomy term translation mode change operations.
      for ($i = 0; $i < $count; $i += $concepts_per_request) {
        $terms = array_slice($drupal_vocabulary_tree, $i, $concepts_per_request);
        $tids = array();
        foreach ($terms as $term) {
          $tids[] = $term->tid;
        }

        $batch['operations'][] = array(
          'pp_taxonomy_manager_taxonomy_change_translation_mode',
          array(
            $this,
            $tids,
            $taxonomy->machine_name,
            $languages,
            $info
          ),
        );
      }
    }

    // Set the Drupal language to LANGUAGE_NONE if required.
    $default_language = language_default('language');
    if (module_exists('i18n_taxonomy') && !in_array($taxonomy->i18n_mode, array(I18N_MODE_TRANSLATE, I18N_MODE_LANGUAGE))) {
      foreach ($concepts as &$concept) {
        if ($concept->drupalLang == $default_language) {
          $concept->drupalLang = LANGUAGE_NONE;
        }
      }
    }

    // Set the update operations.
    for ($i = 0; $i < $count; $i += $concepts_per_request) {
      $concept_list = array_slice($concepts, $i, $concepts_per_request);
      $batch['operations'][] = array(
        'pp_taxonomy_manager_taxonomy_update',
        array(
          $this,
          $concept_list,
          $taxonomy->vid,
          $taxonomy->machine_name,
          $info,
        ),
      );
    }

    // Set the update parents operations.
    for ($i = 0; $i < $count; $i += $concepts_per_request) {
      $concept_list = array_slice($concepts, $i, $concepts_per_request);
      $batch['operations'][] = array(
        'pp_taxonomy_manager_taxonomy_update_parents',
        array($this, $concept_list, $info),
      );
    }

    // Set the delete operations for the removed concepts.
    $batch['operations'][] = array(
      'pp_taxonomy_manager_taxonomy_delete',
      array($this, $taxonomy->vid, $preserve_concepts),
    );

    // Set the log operation.
    if (!empty($inform_user)) {
      $info['type'] = $update_type;
      $info['inform_user_email'] = $inform_user['user_email'];
      $info['server_name'] = $this->config->getConnection()->getTitle();
      $info['batch_url'] = $inform_user['batch_url'];
    }
    $batch['operations'][] = array(
      'pp_taxonomy_manager_taxonomy_log_save',
      array($this, $taxonomy->vid, $info),
    );

    // Start the batch.
    batch_set($batch);
  }

  /**
   * Batch process method for updating taxonomy terms from a PoolParty server.
   *
   * @param array $concepts
   *   The concepts that are to be updated.
   * @param string $vid
   *   The taxonomy ID where the terms should be updated.
   * @param string $machine_name
   *   The taxonomy machine_name where the terms should be updated.
   * @param array $info
   *   An associative array of information about the batch process.
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function updateBatch($concepts, $vid, $machine_name, $info, &$context) {
    $uris = array_keys($concepts);
    $processed_uris = array();

    // Get mapping data for every concept.
    $result = db_select('pp_taxonomy_manager_terms', 't')
      ->fields('t', array('tid', 'uri', 'hash'))
      ->condition('t.tmid', $this->config->getId())
      ->condition('t.vid', $vid)
      ->condition('t.uri', $uris)
      ->execute();

    // Update all existing terms.
    while ($record = $result->fetch()) {
      $concept = $concepts[$record->uri];
      $hash = $this->hash($concept);
      if ($record->hash != $hash) {
        $term = taxonomy_term_load($record->tid);
        $term = $this->mapTaxonomyTermDetails($term, $concept);
        taxonomy_term_save($term);
        $this->updateHashData($term, $hash, $info['start_time']);
        $context['results']['updated_terms'][$record->uri] = $record->tid;
        watchdog('PoolParty Taxonomy Manager', 'Taxonomy term updated: %name (TID = %tid)', array(
          '%name' => $term->name,
          '%tid' => $term->tid,
        ));
      }
      else {
        $context['results']['skipped_terms'][$record->uri] = $record->tid;
      }

      $processed_uris[] = $record->uri;
      $context['results']['processed']++;
    }

    // Create new terms for new existing concepts.
    $new_uris = array_diff($uris, $processed_uris);
    if (!empty($new_uris)) {
      foreach ($new_uris as $uri) {
        // Check if a term with the same URI and language already exists
        // in the taxonomy.
        $concept = $concepts[$uri];
        $query = db_select('field_data_field_uri', 'u')
          ->fields('u', array('entity_id'))
          ->condition('u.bundle', $machine_name)
          ->condition('u.field_uri_value', $concept->uri);
        $query->join('taxonomy_term_data', 't', 'u.entity_id = t.tid');

        // Add language condition if multilingual for taxonomies is enabled.
        if (module_exists('i18n_taxonomy')) {
          $query->condition('t.language', $concept->drupalLang);
        }

        // Get the taxonomy term ID if the URI exists.
        $tid = $query->execute()
          ->fetchField();

        $term_exists = FALSE;
        if ($tid) {
          $term = taxonomy_term_load($tid);
          if ($term !== FALSE) {
            $term_exists = TRUE;
          }
        }
        // Check if a term with the same label (without a URI) already exists.
        else {
          $label_query = db_select('taxonomy_term_data', 't');
          $label_query->fields('t', array('tid'));
          $label_query->condition('t.vid', $vid);
          $label_query->condition('t.name', $concept->prefLabel);
          // Only the ones without a URI, even though the rest should already
          // be filtered out.
          $label_query->leftJoin('field_data_field_uri', 'u', 'u.entity_type = \'taxonomy_term\' AND t.tid = u.entity_id');
          $label_query->isNull('u.field_uri_value');

          $tid = $label_query->execute()
            ->fetchField();

          if ($tid) {
            $term = taxonomy_term_load($tid);
            if ($term !== FALSE) {
              $term_exists = TRUE;
            }
          }
        }

        if (!$term_exists) {
          $term = new stdClass();
          $term->vid = $vid;
        }

        // Map data and save the term.
        $term = $this->mapTaxonomyTermDetails($term, $concept);
        taxonomy_term_save($term);

        // Add the term to a translation set.
        if (module_exists('i18n_taxonomy')) {
          $default_language = language_default('language');
          if ($term->language == $default_language) {
            $translation_set = ($term->i18n_tsid) ? i18n_translation_set_load($term->i18n_tsid) : i18n_translation_set_create('taxonomy_term', $machine_name);
          }
          else {
            // Get the translation set ID from the default term.
            $query = db_select('taxonomy_term_data', 't')
              ->fields('t', array('i18n_tsid'))
              ->condition('t.language', $default_language)
              ->condition('u.bundle', $machine_name)
              ->condition('u.field_uri_value', $concept->uri);
            $query->join('field_data_field_uri', 'u', 't.tid = u.entity_id');
            $i18n_tsid = $query->execute()
              ->fetchField();
            $translation_set = ($i18n_tsid) ? i18n_translation_set_load($i18n_tsid) : i18n_translation_set_create('taxonomy_term', $machine_name);
          }
          $translation_set->add_item($term, $term->language);
          $translation_set->save();
        }

        // Add the hash to the hash table.
        $uri = $this->getUri($concept);
        $hash = $this->hash($concept);
        $this->addHashData($term, $uri, $hash, $info['start_time']);
        if ($term_exists) {
          $context['results']['updated_terms'][$uri] = $term->tid;
        }
        else {
          $context['results']['created_terms'][$uri] = $term->tid;
        }
        $context['results']['processed']++;
        watchdog('PoolParty Taxonomy Manager', 'Taxonomy term created: %name (TID = %tid)', array(
          '%name' => $term->name,
          '%tid' => $term->tid,
        ));
      }
    }
  }

  /**
   * Batch process method for updating the parents for the taxonomy terms.
   *
   * @param array $concepts
   *   The concepts that are to be updated.
   * @param array $info
   *   An associative array of information about the batch process.
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function updateParentsBatch($concepts, $info, &$context) {
    $changed_terms = $context['results']['updated_terms'] + $context['results']['created_terms'];
    $all_terms = $changed_terms + $context['results']['skipped_terms'];
    foreach ($concepts as $concept) {
      $concept_uri = $this->getUri($concept);

      // Check if concept is updated or new.
      if (!isset($changed_terms[$concept_uri])) {
        $context['results']['processed_parents']++;
        continue;
      }

      // If the concept is a top concept then set to the top of the tree.
      $parents = array();
      if (in_array($concept_uri, $info['top_concept_uris'])) {
        $parents[] = 0;
      }
      if (isset($concept->broaders) && !empty($concept->broaders)) {
        foreach ($concept->broaders as $broader) {
          $broader_uri = $broader . '@' . $concept->ppLang;
          if (isset($all_terms[$broader_uri])) {
            $parents[] = $all_terms[$broader_uri];
          }
        }
      }
      $term = taxonomy_term_load($all_terms[$concept_uri]);
      $term->parent = empty($parents) ? 0 : $parents;
      taxonomy_term_save($term);

      $context['results']['processed_parents']++;
    }
  }

  /**
   * Batch process method for deleting taxonomy terms.
   *
   * @param string $vid
   *   The taxonomy ID where the terms should be updated.
   * @param bool $preserve_concepts
   *   TRUE if old concepts should be converted into freeterms, FALSE if they
   * @param array $context
   *   The batch context to transmit data between different calls.
   */
  public function deleteBatch($vid, $preserve_concepts, &$context) {
    $all_terms = $context['results']['updated_terms'] + $context['results']['created_terms'] + $context['results']['skipped_terms'];
    $all_terms = array_values($all_terms);

    // Delete all terms with a URI, which were not part of the sync/import process.
    $term_delete_query = db_select('taxonomy_term_data', 't');
    $term_delete_query->fields('t', array('tid', 'name'));
    $term_delete_query->condition('t.vid', $vid);
    $term_delete_query->join('field_data_field_uri', 'u', 't.tid = u.entity_id AND u.entity_type = \'taxonomy_term\'');
    $term_delete_query->addField('u', 'field_uri_value', 'uri');
    if (!empty($all_terms)) {
      $term_delete_query->condition('t.tid', $all_terms, 'NOT IN');
    }
    $delete_concepts = $term_delete_query->execute()
      ->fetchAllAssoc('uri', PDO::FETCH_ASSOC);

    // Delete the old concepts.
    if (!$preserve_concepts) {
      foreach ($delete_concepts as $uri => $delete_concept) {
        taxonomy_term_delete($delete_concept['tid']);
        $context['results']['deleted_terms'][$uri] = $delete_concept['tid'];
        watchdog('PoolParty Taxonomy Manager', 'Taxonomy term deleted: %name (TID = %tid)', array(
          '%name' => $delete_concept['name'],
          '%tid' => $delete_concept['tid'],
        ));
      }

      // Check if a freeterm-term already exists.
      $freeterm_tid_query = db_select('taxonomy_term_data', 't');
      $freeterm_tid_query->fields('t', array('tid'));
      $freeterm_tid_query->condition('vid', $vid);
      $freeterm_tid_query->condition('name', 'Free Terms');
      $freeterm_tid_query->leftJoin('field_data_field_uri', 'u', 't.tid = u.entity_id AND u.entity_type = \'taxonomy_term\'');
      $freeterm_tid_query->isNull('u.field_uri_value');
      $freeterm_tid = $freeterm_tid_query->execute()
        ->fetchField();

      // Check if there are any terms without URI left, which are not marked as
      // freeterms.
      $no_uri_query = db_select('taxonomy_term_data', 't');
      $no_uri_query->fields('t', array('tid', 'name'));
      $no_uri_query->condition('t.vid', $vid);
      $no_uri_query->condition('t.name', ['Concepts', 'Free Terms'], 'NOT IN');
      $no_uri_query->leftJoin('field_data_field_uri', 'u', 't.tid = u.entity_id AND u.entity_type = \'taxonomy_term\'');
      $no_uri_query->isNull('u.field_uri_value');
      if ($freeterm_tid) {
        $no_uri_query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
        $no_uri_query->condition('parent', $freeterm_tid, '<>');
      }
      $no_uri_terms = $no_uri_query->execute()
        ->fetchAllAssoc('tid', PDO::FETCH_ASSOC);

      // Delete the terms.
      foreach ($no_uri_terms as $no_uri_term_tid => $no_uri_term) {
        taxonomy_term_delete($no_uri_term_tid);
        $context['results']['deleted_terms']['__delete_freeterm__' . $no_uri_term_tid] = $no_uri_term_tid;
        watchdog('PoolParty Taxonomy Manager', 'Taxonomy term deleted: %name (TID = %tid)', array(
          '%name' => $no_uri_term['name'],
          '%tid' => $no_uri_term['tid'],
        ));
      }
    }
    // Turn the old concepts into freeterms.
    else {
      $make_freeterm_tids = [];
      foreach ($delete_concepts as $uri => $delete_concept) {
        $make_freeterm_tids[] = $delete_concept['tid'];
      }

      if (!empty($make_freeterm_tids)) {
        // Remove the URIs.
        db_delete('field_data_field_uri')
          ->condition('entity_type', 'taxonomy_term')
          ->condition('entity_id', $make_freeterm_tids, 'IN')
          ->execute();
        db_delete('field_revision_field_uri')
          ->condition('entity_type', 'taxonomy_term')
          ->condition('entity_id', $make_freeterm_tids, 'IN')
          ->execute();

        // Remove them from the hash table.
        db_delete('pp_taxonomy_manager_terms')
          ->condition('tid', $make_freeterm_tids, 'IN')
          ->execute();
      }

      // Check for all terms without a URI.
      $update_parent_query = db_select('taxonomy_term_data', 't')
        ->fields('t', array('tid'))
        ->condition('t.vid', $vid)
        ->condition('t.name', ['Concepts', 'Free Terms'], 'NOT IN');
      $update_parent_query->leftJoin('field_data_field_uri', 'u', 't.tid = u.entity_id AND u.entity_type = \'taxonomy_term\'');
      $update_parent_query->isNull('u.field_uri_value');
      $update_parent_tids = $update_parent_query->execute()
        ->fetchCol();

      // Move them under a freeterms-term.
      if (!empty($update_parent_tids)) {
        // Check if a freeterm-term already exists.
        $freeterm_tid_query = db_select('taxonomy_term_data', 't')
          ->fields('t', array('tid'))
          ->condition('vid', $vid)
          ->condition('name', 'Free Terms');
        $freeterm_tid_query->leftJoin('field_data_field_uri', 'u', 't.tid = u.entity_id AND u.entity_type = \'taxonomy_term\'');
        $freeterm_tid_query->isNull('u.field_uri_value');
        $freeterm_tid = $freeterm_tid_query->execute()
          ->fetchField();

        // Create the freeterm-term if it doesn't exist yet.
        if (!$freeterm_tid) {
          $term = new stdClass();
          $term->name = 'Free Terms';
          $term->vid = $vid;
          taxonomy_term_save($term);
          $freeterm_tid = $term->tid;
        }

        // Update the parents of the preserved tags.
        db_update('taxonomy_term_hierarchy')
          ->fields(array(
            'parent' => $freeterm_tid,
          ))
          ->condition('tid', $update_parent_tids, 'IN')
          ->execute();
      }
    }

    // Delete the "Concepts" term if it was created by the PowerTagging module.
    $static_delete_query = db_select('taxonomy_term_data', 't');
    $static_delete_query->fields('t', array('tid'));
    $static_delete_query->condition('t.vid', $vid);
    $static_delete_query->condition('t.name', 'Concepts');
    $static_delete_query->leftJoin('field_data_field_uri', 'u', 't.tid = u.entity_id AND u.entity_type = \'taxonomy_term\'');
    $static_delete_query->isNull('u.field_uri_value');
    $concepts_tid = $static_delete_query->execute()
      ->fetchField();
    if (!empty($concepts_tid)) {
      taxonomy_term_delete($concepts_tid);
    }
  }

  /**
   * Deletes the term from the hash table.
   *
   * @param int $tid
   *   The ID of the taxonomy term.
   */
  public static function deleteTaxonomyTerm($tid) {
    db_delete('pp_taxonomy_manager_terms')
      ->condition('tid', $tid)
      ->execute();
  }

  /**
   * Inserts the new statistic log.
   *
   * @param int $vid
   *   The Drupal taxonomy ID.
   * @param int $start_time
   *   The start time of the batch.
   * @param int $end_time
   *   The end time of the batch.
   */
  public function addLog($vid, $start_time, $end_time) {
    global $user;

    db_insert('pp_taxonomy_manager_logs')
      ->fields(array(
        'tmid' => $this->config->getId(),
        'vid' => $vid,
        'start_time' => $start_time,
        'end_time' => $end_time,
        'uid' => $user->uid,
      ))
      ->execute();
  }

  /**
   * Deletes all synchronization data.
   *
   * @param int $vid
   *   The Drupal taxonomy ID.
   */
  public function deleteSyncData($vid) {
    // Delete the log data.
    db_delete('pp_taxonomy_manager_logs')
      ->condition('tmid', $this->config->getId())
      ->condition('vid', $vid)
      ->execute();

    // Delete the hash data.
    db_delete('pp_taxonomy_manager_terms')
      ->condition('tmid', $this->config->getId())
      ->condition('vid', $vid)
      ->execute();
  }

  /**
   * Calculates the remaining time of a batch process.
   *
   * @param int $start_time
   *   The start time.
   * @param int $processed
   *   The count of processed items.
   * @param int $total
   *   The total count of items.
   *
   * @return string
   *   The remaining time in a human readable string.
   */
  public function calculateRemainingTime($start_time, $processed, $total) {
    $time_string = '';
    if ($processed > 0) {
      $remaining_time = floor((time() - $start_time) / $processed * ($total - $processed));
      if ($remaining_time > 0) {
        $time_string = (floor($remaining_time / 3600) % 24) . ' hours ' . (floor($remaining_time / 60) % 60) . ' minutes ' . ($remaining_time % 60) . ' seconds';
      }
      else {
        $time_string = t('Done.');
      }
    }

    return $time_string;
  }

  /**
   * Maps a taxonomy term data with a PoolParty concept.
   *
   * @param object $term
   *   The object of the taxonomy term, which will receive the new detail data.
   * @param object $concept
   *   A concept detail data to update the term with.
   *
   * @return object
   *   The mapped taxonomy term.
   */
  protected function mapTaxonomyTermDetails($term, $concept) {
    $term->name = $concept->prefLabel;
    $term->language = $concept->drupalLang;
    $term->description = empty($concept->definitions) ? '' : implode(' ', $concept->definitions);
    $term->field_uri[LANGUAGE_NONE][0]['value'] = $concept->uri;

    $fields = self::taxonomyFields();
    foreach ($fields as $field_id => $field_schema) {
      if (!isset($field_schema->pull_key)) {
        continue;
      }
      if (isset($field_schema->cardinality) && $field_schema->cardinality != 1) {
        $term->{$field_id} = [];
        if (!empty($concept->{$field_schema->pull_key})) {
          $term->{$field_id}[LANGUAGE_NONE] = [];
          foreach ($concept->{$field_schema->pull_key} as $value) {
            // Remove multibyte-characters.
            $term->{$field_id}[LANGUAGE_NONE][] = ['value' => preg_replace('/[[:^print:]]/', "", $value)];
          }
        }
      }
      else {
        $term->{$field_id}[LANGUAGE_NONE][0]['value'] = '';
        if (!empty($concept->{$field_schema->pull_key})) {
          $char = isset($field_schema->merge_char) ? $field_schema->merge_char : ',';
          $value = implode($char, $concept->{$field_schema->pull_key});
          // Remove multibyte-characters.
          $term->{$field_id}[LANGUAGE_NONE][0]['value'] = preg_replace('/[[:^print:]]/', "", $value);
        }
      }
    }

    return $term;
  }

  /**
   * Creates a list of concepts from a tree.
   *
   * @param array $tree
   *   A list of concepts in tree format.
   * @param string $drupal_lang
   *   The Drupal language of the concepts.
   * @param string $pp_lang
   *   The PoolParty project language of the concepts.
   * @param int $depth
   *   The depth of the recursive function call.
   *
   * @return array
   *   A list of concept objects.
   */
  protected function tree2list($tree, $drupal_lang, $pp_lang, $depth = 0) {
    $concepts = array();
    foreach ($tree as $subtree) {
      if (is_object($subtree) && !empty($subtree->concept)) {
        // If a concept is in the top level but is not a top concept, then
        // remove its broaders (it's important for other languages if is not
        // translated consistently).
        if ($depth == 0) {
          unset($subtree->concept->broaders);
        }
        $subtree->concept->drupalLang = $drupal_lang;
        $subtree->concept->ppLang = $pp_lang;
        $concept_uri = $this->getUri($subtree->concept);
        $concepts[$concept_uri] = $subtree->concept;
        if (!empty($subtree->narrowers)) {
          $tree_list = $this->tree2list($subtree->narrowers, $drupal_lang, $pp_lang, ($depth + 1));
          $concepts = array_merge($concepts, $tree_list);
        }
      }
    }
    return $concepts;
  }

  /**
   * Inserts the new created concept to the database.
   *
   * @param object $term
   *   A Drupal taxonomy term.
   * @param string $uri
   *   The URI with language prefix of a concept.
   * @param string $hash
   *   The new hash data.
   * @param int $start_time
   *   The start time of the batch.
   */
  protected function addHashData($term, $uri, $hash, $start_time) {
    db_insert('pp_taxonomy_manager_terms')
      ->fields(array(
        'tid' => $term->tid,
        'vid' => $term->vid,
        'tmid' => $this->config->getId(),
        'synced' => $start_time,
        'uri' => $uri,
        'hash' => $hash,
      ))
      ->execute();
  }

  /**
   * Updates the hash data for a taxonomy term.
   *
   * @param object $term
   *   The taxonomy term.
   * @param string $hash
   *   The new hash data.
   * @param int $start_time
   *   The synchonization start time.
   */
  protected function updateHashData($term, $hash, $start_time) {
    db_update('pp_taxonomy_manager_terms')
      ->fields(array(
        'synced' => $start_time,
        'hash' => $hash,
      ))
      ->condition('vid', $term->vid)
      ->condition('tid', $term->tid)
      ->execute();
  }

  /**
   * Creates a hash code from a concept $concept.
   *
   * @param object $concept
   *   A concept object from PoolParty.
   *
   * @return string
   *   The hash code.
   */
  protected function hash($concept) {
    return hash('md5', serialize($concept));
  }

  /**
   * Returns all SKOS properties of the taxonomy fields.
   *
   * @return array
   *   List of SKOS properties.
   */
  protected function skosProperties() {
    $fields = self::taxonomyFields();
    $properties = array();
    foreach ($fields as $field) {
      if (isset($field->property)) {
        $properties[] = $field->property;
      }
    }
    $properties[] = 'skos:broader';
    $properties[] = 'skos:definition';

    return $properties;
  }

  /**
   * Returns all SKOS properties of the taxonomy fields.
   *
   * @return array
   *   List of SKOS properties.
   */
  public static function getTaxonomyFieldProperties() {
    $fields = self::taxonomyFields();
    $properties = [];
    foreach ($fields as $field) {
      if (isset($field->property) && !empty($field->property)) {
        $properties[$field->property] = $field->label;
      }
    }

    return $properties;
  }

  /**
   * Creates a machine readable name from a human readable name.
   *
   * @param string $name
   *   The human readable name.
   *
   * @return string
   *   The machine readable name.
   */
  public static function machineName($name) {
    $name = strtolower($name);
    return preg_replace(array('@[^a-z0-9_]+@', '@_+@'), '_', $name);
  }

  /**
   * Returns the URI with language of a concept.
   *
   * @param object $concept
   *   The object of a concept.
   *
   * @return string
   *   The uri with the language (e.g., http://a.concept.uri/1234@en).
   */
  protected function getUri($concept) {
    return $concept->uri . '@' . $concept->ppLang;
  }

  /**
   * Load taxonomy terms with their "parents"-property set.
   *
   * @param array $tids
   *   The term IDs of the terms to load.
   *
   * @return array
   *   An array of taxonomy term objects.
   */
  protected function loadTermsWithParents(array $tids) {
    $terms = taxonomy_term_load_multiple($tids);

    // Add the parents.
    if (!empty($terms)) {
      // Add the empty parents-property first.
      foreach (array_keys($terms) as $tid) {
        $terms[$tid]->parents = array();
      }

      // Load the parents.
      $parents = db_select('taxonomy_term_hierarchy', 't')
        ->condition('t.tid', array_keys($terms), 'IN')
        ->fields('t')
        ->execute();

      // Add parents to the terms.
      while($parent = $parents->fetchAssoc()) {
        $terms[$parent['tid']]->parents[] = $parent['parent'];
      }
    }

    return $terms;
  }
}
