<?php

/**
 * @file
 *
 * This file contains all admin and callback functions for solution pack
 * management.
 */

/**
 * Solution pack admin page callback.
 *
 * @return string
 *   The html repersentation of all solution pack forms for required objects.
 */
function islandora_solution_packs_admin() {
  module_load_include('inc', 'islandora', 'includes/utilities');
  if (!islandora_describe_repository()) {
    islandora_display_repository_inaccessible_message();
    return '';
  }

  $connection = islandora_get_tuque_connection();
  drupal_add_css(drupal_get_path('module', 'islandora') . '/css/islandora.admin.css');
  $output = '';
  $enabled_solution_packs = module_invoke_all('islandora_required_objects', $connection);
  foreach ($enabled_solution_packs as $solution_pack_module => $solution_pack_info) {
    // @todo We should probably get the title of the solution pack from the
    // systems table for consistency in the interface.
    $solution_pack_name = $solution_pack_info['title'];
    $objects = array_filter($solution_pack_info['objects']);
    $form = drupal_get_form('islandora_solution_pack_form_' . $solution_pack_module, $solution_pack_module, $solution_pack_name, $objects);
    $output .= drupal_render($form);
  }
  return $output;
}

/**
 * A solution pack form for the given module, it lists all the given objects and
 * their status, allowing the user to re-ingest them.
 *
 * @param array $form
 *   The Drupal form definition.
 * @param array $form_state
 *   The Drupal form state.
 * @param string $solution_pack_module
 *   The module which requires the given objects.
 * @param string $solution_pack_name
 *   The name of the solution pack to display to the users.
 * @param array $objects
 *   An array of NewFedoraObjects which describe the state in which objects
 *   should exist.
 *
 * @return array
 *   The Drupal form definition.
 */
function islandora_solution_pack_form(array $form, array &$form_state,  $solution_pack_module, $solution_pack_name, $objects = array()) {
  // The order is important in terms of severity of the status, where higher
  // index indicates the status is more serious, this will be used to determine
  // what messages get displayed to the user.
  $ok_image = theme_image(array('path' => 'misc/watchdog-ok.png', 'attributes' => array()));
  $warning_image = theme_image(array('path' => 'misc/watchdog-warning.png', 'attributes' => array()));
  $status_info = array(
    'up_to_date' => array(
      'solution_pack' => t('All required objects are installed and up-to-date.'),
      'image' => $ok_image,
      'button' => t("Force reinstall objects"),
    ),
    'modified_datastream' => array(
      'solution_pack' => t('Some objects must be reinstalled. See objects list for details.'),
      'image' => $warning_image,
      'button' => t("Reinstall objects")
    ),
    'out_of_date' => array(
      'solution_pack' => t('Some objects must be reinstalled. See objects list for details.'),
      'image' => $warning_image,
      'button' => t("Reinstall objects")
    ),
    'missing_datastream' => array(
      'solution_pack' => t('Some objects must be reinstalled. See objects list for details.'),
      'image' => $warning_image,
      'button' => t("Reinstall objects")
    ),
    'missing' => array(
      'solution_pack' => t( 'Some objects are missing and must be installed. See objects list for details.'),
      'image' => $warning_image,
      'button' => t("Install objects")
    ),
  );
  $status_severities = array_keys($status_info);
  $solution_pack_status_severity = array_search('up_to_date', $status_severities);
  $table_rows = array();
  foreach ($objects as $object) {
    $object_status = islandora_check_object_status($object);
    $object_status_info = $status_info[$object_status['status']];
    $object_status_severity = array_search($object_status['status'], $status_severities);
    // The solution pack status severity will be the highest severity of the objects.
    $solution_pack_status_severity = max($solution_pack_status_severity, $object_status_severity);
    $exists = $object_status['status'] != 'missing';
    $label = $exists ? l($object->label, "islandora/object/{$object->id}") : $object->label;
    $status_msg = "{$object_status_info['image']}&nbsp{$object_status['status_friendly']}";
    $table_rows[] = array($label, $object->id, $status_msg);
  }
  $solution_pack_status = $status_severities[$solution_pack_status_severity];
  $solution_pack_status_info = $status_info[$solution_pack_status];
  return array(
    'solution_pack' => array(
      '#type' => 'fieldset',
      '#collapsible' => FALSE,
      '#collapsed' => FALSE,
      '#attributes' => array('class' => array('islandora-solution-pack-fieldset')),
      'solution_pack_module' => array(
        '#type' => 'value',
        '#value' => $solution_pack_module,
      ),
      'solution_pack_name' => array(
        '#type' => 'value',
        '#value' => $solution_pack_name,
      ),
      'objects' => array(
        '#type' => 'value',
        '#value' => $objects,
      ),
      'solution_pack_label' => array(
        '#markup' => $solution_pack_name,
        '#prefix' => '<h3>',
        '#suffix' => '</h3>',
      ),
      'install_status' => array(
        '#markup' => t('<strong>Object status:</strong> !image !status', array(
                     '!image' => $solution_pack_status_info['image'],
                     '!status' => $solution_pack_status_info['solution_pack']
                   )),
        '#prefix' => '<div class="islandora-solution-pack-install-status">',
        '#suffix' => '</div>',
      ),
      'table' => array(
        '#type' => 'item',
        '#markup' => theme('table', array('header' => array(t('Label'), t('PID'), t('Status')), 'rows' => $table_rows))
      ),
      'submit' => array(
        '#type' => 'submit',
        '#name' => $solution_pack_module,
        '#value' => $solution_pack_status_info['button'],
        '#attributes' => array('class' => array('islandora-solution-pack-submit')),
      )
    )
  );
}

/**
 * Submit handler for solution pack form.
 *
 * @param array $form
 *   The form submitted.
 * @param array $form_state
 *   The state of the form submited.
 */
function islandora_solution_pack_form_submit(array $form, array &$form_state) {
  $solution_pack_module = $form_state['values']['solution_pack_module'];
  $objects = $form_state['values']['objects'];
  $batch = array(
    'title' => t('Installing / Updating solution pack objects'),
    'file' => drupal_get_path('module', 'islandora') . '/includes/solution_packs.inc',
    'operations' => array(),
  );
  foreach ($objects as $object) {
    $batch['operations'][] = array('islandora_solution_pack_batch_operation_reingest_object', array($object));
  }
  batch_set($batch);
  // Hook to let solution pack objects be modified.
  // Not using module_invoke so solution packs can be expanded by other modules.
  // @todo shouldn't we send the object list along as well?
  module_invoke_all('islandora_postprocess_solution_pack', $solution_pack_module);
}

/**
 * Batch operation used by the solution pack forms to ingest/reingest required
 *  object(s)
 *
 * @param NewFedoraObject $object
 *   The object to ingest/reingest.
 * @param array $context
 *   The context of this batch operation.
 */
function islandora_solution_pack_batch_operation_reingest_object(NewFedoraObject $object, array &$context) {
  $deleted = FALSE;
  $existing_object = islandora_object_load($object->id);
  if ($existing_object) {
    $deleted = islandora_delete_object($existing_object);
    $purged = $deleted && $existing_object == NULL;
    if (!$purged) {
      $object_link = l($existing_object->label, "islandora/object/{$existing_object->id}");
      drupal_set_message(t('Failed to purge existing object !object_link.', array(
            '!object_link' => $object_link,
          )), 'error');
      // Failed to purge don't attempt to ingest.
      return;
    }
  }
  // Object was deleted or did not exist.
  $pid = $object->id;
  $label = $object->label;
  $action = $deleted ? 'reinstalled' : 'installed';
  $object_link = l($label, "islandora/object/{$pid}");
  $object = islandora_add_object($object);
  $msg = $object ? "Successfully $action !object_link." : "Failed to $action @label identified by @pid.";
  $status = $object ? 'status' : 'error';
  drupal_set_message(t($msg, array(
        '@pid' => $pid,
        '@label' => $label,
        '!object_link' => $object_link
      )), $status);
}

/**
 * This is to be called from the solution pack's hook_install()
 * and hook_uninstall() functions. It provides a convient way to have a
 * solution pack's required objects ingested at install time.
 *
 * @param string $module_name
 *   The name of the module that is calling this function in its
 *   install/unistall hooks.
 * @param string $op
 *   The operation to perform, either install or uninstall.
 *
 * @todo Implement hook_modules_installed/hook_modules_uninstalled instead of
 *   calling this function directly.
 * @todo Remove the second parameter and have two seperate functions.
 */
function islandora_install_solution_pack($module, $op = 'install') {
  if ($op == 'uninstall') {
    islandora_uninstall_solution_pack($module);
    return;
  }
  module_load_include('module', 'islandora', 'islandora');
  module_load_include('inc', 'islandora', 'includes/utilities');
  module_load_include('module', $module, $module);
  $info_file = drupal_get_path('module', $module) . "/{$module}.info";
  $info_array = drupal_parse_info_file($info_file);
  $module_name = $info_array['name'];
  $admin_link = l(t('Solution Pack admin'), 'admin/islandora/solution_packs');
  $config_link = l(t('Islandora configuration'), 'admin/islandora/configure');
  if (!islandora_describe_repository()) {
    $msg = '@module: Did not install any objects. Could not connect to the ';
    $msg .= 'repository. Please check the settings on the !config_link page ';
    $msg .= 'and install the required objects manually on the !admin_link page.';
    drupal_set_message(st($msg, array(
          '@module' => $module_name,
          '!config_link' => $config_link,
          '@admin_link' => $admin_link
        )), 'error');
    return;
  }
  $connection = islandora_get_tuque_connection();
  $required_objects = module_invoke($module, 'islandora_required_objects', $connection);
  $objects = $required_objects[$module]['objects'];
  $status_messages = array(
    'up_to_date' => 'The object already exists and is up-to-date',
    'missing_datastream' => 'The object already exists but is missing a datastream. Please reinstall the object on the !admin_link page',
    'out_of_date' => 'The object already exists but is out-of-date. Please update the object on the !admin_link page',
    'modified_datastream' => 'The object already exists but datastreams are modified. Please reinstall the object on the !admin_link page',
  );
  foreach ($objects as $object) {
    $query = $connection->api->a->findObjects('query', 'pid=' . $object->id);
    $already_exists = !empty($query['results']);
    $label =  $object->label;
    $object_link = l($label, "islandora/object/{$object->id}");
    if ($already_exists) {
      $object_status = islandora_check_object_status($object);
      $status_msg = $status_messages[$object_status['status']];
      drupal_set_message(st("@module: Did not install !object_link. $status_msg.", array(
            '@module' => $module_name,
            '!object_link' => $object_link,
            '!admin_link' => $admin_link,
          )), 'warning');
    }
    else {
      $object = islandora_add_object($object);
      if ($object) {
        drupal_set_message(t('@module: Successfully installed. !object_link.', array(
            '@module' => $module_name,
            '!object_link' => $object_link,
          )), 'status');
      }
      else {
        drupal_set_message(t('@module: Failed to install. @label.', array(
              '@module' => $module_name,
              '@label' => $label,
            )), 'warning');
      }
    }
  }
}

/**
 * Uninstalls the given solution pack.
 *
 * @param string $module
 *   The solution pack to uninstall.
 *
 * @todo Implement hook_modules_uninstalled instead of calling this function
 *    directly for each solution pack.
 */
function islandora_uninstall_solution_pack($module) {
  module_load_include('module', 'islandora', 'islandora');
  module_load_include('inc', 'islandora', 'includes/utilities');
  module_load_include('module', $module, $module);
  $config_link = l(t('Islandora configuration'), 'admin/islandora/configure');
  $info_file = drupal_get_path('module', $module) . "/{$module}.info";
  $info_array = drupal_parse_info_file($info_file);
  $module_name = $info_array['name'];
  if (!islandora_describe_repository()) {
    $msg = '@module: Did not uninstall any objects. Could not connect to the ';
    $msg .= 'repository. Please check the settings on the !config_link page ';
    $msg .=  'and uninstall the required objects manually if necessary.';
    drupal_set_message(st($msg, array(
          '@module' => $module_name,
          '!config_link' => $config_link
        )), 'error');
    return;
  }
  $connection = islandora_get_tuque_connection();
  $required_objects = module_invoke($module, 'islandora_required_objects', $connection);
  $objects = $required_objects[$module]['objects'];
  $existing_objects = array_filter($objects, function($o) use($connection) {
                        $param = "pid={$o->id}";
                        $query = $connection->api->a->findObjects('query', $param);
                        return !empty($query['results']);
                      }
  );
  foreach ($existing_objects as $object) {
    $msg = '@module: Did not remove !object_link. It may be used by other sites.';
    $object_link = l($object->label, "islandora/object/{$object->id}");
    drupal_set_message(st($msg, array(
          '!object_link' => $object_link,
          '@module' => $module_name
        )), 'warning');
  }
}

/**
 * Function to check the status of an object against an object model array.
 *
 * @param NewFedoraObject $object_definition
 *   A new fedora object that defines what the object should contain.
 *
 * @return string
 *   Returns one of the following values:
 *   up_to_date, missing, missing_datastream or out_of_date
 *   You can perform an appropriate action based on this value.
 *
 * @see islandora_solution_pack_form()
 * @see islandora_install_solution_pack()
 */
function islandora_check_object_status(NewFedoraObject $object_definition) {
  $existing_object = islandora_object_load($object_definition->id);
  if (!$existing_object) {
    return array('status' => 'missing', 'status_friendly' => t('Missing'));
  }

  $existing_datastreams = array_keys(iterator_to_array($existing_object));
  $expected_datastreams = array_keys(iterator_to_array($object_definition));
  $datastream_diff = array_diff($expected_datastreams, $existing_datastreams);
  if (!empty($datastream_diff)) {
    $status_friendly = format_plural(count($datastream_diff), 'Missing Datastream: %dsids.', 'Missing Datastreams: %dsids.', array('%dsids' => implode(', ', $datastream_diff)));
    return array('status' => 'missing_datastream', 'status_friendly' => $status_friendly, 'data' => $datastream_diff);
  }

  $is_xml_datastream = function($ds) { return $ds->mimetype == 'text/xml'; };
  $xml_datastreams = array_filter(iterator_to_array($object_definition), $is_xml_datastream);
  $out_of_date_datastreams = array();
  foreach ($xml_datastreams as $ds) {
    $installed_version = islandora_get_islandora_datastream_version($existing_object, $ds->id);
    $available_version = islandora_get_islandora_datastream_version($object_definition, $ds->id);
    if ($available_version > $installed_version) {
      $out_of_date_datastreams[] = $ds->id;
    }
  }

  if (count($out_of_date_datastreams)) {
    $status_friendly = format_plural(count($out_of_date_datastreams), 'Datastream out of date: %dsids.', 'Datastreams out of date: %dsids.', array('%dsids' => implode(', ', $out_of_date_datastreams)));
    return array('status' => 'out_of_date', 'status_friendly' => $status_friendly, 'data' => $out_of_date_datastreams);
  }

  // This is a pretty heavy function, but I'm not sure a better way. If we have
  // performance trouble, we should maybe remove this.
  $modified_datastreams = array();
  foreach ($object_definition as $ds) {
    if ($ds->mimetype == 'text/xml'
      || $ds->mimetype == 'application/rdf+xml'
      || $ds->mimetype == 'application/xml') {
      // If the datastream is XML we use the domdocument C14N cannonicalization
      // function to test if they are equal, because the strings likely won't
      // be equal as Fedora does some XML mangling. In order for C14N to work
      // we need to replace the info:fedora namespace, as C14N hates it.
      // C14N also doesn't normalize whitespace at the end of lines and Fedora
      // may add some whitespace on some lines.
      $object_definition_dom = new DOMDocument();
      $object_definition_dom->preserveWhiteSpace = FALSE;
      $object_definition_dom->loadXML(str_replace('info:', 'http://', $ds->content));
      $object_actual_dom = new DOMDocument();
      $object_actual_dom->preserveWhiteSpace = FALSE;
      $object_actual_dom->loadXML(str_replace('info:', 'http://', $existing_object[$ds->id]->content));

      // Fedora changes the xml structure so we need to cannonize it.
      if ($object_actual_dom->C14N() != $object_definition_dom->C14N()) {
        $modified_datastreams[] = $ds->id;
      }
    }
    else {
      $object_definition_hash = md5($ds->content);
      $object_actual_hash = md5($existing_object[$ds->id]->content);
      if ($object_definition_hash != $object_actual_hash) {
        $modified_datastreams[] = $ds->id;;
      }
    }
  }
  if (count($modified_datastreams)) {
    $status_friendly = format_plural(count($modified_datastreams), 'Modified Datastream: %dsids.', 'Modified Datastreams: %dsids.', array('%dsids' => implode(', ', $modified_datastreams)));
    return array('status' => 'modified_datastream', 'data' => $modified_datastreams, 'status_friendly' => $status_friendly);
  }


  // If not anything else we can assume its up to date.
  return array('status' => 'up_to_date', 'status_friendly' => t('Up-to-date'));
}

/**
 * @defgroup viewer-functions
 * @{
 * Helper functions to include viewers for solution packs.
 */

/**
 * A form construct to create a viewer selection table.
 *
 * The list of selectable viewers is limited by the $mimetype and the $model
 * parameters. When neither are given all defined viewers are listed. If only
 * $mimetype is given only viewers that support that mimetype will be listed,
 * the same goes for the $model parameter. If both are given, than any viewer
 * that supports either the give $mimetype or $model will be listed.
 *
 * @param string $variable_id
 *   The ID of the Drupal variable to save the viewer settings in
 * @param string $mimetype
 *   The table will be populated with viewers supporting this mimetype
 * @param string $model
 *   The table will be populated with viewers supporting this content model
 *
 * @return
 *   A form api array which defines a themed table to select a viewer.
 */
function islandora_viewers_form($variable_id = NULL, $mimetype = NULL, $model = NULL) {
  $form = array();
  // get viewers
  $viewers = islandora_get_viewers($mimetype, $model);
  if (!empty($viewers)) {
    // add option for no viewer
    $no_viewer = array();
    $no_viewer['none'] = array(
      'label' => t('None'),
      'description' => t('Don\'t use a viewer for this solution pack.'),
    );
    // merge to viewers array
    $viewers = array_merge_recursive($no_viewer, $viewers);
  }
  // get viewer settings
  $viewers_config = variable_get($variable_id, array());
  // viewer
  $form['viewers'] = array(
    '#type' => 'fieldset',
    '#title' => t('Viewers'),
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
  );

  if (!empty($viewers)) {
    // viewers table
    $form['viewers'][$variable_id] = array(
      '#type' => 'item',
      '#title' => t('Select a viewer'),
      '#description' => t('Preferred viewer for your solution pack. These may be provided by third-party modules.'),
      // This attribute is important to return the submitted values in a deeper
      // nested arrays in
      '#tree' => TRUE,
      '#theme' => 'islandora_viewers_table',
    );

    // table loop
    foreach ($viewers as $name => $profile) {

      $options[$name] = '';
        // machine name
      $form['viewers'][$variable_id]['name'][$name] = array(
        '#type' => 'hidden',
        '#value' => $name,
      );
      // label
      $form['viewers'][$variable_id]['label'][$name] = array(
        '#type' => 'item',
        '#markup' => $profile['label'],
      );
      // description
      $form['viewers'][$variable_id]['description'][$name] = array(
        '#type' => 'item',
        '#markup' => $profile['description'],
      );
      // configuration url
      $form['viewers'][$variable_id]['configuration'][$name] = array(
        '#type' => 'item',
        '#markup' => (isset($profile['configuration']) AND $profile['configuration'] != '') ? l(t('configure'), $profile['configuration']) : '',
      );
    }
    // radios
    $form['viewers'][$variable_id]['default'] = array(
      '#type' => 'radios',
      '#options' => isset($options) ? $options : array(),
      '#default_value' => !empty($viewers_config) ? $viewers_config['default'] : 'none',
    );
  }
  else {
    // No viewers found
    $form['viewers'][$variable_id . '_no_viewers'] = array(
      '#markup' => t('No viewers detected.'),
    );
    // remove viewers variable
    variable_del($variable_id);
  }
  return $form;
}

/**
 * Returns all defined viewers.
 *
 * The list of selectable viewers is limited by the $mimetype and the
 * $content_model parameters. When neither are given all defined viewers are
 * listed. If only $mimetype is given only viewers that support that mimetype
 * will be listed, the same goes for the $content_model parameter. If both are
 * given, than any viewer that supports either the give $mimetype or $model will
 * be listed.
 *
 * @param string $mimetype
 *  Specify a mimetype to return only viewers that support this certain
 *  mimetype.
 * @param string $content_model
 *  Specify a content model to return only viewers that support the content
 *  model.
 *
 * @return array
 *  Viewer definitions, or FALSE if none are found.
 */
function islandora_get_viewers($mimetype = NULL, $content_model = NULL) {
  $viewers = array();
  // get all viewers
  $defined_viewers = module_invoke_all('islandora_viewer_info');
  // filter viewers by mimetype
  foreach ($defined_viewers as $key => $value) {
    $value['mimetype'] = isset($value['mimetype']) ? $value['mimetype'] : array();
    $value['model'] = isset($value['model']) ? $value['model'] : array();
    if (in_array($mimetype, $value['mimetype']) OR in_array($content_model, $value['model'])) {
      $viewers[$key] = $value;
    }
  }
  if (!empty($viewers)) {
    return $viewers;
  }
  return FALSE;
}

/**
 * Theme function for the admin primary display table
 *
 * @param type $variables
 *   render element: $form
 *   Contains the form array
 * @return
 *   rendered form element
 *
 *  @see islandora_large_image_admin()
 */
function theme_islandora_viewers_table($variables) {
  // set form
  $form = $variables['form'];
  $rows = array();

  foreach ($form['name'] as $key => $element) {
    // Do not take form control structures.
    if (is_array($element) && element_child($key)) {
      // set rows
      $row = array();
      $row[] = array('data' => drupal_render($form['default'][$key]));
      $row[] = array('data' => drupal_render($form['label'][$key]));
      $row[] = array('data' => drupal_render($form['description'][$key]));
      $row[] = array('data' => drupal_render($form['configuration'][$key]));

      // add to rows
      $rows[] = array('data' => $row);
    }
  }

  // Individual table headers.
  // default | label | description | configuration
  $header = array();
  $header[] = array('data' => t('Default'));
  $header[] = array('data' => t('Label'));
  $header[] = array('data' => t('Description'));
  $header[] = array('data' => t('Configuration'));

  // render form
  $output = '';
  $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'islandora-viewers-table')));
  $output .= drupal_render_children($form);

  return $output;
}

/**
 * Gather information and return a rendered viewer
 *
 * @param array/string $params
 *   Array or string with data the module needs in order to render a full viewer
 * @param string $variable_id
 *   The id of the Drupal variable the viewer settings are saved in
 * @return
 *   The callback to the viewer module. Returns a rendered viewer. Returns FALSE
 *   if no viewer is set.
 */
function islandora_get_viewer($params = NULL, $variable_id = NULL) {
  // get viewer from settings
  $settings = variable_get($variable_id, array());
  // make sure a viewer is set
  if (!empty($settings) AND $settings['default'] !== 'none') {
    // get callback function
    $viewer_id = islandora_get_viewer_id($variable_id);
    if ($viewer_id AND $params !== NULL) {
      $callback = islandora_get_viewer_callback($viewer_id);
      // call callback function
      return $callback($params);
    }
  }
  return NULL;
}

/**
 * Get id of the enabled viewer.
 *
 * @param string $variable_id
 *   The id of the Drupal variable the viewer settings are saved in
 * @return
 *   The enabled viewer id. Returns FALSE if no viewer config is set.
 */
function islandora_get_viewer_id($variable_id) {
  $viewers_config = variable_get($variable_id, array());
  if (!empty($viewers_config)) {
    return $viewers_config['default'];
  }
  return FALSE;
}

/**
 * Get callback function for a viewer.
 *
 * @param string $viewer_id
 *   The ID of a viewer
 * @return
 *   The callback function as a string as defined by the viewer.
 */
function islandora_get_viewer_callback($viewer_id = NULL) {
  if ($viewer_id !== NULL) {
    $viewers = module_invoke_all('islandora_viewer_info');
    if (isset($viewers[$viewer_id]['callback'])) {
      return $viewers[$viewer_id]['callback'];
    }
  }
  return FALSE;
}

/**
 * @} End of "defgroup viewer-functions".
 */