<?php

/**
 * @file
 * Page callbacks.
 */

/**
 * Replace a node with another one.
 */
function archibald_import_replace_node_form($form, $form_state, $new_node, $old_node) {
  // Make sure both nodes are archibald_lomch_description nodes.
  if (
    $old_node->type !== 'archibald_lomch_description' ||
    $new_node->type !== 'archibald_lomch_description'
  ) {
    drupal_access_denied();
    return;
  }

  $form['#old_node'] = $old_node;
  $form['#new_node'] = $new_node;

  return confirm_form($form,
    t("Are you sure you want to replace !to_title (nid @to_nid) with the data from !from_title (nid @from_nid)?", array(
      '!from_title' => l($old_node->title, "node/{$old_node->nid}"),
      '!to_title' => l($new_node->title, "node/{$new_node->nid}"),
      '@from_nid' => $old_node->nid,
      '@to_nid' => $new_node->nid,
    )),
    'admin/archibald/description/unfinished-imports',
    t('This action cannot be undone.'),
    t('Replace values'),
    t('Cancel')
  );
}

/**
 * Submission callback for archibald_import_replace_node_form().
 */
function archibald_import_replace_node_form_submit($form, &$form_state) {
  // We simply give the "from" node the ID if the "to" node. Upon saving, this
  // will override all data in "to" with data from "from".
  $form['#old_node']->nid = $form['#new_node']->nid;
  $form['#old_node']->vid = $form['#new_node']->vid;
  $form['#old_node']->status = $form['#new_node']->status;
  // Activate Revisioning stuff.
  $form['#old_node']->revision_moderation = FALSE;
  $form['#old_node']->revision = TRUE;

  // Handle field collections. These pose several problems, as you cannot just
  // move a field collection between entities. We need to create a copy.
  foreach ($form['#old_node']->lomch_relations as $langcode => $items) {
    foreach ($items as $i => $item) {
      if ($entity = field_collection_field_get_entity($item)) {
        // Create a copy.
        $new_entity = clone $entity;
        $new_entity->item_id = NULL;
        $new_entity->revision_id = NULL;
        $new_entity->is_new = TRUE;
        $entity = $new_entity;

        $entity->setHostEntity(
          'node',
          $form['#old_node'],
          $langcode,
          FALSE
        );
        $entity->save(TRUE);

        $form['#old_node']->lomch_relations[$langcode][$i] = array(
          'value' => $entity->item_id,
          'revision_id' => $entity->revision_id,
        );
      }
    }
  }

  // Add a log message.
  $form['#old_node']->log = t("Overwrite with data from %title (node @nid)", array(
    '%title' => $form['#old_node']->title,
    '@nid' => $form['#old_node']->nid,
  ));

  // Save.
  node_save($form['#old_node']);

  drupal_set_message(t("Node !title was successfully overwritten.", array(
    '!title' => l($form['#new_node']->title, "node/{$form['#new_node']->nid}"),
  )));
  $form_state['redirect'] = "node/{$form['#new_node']->nid}";
}

/**
 * Import descriptions via ZIP upload.
 *
 * @see archibald_import_upload_zip_form_validate()
 * @see archibald_import_upload_zip_form_submit()
 */
function archibald_import_upload_zip_form($form, $form_state) {
  $form['#enctype'] = 'multipart/form-data';

  $form['zip'] = array(
    '#type' => 'file',
    '#title' => t("File upload"),
    '#description' => t("The ZIP archive must contain one or more descriptions in JSON format. Please read the !help page for more information", array(
      '!help' => l(t("help"), 'node'),
    )),
  );

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t("Upload"),
  );

  return $form;
}

/**
 * Validation callback for archibald_import_upload_zip_form().
 *
 * @see archibald_import_upload_zip_form()
 * @see archibald_import_upload_zip_form_submit()
 */
function archibald_import_upload_zip_form_validate($form, &$form_state) {
  $file = file_save_upload('zip', array(
    'file_validate_extensions' => array('zip'),
  ));

  if ($file) {
    if ($file = file_move($file, 'public://')) {
      $form_state['storage']['zip'] = $file;
    }
    else {
      form_set_error('zip', t("Failed to write the uploaded file to the site's file folder."));
    }
  }
  else {
    form_set_error('zip', t("No file was uploaded."));
  }
}

/**
 * Submit callback for dsb_portal_admin_settings_form().
 *
 * @see archibald_import_upload_zip_form()
 * @see archibald_import_upload_zip_form_validate()
 * @see archibald_import_upload_zip_batch_process()
 * @see archibald_import_upload_zip_batch_finished()
 */
function archibald_import_upload_zip_form_submit($form, $form_state) {
  batch_set(array(
    'operations' => array(
      array('archibald_import_upload_zip_batch_process', array($form_state['storage']['zip'])),
    ),
    'finished' => 'archibald_import_upload_zip_batch_finished',
    'file' => drupal_get_path('module', 'archibald_import') . '/includes/archibald_import.pages.inc',
  ));
}

/**
 * Batch import process.
 *
 * Batch processing callback for reading a ZIP archive and importing the data.
 *
 * @see archibald_import_upload_zip_form()
 * @see archibald_import_upload_zip_form_submit()
 * @see archibald_import_upload_zip_batch_finished()
 */
function archibald_import_upload_zip_batch_process($zip_file, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    // The unzipping can take quite some time. To prevent timeouts, we don't
    // start importing in the same iteration as the unzipping. Set a flag, so
    // we know we must skip the importing later on.
    $init_run = TRUE;

    // First, open the ZIP archive, and list its contents.
    $wrapper = file_stream_wrapper_get_instance_by_uri($zip_file->uri);
    $zip = new ZipArchive();
    $error = $zip->open($wrapper->realpath());

    // We opened it successfully. List all files, so we can filter out the ones
    // we want. For image files, we store them directly into Drupal's file
    // system (as temporary files).
    if ($error === TRUE) {
      $context['sandbox']['files'] = array(
        'json' => array(),
        'images' => array(),
      );

      for ($i = 0; $i < $zip->numFiles; $i++) {
        $filename = $zip->getNameIndex($i);
        if (
          preg_match('/\.json$/i', $filename) &&
          !preg_match('/_(meta|revisions)\.json$/i', $filename)
        ) {
          $context['sandbox']['files']['json'][$i] = $filename;
        }
        elseif (preg_match('/\.(jpe?g|png|gif|svg)$/i', $filename)) {
          // Unzip it and copy it to the public file system.
          $file = file_save_data(
            $zip->getFromName($filename),
            file_default_scheme() . '://' . $filename
          );

          // By default, Drupal helpfully turns these files into permanent ones.
          // But we don't want that. If the import doesn't use a file, we want
          // to make sure Drupal can garbage collect it, so we don't pollute the
          // file system. Set it as a temporary file, and save it again.
          $file->status = 0;
          file_save($file);

          $context['sandbox']['files']['images']["zip://{$filename}"] = $file->fid;
        }
      }
      $zip->close();

      $context['sandbox']['progress'] = 0;
      $context['sandbox']['max'] = count($context['sandbox']['files']['json']);
    }
    else {
      // Something went wrong. Put up an error message.
      switch ($error) {
        case ZipArchive::ER_INCONS:
          $error_value = t("ZIP archive is inconsistent");
          break;
        case ZipArchive::ER_INVAL:
          $error_value = t("Path to ZIP archive is invalid");
          break;
        case ZipArchive::ER_MEMORY:
          $error_value = t("Memory failure");
          break;
        case ZipArchive::ER_NOENT:
          $error_value = t("ZIP archive doesn't exist");
          break;
        case ZipArchive::ER_NOZIP:
          $error_value = t("File exists, but is not a ZIP archive");
          break;
        case ZipArchive::ER_OPEN:
          $error_value = t("File exists, but could not be opened");
          break;
        case ZipArchive::ER_READ:
          $error_value = t("File exists, but could not be read");
          break;
        case ZipArchive::ER_SEEK:
          $error_value = t("Seek error");
          break;
        default:
          $error_value = t("Unknown error");
          break;
      }

      watchdog('archibald_import', "Could not import ZIP archive. Error: !error", array(
        '!error' => $error_value,
      ), WATCHDOG_ERROR);

      drupal_set_message(t("Could not import the ZIP archive! Check site logs for more information, or contact your site administrator."), 'error');

      $context['sandbox']['finished'] = 1;
      $context['success'] = FALSE;
      return;
    }
  }

  if (empty($init_run)) {
    $wrapper = file_stream_wrapper_get_instance_by_uri($zip_file->uri);
    $zip = new ZipArchive();
    $zip->open($wrapper->realpath());

    $limit = 2;
    foreach ($context['sandbox']['files']['json'] as $index => $filename) {
      $json = $zip->getFromName($filename);

      // Try to read any metadata. These files are normally suffixed with
      // "_meta.json". However, we also support "_META.json". Try both.
      $meta_json = $zip->getFromName(preg_replace('/(\.json)$/i', '_meta$1', $filename));
      if (!$meta_json) {
        $meta_json = $zip->getFromName(preg_replace('/(\.json)$/i', '_META$1', $filename));
      }

      // Try to read any revision data. These files are normally suffixed with
      // "_revisions.json". However, we also support "_REVISIONS.json". Try
      // both.
      $revision_json = $zip->getFromName(preg_replace('/(\.json)$/i', '_revisions$1', $filename));
      if (!$revision_json) {
        $revision_json = $zip->getFromName(preg_replace('/(\.json)$/i', '_REVISIONS$1', $filename));
      }

      // Launch the import.
      if ($node = archibald_import_from_json(
        $json,
        $meta_json,
        $revision_json,
        $context['sandbox']['files']['images']
      )) {
        // Store the result.
        $context['results'][$filename] = $node;

        $context['sandbox']['progress']++;
        $context['message'] = t('Now processing %filename', array('%filename' => $filename));

        // Remove the file from the context.
        unset($context['sandbox']['files']['json'][$index]);
      }
      else {
        $context['sandbox']['finished'] = 1;
        $context['success'] = FALSE;
        return;
      }

      if (!$limit) {
        break;
      }
      $limit--;
    }
    $zip->close();
  }

  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch import finish.
 *
 * @see archibald_import_upload_zip_form()
 * @see archibald_import_upload_zip_form_submit()
 * @see archibald_import_upload_zip_batch_process()
 */
function archibald_import_upload_zip_batch_finished($success, $results, $operations) {
  if ($success) {
    // Here we do something meaningful with the results.
    $message = t("!count items were imported.", array(
      '!count' => count($results),
    ));
    $message .= theme('item_list', array('items' => array_map(function($node) {
      return l($node->title, "node/{$node->nid}");
    }, $results)));
    drupal_set_message($message);
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = t('An error occurred while processing %error_operation with arguments: @arguments', array(
      '%error_operation' => $error_operation[0],
      '@arguments' => print_r($error_operation[1], TRUE)
    ));
    drupal_set_message($message, 'error');
  }
}

/**
 * Import descriptions directly from the dsb API.
 */
function archibald_import_from_catalog_form($form, $form_state, $partner_id = NULL) {
  $form['#attached']['css'][] = drupal_get_path('module', 'archibald_import') . '/css/archibald_import.css';

  $form['partner'] = array(
    '#type' => 'hidden',
    // If there's no partner ID, use the default.
    '#value' => $partner_id ? $partner_id : archibald_load_default_partner()->id,
  );

  // Drupal will (helpfully) keep the GET parameters intact upon form
  // submissions. But, the pager API will override any parameters we
  // pass it with the ones it finds in the URL. So, if the user starts using the
  // pager, she can no longer overwrite her own filter values. In order to
  // force Drupal to remove the GET parameters on new form submissions, we force
  // the action to point our page. This will clear all GET params.
  $form['#action'] = url('admin/archibald/description/import/catalog/' . $form['partner']['#value']);
  if (empty($form_state['values']['search']) && isset($_GET['search'])) {
    $form_state['values']['search'] = $_GET['search'];
  }
  if (!isset($form_state['values']['show_local']) && isset($_GET['show_local'])) {
    $form_state['values']['show_local'] = $_GET['show_local'];
  }

  // Cache the full partner entity as well.
  $form['#partner'] = archibald_partner_load($form['partner']['#value']);

  // Pager data. By setting it as a form properties, other modules can influence
  // this, if need be.
  $form['#limit'] = 20;
  $form['page'] = array(
    '#type' => 'hidden',
    // We have to use "input" here, as "values" are not always available when
    // the form is rebuilt.
    '#value' => isset($form_state['input']['page']) ?
      $form_state['input']['page'] :
      pager_find_page(),
  );
  $form['#offset'] = $form['page']['#value'] * $form['#limit'];

  // Prepare the filter fieldset.
  $form['filter'] = array(
    '#type' => 'fieldset',
  );
  $form['filter']['search'] = array(
    '#type' => 'textfield',
    '#title' => t("Search"),
    '#default_value' => isset($form_state['values']['search']) ?
      $form_state['values']['search'] :
      '',
  );
  $form['filter']['show_local'] = array(
    '#type' => 'checkbox',
    '#title' => t("Show descriptions that exist locally"),
    '#default_value' => isset($form_state['values']['show_local']) ?
      $form_state['values']['show_local'] :
      0,
  );
  $form['filter']['refresh'] = array(
    '#type' => 'button',
    '#value' => t("Refresh"),
    '#name' => 'refresh',
  );

  // Fetch the descriptions.
  if (empty($form['#results'])) {
    // If the refresh button was hit, the results are in the form_state.
    if (!empty($form_state['storage'])) {
      $form['#results'] = $form_state['storage'];
    }
    else {
      // Load them now.
      try {
        $form['#results'] = _archibald_import_from_catalog_form_fetch_available_description($form, $form_state);
      }
      catch(Exception $e) {
        drupal_set_message(t("An error occurred. Please contact a site administrator for more information."), 'error');
        watchdog_exception('archibald_import', $e);
      }
    }
  }

  if (!empty($form['#results'])) {
    if (empty($form['#results']['numFound'])) {
      drupal_set_message(t("There were no results found to display."), 'warning');
    }
    else {
      pager_default_initialize($form['#results']['numFound'], $form['#limit']);

      $result_options = array();
      foreach ($form['#results']['result'] as $result) {
        $lom = new \Educa\DSB\Client\Lom\LomDescriptionSearchResult($result);

        $image = $lom->getPreviewImage();
        if (!empty($image)) {
          $image = '<img class="archibald-import-from-catalog-table__preview-image" src="' . $image . '" />';
        }

        $existing_node = archibald_lomch_description_load_by_lom_id($lom->getLomId());

        if (!empty($existing_node) && empty($form_state['values']['show_local'])) {
          // Skip this one.
          // @todo
          continue;
        }

        $doc_id = $lom->getLomId();

        // Multiple languages count as separate results.
        if (!isset($result_options[$doc_id])) {
          $result_options[$doc_id] = array(
            'title' => check_plain($lom->getTitle()),
            'lom_id' => check_plain($lom->getLomId()),
            'teaser' => check_plain($lom->getTeaser()),
          );

          // If we're not showing local version, there's no need to show conflict
          // information.
          if (!empty($form_state['values']['show_local'])) {
            $result_options[$doc_id]['exists'] = isset($existing_node->nid) ? t("Yes") : t("No");
            $result_options[$doc_id]['existing_node'] = isset($existing_node->nid) ?
              l($existing_node->title, "node/{$existing_node->nid}") :
              '-';
          }
        }

        $result_options[$doc_id]['languages'][] = $lom->getField('language');
      }

      // Transform the language list to a plain text list, but flag languages
      // that are missing.
      $active_languages = array_keys(language_list());
      $result_options = array_map(function($item) use($active_languages) {
        $list = array();
        foreach ($item['languages'] as $langcode) {
          $title = t("Language @langcode is available on your system", array(
            '@langcode' => $langcode,
          ));
          $class = 'archibald-import-from-catalog-form__language';

          if (!in_array($langcode, $active_languages)) {
            $class .= ' archibald-import-from-catalog-form__language--unavailable';
            $title = t("Language @langcode is not available on your system", array(
              '@langcode' => $langcode,
            ));
          }

          $list[] = '<span class="' . $class . '" title="' . $title . '">' . check_plain($langcode) . '</span>';
        }
        $item['languages'] = implode(', ', $list);
        return $item;
      }, $result_options);

      $header = array(
        'title' => t("Title", array(), array('context' => 'archibald_import')),
        'lom_id' => t("ID", array(), array('context' => 'archibald_import')),
        'languages' => t("Languages", array(), array('context' => 'archibald_import')),
        'teaser' => t("Teaser", array(), array('context' => 'archibald_import')),
      );

      // If we're not showing local version, there's no need to show conflict
      // information.
      if (!empty($form_state['values']['show_local'])) {
        $header['exists'] = t("Already exists locally", array(), array('context' => 'archibald_import'));
        $header['existing_node'] = t("Local description", array(), array('context' => 'archibald_import'));
      }

      $form['results'] = array(
        '#type' => 'tableselect',
        '#header' => $header,
        '#options' => $result_options,
        '#empty' => t('No content available.'),
      );

      $form['pager'] = array(
        '#markup' => theme('pager', array(
          'parameters' => array(
            'search' => isset($form_state['values']['search']) ?
              $form_state['values']['search'] :
              '',
            'show_local' => isset($form_state['values']['show_local']) ?
              $form_state['values']['show_local'] :
              0,
          ),
        )),
      );

      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t("Import selection"),
        '#name' => 'submit',
      );
    }
  }

  return $form;
}

/**
 * Validation callback for archibald_import_from_catalog_form().
 */
function archibald_import_from_catalog_form_validate($form, &$form_state) {
  if ($form_state['triggering_element']['#name'] == 'refresh') {
    try {
      $form_state['storage'] = _archibald_import_from_catalog_form_fetch_available_description($form, $form_state);
      $form_state['rebuild'] = TRUE;
    }
    catch (Exception $e) {
      form_set_error('filter', t("Couldn't load descriptions. Error: @e", array(
        '@e' => $e->getMessage(),
      )));
    }
  }
  else {
    if (!empty($form_state['values']['results'])) {
      $form_state['values']['results'] = array_filter($form_state['values']['results']);
    }

    if (empty($form_state['values']['results'])) {
      form_set_error('results', t("Please select one or more descriptions to import"));
    }
  }
}

/**
 * Submission callback for archibald_import_from_catalog_form().
 */
function archibald_import_from_catalog_form_submit($form, $form_state) {
  batch_set(array(
    'operations' => array(
      array('archibald_import_from_catalog_batch_process', array(
        $form_state['values']['partner'],
        array_values(array_filter($form_state['values']['results'])),
      )),
    ),
    'finished' => 'archibald_import_from_catalog_batch_finished',
    'file' => drupal_get_path('module', 'archibald_import') . '/includes/archibald_import.pages.inc',
  ));
}

/**
 * Helper function to load available descriptions for import.
 *
 * @see
 *
 * @throws Exception
 *    Throws exceptions when authentication fails, or the search request is not
 *    valid. Refer to the dsb Client Library documentation for more information.
 *
 * @param array $form
 * @param array $form_state
 *
 * @return array
 *    A search result, as returned by the dsb Client Library. Refer to its
 *    documentation for more information.
 */
function _archibald_import_from_catalog_form_fetch_available_description($form, $form_state) {
  $client = archibald_get_client_based_on_config($form['#partner']->id);
  return $client->search(
    !empty($form_state['values']['search']) ? $form_state['values']['search'] : NULL,
    array(),
    array(
      // @todo Filter out lom IDs that exist locally! Doing it client-side
      // messes up the pager!!
      'ownerUsername' => array(
        $form['#partner']->partner_username[LANGUAGE_NONE][0]['value'],
      ),
      'catalogs' => array('national', 'own'),
    ),
    array('language'),
    $form['#offset'],
    $form['#limit'],
    'relevance'
  );
}

/**
 * Batch import process.
 *
 * Batch processing callback for importing a description directly from the
 * dsb API.
 *
 * @see archibald_import_from_catalog_form()
 * @see archibald_import_from_catalog_form_submit()
 * @see archibald_import_upload_zip_batch_finished()
 */
function archibald_import_from_catalog_batch_process($partner_id, $lom_ids, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['partner'] = archibald_partner_load($partner_id);
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($lom_ids);
  }

  try {
    $client = archibald_get_client_based_on_config($partner_id);

    $limit = 10;
    while($limit) {
      if (!isset($lom_ids[$context['sandbox']['progress']])) {
        // Finished.
        break;
      }

      $lom_id = $lom_ids[$context['sandbox']['progress']];
      $json = $client->loadDescription($lom_id);

      // Launch the import.
      if ($node = archibald_import_from_json(
        // We need to encode it to JSON again, as the import function expects
        // JSON strings.
        json_encode($json),
        json_encode(array(
          'lomId' => $lom_id,
          'partner' => $context['sandbox']['partner']->partner_username[LANGUAGE_NONE][0]['value'],
          'catalog' => 'own',
        ))
      )) {
        // Store the result.
        $context['results'][] = $node;

        $context['sandbox']['progress']++;

        $limit--;
      }
      else {
        $context['sandbox']['finished'] = 1;
        $context['success'] = FALSE;
        return;
      }
    }
  }
  catch(Exception $e) {
    watchdog_exception('archibald_import', $e);
    drupal_set_message(t("Could not import from the catalog! Check site logs for more information, or contact your site administrator."), 'error');

    $context['sandbox']['finished'] = 1;
    $context['success'] = FALSE;
    return;
  }

  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch import finish.
 *
 * @see archibald_import_from_catalog_form()
 * @see archibald_import_from_catalog_form_submit()
 * @see archibald_import_from_catalog_batch_process()
 */
function archibald_import_from_catalog_batch_finished($success, $results, $operations) {
  if ($success) {
    // Here we do something meaningful with the results.
    $message = t("!count items were imported.", array(
      '!count' => count($results),
    ));
    $message .= theme('item_list', array('items' => array_map(function($node) {
      return l($node->title, "node/{$node->nid}");
    }, $results)));
    drupal_set_message($message);
  }
  else {
    // An error occurred.
    // $operations contains the operations that remained unprocessed.
    $error_operation = reset($operations);
    $message = t('An error occurred while processing %error_operation with arguments: @arguments', array(
      '%error_operation' => $error_operation[0],
      '@arguments' => print_r($error_operation[1], TRUE)
    ));
    drupal_set_message($message, 'error');
  }
}