<?php

/**
 * @file
 * Provide the curriculum integration for the Classification Systems curriculum.
 *
 * The "Classification Systems" curriculum is a standard used to bridge the gaps
 * between the different curricula that exist in Switzerland. It can be used
 * as a stand-alone curriculum, but can also provide automatic suggestions based
 * on selections made in other curricula.
 *
 * Before the "Classification Systems", there was another curriculum called
 * "Standard Lehrplan". This was deprecated in favor of the Classification
 * Systems. Because this module was originally built for the Standard Lehrplan,
 * the machine name remained "archibald_stdlehr", to avoid tricky migrations,
 * as most of the field data remained the same.
 */

// 1 week.
define('ARCHIBALD_STDLEHR_REFRESH_INTERVAL', 604800);
define('ARCHIBALD_STDLEHR_SCHOOL_LEVELS', 'school context');
define('ARCHIBALD_STDLEHR_SCHOOL_SUBJECTS', 'school subjects');
define('ARCHIBALD_STDLEHR_SWISSDOC', 'swissdoc');
define('ARCHIBALD_STDLEHR_EDU_LEVEL_PURPOSE', 'educational level');
define('ARCHIBALD_STDLEHR_DISCIPLINE_PURPOSE', 'discipline');

/**
 * Implements hook_init().
 */
function archibald_stdlehr_init() {
  // This is a hack for adding our custom fields to the
  // archibald_lomch_description content type. We cannot do this on install, as
  // our custom field type doesn't exist at that time yet. We put it in here,
  // meaning we actually add the fields on the first page load. We set a system
  // variable when done, so we only do this once.
  if (!variable_get('archibald_stdlehr_entities_added_fields', 0)) {
    module_load_include('inc', 'archibald', 'includes/archibald.entities');
    $entity = 'node';
    $bundle = 'archibald_lomch_description';

    // Add our field to the curricula field group.
    archibald_add_field_to_entity($entity, $bundle, 'archibald_stdlehr', array(
      'label' => "Classification Systems",
      'cardinality' => 1,
      'type' => 'archibald_stdlehr',
      'widget' => array(
        'type' => 'archibald_stdlehr',
      ),
    ));

    // Add our field to the Classification group, both on the form and in the
    // default display.
    foreach (array('form', 'default') as $display) {
      $group = field_group_load_field_group('group_classification', $entity, $bundle, $display);
      $group->children[] = 'archibald_stdlehr';
      $group->children = array_unique($group->children);
      field_group_group_save($group);
    }

    variable_set('archibald_stdlehr_entities_added_fields', 1);
  }

  if (user_access('administer site configuration')) {
    if ($cache = cache_get('archibald_stdlehr:curriculum_data', 'cache_curricula')) {
      if ($cache->created <= REQUEST_TIME - ARCHIBALD_STDLEHR_REFRESH_INTERVAL) {
        drupal_set_message(t("The cached Classification Systems data may be stale (last imported on %date). It is recommended to refresh it. You can do so <a href='!link'>here</a>.", array(
          '%date' => format_date($cache->created),
          '!link' => url('admin/archibald-stdlehr/load-stdlehr-data'),
        )), 'warning', FALSE);
      }
    }
  }
}

/**
 * Implements hook_cron().
 */
function archibald_stdlehr_cron() {
  // Check if we need to refresh the curriculum data.
  if ($cache = cache_get('archibald_stdlehr:curriculum_data', 'cache_curricula')) {
    $needs_refresh = $cache->created <= REQUEST_TIME - ARCHIBALD_STDLEHR_REFRESH_INTERVAL;

    if ($needs_refresh) {
      // Refresh the data.
      watchdog('archibald_stdlehr', "Refreshing classification system information.", array(), WATCHDOG_NOTICE);
      if (archibald_stdlehr_load_data(TRUE)) {
        watchdog('archibald_stdlehr', "Successfully refreshed classification system information.", array(), WATCHDOG_NOTICE);
      }
      else {
        watchdog('archibald_stdlehr', "Could not refresh classification system information", array(), WATCHDOG_ERROR);
      }
    }
  }
}

/**
 * Implements hook_menu().
 */
function archibald_stdlehr_menu() {
  return array(
    'archibald-stdlehr/item-database' => array(
      'page callback' => 'archibald_stdlehr_ajax_curriculum_data',
      'access callback' => 'archibald_stdlehr_curriculum_data_access',
      'type' => MENU_CALLBACK,
    ),
    'admin/archibald-stdlehr/load-stdlehr-data' => array(
      // As this page can be called directly after the installation, the user
      // may not have any of our custom permissions yet. But, if the user has
      // access to administer the site, it is safe to assume she can import a
      // few vocabularies. This is why we simply use the 'administer site
      // configuration' permission here.
      'access arguments' => array('administer site configuration'),
      'page callback' => 'archibald_stdlehr_load_data_trigger',
      'type' => MENU_CALLBACK,
    ),
  );
}

/**
 * Implements hook_field_info().
 */
function archibald_stdlehr_field_info() {
  return array(
    'archibald_stdlehr' => array(
      'label' => t("Classification Systems curriculum"),
      'description' => t("Classification Systems curriculum field, using the Curricula UI JS component."),
      'default_widget' => 'archibald_stdlehr',
      'default_formatter' => 'archibald_stdlehr',
    ),
  );
}

/**
 * Implements hook_field_is_empty().
 */
function archibald_stdlehr_field_is_empty($item, $field) {
  return empty($item['item_ids']);
}

/**
 * Implements hook_field_widget_info().
 */
function archibald_stdlehr_field_widget_info() {
  return array(
    'archibald_stdlehr' => array(
      'label' => t("Classification Systems curriculum"),
      'field types' => array('archibald_stdlehr'),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function archibald_stdlehr_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $item = isset($items[$delta]) ? $items[$delta] : NULL;

  $element = array(
    '#type' => 'archibald_curricula',
    '#curricula_data_src' => url('archibald-stdlehr/item-database'),
    '#curricula_name' => 'classification_system',
    '#curricula_settings' => array(
      'useItemInfo' => FALSE,
    ),
    '#default_value' => isset($item['item_ids']) ? $item['item_ids'] : NULL,
  ) + $element;

  return $element;
}

/**
 * Implements hook_archibald_curr_mapping().
 */
function archibald_stdlehr_archibald_curr_mapping() {
  $provides_for = array();
  if (module_exists('archibald_per')) {
    $provides_for['per'] = t("PER");
  }

  return array(
    'classification_system' => array(
      'label' => t("Classification Systems"),
      'provides_for' => $provides_for,
      'suggestion_callback' => 'archibald_stdlehr_provide_suggestions',
    ),
  );
}

/**
 * Implements hook_field_formatter_info().
 */
function archibald_stdlehr_field_formatter_info() {
  return array(
    'archibald_stdlehr' => array(
      'label' => t("Classification Systems curriculum display"),
      'field types' => array('archibald_stdlehr'),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function archibald_stdlehr_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  // We want to use the CSS provided by the Curricula JS app. Load it.
  libraries_load('curricula-ui');

  $elements = array();
  if ($data = archibald_stdlehr_load_data()) {
    foreach ($items as $delta => $item) {
      // Activate all items that were selected.
      if (!empty($item['item_ids'])) {
        $item_ids = array_map('trim', explode(',', $item['item_ids']));

        // Here we actually alter the data. But, because for each field item, we
        // pass through the *whole* data tree, we will reset the "active" key
        // correctly. So, there is no risk we mix up items.
        foreach ($data as $group => &$models) {
          foreach ($models as &$model) {
            $model = (array) $model;
            $model['active'] = in_array($model['id'], $item_ids);
          }
        }

        $elements[$delta] = array(
          '#markup' => theme('archibald_curricula_summary', array(
            'items' => $data,
          )),
        );
      }
      else {
        $elements[$delta] = '';
      }
    }
  }
  else {
    drupal_set_message(archibald_curricula_curricula_load_error(t("Classification Systems")), 'warning');
  }

  return $elements;
}

/**
 * Implements hook_archibald_validation_report_alter().
 */
function archibald_stdlehr_archibald_validation_report_alter(&$report, $context) {
  // Check for any issues with "unknown" fields.
  if (!empty($report['unknown']['errors'])) {
    $json_data = archibald_json_export($context['node']);

    // The LomDescription expects an associative array, not the stdClass we
    // get from archibald_json_export(). Encoding/decoding in JSON is the
    // easiest way to achieve a recursive mapping.
    $lom = new Educa\DSB\Client\Lom\LomDescription(
      json_decode(json_encode($json_data), TRUE)
    );

    // Are there issues with the curricula field?
    foreach ($report['unknown']['errors'] as $key => $value) {
      $match;
      if (preg_match('/classification\.\d+\.taxonPath\.\d+/', $key, $match)) {
        // Check if the issue is for one of the Classification Systems entries.
        $source = \Educa\DSB\Client\Utils::getLSValue(
          $lom->getField("{$match[0]}.source"),
          archibald_language_fallback_list()
        );

        if (in_array($source, array('educa', 'classification system'))) {
          // Prepare the error entry, if it doesn't exist.
          if (empty($report['archibald_stdlehr'])) {
            $report['archibald_stdlehr'] = array(
              'field_label' => t("Classification Systems"),
              'errors' => array(),
            );
          }

          // Update the report.
          $report['archibald_stdlehr']['errors'][$key] = $value;
          unset($report['unknown']['errors'][$key]);
        }
      }
    }
  }
}

/**
 * Implements hook_archibald_json_export_alter().
 */
function archibald_stdlehr_archibald_json_export_alter(&$json_data, $node) {
  // Load the item database.
  $data = archibald_stdlehr_load_data();
  $curriculum = _archibald_stdlehr_get_curriculum();

  if (!$data || !$curriculum) {
    drupal_set_message(archibald_curricula_curricula_load_error(t("Classification Systems")), 'error');
    return;
  }

  foreach ($node->archibald_stdlehr as $langcode => $items) {
    foreach ($items as $item) {
      // Activate all items that were selected.
      if (!empty($item['item_ids'])) {
        $item_ids = array_map('trim', explode(',', $item['item_ids']));

        // Here we actually alter the data. But, because for each field item, we
        // pass through the *whole* data tree, we will reset the "active" key
        // correctly. So, there is no risk we mix up items.
        foreach ($data as $group => &$models) {
          foreach ($models as &$model) {
            $model = (array) $model;
            $model['active'] = in_array($model['id'], $item_ids);
          }
        }

        // We handle 2 types of paths, namely discipline and educational level.
        // Both are handled independently in the UI, but are part of the same
        // database. We split on the 1st level, removing (temporarily) the
        // "other" tree from the database when calling the render function.
        foreach (array(
          ARCHIBALD_STDLEHR_SCHOOL_LEVELS => ARCHIBALD_STDLEHR_EDU_LEVEL_PURPOSE,
          ARCHIBALD_STDLEHR_SCHOOL_SUBJECTS => ARCHIBALD_STDLEHR_DISCIPLINE_PURPOSE,
          ARCHIBALD_STDLEHR_SWISSDOC => ARCHIBALD_STDLEHR_DISCIPLINE_PURPOSE,
        ) as $key => $purpose) {
          // Make a copy, as we'll alter the data.
          $new_data = $data;

          // Deactivate the unwanted trees.
          $new_data['root'] = array_map(function($item) use($key) {
            if ($item['id'] != $key) {
              $item['active'] = FALSE;
            }
            return $item;
          }, $new_data['root']);

          $paths = theme('archibald_curricula_json_classification', array(
            'items' => $new_data,
          ));

          $json_data->classification[] = array(
            'purpose' => array(
              'source' => 'LOMv1.0',
              'value' => $purpose,
            ),
            // Convert the multiple paths returned by
            // theme_archibald_curricula_json_classification() to something
            // LOM-CHv1.1 can understand.
            'taxonPath' => array_map(function($path) use($curriculum) {
              // The data was loaded for the current language, but we want to
              // make sure that all items have the fully available LangString.
              // Use the "raw" curriculum data to search for each item, and
              // update the entries.
              $path = array_map(function($item) use($curriculum) {
                $term = $curriculum->getTree()->findChildByIdentifierRecursive(
                  $item['id']
                );

                if ($term) {
                  $item['entry'] = $term->describe()->name;
                }

                return $item;
              }, $path);

              return array(
                'source' => array(
                  'en' => 'classification system',
                ),
                // Filter out "root" items.
                'taxon' => array_values(array_filter($path, function($item) {
                  return !in_array($item['id'], array(
                    ARCHIBALD_STDLEHR_SCHOOL_LEVELS,
                    ARCHIBALD_STDLEHR_SCHOOL_SUBJECTS,
                    ARCHIBALD_STDLEHR_SWISSDOC,
                  ));
                })),
              );
            }, $paths),
          );
        }
      }
    }
  }

}

/**
 * Implements hook_archibald_json_import_alter().
 */
function archibald_stdlehr_archibald_json_import_alter($node, &$context) {
  $data = json_decode($context['json'], TRUE);

  // Look for Classification Systems entries.
  if (!empty($data['classification'])) {
    $import_curriculum = _archibald_stdlehr_get_curriculum();
    $check_curriculum = _archibald_stdlehr_get_curriculum();

    if (!$import_curriculum || !$check_curriculum) {
      // The cache is empty, we cannot import. We need to abort the import.
      $context['abort'] = TRUE;
      drupal_set_message(archibald_curricula_curricula_load_error(t("Classification Systems")), 'error');
      drupal_set_message(t("To import <em>Classification Systems</em> data, we need to pre-load the curriculum data. Aborting import."), 'error');
      return;
    }

    // We want to import the data in 2 steps: disciplines, and educational
    // levels. On older descriptions, discipline paths contain educational
    // levels. Normally, we would need to clean these up. But in this case,
    // because we simply store IDs, we can leave them mixed up; the end result
    // will be the same.
    $import_curriculum->setTreeBasedOnTaxonPath($data['classification'], array(
      'discipline',
      'educational level',
    ));

    // Recursively fetch all items and their IDs.
    $ids = array();
    $recursive = function($term) use (&$ids, &$recursive) {
      $ids[] = $term->describe()->id;
      if ($term->hasChildren()) {
        foreach ($term->getChildren() as $child) {
          $recursive($child);
        }
      }
    };
    $recursive($import_curriculum->getTree());

    // Filter out duplicates, and remove the "root" element.
    $ids = array_filter(array_unique($ids), function($item) {
      return $item != 'root';
    });

    // Now, in our UI, we actually have 2 root elements that are not part of
    // the curriculum per-se, but are part of the Ontology data. These serve
    // as "containers" for the educational levels and disciplines, respectively.
    // We need to check if any educational levels are part of the active IDs,
    // and if so, activate this "container". Same for the disciplines.
    $added_levels = FALSE;
    $added_disciplines = FALSE;
    foreach ($ids as $id) {
      // Break if we found everything. We could be more efficient by breaking
      // at several levels, but for code readability and DRYness, we'll only
      // break here.
      if ($added_levels && $added_disciplines) {
        break;
      }

      if ($term = $check_curriculum->getTree()->findChildByIdentifierRecursive($id)) {
        while ($term->hasParent()) {
          $term = $term->getParent();
          if (
            $term->describe()->id == ARCHIBALD_STDLEHR_SCHOOL_LEVELS &&
            !$added_levels
          ) {
            $added_levels = TRUE;
            $ids[] = ARCHIBALD_STDLEHR_SCHOOL_LEVELS;
          }
          elseif (
            $term->describe()->id == ARCHIBALD_STDLEHR_SCHOOL_SUBJECTS &&
            !$added_disciplines
          ) {
            $added_disciplines = TRUE;
            $ids[] = ARCHIBALD_STDLEHR_SCHOOL_SUBJECTS;
          }
        }
      }
    }

    // If we found anything, populate our field.
    if (!empty($ids)) {
      $node->archibald_stdlehr[LANGUAGE_NONE][0]['item_ids'] = implode(',', array_unique($ids));
    }
  }
}

/**
 * Implements hook_archibald_json_import_revision_alter().
 */
function archibald_stdlehr_archibald_json_import_revision_alter($node, $context) {
  archibald_stdlehr_archibald_json_import_alter($node, $context);
}

/**
 * Implements hook_form_FORM_ID_alter() for archibald_lomch_description_node_form.
 */
function archibald_stdlehr_form_archibald_lomch_description_node_form_alter(&$form, $form_state) {
  if (module_exists('archibald_help')) {
    $title = &$form['archibald_stdlehr'][LANGUAGE_NONE][0]['#title'];
    $value = $form['archibald_stdlehr'][LANGUAGE_NONE][0]['#title'];

    $title .= '<span class="archibald-help-tooltip qtip-link"></span>';
    $title .= theme('qtip_form', array(
      'instance' => 'archibald_help',
      'title' => $value,
      'tooltip' => t("Place the resource within the classification systems:<ul><li>School context: School level and type</li><li>Subjects: Subjects and interdisciplinary topics and skills</li></ul>"),
    ));
  }
}

/**
 * Access callback.
 */
function archibald_stdlehr_curriculum_data_access($account = NULL) {
  if (!isset($account)) {
    global $user;
    $account = $user;
  }

  foreach (array(
    'bypass node access',
    'administer nodes',
    'create archibald_lomch_description content',
    'edit own archibald_lomch_description content',
    'edit any archibald_lomch_description content',
  ) as $permission) {
    if (user_access($permission, $account)) {
      return TRUE;
    }
  }

  return FALSE;
}


/**
 * Page callback: load curriculum data and send it back as JSON.
 */
function archibald_stdlehr_ajax_curriculum_data() {
  if (!$data = archibald_stdlehr_load_data()) {
    drupal_add_http_header('status', '404');
    $data = array(
      'error' => TRUE,
      'message' => archibald_curricula_curricula_load_error(t("Classification Systems")),
    );
  }
  drupal_json_output($data);
}

/**
 * Page callback: trigger the batch for importing the Classification Systems.
 */
function archibald_stdlehr_load_data_trigger() {
  // Refresh the curricula cache.
  archibald_stdlehr_load_data(TRUE);
  drupal_goto('admin/reports/status');
}

/**
 * Load the Classification Systems data.
 *
 * Load the Classification Systems data from the official dsb REST API, and compile
 * it to a format the Curricula UI component can understand.
 *
 * @param bool $reset
 *    (optional) Reset the cache. Defaults to false.
 *
 * @return array|false
 *    The Classification Systems data, or false if the cache is empty. The cache can
 *    only be refreshed by explicitly setting reset to true.
 */
function archibald_stdlehr_load_data($reset = FALSE) {
  global $language;
  $cid_base = 'archibald_stdlehr:item_db:';
  $cid = $cid_base . $language->language;

  if (!$reset) {
    $cache = cache_get($cid, 'cache_curricula');
    if (!empty($cache->data)) {
      return $cache->data;
    }
    else {
      return FALSE;
    }
  }

  // Prepare a function for recursively parsing the curriculum tree.
  $recursive_fetch = function($term, $lang_fallback) use(&$recursive_fetch) {
    $data = array();
    if ($term->hasChildren()) {
      $parent_id = $term->describe()->id;

      foreach ($term->getChildren() as $child) {
        $description = $child->describe();
        $data[$parent_id][$description->id] = array(
          'id' => $description->id,
          'type' => $description->type,
          'name' => array(trim(
            \Educa\DSB\Client\Utils::getLSValue(
              $description->name,
              $lang_fallback
            )
          )),
          'hasChildren' => $child->hasChildren(),
        );

        if ($child->hasChildren()) {
          $data = array_merge($data, $recursive_fetch($child, $lang_fallback));
        }
      }
    }
    return $data;
  };

  $curriculum = _archibald_stdlehr_get_curriculum($reset);

  // We need to fetch the data for each language, as the JSON definition
  // contains everything.
  foreach(array_keys(language_list()) as $langcode) {
    $fallback_list = array_unique(array_merge(
      array($langcode),
      archibald_language_fallback_list()
    ));

    // Load the tree.
    $data = $recursive_fetch($curriculum->getTree(), $fallback_list);

    // Translate all 1st level associative arrays to regular arrays, removing
    // the keys.
    $data = array_map(function($list) {
      return array_values($list);
    }, $data);

    // Cache.
    cache_set($cid_base . $langcode, $data, 'cache_curricula');
  }

  // Now, because we refreshed the cache, fetch it and return.
  $cache = cache_get($cid, 'cache_curricula');
  return $cache->data;
}

/**
 * Get an instance of the ClassificationSystemCurriculum class.
 *
 * @param bool $reset
 *     (optional) Whether to reset the curriculum data cache. Defaults to false.
 *
 * @return Educa\DSB\Client\Curriculum\ClassificationSystemCurriculum
 */
function _archibald_stdlehr_get_curriculum($reset = FALSE) {
  $cid = 'archibald_stdlehr:curriculum_data';
  if (!$reset) {
    $cache = cache_get($cid, 'cache_curricula');
  }

  if (empty($cache->data)) {
    // Load the curriculum data.
    try {
      $client = archibald_get_client_based_on_config();
      $response = $client->loadOntologyData('raw', array('classification_system'));
    }
    catch(Exception $e) {
      watchdog_exception('archibald_stdlehr', $e);
      return;
    }

    $curriculum_data = Educa\DSB\Client\Curriculum\ClassificationSystemCurriculum::parseCurriculumJson(
      // The client already decodes JSON. We must re-encode to make
      // ClassificationSystemCurriculum handle it.
      json_encode($response)
    );

    // Cache, as we will use it again.
    cache_set($cid, $curriculum_data, 'cache_curricula');

    $curriculum = new Educa\DSB\Client\Curriculum\ClassificationSystemCurriculum($curriculum_data->curriculum);
    $curriculum->setCurriculumDictionary($curriculum_data->dictionary);
  }
  else {
    $curriculum = new Educa\DSB\Client\Curriculum\ClassificationSystemCurriculum($cache->data->curriculum);
    $curriculum->setCurriculumDictionary($cache->data->dictionary);
  }

  return $curriculum;
}

/**
 * Suggestions callback.
 *
 * @see hook_archibald_curr_mapping()
 */
function archibald_stdlehr_provide_suggestions($source, $target, $item_id) {
  $suggestions = array();

  // If the source is PER, we need to adapt the item ID a bit.
  if ($source == 'per') {
    // In PER, the identifiers are stored as "{type}-{id}", because IDs are not
    // unique. However, because certain items are shared, archibald_per has to
    // store them with context information. This is why the PER ids in our case
    // look like "{parent1-type}-{parent1-id}-{parent2-type}-{parent2-id}...".
    // In order to get suggestions from the REST API, we only use the last part.
    // We still keep the item type as a prefix though, because the REST API
    // expects PER item IDs to be formatted that way.
    preg_match('/-?(\w+-\d+)$/', $item_id, $match);
    $item_id = $match[1];
  }

  // Just in case the dsb Client library wasn't updated.
  if (!method_exists($client, 'getCurriculaMappingSuggestions')) {
    watchdog('archibald_stdlehr:mapping', "Make sure the dsb Client library is at the correct version! We require at least version 0.17.0.", array(), WATCHDOG_ERROR);
    return array();
  }

  try {
    $client = archibald_get_client_based_on_config();
    $result = $client->getCurriculaMappingSuggestions(
      $source,
      $target,
      $item_id
    );

    // Treat each suggestion, adding human readable labels in the correct
    // language.
    if (
      !empty($result[$item_id]) &&
      ($curriculum = _archibald_stdlehr_get_curriculum()) &&
      $curriculum->getTree()->hasChildren()
    ) {
      foreach ($result[$item_id] as $id) {
        if ($term = $curriculum->getTree()->findChildByIdentifierRecursive($id)) {
          $suggestions[] = array(
            'id' => $id,
            'label' => \Educa\DSB\Client\Utils::getLSValue(
              $term->describe()->name,
              archibald_language_fallback_list()
            ),
          );
        }
      }
    }
  }
  catch(Exception $e) {
    // If a 404, don't log the exception. There are simply no suggestions.
    if ($e->getCode() != 404) {
      watchdog_exception('archibald_stdlehr:mapping', $e);
    }
  }

  return $suggestions;
}
