<?php

/**
 * @file
 * Provide the curriculum integration for the PER curriculum.
 *
 * The PER (Plan d'études romand) curriculum is the standard used by the French
 * speaking part of Switzerland to standardize school curriculum across cantons.
 */

// 1 month.
define('ARCHIBALD_PER_REFRESH_INTERVAL', 2592000);
define('ARCHIBALD_PER_BDPER_ENDPOINT', 'https://bdper.plandetudes.ch/api/v1/');
define('ARCHIBALD_PER_BDPER_ENDPOINT_CALL_LIMIT', 50);

/**
 * Implements hook_init().
 */
function archibald_per_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_per_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_per', array(
      'label' => "Plan d’études romand",
      'cardinality' => 1,
      'type' => 'archibald_per',
      'widget' => array(
        'type' => 'archibald_per',
      ),
    ));

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

    variable_set('archibald_per_entities_added_fields', 1);
  }

  if (user_access('administer site configuration')) {
    if ($cache = cache_get('archibald_per:item_db', 'cache_curricula')) {
      if ($cache->created <= REQUEST_TIME - ARCHIBALD_PER_REFRESH_INTERVAL) {
        drupal_set_message(t("The cached PER 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-per/load-per-data'),
        )), 'warning', FALSE);
      }
    }
  }
}

/**
 * Implements hook_menu().
 */
function archibald_per_menu() {
  return array(
    'archibald-per/item-database' => array(
      'page callback' => 'archibald_per_ajax_curriculum_data',
      'access callback' => 'archibald_per_curriculum_data_access',
      'type' => MENU_CALLBACK,
    ),
    'admin/archibald-per/load-per-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_per_load_data_trigger',
      'type' => MENU_CALLBACK,
    ),
  );
}

/**
 * Implements hook_theme().
 */
function archibald_per_theme() {
  return array(
    'archibald_per_json_curricula' => array(
      'variables' => array('items' => NULL),
    ),
    'archibald_per_json_curricula_item' => array(
      'variables' => array('item' => NULL, 'children' => NULL, 'langcode' => NULL),
    ),
  );
}

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

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

/**
 * Implements hook_field_widget_info().
 */
function archibald_per_field_widget_info() {
  return array(
    'archibald_per' => array(
      'label' => t("PER curriculum"),
      'field types' => array('archibald_per'),
    ),
  );
}

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

  $path = drupal_get_path('module', 'archibald_per');
  $form['#attached']['library'][] = array('system', 'ui.dialog');
  $form['#attached']['js']["$path/js/archibald_per.templates.js"] = array('weight' => -100);
  $form['#attached']['js'][] = "$path/js/archibald_per.js";
  $form['#attached']['css'][] = "$path/css/archibald_per.css";

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

  return $element;
}

/**
 * Implements hook_field_formatter_info().
 */
function archibald_per_field_formatter_info() {
  return array(
    'archibald_per' => array(
      'label' => t("PER curriculum display"),
      'field types' => array('archibald_per'),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function archibald_per_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');
  drupal_add_css(drupal_get_path('module', 'archibald_per') . '/css/archibald_per.css');

  $elements = array();
  if ($data = archibald_per_load_data()) {
    foreach ($data as $group => &$models) {
      // Prepare the data for formatting.
      foreach ($models as &$model) {
        $model = (array) $model;

        if (
          $model['type'] == 'objectifs' &&
          !empty($model['data']['perCode'])
        ) {
          $code = str_replace(array(' ', '&'), array('_', ''), $model['data']['perCode']);
          $model['name'][0] = l(
            $model['name'][0],
            'https://www.plandetudes.ch/web/guest/' . $code,
            array('attributes' => array('target' => '_blank'))
          );
        }
        else {
          // PER names already contain HTML. The Archibald Curricula module
          // "glues" names together with <br> tags, which messes up the display.
          // Pre-format it here.
          $model['name'] = array(implode('', $model['name']));
        }
      }
    }

    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,
            'html' => TRUE,
          )),
        );
      }
      else {
        $elements[$delta] = '';
      }
    }
  }
  else {
    drupal_set_message(archibald_curricula_curricula_load_error("PER"), 'warning');
  }

  return $elements;
}

/**
 * Implements hook_archibald_validation_report_alter().
 */
function archibald_per_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('/curriculum\.\d+/', $key, $match)) {
        // Check if the issue is for one of the PER entries.
        $source = $lom->getField("{$match[0]}.source.name");
        if ($source == 'per') {
          // Prepare the error entry, if it doesn't exist.
          if (empty($report['archibald_per'])) {
            $report['archibald_per'] = array(
              'field_label' => t("PER"),
              'errors' => array(),
            );
          }

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

/**
 * Implements hook_archibald_json_export_alter().
 */
function archibald_per_archibald_json_export_alter(&$json_data, $node) {
  // Load the item database and curriculum.
  $data = archibald_per_load_data();
  $curriculum = _archibald_per_get_curriculum();
  if (!$data || !$curriculum) {
    drupal_set_message(archibald_curricula_curricula_load_error("PER"), 'error');
    return;
  }

  $map_purpose = function($type) use($curriculum) {
    foreach ($curriculum->describeTermTypes() as $type_info) {
      if ($type_info->type == $type) {
        return $type_info->purpose['LOM-CHv1.2'];
      }
    }
    return 'note';
  };

  // Add purpose vocabulary data to the models.
  foreach ($data as $group => &$models) {
    foreach ($models as &$model) {
      $model = (array) $model;
      if (!isset($model['data'])) {
        $model['data'] = array();
      }

      $model['data']['purpose'] = array(
        'source' => 'LOM-CHv1.2',
        'value' => $map_purpose($model['type']),
      );
    }
  }

  $taxon_trees = array();
  foreach ($node->archibald_per 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);
          }
        }

        $taxon_trees = array_merge(
          $taxon_trees,
          theme('archibald_per_json_curricula', array(
            'items' => $data,
          ))
        );
      }
    }
  }

  if (!empty($taxon_trees)) {
    $json_data->curriculum[] = array(
      'source' => array(
        'name' => 'per',
        'hierarchy' => $curriculum->describeDataStructure(),
      ),
      'taxonTree' => $taxon_trees,
    );
  }
}

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

  // Look for PER entries.
  if (!empty($data['curriculum'])) {
    $ids = array();
    // Check the metadata schema. If it's LOM-CHv1.2 or higher, the import is
    // pretty straightforward. Otherwise...
    $lom = new Educa\DSB\Client\Lom\LomDescription($data);
    $is_legacy = TRUE;
    $schemas = $lom->getField('metaMetadata.metadataSchema');

    if (!is_array($schemas)) {
      $schemas = array($schemas);
    }

    foreach ($schemas as $schema) {
      if (
        preg_match('/^LOM-CHv\d/i', $schema) &&
        version_compare($schema, 'LOM-CHv1.2') !== -1
      ) {
        // Found a match. Don't treat this as a legacy format.
        $is_legacy = FALSE;
        break;
      }
    }

    if ($is_legacy) {
      $curriculum = _archibald_per_get_curriculum();
      if (!$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("PER"), 'error');
        drupal_set_message(t("To import legacy PER data, we need to pre-load the curriculum data. Aborting import."), 'error');
        return;
      }

      $recursive_cleanup = function($element) use (&$recursive_cleanup) {
        if ($element->hasChildren()) {
          foreach ($element->getChildren() as $child) {
            $description = $child->describe();
            // First, replace all opening <li>s with an actual bullet-point.
            // Then, strip all HTML tags from the text. Finally, normalize all
            // newlines.
            $description->name->fr = str_replace(
              array("\r\n", "\r"),
              "\n",
              strip_tags(
                str_replace('<li>', '• ', $description->name->fr)
              )
            );
            $child->setDescription(
              $description->type,
              $description->id,
              $description->name
            );
            $recursive_cleanup($child);
          }
        }
      };

      $recursive_fetch = function($elements, $objective_term, $objective_id) use (&$ids, &$recursive_fetch, $node, $meta) {
        foreach ($elements as $element) {
          if (!empty($element->details)) {
            foreach ($element->details as $detail) {
              if (!empty($detail->text)) {
                $terms = $objective_term->findChildrenByNameRecursive((object) array(
                  // Normalize new lines.
                  'fr' => str_replace(array("\r\n", "\r"), "\n", $detail->text),
                ));
                if (!empty($terms)) {
                  foreach ($terms as $term) {
                    $description = $term->describe();
                    $term_id = "{$objective_id}-{$description->type}-{$description->id}";
                    $ids[] = $term_id;
                  }
                }
                else {
                  drupal_set_message(
                    t("One of the <em>progressions d'apprentissage</em> could not be found in the existing PER database, and was ignored.<br />Description: %title (@lom_id)<br />Progression: %name<br />Objective: %code.", array(
                      '%name' => $detail->text,
                      '%code' => !empty($objective_term->describe()->name->fr) ?
                        $objective_term->describe()->name->fr :
                        $objective_term->getCode(),
                      '%title' => $node->title,
                      '@lom_id' => isset($meta['lomId']) ? $meta['lomId'] : 'n/a',
                    )),
                    'warning'
                  );
                }
              }
            }
          }
          if (!empty($element->childs)) {
            $recursive_fetch($element->childs, $objective_term, $objective_id);
          }
        }
      };

      foreach ($data['curriculum'] as $i => $_) {
        if ($lom->getField("curriculum.{$i}.source") == 'per') {
          if ($tree = json_decode($lom->getField("curriculum.{$i}.entity"))) {
            foreach ($tree as $cycle => $domains) {
              $cycle_term = @reset($curriculum->getTree()->findChildrenByNameRecursive((object) array(
                'fr' => $cycle,
              )));

              if ($cycle_term) {
                $description = $cycle_term->describe();
                $cycle_id = "{$description->type}-{$description->id}";
                $ids[] = $cycle_id;
              }

              foreach ($domains as $domain => $objectives) {
                $domain_term = @reset($cycle_term->findChildrenByNameRecursive((object) array(
                  'fr' => $domain,
                )));

                if ($domain_term) {
                  $description = $domain_term->describe();
                  $domain_id = "{$cycle_id}-{$description->type}-{$description->id}";
                  $ids[] = $domain_id;

                  foreach ($objectives as $code => $objective_data) {
                    $discipline_term = @reset($domain_term->findChildrenByNameRecursive((object) array(
                      'fr' => $objective_data->discipline,
                    )));

                    if ($discipline_term) {
                      $description = $discipline_term->describe();
                      $discipline_id = "{$domain_id}-{$description->type}-{$description->id}";
                      $ids[] = $discipline_id;

                      $objective_term = $discipline_term->findChildByCodeRecursive($code);

                      if ($objective_term) {
                        $description = $objective_term->describe();
                        $objective_id = "{$discipline_id}-{$description->type}-{$description->id}";
                        $ids[] = $objective_id;

                        if (!empty($objective_data->object_elements)) {
                          // The new PER API data can contain HTML elements, but
                          // the old, legacy format, didn't. In order for our
                          // child searches to match, we need to process the
                          // objective child elements, and clean up the names.
                          // Do this recursively.
                          $recursive_cleanup($objective_term);

                          // Now recursively go through the tree, and fetch the
                          // elements.
                          $recursive_fetch(
                            $objective_data->object_elements,
                            $objective_term,
                            $objective_id
                          );
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }

      // If we found anything, prepropulate our field.
      if (!empty($ids)) {
        $node->archibald_per[LANGUAGE_NONE][0]['item_ids'] = implode(',', array_unique($ids));
      }
    }
    else {
      // Recursively fetch all items and their IDs.
      $recursive = function($items, $parent_id = NULL) use (&$ids, &$recursive) {
        foreach ($items as $item) {
          if (isset($item['id'])) {
            // Internally, we need to prefix item IDs with the parent ID. This
            // is necessary to maintain context. For example, an objective may
            // be available for multiple disciplines. We need to be specific as
            // to which disciplines it has been activated for.
            $id = isset($parent_id) ?
              "{$parent_id}-{$item['type']}-{$item['id']}" :
              "{$item['type']}-{$item['id']}";
            $ids[] = $id;
          }
          if (!empty($item['childTaxons']) && !empty($id)) {
            $recursive($item['childTaxons'], $id);
          }
        }
      };

      foreach ($data['curriculum'] as $i => $_) {
        // Use a LomDescription object for easier access.
        if ($lom->getField("curriculum.{$i}.source.name") == 'per') {
          if ($tree = $lom->getField("curriculum.{$i}.taxonTree")) {
            $recursive($tree);
          }
        }
      }
    }

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

/**
 * Implements hook_archibald_json_import_revision_alter().
 */
function archibald_per_archibald_json_import_revision_alter($node, $context) {
  archibald_per_archibald_json_import_alter($node, $context);
}

/**
 * Implements hook_form_FORM_ID_alter() for archibald_lomch_description_node_form.
 */
function archibald_per_form_archibald_lomch_description_node_form_alter(&$form, $form_state) {
  if (module_exists('archibald_help')) {
    $title = &$form['archibald_per'][LANGUAGE_NONE][0]['#title'];
    $value = $form['archibald_per'][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("The Plan d’études romand is the common curriculum for compulsory education in the French and bilingual cantons of west Switzerland."),
    ));
  }
}

/**
 * Access callback.
 */
function archibald_per_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_per_ajax_curriculum_data() {
  if (!$data = archibald_per_load_data()) {
    drupal_add_http_header('status', '404');
    $data = array(
      'error' => TRUE,
      'message' => archibald_curricula_curricula_load_error("PER"),
    );
  }
  drupal_json_output($data);
}

/**
 * Page callback: trigger the batch for importing the PER curriculum.
 */
function archibald_per_load_data_trigger() {
  $batch = array(
    'title' => t("Import PER data"),
    'operations' => array(
      array('archibald_per_batch_load_data_step1', array()),
      array('archibald_per_batch_load_data_step2', array()),
      array('archibald_per_batch_load_data_step3', array()),
      array('archibald_per_batch_load_data_step4', array()),
    ),
  );
  batch_set($batch);

  // Redirect to the status page when finished.
  batch_process('admin/reports/status');
}

/**
 * Batch operation for importing PER data, step 1.
 *
 * This will load the domains and disciplines.
 */
function archibald_per_batch_load_data_step1(&$context) {
  if (!method_exists('Educa\DSB\Client\Curriculum\PerCurriculum', 'prepareFetch')) {
    $context['finished'] = 1;
    drupal_set_message(t("Please make sure you are using dsb Client 0.20.0 or greater."), 'error');
    return;
  }

  $context['message'] = t("Importing domains and disciplines...");

  list(
    $root,
    $dictionary
  ) = Educa\DSB\Client\Curriculum\PerCurriculum::prepareFetch();

  list(
    $root,
    $dictionary,
    $objective_ids
  ) = Educa\DSB\Client\Curriculum\PerCurriculum::fetchDomainsAndDisciplines(
    ARCHIBALD_PER_BDPER_ENDPOINT,
    $root,
    $dictionary
  );

  // The sandbox is not kept between batch operations, and arbitrary keys are
  // discarded. Only "results" is kept, so we use it instead.
  $context['results']['root'] = $root;
  $context['results']['dictionary'] = $dictionary;
  $context['results']['objective_ids'] = $objective_ids;
}

/**
 * Batch operation for importing PER data, step 2.
 *
 * This will load the objectives and progressions.
 */
function archibald_per_batch_load_data_step2(&$context) {
  if (!method_exists('Educa\DSB\Client\Curriculum\PerCurriculum', 'prepareFetch')) {
    $context['finished'] = 1;
    return;
  }

  $context['message'] = t("Importing objectives and progressions...");

  // See archibald_per_batch_load_data_step1 for an explanation on why we use
  // "results" and not "sandbox" for some data.
  if (!isset($context['sandbox']['max'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($context['results']['objective_ids']);
  }

  $objective_ids = array_splice(
    $context['results']['objective_ids'],
    0,
    ARCHIBALD_PER_BDPER_ENDPOINT_CALL_LIMIT
  );

  if (empty($objective_ids)) {
    // Nothing left to import. Finished.
    $context['finished'] = 1;
  }
  else {
    list(
      $root,
      $dictionary
    ) = Educa\DSB\Client\Curriculum\PerCurriculum::fetchObjectivesAndProgressions(
      ARCHIBALD_PER_BDPER_ENDPOINT,
      $context['results']['root'],
      $context['results']['dictionary'],
      $objective_ids
    );

    $context['sandbox']['progress'] += count($objective_ids);
    $context['results']['root'] = $root;
    $context['results']['dictionary'] = $dictionary;

    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }

  if ($context['finished'] == 1) {
    cache_set(
      'archibald_per:curriculum_data',
      (object) array(
        'curriculum' => $root,
        'dictionary' => $dictionary,
      ),
      'cache_curricula'
    );
  }
}

/**
 * Batch operation for importing PER data, step 3.
 *
 * This will transform the curriculum tree to our item DB format.
 */
function archibald_per_batch_load_data_step3(&$context) {
  if (!method_exists('Educa\DSB\Client\Curriculum\PerCurriculum', 'prepareFetch')) {
    $context['finished'] = 1;
    return;
  }

  $context['message'] = t("Converting to Archibald Curricula data format...");

  // Prepare some basic data structure.
  $data = array();

  // Prepare a function for recursively parsing the curriculum tree.
  $recursive_fetch = function($term, $parent, $parent_id, $id_prefix = NULL, $dependent_id = NULL) use(&$data, &$recursive_fetch) {
    if ($term->hasChildren()) {
      foreach ($term->getChildren() as $child) {
        $description = $child->describe();
        $id = isset($id_prefix) ?
            "{$id_prefix}-{$description->type}-{$description->id}" :
            "{$description->type}-{$description->id}";
        $data[$parent_id][$id] = array(
          'id' => $id,
          'type' => $description->type,
          'name' => explode("\n", trim($description->name->fr)),
          'data' => array(
            'isGroup' => $description->type == 'disciplines',
            'isSelectable' => $description->type != 'disciplines',
            'perSchoolYears' => $child->getSchoolYears(),
            'perCode' => $child->getCode(),
          ),
          'hasChildren' => in_array($description->type, array('disciplines', 'objectifs')) ?
            FALSE :
            $child->hasChildren(),
          'dependencies' => !empty($dependent_id) ?
            array($dependent_id) :
            array(),
        );

        if ($child->hasChildren()) {
          $recursive_fetch(
            $child,
            $description->type == 'disciplines' ?
              $parent :
              $child,
            $description->type == 'disciplines' ?
              $parent_id :
              $id,
            $id,
            $description->type == 'disciplines' ?
              $id :
              NULL
          );
        }
      }
    }
  };

  // Load the tree. See archibald_per_batch_load_data_step1 for an explanation
  // on why we use "results" here, and not "sandbox".
  $recursive_fetch(
    $context['results']['root'],
    $context['results']['root'],
    'root'
  );

  $context['results']['data'] = $data;
}

/**
 * Batch operation for importing PER data, step 4.
 *
 * This will load the table definitions.
 */
function archibald_per_batch_load_data_step4(&$context) {
  if (!method_exists('Educa\DSB\Client\Curriculum\PerCurriculum', 'prepareFetch')) {
    $context['finished'] = 1;
    return;
  }

  $context['message'] = t("Load progressions table structure information...");

  // See archibald_per_batch_load_data_step1 for an explanation on why we use
  // "results" and not "sandbox" for some data.
  if (!isset($context['sandbox']['objectives'])) {
    $context['sandbox']['objectives'] = $context['results']['root']->findChildrenByTypeRecursive('objectifs');
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($context['sandbox']['objectives']);
  }

  $limit = ARCHIBALD_PER_BDPER_ENDPOINT_CALL_LIMIT;
  while($limit && !empty($context['sandbox']['objectives'])) {
    $objective = array_pop($context['sandbox']['objectives']);
    $parent_id = sprintf(
      'cycles-%d-domaines-%d',
      $objective->getParent()->getParent()->getParent()->describe()->id,
      $objective->getParent()->getParent()->describe()->id
    );
    $objective_id = sprintf(
      '%s-disciplines-%d-objectifs-%d',
      $parent_id,
      $objective->getParent()->describe()->id,
      $objective->describe()->id
    );

    // Load the table, and fetch the "progressions d'apprentissage". Fetch the
    // raw table data.
    // Careful!! This URL needs a trailing slash for some reason...
    $ch = curl_init(
      ARCHIBALD_PER_BDPER_ENDPOINT . "objectifs/{$objective->describe()->id}/tableau/"
    );
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $json = curl_exec($ch);
    curl_close($ch);
    $table_data = json_decode($json, TRUE);
    $item = isset($context['results']['data'][$parent_id][$objective_id]) ?
      $context['results']['data'][$parent_id][$objective_id] :
      NULL;

    if (!empty($table_data) && $item) {
      $table = array();
      if (!empty($table_data['tableau'])) {
        foreach ($table_data['tableau'] as $i => $row) {
          foreach ($row as $j => $cell) {
            if (empty($cell['type'])) {
              continue;
            }

            // We only treat progressions and titles.
            if (in_array($cell['type'], array('progression', 'titre'))) {
              // Some items are empty (?).
              if (!empty($cell['contenus'])) {
                $table[$i][$j] = array(
                  'type' => $cell['type'] == 'titre' ? 'title' : $cell['type'],
                  // Some titles span further than we want to show. Make
                  // sure we don't span further than the number of school
                  // years.
                  'colspan' => min(count($item['data']['perSchoolYears']), $cell['colspan']),
                  'rowspan' => $cell['rowspan'],
                  'content' => array_map(function($content) use($cell, $item) {
                    return array(
                      'id' => $item['id'] . '-' . $cell['type'] . 's-' . $content['id'],
                      'value' => strip_tags($content['texte'], '<span><em><strong><br><ul><ol><li>'),
                    );
                  }, $cell['contenus']),
                  'isSelectable' => $cell['type'] == 'progression',
                  'level' => $cell['type'] == 'titre' ?
                    (!empty($cell['niveau']) ? $cell['niveau'] : 1) :
                    null,
                );
              }
              else {
                // Empty (padding) cell.
                $table[$i][$j] = array(
                  'type' => 'empty',
                  'rowspan' => 1,
                  'colspan' => 1,
                  'content' => [],
                  'isSelectable' => FALSE,
                );
              }
            }
          }

          if (!empty($table[$i])) {
            // If this line contains only empty cells, remove it.
            $found = FALSE;
            foreach ($table[$i] as $cell_to_check) {
              if ($cell_to_check['type'] != 'empty') {
                $found = TRUE;
                break;
              }
            }
            if (!$found) {
              // Unset this row.
              unset($table[$i]);
            }
            else {
              // Remove keys, only keep a list.
              $table[$i] = array_values($table[$i]);
            }
          }
        }

        // Remove keys, only keep a list.
        $table = array_values($table);
        $context['results']['data'][$parent_id][$objective_id]['data']['perTable'] = $table;
      }
    }

    $context['sandbox']['progress']++;
    $limit--;
  }

  $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];

  if ($context['finished'] == 1) {
    cache_set(
      'archibald_per:item_db',
      $context['results']['data'],
      'cache_curricula'
    );

    drupal_set_message(t("Successfully imported the PER curriculum data"));
  }
}

/**
 * Renders a list of LOM-CHv1.2 Curricula taxonomy trees.
 *
 * We use this theme callback as a commodity, as it allows other modules to
 * override or preprocess it. It doesn't actually *render* a string,
 * but simply returns an object, suitable for exporting as JSON.
 *
 * @see theme_archibald_curricula_json_curricula_item()
 * @see hook_archibald_json_export_alter()
 *
 * @param $vars
 *    An associative array containing:
 *   - items: An array containing the item database. This array will be filtered
 *     to only include items with the "active" property set to true. Each active
 *     item will be "rendered" as an object suitable for JSON export. Each item
 *     is "rendered" using the theme callback defined in the
 *     "item_theme_callback" variable (see below). This callback gets passes the
 *     item and its children items, pre-rendered (recursive).
 *   - item_theme_callback: The theme callback to use to render single items.
 *     Defaults to theme_archibald_curricula_json_curricula_item().
 *
 * @return array
 *    An array of taxonomy trees, as expected by the LOM-CHv1.2 Curricula field.
 *    Refer to the official documentation of the LOM-CHv1.2 format for more
 *    information.
 */
function theme_archibald_per_json_curricula($vars) {
  $items = $vars['items'];

  // Prepare a function for recursively rendering items.
  $recursive = function($children) use(&$items, &$recursive) {
    if (empty($children)) {
      return array();
    }

    $children = array_values(array_filter($children, function($item) {
      return !empty($item['active']);
    }));
    $list = array();
    $array_search = function($type, $children, $offset) {
      foreach(array_slice($children, $offset, count($children), TRUE) as $i => $item) {
        if ($item['type'] == $type) {
          return $i;
        }
      }
      return FALSE;
    };

    if (!empty($children)) {
      $j = -1;
      foreach ($children as $i => $child) {
        if ($child['type'] == 'disciplines') {
          // In our item database, objectives are siblings of disciplines, but
          // in fact, they are supposed to be child items. Find the next
          // discipline (or the end of the array), and remove all objectives
          // between those points. Use them as the current children, and carry
          // on.
          // We cannot use array_search() combined with array_column() in our
          // case, as we need to keep the indexes. So, we have to use a custom
          // logic here.
          $j = $array_search('disciplines', $children, $i + 1);
          if ($j === FALSE) {
            // We have to go up to the end of the array.
            $j = count($children);
          }
          // Always decrement by 1.
          $j--;
          // Process it.
          $list[] = theme('archibald_per_json_curricula_item', array(
            'item' => $child,
            'children' => $recursive(array_slice($children, $i + 1, $j - $i)),
          ));
        }
        elseif ($i > $j) {
          $list[] = theme('archibald_per_json_curricula_item', array(
            'item' => $child,
            'children' => !empty($items[$child['id']]) ?
              $recursive($items[$child['id']]) :
              array(),
          ));
        }
      };
    }

    return $list;
  };

  return $recursive($items['root']);
}

/**
 * Renders a single item in the LOM-CHv1.2 Curricula structure.
 *
 * We use this theme callback as a commodity, as it allows other modules to
 * override or preprocess it. It doesn't actually *render* a string,
 * but simply returns an object, suitable for exporting as JSON.
 *
 * @see theme_archibald_per_json_curricula()
 * @see theme_archibald_curricula_json_curricula_item()
 * @see hook_archibald_json_export_alter()
 *
 * @param $vars
 *    An associative array containing:
 *   - item: The item to be "rendered". If the item's "data" key contains a
 *     "purpose" key, it will be used as the "purpose" value. It's up to the
 *     caller to make sure that "purpose" is a valid LOM Vocabulary element.
 *   - children: An array of pre-rendered child items.
 *   - langcode: In case an item's "name" key is not a valid LangString, it will
 *     be converted to a LangString format. If a langcode is provided, it will
 *     used as the sole language available. If it's not available, the default
 *     language will be used instead.
 *
 * @return array
 *    A taxon item as expected by the LOM-CHv1.2 Curricula field. Refer to the
 *    official documentation of the LOM-CHv1.2 format for more information.
 */
function theme_archibald_per_json_curricula_item($vars) {
  // The PER data is in French only.
  $vars['langcode'] = 'fr';
  $vars['html'] = TRUE;
  $data = theme('archibald_curricula_json_curricula_item', $vars);

  // We need to reset the IDs. Internally, we use a different format than the
  // standard, because we need contextual information. Map all IDs back to the
  // standard ones.
  $data['id'] = @end(explode('-', $data['id']));

  return $data;
}

/**
 * Load the PER data.
 *
 * Load the PER data from the official PER REST API, and compile it to a format
 * the Curricula UI component can understand.
 *
 * @return array|false
 *    The PER data, or false if the cache is empty. The cache can only be
 *    refreshed by explicitly visiting admin/archibald-per/load-per-data.
 */
function archibald_per_load_data() {
  $cid = 'archibald_per:item_db';
  $cache = cache_get($cid, 'cache_curricula');
  if (!empty($cache->data)) {
    return $cache->data;
  }
  else {
    return FALSE;
  }
}

/**
 * Get an instance of the PerCurriculum class.
 *
 * @param bool $reset
 *     (optional) Whether to reset the curriculum data cache. Defaults to false.
 *
 * @return Educa\DSB\Client\Curriculum\PerCurriculum|false
 *     False if the cache is empty. The cache can only be refreshed by setting
 *     $reset explicitly to true.
 */
function _archibald_per_get_curriculum($reset = FALSE) {
  if (!$reset) {
    $cache = cache_get('archibald_per:curriculum_data', 'cache_curricula');

    if (!empty($cache->data)) {
      $curriculum = new Educa\DSB\Client\Curriculum\PerCurriculum($cache->data->curriculum);
      $curriculum->setCurriculumDictionary($cache->data->dictionary);
      return $curriculum;
    }
    else {
      return FALSE;
    }
  }

  // Load the curriculum data.
  $curriculum_data = Educa\DSB\Client\Curriculum\PerCurriculum::fetchCurriculumData('http://bdper.plandetudes.ch/api/v1/');

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

  $curriculum = new Educa\DSB\Client\Curriculum\PerCurriculum($curriculum_data->curriculum);
  $curriculum->setCurriculumDictionary($curriculum_data->dictionary);


  return $curriculum;
}
