<?php

/**
 * @file
 * Custom Rules actions for Course Planner.
 *
 * Note that these actions are not intended for general use, but for some
 * scripts used by this module only. Sorry. (For most actions it would be
 * possible to use the 'bundle' property to specify that it only works for
 * selected content types, but then you would have to add conditions to all the
 * Rules components to verify that the parameters fit. Since these actions are
 * not ment for general use, the custom-coded verification seemed more
 * appropriate.)
 */

/**
 * Implements hook_rules_action_info().
 */
function courseplanner_rules_action_info() {
  $actions = array(
    'cp_add_sections' => array(
      'label' => t('Create new sections for a course outline'),
      'group' => t('Course planner'),
      'parameter' => array(
        'outline' => array(
          'type' => 'node',
          'label' => t('Course outline'),
          'save' => TRUE,
        ),
        'sections_info' => array(
          'type' => 'list<text>',
          'label' => t('Sections and their sizes'),
          'description' => t('Add the name of sections, one per line. If you want a size larger than one, add size separated by a comma (eg. "My section, 5").'),
        ),
      ),
    ),
    'cp_clear_lesson' => array(
      'label' => t('Mark a lesson empty'),
      'group' => t('Course planner'),
      'parameter' => array(
        'lesson' => array(
          'type' => 'node',
          'label' => t('Lesson'),
          'save' => TRUE,
        ),
      ),
    ),
    'cp_repeat_lesson' => array(
      'label' => t('Repeat lesson'),
      'group' => t('Course planner'),
      'parameter' => array(
        'lesson' => array(
          'type' => 'node',
          'label' => t('Lesson'),
        ),
        'end_date' => array(
          'type' => 'date',
          'label' => t('Last date'),
        ),
      ),
    ),
    'cp_copy_lesson_to_weeks' => array(
      'label' => t('Copy lessons to weeks'),
      'group' => t('Course planner'),
      'parameter' => array(
        'lessons' => array(
          'type' => 'list<node>',
          'label' => t('Lessons'),
        ),
        'weeks' => array(
          'type' => 'list<text>',
          'label' => t('Weeks'),
        ),
      ),
    ),
    'cp_fill_lessons' => array(
      'label' => t('Fill lessons with course outline'),
      'group' => t('Course planner'),
      'parameter' => array(
        'lessons' => array(
          'type' => 'list<node>',
          'label' => t('List of lessons'),
        ),
        'outline' => array(
          'type' => 'node',
          'label' => t('Course outline'),
        ),
      ),
    ),
    'cp_fill_lessons_with_section' => array(
      'label' => t('Fill lessons with a section'),
      'group' => t('Course planner'),
      'parameter' => array(
        'lessons' => array(
          'type' => 'list<node>',
          'label' => t('List of lessons'),
        ),
        'section' => array(
          'type' => 'node',
          'label' => t('Section'),
        ),
      ),
    ),
    'cp_lesson_set_date' => array(
      'label' => t('Set start date for a lesson'),
      'group' => t('Course planner'),
      'parameter' => array(
        'lesson' => array(
          'type' => 'node',
          'save' => TRUE,
          'label' => t('Lesson'),
        ),
        'date' => array(
          'type' => 'date',
          'label' => t('Start date'),
        ),
      ),
    ),
    'cp_shift_lessons' => array(
      'label' => t('Push lesson content down'),
      'group' => t('Course planner'),
      'parameter' => array(
        'lessons' => array(
          'type' => 'list<node>',
          'label' => t('Lessons'),
        ),
        'steps' => array(
          'type' => 'integer',
          'label' => t('Number of steps'),
          'description' => t('Enter a negative number to shift lessons up.'),
        ),
      ),
    ),
    'cp_lessons_import' => array(
      'label' => t('Import lessons'),
      'group' => t('Course planner'),
      'parameter' => array(
        'offering' => array(
          'type' => 'node',
          'label' => t('Course offering'),
        ),
        'dates' => array(
          'type' => 'list<text>',
          'label' => t('Start dates'),
          'description' => t('Enter one start date/time per line. All formats parsable by strtotime() are accepted.'),
        ),
      ),
    ),
  );
  return $actions;
}

/**
 * Helper function that verifies that the nodes in a list are of a given type.
 *
 * Any nodes that aren't of the wanted type will be removed from the list. If
 * any nodes are removed, a warning message will be displayed to the user. The
 * resulting nodes are processed into a non-associative arrary, meaning that the
 * keys start on 0 and go up from there. No key-by-id.
 *
 * @param $entity_list
 *   A list of nodes, as passed from Rules.
 * @param $type
 *   The expected node type.
 */
function courseplanner_verify_type(array &$node_list, $type) {
  // Clean the list of any nodes that aren't actually lessons. This also builds
  // the array of lessons we will use later, where the keys are serials.
  $errors = 0;
  foreach ($node_list as $key => $node) {
    if ($node->type != $type) {
      unset($node_list[$key]);
      $errors++;
    }
  }

  // Process the list so the keys are in order again. (Rules technically works
  // with lists, not arrays.)
  $node_list = array_values($node_list);

  if ($errors) {
    // Alert the user that there were incorrect objects in the list.
    drupal_set_message(t('@n items were ignored, since they are not of type @type.', array('@n' => $errors, '@type' => $type)), 'warning');
  }
}

/**
 *
 */
function courseplanner_get_section_by_title($title, $account_id = NULL) {
  // Get the default user ID, if necessary.
  if (is_null($account_id)) {
    global $user;
    $account_id = $user->uid;
  }

  // Try finding a matching section created by the given user.
  $query = new EntityFieldQuery();
  $query->entityCondition('entity_type', 'node')
    ->entityCondition('bundle', 'cp_section')
    ->propertyCondition('title', $title)
    ->propertyCondition('uid', $user->uid)
    ->range(0, 1)
    ->execute();
  // If we have any results, use the first one as the section.
  if (isset($query->ordered_results)) {
    return $query->ordered_results[0]->entity_id;
  }

  // If no results, check if there is a section with this name created by
  // someone else.
  $query = new EntityFieldQuery();
  $query->entityCondition('entity_type', 'node')
    ->entityCondition('bundle', 'cp_section')
    ->propertyCondition('title', $title)
    ->range(0, 1)
    ->execute();
  if (isset($query->ordered_results)) {
    return $query->ordered_results[0]->entity_id;
  }

  // If no hits at all, return FALSE.
  return FALSE;
}

/**
 * Action callback, creating sections and adding them to a course outline.
 *
 * @param $outline
 *   A node object for a course outline.
 * @param $sections_info
 *   An array of strings, containing titles for the new sections. Each title may
 *   be appended with a size, like so: "My section, 5".
 */
function cp_add_sections($outline, $sections_info) {
  // Verify that the course outline is actually a course outline.
  if ($outline->type != 'cp_outline') {
    drupal_set_message(t('Sections can only be added to course outlines.'), 'warning');
    return;
  }

  foreach ($sections_info as $section_info) {
    // Build a title and a size, taking any trailing section size into
    // consideration.
    $pieces = explode(',', $section_info);
    if (ctype_digit(trim(end($pieces)))) {
      $size = trim(array_pop($pieces));
      $title = implode(',', $pieces);
    }
    else {
      $size = 1;
      $title = $section_info;
    }

    // Take care of the case of empty lines by just skipping them.
    if (!$title) {
      continue;
    }

    // If the line starts with an asterisk, we should try and fetch an existing
    // section with the given title.
    global $user;
    if (substr($title, 0, 1) == '*') {
      $title = trim(substr($title, 1));
      $section_nid = courseplanner_get_section_by_title($title);
    }
    // If not, we should create a new section.
    else {
      // Create a new section, so we can rerence it from the outline.
      $section = entity_create('node', array('type' => 'cp_section', 'title' => $title, 'uid' => $user->uid));
      entity_save('node', $section);
      $section_nid = $section->nid;
    }

    // Add the section, and its size, to the field collection in the outline.
    // Thanks to dale42 and for the guide at http://drupal.org/node/1477186
    // explaining how to add field collection items.
    $values = array(
      'field_name' => 'cp_sections',
      // @TODO: Check if multilingual sites will need language-specific settings.
      'cp_section' => array(LANGUAGE_NONE => array(array('target_id' => $section_nid))),
      'cp_size' => array(LANGUAGE_NONE => array(array('value' => $size))),
    );
    $entity = entity_create('field_collection_item', $values);
    $entity->setHostEntity('node', $outline);
    $entity->save();
  }

  if (isset($section)) {
    drupal_set_message(t('Sections added. You may add section descriptions by using the edit links on the course outline page.'));
  }
}

/**
 * Removes the title of a lesson and clears any section reference.
 *
 * @param $lesson
 *   The lesson node object.
 */
function cp_clear_lesson($lesson) {
  // Verify that $lesson is actually a lesson node.
  if ($lesson->type != 'cp_lesson') {
    drupal_set_message(t('%lesson is not a valid lesson..', array('%lesson' => $lesson->title)), 'warning');
    return;
  }

  $lesson->title = '';
  $lesson->cp_section = array();
}

/**
 * Action callback for creating repeated lessons.
 *
 * @param $lesson
 *   A node object representing a lesson. Must be of the content type cp_lesson.
 * @param $end_date
 *   A timestamp representing the last date for the repetition.
 */
function cp_repeat_lesson($lesson, $end_date) {
  // Verify that $lesson is actually a lesson node.
  if ($lesson->type != 'cp_lesson') {
    drupal_set_message(t('%lesson is not a valid lesson, and cannot be repeated.', array('%lesson' => $lesson->title)), 'warning');
    return;
  }

  // Get start date and set repeat interval to one week.
  $date = $lesson->cp_lesson_date[LANGUAGE_NONE][0];
  $interval = 60 * 60 * 24* 7;

  $count = 0;
  while ($date['value'] <= ($end_date - $interval)) {
    $count++;
    // Add one week to start date and, if set, end date.
    $date['value'] += $interval;
    if (isset($date['value2'])) {
      $date['value2'] += $interval;
    }

    // Create a new lesson with the new dates.
    $new_lesson = entity_create('node', array('type' => 'cp_lesson'));
    $new_lesson->cp_lesson_date[LANGUAGE_NONE][0] = $date;
    $new_lesson->cp_course_offering = $lesson->cp_course_offering;
    $new_lesson->uid = $lesson->uid;
    entity_save('node', $new_lesson);

    // Abort the repetition after 52+ times. Chances are that someone is
    // repeating lessons with a whacky date somewhere. (Not that it ever
    // happened to the module maintainer...)
    if ($count > 52) {
      drupal_set_message(t('Maximum lesson repetition is one year. (Last date was @date.)', array('@date' => date('Y-m-d', $date))), 'warning');
      return;
    }
  }
}

/**
 * Action callback for cp_copy_lessons_to_weeks().
 *
 * This action takes a list of lessons and a list of weeks, and copies each
 * lesson into each week.
 *
 * @param $lessons
 *   A list of nodes of the type cp_lesson.
 * @param $weeks
 *   A list of strings representing weeks, provided on the form "2013 30".
 */
function cp_copy_lesson_to_weeks($lessons, $weeks) {
  courseplanner_verify_type($lessons, 'cp_lesson');
  if (count($lessons) == 0) {
    drupal_set_message(t('There are no valid lessons to copy.'), 'warning');
    return;
  }

  $year_now = date('Y');
  $week_now = date('W');
  foreach ($weeks as $week) {
    // Parse the week input to a year and a week.
    $week = explode(' ', $week);
    // If only week is set, assume that it is now or closest future week with
    // that number.
    if (count($week) == 1) {
      $week = $week[0];
      $year = $year_now + ($week_now > $week);
    }
    else {
      $year = $week[0];
      $week = $week[1];
    }
    if ($year < 1950 || $week > 52 || $week < 1) {
      drupal_set_message(t('Cannot parse "@year @week", sorry. Please enter year and week number separated with space, on on each line', array('@year' => $year, '@week' => $week)), 'warning');
      continue;
    }
    // The week number must be two-digit to be understood by strtotime.
    $week = (string) $week;
    if (strlen($week) == 1) {
      $week = "0$week";
    }

    // Loop through each lesson and copy it to the $week.
    foreach ($lessons as $lesson) {
      // Create a new lesson and set some default values.
      $empty_lesson = entity_create('node', array(
        // Copy over all the essential data from the first lesson.
        'type' => 'cp_lesson',
        'uid' => $lesson->uid,
        'cp_lesson_date' => $lesson->cp_lesson_date,
        'cp_course_offering' => $lesson->cp_course_offering,
      ));

      // Get the relevant timestamps. This is some black magic strtotime().
      $start_time = date('H:i +N', $lesson->cp_lesson_date[LANGUAGE_NONE][0]['value']);
      $empty_lesson->cp_lesson_date[LANGUAGE_NONE][0]['value'] = strtotime($year . 'W' . "$week $start_time days -1 day");
      // Check for end date as well.
      if (isset($empty_lesson->cp_lesson_date[LANGUAGE_NONE][0]['value2'])) {
        $end_time = date('H:i +N', $lesson->cp_lesson_date[LANGUAGE_NONE][0]['value2']);
        $empty_lesson->cp_lesson_date[LANGUAGE_NONE][0]['value2'] = strtotime($year . 'W' . "$week $end_time days -1 day");
      }

      entity_save('node', $empty_lesson);
    }
  }
}

/**
 * Action callback. Points a list of lessons to the sections in an outline.
 *
 * This function will read the sections in the course outline, and their sizes,
 * and update the list of lessons so that they refer to the sections. If the
 * first section in the outline has size 3, the three first lessons will point
 * to that section before moving on to the next section in the outline.
 *
 * @param $lessons
 *   An array with node objects. Must be of the content type cp_lesson.
 * @param $outline
 *   A node object representing a course outline.
 */
function cp_fill_lessons($lessons, $outline) {
  // Verify that the course outline is actually a course outline.
  if ($outline->type != 'cp_outline') {
    drupal_set_message(t('@outline is not a valid course outline.', array('@outline' => $outline->title)), 'warning');
    return;
  }

  // Verify the list of lessons.
  courseplanner_verify_type($lessons, 'cp_lesson');
  // If no valid lesson is found, display an error message and quit.
  if (count($lessons) == 0) {
    drupal_set_message(t('None of the selected items are valid lessons, sorry.'), 'warning');
    return;
  }

  // Get the first valid lesson in the list.
  $lesson = reset($lessons);

  // Get the course offering for the first lesson. We assume that all lessons
  // belong to the same course offering (which should really be the case).
  $offering = entity_load_single('node', $lesson->cp_course_offering[LANGUAGE_NONE][0]['target_id']);

  // Build an array with section data, to use with the lessons.
  $i = 0;
  foreach ($outline->cp_sections[LANGUAGE_NONE] as $section) {
    $section_info = field_collection_field_get_entity($section);
    $section_title = entity_label('node', entity_load_single('node', $section_info->cp_section[LANGUAGE_NONE][0]['target_id']));
    for ($repeat = 1; $repeat <= $section_info->cp_size[LANGUAGE_NONE][0]['value']; $repeat++) {
      $section_data[$i]['id'] = $section_info->cp_section[LANGUAGE_NONE][0]['target_id'];
      if ($section_info->cp_size[LANGUAGE_NONE][0]['value'] > 1) {
        $section_data[$i]['lesson_title'] = t('@section-title (@repeat of @size)', array('@section-title' => $section_title, '@repeat' => $repeat, '@size' => $section_info->cp_size[LANGUAGE_NONE][0]['value']));
      }
      else {
        $section_data[$i]['lesson_title'] = $section_title;
      }
      $i++;
    }
  }

  // Walk through all the lessons or all the section parts, whichever is the
  // least, and update the lessons.

  for ($i = 0; $i < min(array(count($lessons), count($section_data))); $i++) {
    $lessons[$i]->cp_section[LANGUAGE_NONE][0]['target_id'] = $section_data[$i]['id'];
    $lessons[$i]->title = $section_data[$i]['lesson_title'];
    entity_save('node', $lessons[$i]);
  }

  // Update the course offering, to point to the new outline.
  $offering->cp_offering_outline[LANGUAGE_NONE][0]['target_id'] = $outline->nid;
  entity_save('node', $offering);

  if (count($section_data) > count($lessons)) {
    drupal_set_message(t('Updated @lessons lessons. @diff lesson(s) were missing to fit the whole outline.', array('@lessons' => count($lessons), '@diff' => count($section_data) - count($lessons))));
  }
  else {
    drupal_set_message(t('Added all @lessons lessons in the outline. @diff lesson(s) overshooting lessons were left untouched.', array('@lessons' => count($section_data), '@diff' => count($lessons) - count($section_data))));
  }
}

/**
 * Action callback. Points a list of lessons to a single section.
 *
 * This function updates all the lessons to point to the given section, and also
 * updates the lesson titles to fit the section title.
 *
 * @param $lessons
 *   An array with node objects. Must be of the content type cp_lesson.
 * @param $section
 *   A node object representing a course section.
 */
function cp_fill_lessons_with_section($lessons, $section) {
  // Verify that the course section is actually a course section.
  if ($section->type != 'cp_section') {
    drupal_set_message(t('@section is not a valid course section.', array('@section' => $section->title)), 'warning');
    return;
  }

  // Verify the list of lessons.
  courseplanner_verify_type($lessons, 'cp_lesson');
  // If no valid lesson is found, display an error message and quit.
  if (count($lessons) == 0) {
    drupal_set_message(t('None of the selected items are valid lessons, sorry.'), 'warning');
    return;
  }

  foreach ($lessons as $i => $lesson) {
    $lesson->cp_section[LANGUAGE_NONE][0]['target_id'] = $section->nid;
    if (count($lessons) > 1) {
      $lesson->title = t('@section-title (@repeat of @size)', array('@section-title' => $section->title, '@repeat' => $i + 1, '@size' => count($lessons)));
    }
    else {
      $lesson->title = $section->title;
    }
    entity_save('node', $lesson);
  }
}

/**
 * Action callback, setting a start date for an empty lesson node.
 *
 * This function shouldn't really be necessary, but it is here as a quick fix.
 * See http://drupal.org/node/1862184
 *
 * @param $lesson
 *   A lesson node.
 * @param $date
 *   A unix timestamp, or a string that may be converted to a unix timestamp.
 */
function cp_lesson_set_date($lesson, $date) {
  // Take care of non-numeric date parameters, for example from a date popup.
  if (!is_numeric($date)) {
    $date = strtotime($date);
  }

  // Verify that $lesson is actually a lesson node.
  if ($lesson->type != 'cp_lesson') {
    drupal_set_message(t('%lesson is not a valid lesson.', array('%lesson' => $lesson->title)), 'warning');
    return;
  }

  $lesson->cp_lesson_date[LANGUAGE_NONE][0] = array('value' => $date);
}

/**
 * Action callback for moving lesson content further down in a course outline.
 *
 * This function inserts a number of empty lessons on top of a list, shifts the
 * dates of the existing lessons donw, and cuts off the overshooting lessons
 * from the end. This function can also be used to shift lesson content up.
 *
 * @param $lessons
 *   An array with lesson nodes.
 * @param $steps
 *   The number of steps to push lessons down the list. If a negative number is
 *   used, the lesson content will be moved up instead.
 */
function cp_shift_lessons($lessons, $steps) {
  // Make sure that the lessons are actually lessons.
  courseplanner_verify_type($lessons, 'cp_lesson');

  // Check if we should shift lessons up or down.
  $steps = (int) $steps;
  if ($steps > 0) {
    $mode = 'down';
  }
  if ($steps < 0) {
    // Tricky trick: If we are to move the lessons up the list, flip the list
    // and move them down instead.
    $mode = 'up';
    $steps = -$steps;
    $lessons = array_reverse($lessons);
  }

  // If the number of steps isn't well defined, alert the user and quit.
  if (!isset($mode)) {
    drupal_set_message(t('@steps is not a valid number of steps to shift the lessons. (Must be a non-zero integer.)', array('@steps' => $steps)), 'warning');
    return;
  }

  // If we are asked to shift the list so much that all lessons will be pushed
  // out, display a warning and quit.
  if ($steps >= count($lessons)) {
    drupal_set_message(t('The list of lessons is to small to shift @steps step(s).', array('@steps' => $steps)), 'warning');
    return;
  }

  // Create $steps new lessons and give them dates mathing the $steps first
  // lessons in the list.
  for ($i = 0; $i < $steps; $i++) {
    $empty_lesson = entity_create('node', array(
      // Copy over all the essential data from the original lessons. The rest
      // should be skipped – these are empty lessons.
      'type' => 'cp_lesson',
      'uid' => $lessons[$i]->uid,
      'cp_lesson_date' => $lessons[$i]->cp_lesson_date,
      'cp_course_offering' => $lessons[$i]->cp_course_offering,
    ));
    entity_save('node', $empty_lesson);
  }

  // Shift the dates for the lessons in the list $step steps, but skip the last
  // $step ones. These will be removed.
  for ($i = 0; $i < count($lessons) - $steps; $i++) {
    $lessons[$i]->cp_lesson_date = $lessons[$i + $steps]->cp_lesson_date;
    entity_save('node', $lessons[$i]);
  }

  // Remove the last $step lessons in the list.
  $ids = array();
  for ($i = count($lessons) - $steps; $i < count($lessons); $i++) {
    $ids[] = $lessons[$i]->nid;
  }
  entity_delete_multiple('node', $ids);
}

/**
 * Adds a number of lessons to a course offering, based on a text import.
 *
 * @param $offering
 *   The course offering the lessons should be attached to.
 * @param $dates
 *   A list of dates, represented by strings.
 */
function cp_lessons_import($offering, $dates) {
  // Verify that $offering is actually a course offerig.
  if ($offering->type != 'cp_offering') {
    drupal_set_message(t('%offering is not a valid course offering.', array('%offering' => $offering->title)), 'warning');
    return;
  }

  global $user;
  $skips = 0;
  $title = '';

  foreach ($dates as $date) {
    // Check if the line starts with a "#". If so, treat it as a title for the
    // next lesson.
    if (substr($date, 0, 1) == '#') {
      $title = trim(substr($date, 1));
      $skips++;
      continue;
    }

    // Convert the date into a unix timestamp, if necessary.
    $parts = explode(',', $date);
    if (!is_numeric($parts[0])) {
      $parts[0] = strtotime($parts[0]);
    }
    // If there still isn't a sensible timestamp, skip this row.
    if (!is_numeric($parts[0])) {
      drupal_set_message(t('Skipped date "@date": unable to parse.', array('@date' => $date)), 'warning');
      $skips++;
      continue;
    }

    // Check if we have an end date set. If it is relative, make it relative to
    // the start date.
    if (isset($parts[1]) && !is_numeric($parts[1])) {
      $parts[1] = strtotime($parts[1], $parts[0]);
    }
    if (isset($parts[1]) && !is_numeric($parts[1])) {
      unset($parts[1]);
    }

    // Create a new lesson and add appropriate data.
    $new_lesson = entity_create('node', array('type' => 'cp_lesson'));
    $new_lesson->title = $title;
    $title = '';
    $new_lesson->cp_lesson_date[LANGUAGE_NONE][0]['value'] = $parts[0];
    if (isset($parts[1])) {
      $new_lesson->cp_lesson_date[LANGUAGE_NONE][0]['value2'] = $parts[1];
    }
    $new_lesson->cp_course_offering[LANGUAGE_NONE][0]['target_id'] = $offering->nid;
    $new_lesson->uid = $user->uid;
    entity_save('node', $new_lesson);
  }

  drupal_set_message(t('Added @count empty lessons.', array('@count' => count($dates) - $skips)));
}
