<?php

/**
 * @file
 * Provide the curriculum integration for the LP21 curriculum.
 *
 * The LP21 (Lehrplan 21) curriculum is the standard used by the German
 * speaking part of Switzerland to standardize school curriculum across cantons.
 */

/**
 * Implements hook_init().
 */
function archibald_lp21_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_lp21_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_lp21', array(
      'label' => "Lehrplan 21",
      'cardinality' => 1,
      'type' => 'archibald_lp21',
      'widget' => array(
        'type' => 'archibald_lp21',
      ),
    ));

    // 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_lp21';
      $group->children = array_unique($group->children);
      field_group_group_save($group);
    }

    variable_set('archibald_lp21_entities_added_fields', 1);
  }
}

/**
 * Implements hook_menu().
 */
function archibald_lp21_menu() {
  return array(
    'archibald-lp21/item-database' => array(
      'page callback' => 'archibald_lp21_ajax_curriculum_data',
      'access callback' => 'archibald_lp21_curriculum_data_access',
      'type' => MENU_CALLBACK,
    ),
    'admin/config/services/archibald-lp21' => array(
      'title' => 'Archibald Lehrplan 21',
      'description' => 'Configure Archibald Lehrplan 21 settings.',
      'access arguments' => array('administer archibald'),
      'page callback' => 'drupal_get_form',
      'page arguments' => array('archibald_lp21_admin_settings_form'),
    ),
    'admin/archibald-lp21/load-lp21-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_lp21_load_data_trigger',
      'type' => MENU_CALLBACK,
    ),
  );
}

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

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

/**
 * Implements hook_field_widget_info().
 */
function archibald_lp21_field_widget_info() {
  return array(
    'archibald_lp21' => array(
      'label' => t("LP21 curriculum"),
      'field types' => array('archibald_lp21'),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function archibald_lp21_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_lp21');
  $form['#attached']['js']["$path/js/archibald_lp21.templates.js"] = array('weight' => -100);
  $form['#attached']['js'][] = "$path/js/archibald_lp21.js";
  $form['#attached']['css'][] = "$path/css/archibald_lp21.css";

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

  $element['cycles'] = array(
    '#type' => 'textfield',
    '#default_value' => isset($item['cycles']) ? $item['cycles'] : NULL,
    '#maxlength' => 20,
    '#weight' => 100,
    '#attributes' => array(
      'class' => array('archibald-lp21-editor__cycles'),
    ),
  );

  return $element;
}

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

/**
 * Implements hook_field_formatter_view().
 */
function archibald_lp21_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_lp21_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]['tree'] = array(
          '#markup' => theme('archibald_curricula_summary', array(
            'items' => $data,
          )),
          '#prefix' => '<div class="archibald-lp21-field__tree">',
          '#suffix' => '</div>',
        );
      }

      if (!empty($item['cycles'])) {
        $elements[$delta]['cycles'] = array(
          '#markup' => '<span class="field-label archibald-lp21-field-label">' . t("Cycles") . ':</span> ' . implode(', ',
            array_map(
              'check_plain',
              array_map('trim', explode(',', $item['cycles']))
            )
          ),
          '#prefix' => '<div class="archibald-lp21-field__cycles">',
          '#suffix' => '</div>',
        );
      }

      if (empty($item['item_ids']) && empty($item['cycles'])) {
        $elements[$delta] = '';
      }
      else {
        $elements[$delta]['#prefix'] = '<div class="archibald-lp21-field">';
        $elements[$delta]['#suffix'] = '</div>';
      }
    }
  }
  else {
    drupal_set_message(archibald_curricula_curricula_load_error(t("Lehrplan 21")), 'warning');
  }

  return $elements;
}

/**
 * Implements hook_archibald_validation_report_alter().
 */
function archibald_lp21_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 LP21 entries.
        $source = $lom->getField("{$match[0]}.source.name");
        if ($source == 'lp21') {
          // Prepare the error entry, if it doesn't exist.
          if (empty($report['archibald_lp21'])) {
            $report['archibald_lp21'] = array(
              'field_label' => t("LP21"),
              'errors' => array(),
            );
          }

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

/**
 * Implements hook_archibald_json_export_alter().
 */
function archibald_lp21_archibald_json_export_alter(&$json_data, $node) {
  // Load the item database and curriculum.
  $data = archibald_lp21_load_data();
  $curriculum = _archibald_lp21_get_curriculum();
  if (!$data || !$curriculum) {
    drupal_set_message(archibald_curricula_curricula_load_error(t("Lehrplan 21")), '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_lp21 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_curricula_json_curricula', array(
            'items' => $data,
            'langcode' => 'de',
          ))
        );
      }

      // Add cycle information. The cycle information goes into the
      // "classification" LOM field, not the curriculum. The reason is that, in
      // LP21, there's no such thing as a "cycle". It is an implicit meta-data,
      // associated with Kompetenzstufe. However, as we have to add it somewhere
      // (because reasons), we add it in the classification, as an "educational
      // level" purpose path.
      if (!empty($item['cycles'])) {
        $json_data->classification[] = array(
          'purpose' => array(
            'source' => 'LOMv1.0',
            'value' => 'educational level',
          ),
          'taxonPath' => array_map(function($item) {
            $cycle = trim($item);

            return array(
              'source' => array(
                'en' => 'lp21',
              ),
              'taxon' => array(
                array(
                  'id' => $cycle,
                  'entry' => array(
                    // LP21 is in German, so let's hard code this one in
                    // German.
                    'de' => sprintf("%d. Zyklus", $cycle),
                  ),
                ),
              ),
            );
          }, explode(',', $item['cycles'])),
        );
      }
    }
  }

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

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

  // Look for LP21 entries.
  if (!empty($data['curriculum'])) {
    $lom = new Educa\DSB\Client\Lom\LomDescription($data);
    $ids = array();

    // Recursively fetch all items and their IDs.
    $recursive = function($items) use (&$ids, &$recursive) {
      foreach ($items as $item) {
        if (isset($item['id'])) {
          $ids[] = $item['id'];
        }

        if (!empty($item['childTaxons'])) {
          $recursive($item['childTaxons']);
        }
      }
    };

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

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

  // Cycle data is in the classification property.
  if (!empty($data['classification'])) {
    $lom = new Educa\DSB\Client\Lom\LomDescription($data);
    $cycles = array();

    foreach ($lom->getField('classification') as $i => $classification) {
      foreach ($lom->getField("classification.{$i}.taxonPath") as $j => $taxon_path) {
        if (
          $lom->getField("classification.{$i}.taxonPath.{$j}.source") == 'lp21' &&
          $lom->getField("classification.{$i}.purpose.value") == 'educational level'
        ) {
          // This one contains our cycle information.
          foreach ($lom->getField("classification.{$i}.taxonPath.{$j}.taxon") as $taxon) {
            if (is_numeric($taxon['id'])) {
              $cycles[] = $taxon['id'];
            }
          }
        }
      }
    }

    if (!empty($cycles)) {
      $node->archibald_lp21[LANGUAGE_NONE][0]['cycles'] = implode(',', array_unique($cycles));
    }
  }
}

/**
 * Implements hook_archibald_json_import_revision_alter().
 */
function archibald_lp21_archibald_json_import_revision_alter($node, $context) {
  archibald_lp21_archibald_json_import_alter($node, $context);
}

/**
 * Implements hook_form_FORM_ID_alter() for archibald_lomch_description_node_form.
 */
function archibald_lp21_form_archibald_lomch_description_node_form_alter(&$form, $form_state) {
  if (module_exists('archibald_help')) {
    $title = &$form['archibald_lp21'][LANGUAGE_NONE][0]['#title'];
    $value = $form['archibald_lp21'][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("Lehrplan 21 is the common curriculum for compulsory education in the German and multilingual cantons of Switzerland."),
    ));
  }
}

/**
 * Settings form callback.
 */
function archibald_lp21_admin_settings_form($form, $form_state) {
  $form['archibald_lp21_curriculum_variant'] = array(
    '#title' => t("Lehrplan 21 variant"),
    '#type' => 'radios',
    '#description' => t("Choose the Lehrplan 21 variant you want to use when cataloging."),
    '#options' => array(
      'V_EF' => t("1st foreign language English (with Latin)"),
      'V_FE' => t("1st foreign language French (with Latin)"),
    ),
    '#default_value' => variable_get('archibald_lp21_curriculum_variant', 'V_EF'),
  );
  return system_settings_form($form);
}

/**
 * Access callback.
 */
function archibald_lp21_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_lp21_ajax_curriculum_data() {
  if (!$data = archibald_lp21_load_data()) {
    drupal_add_http_header('status', '404');
    $data = array(
      'error' => TRUE,
      'message' => archibald_curricula_curricula_load_error(t("Lehrplan 21")),
    );
  }
  drupal_json_output($data);
}

/**
 * Page callback: trigger the batch for importing the LP21 curriculum.
 */
function archibald_lp21_load_data_trigger() {
  // Refresh the curricula cache.
  archibald_lp21_load_data(TRUE);
  drupal_goto('admin/reports/status');
}

/**
 * Load the LP21 data.
 *
 * Load the LP21 data from the official LP21 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 LP21 data, or false if the cache is empty. The cache can only be
 *    refreshed by explicitly setting reset to true.
 */
function archibald_lp21_load_data($reset = FALSE) {
  $variant = variable_get('archibald_lp21_curriculum_variant');
  $cid = 'archibald_lp21:item_db:' . $variant;

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

  if (!$curriculum = _archibald_lp21_get_curriculum($reset)) {
    return FALSE;
  }

  $data = array();

  // Prepare a function for recursively parsing the curriculum tree.
  $recursive_fetch = function($term) use(&$data, &$recursive_fetch) {
    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' => explode("\n", trim($description->name->de)),
          'data' => array_filter(array(
            'lp21Code' => $child->getCode(),
            'lp21Url' => $child->getUrl(),
            'lp21Cantons' => $child->getCantons(),
            'lp21Cycles' => $child->getCycles(),
            'lp21Version' => $child->getVersion(),
          )),
          'hasChildren' => $child->hasChildren(),
        );

        if ($child->hasChildren()) {
          $recursive_fetch($child);
        }
      }
    }
  };

  // Load the tree.
  $recursive_fetch($curriculum->getTree());

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

  // Cache and return.
  cache_set($cid, $data, 'cache_curricula');

  return $data;
}

/**
 * Get an instance of the LP21Curriculum class.
 *
 * @param bool $reset
 *     (optional) Whether to reset the curriculum data cache. Defaults to false.
 *
 * @return Educa\DSB\Client\Curriculum\LP21Curriculum
 */
function _archibald_lp21_get_curriculum($reset = FALSE) {
  $variant = variable_get('archibald_lp21_curriculum_variant');
  $cid = 'archibald_lp21:curriculum_data:' . $variant;
  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->get('/protected-file/lp21_curriculum.xml');
    }
    catch(Exception $e) {
      watchdog_exception('archibald_lp21', $e);
      return;
    }

    $curriculum_data = Educa\DSB\Client\Curriculum\LP21Curriculum::parseCurriculumXml($response->getBody(), $variant);

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

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

  return $curriculum;
}
