<?php
/**
 * The Quizzler module allows you to associate quizzes with node instances.
 *
 * Registered (for now) users may take the quiz multiple times.  Quizzler tracks their
 * scores, and allows for users to see their own scores and administrators to see everyone's.
 *
 */

/**
 * Implement hook_permission()
 */
function quizzler_permission() {
  return array(
    'administer quizzler' => array(
      'description' => t('Determine which content types may have quizzes associated with them.  Manage basic Quizzler settings.'),
      'title' => t('Administer Quizzler'),
    ),
  	'configure quizzler' => array(
      'description' => t('Add Quizzler questions to nodes.'),
      'title' => t('Configure Quizzler'),
    ),
  	'access quizzler' => array(
      'description' => t('Access quizzler quizzes.'),
      'title' => t('Access Quizzler'),
    ),
  	'access quizzler reports' => array(
      'description' => t('Access own Quizzler reports.  Users with "administer" or "configure" quizzler permission will also be able to access reports for all users.'),
      'title' => t('Access Quizzler reports'),
    )
  );
}

/**
 * Implement hook_theme()
 */
function quizzler_theme($existing, $type, $theme, $path) {
  return array(
    // theme an array of results
    'quizzler_results'	=> array(
      'variables'	=> array('results' => array()),
      'template'	=> 'templates/quizzler-results'
    ),
		'quizzler_stats'	=> array(
      'variables'	=> array('results' => array()),
      'template'	=> 'templates/quizzler-stats'
    )
  );
}

function template_preprocess_quizzler_results(&$variables) {
  $totals_table = array(
    'header'	=> array(
			array('data' => t("Total questions")),
			array('data' => t("# Correct")),
			array('data' => t("# Answered")),
			array('data' => t("Avg correct (%)")),
			array('data' => t('Avg correct (grade)'))
		),
		'rows' => array(
		  array(
		    $variables['results']['total_questions'],
		    $variables['results']['correct'],
		    $variables['results']['answered'],
		    $variables['results']['percent'],
		    $variables['results']['grade']
		  )
		)
	);

	$totals = array(
    '#type'		      => 'fieldset',
    '#title'	      => t("Overall results for this quiz"),
    '#collapsible'	=> TRUE,
    '#collapsed'		=> TRUE,
	  '#children'			=> theme('table', $totals_table),
		'#attached' => array(
      'js' => array(
        'misc/form.js',
        'misc/collapse.js',
      )
    ),
		'#attributes' => array(
    	'class' => array('collapsible', 'collapsed')
    )
  );

  $results_table = quizzler_quiz_attempt_table();
  foreach ($variables['results']['results'] as $uid => $user_data) {
    $account = user_load($uid);
    $results_table['rows'][] = array(
      array(
      	'data'    => t("Quizzes taken by !name", array('!name' => $account->name)),
        'colspan'	=> 6,
        'class'		=> array('quizzler-data-user')
      )
    );

    foreach ($user_data as $timestamp => $result) {
      $results_table['rows'][] = quizzler_quiz_attempt_row($timestamp, $result);
    }
  }

	$results = array(
    '#type'		      => 'fieldset',
    '#title'	      => t("Quizzes taken to date"),
    '#collapsible'	=> TRUE,
    '#collapsed'		=> TRUE,
	  '#children'			=> theme('table', $results_table),
		'#attached' => array(
      'js' => array(
        'misc/form.js',
        'misc/collapse.js',
      )
    ),
		'#attributes' => array(
    	'class' => array('collapsible', 'collapsed')
    )
	);

  $variables['totals_view'] = theme('fieldset', array('element' => $totals));
  $variables['results_view'] = theme('fieldset', array('element' => $results));
}

function template_preprocess_quizzler_stats(&$variables) {
	$quizzes = array();
	foreach ($variables['results'] as $result) {
		// generate overall results table for all quiz attempts
		$table = array(
			'rows' => array(
				array(
					array(
						'data' 		=> $result['node']->title,
						'colspan'	=> 5,
						'header'	=> TRUE
					)
				),
				array(
					array('data' => t("Attempts"), 'header' => TRUE),
					array('data' => t("# Correct"), 'header' => TRUE),
					array('data' => t("Total questions"), 'header' => TRUE),
					array('data' => t("Avg correct (%)"), 'header' => TRUE),
					array('data' => t('Avg correct (grade)'), 'header' => TRUE)
				),
				array(
					$result['attempts'],
					$result['total_questions'],
					$result['correct'],
					$result['percent'],
					$result['grade']
				)
			)
		);
		$quizzes[] = array('#markup' => theme('table', $table));

		// generate collapsed fieldset to show individual attempt results
		$attempts = quizzler_quiz_attempt_table();
		foreach ($result['quizzes'] as $timestamp => $result) {
			$attempts['rows'][] = quizzler_quiz_attempt_row($timestamp, $result);
		}

		$fieldset = array(
			'#type'		      => 'fieldset',
			'#title'	      => t("Quiz attempts"),
			'#collapsible'	=> TRUE,
			'#collapsed'		=> TRUE,
			'attempts'			=> array('#markup' => theme('table', $attempts)),
			'#attached' => array(
				'js' => array(
					'misc/form.js',
					'misc/collapse.js',
				)
			),
			'#attributes' => array(
				'class' => array('collapsible', 'collapsed')
			)
		);

		$quizzes[] = $fieldset;
	}

	$stats = array(
    '#type'		      => 'fieldset',
    '#title'	      => t("My quizzes"),
    '#collapsible'	=> TRUE,
    '#collapsed'		=> FALSE,
	  'quizzes'				=> $quizzes,
		'#attached' => array(
      'js' => array(
        'misc/form.js',
        'misc/collapse.js',
      )
    ),
		'#attributes' => array(
    	'class' => array('collapsible')
    )
  );

	$variables['quizzes'] = render($stats);
 }

/**
 *  Retrieve a table element to display quiz attempts for a user
 *
 *
 *  @return
 *  A table element
 */
function quizzler_quiz_attempt_table() {
	return array(
    'header'	=> array(
			array('data' => t("Date of quiz")),
			array('data' => t("Total questions")),
			array('data' => t("# Correct")),
			array('data' => t("# Answered")),
			array('data' => t("Avg correct (%)")),
			array('data' => t('Avg correct (grade)'))
		),
    'rows' => array()
  );
}
/**
 *  Retrieve a single row for a quiz attempt table
 *
 *  @param $timestamp
 *
 *  @param $result
 *
 *  @return
 *  A row element
 */
function quizzler_quiz_attempt_row($timestamp, $result) {
  return array(
		format_date($timestamp, 'short'),
		$result['total_questions'],
		$result['correct'],
		$result['answered'],
		$result['percent'],
		$result['grade']
	);
}

/**
 * Implement hook_node_view()
 */
function quizzler_node_view($node, $view_mode, $langcode) {
  // display the quiz, if active for this view mode
  quizzler_quiz_view($node, $view_mode, $langcode);

  // display quiz results, if active for this view mode and the user has taken the quiz at least one (or we're an administrator)
  quizzler_results_view($node, $view_mode, $langcode);
}

/**
 * Implement hook_user_view()
 *
 * Show Quizzler results for this user, if the current user is looking at their own
 * profile page.
 *
 * @todo
 * Allow other user's to see Quizzler results on all user profile pages if they have permission
 */
function quizzler_user_view($account, $view_mode, $langcode) {
	global $user;

	if ($user->uid != $account->uid) {
		return;
	}

  if (!user_access('access quizzler')) {
    return;
  }

  if (!quizzler_active_profile()) {
    return;
  }

	$results = quizzler_user_stats($account);
	if ($results) {
		$account->content['quizzler'] = array(
      '#markup' => theme('quizzler_stats', array('results' => $results)),
      '#weight' => quizzler_quiz_user_view_weight()
    );
	}
}

/**
 *
 * Display the quiz, if this is an active node type and an active node view mode.
 *
 * @param $node
 * @param $view_node
 * @param $langcode
 */
function quizzler_quiz_view($node, $view_mode, $langcode) {
  if (!user_access('access quizzler')) {
    return;
  }

  if (!quizzler_active_type($node->type)) {
    return;
  }

  if (!quizzler_active_quiz_view_mode($node->type, $view_mode)) {
    return;
  }

  $quiz = quizzler_quiz($node);
  if ($quiz && $quiz->fields) {
    $node->content['quizzler_quiz'] = drupal_get_form('quizzler_user_quiz', $quiz);
  }
}

/**
 *
 * Display the quiz results, if this is an active node type and an active node view mode.
 *
 * If this user has 'configure' or 'administer' permissions, show ALL results.
 * Otherwise, just show this user's results.
 *
 * @param $node
 * @param $view_node
 * @param $langcode
 */
function quizzler_results_view($node, $view_mode, $langcode) {
  global $user;

  $active = quizzler_active_type($node->type);
  if (!quizzler_active_type($node->type)) {
    return;
  }

  if (!quizzler_active_results_view_mode($node->type, $view_mode)) {
    return;
  }

  $results = array();
  if (user_access('administer quizzler') || user_access('configure quizzler')) {
    $results = quizzler_user_results($node->nid);
  }
  elseif (user_access('access quizzler')) {
    $results = quizzler_user_results($node->nid, $user->uid);
  }

  if ($results) {
    $node->content['quizzler_results'] = array(
      '#markup' => theme('quizzler_results', array('results' => $results)),
      '#weight' => quizzler_quiz_results_view_weight($node->type)
    );
  }
}

/**
 *
 * Form generator for a quiz.
 */
function quizzler_user_quiz($form, $form_state, $quiz) {
  global $user;

  if (!$user->uid) {
    drupal_set_message(t("The site administrator has configured a quiz so that it is visible to anonymous users.  However, only registered users may take the quiz."), 'warning');
    return array();
  }

  $node = node_load($quiz->nid);

  $form = array(
    '#weight'  => quizzler_quiz_view_weight($node->type),
    'quizzler' => array(
      '#type'		      => 'fieldset',
      '#title'	      => (isset($quiz->title))? $quiz->title : t("Take the quiz"),
      '#collapsible'	=> TRUE,
      '#collapsed'		=> TRUE,
      'qid'	=> array(
        '#type'		=> 'hidden',
        '#value'	=> $quiz->qid
      )
    )
  );

  if (isset($quiz->instructions)) {
    $form['quizzler']['instructions'] = array(
      '#type'		      => 'fieldset',
      '#title'	      => t("Instructions"),
      '#collapsed'    => FALSE,
      '#collapsible'  => FALSE,
      'instructions_text' => array(
        '#markup'		=> $quiz->instructions
      )
    );
  }

  foreach ($quiz->fields as $field) {
    $element = array(
      '#type'		=> 'fieldset',
      '#collapsed'    => FALSE,
      '#collapsible'  => FALSE,
      '#title'				=> $field->question
    );

    $fn = $field->module . "_quizzler_user_element";
    if (function_exists($fn)) {
      $element = $fn($field, $element);
    }
    $form['quizzler']['field-' . $field->delta] = $element;
  }

  $form['quizzler']['submit'] = array(
    '#type'	  => 'submit',
    '#value'	=> t("Submit your test and get your grade")
  );

  return $form;
}

/**
 *
 * Submit handler for the user quiz form
 *
 * To display the results of this quiz, you must configure the content type appropriately in the content type settings.
 *
 * @param $form
 * @param $form_state
 */
function quizzler_user_quiz_submit($form, $form_state) {
  global $user;

  if (!$user->uid) {
    drupal_set_message(t("Ignoring quiz submission from an anonymous user"), 'warning');
    return;
  }

  $quiz = quizzler_load_quiz($form_state['values']['qid']);
  if (!$quiz) {
    watchdog('quizzler', "Error processing quiz submission: no quiz found with this id.  (qid: !qid)", array('!qid' => $form_state['values']['qid']));
    drupal_set_message(t("There was an internal error processing this quiz."), 'error');
    return;
  }

  $errors    = array();
  $timestamp = time();
  foreach ($quiz->fields as $field) {
    $field->timestamp = $timestamp;
    $fn = $field->module . "_quizzler_user_element_submit";
    if (function_exists($fn)) {
      $error = $fn($field, $form_state);
      if ($error) {
        $errors[] = $error;
      }
    }
  }

  if ($errors) {
    foreach ($errors as $error) {
      drupal_set_message($error, 'error');
    }
  }
  else {
    $quiz_data = quizzler_quiz_data($timestamp, $quiz, $user);

    module_invoke_all('quizzler_quiz_submitted', $quiz_data);
    drupal_set_message(t("Your quiz results have been registered."));
  }
}

/**
 *
 * Transform a quiz and user object into a data hash suitable for public viewing / altering
 *
 * @param $timestamp
 * The timestamp for the quiz results to return
 *
 * @param $quiz
 * A quiz object
 *
 * @param $account
 * A user object
 *
 * @return
 * A keyed array with the following properties:
 *   - qid							The id of the quiz
 *   - nid							The nid of the node the quiz is attached to
 *   - title						The title of the quiz
 *   - account					The account taking the quiz
 *   - answered					The count of questions answered.
 *   - total_questions  The count of questions on the quiz
 *   - correct	  			Overall amount of questions answered correctly
 *   - percent					The percentage points correct
 *   - grade						The letter grade
 *   - timestamp				The timestamp associated with this quiz instance
 */
function quizzler_quiz_data($timestamp, $quiz, $account) {
  $data = array(
    'qid'		          => $quiz->qid,
    'nid'		          => $quiz->nid,
    'title'		        => $quiz->title,
    'account'	        => $account,
    'total_questions'	=> 0,
    'answered'	      => 0,
    'correct'		      => 0,
    'percent'		      => 0,
    'grade'			      => 'F',
    'timestamp'				=> $timestamp
  );

  $results = quizzler_user_results($quiz->nid, $account->uid);
  foreach ($results['results'] as $uid => $results_data) {
    if ($uid != $account->uid) {
      continue;
    }

    foreach ($results_data as $t => $d) {
      if ($t != $timestamp) {
        continue;
      }

      foreach ($d as $k => $v) {
        $data[$k] = $v;
      }
    }
  }

  return $data;
}

/**
 * Implement hook_form_alter()
 */
function quizzler_form_alter(&$form, &$form_state, $form_id) {
  if (!user_access('configure quizzler')) {
    return;
  }

  if (!isset($form['#node_edit_form']) || $form['#node_edit_form'] != TRUE) {
    return;
  }

  $active = variable_get("quizzler_active_types", array());
  $type   = $form['#bundle'];
  if (!quizzler_active_type($type)) {
    return;
  }

  // avoid dealing with new nodes, only allow settings on existing nodes
  if (!is_object($form['#node']) || !isset($form['#node']->nid)) {
    $form['quizzler'] = array(
      '#type'		      => 'fieldset',
      '#title'        => t("Quizzler configuration"),
      '#collapsible'	=> TRUE,
      '#collapsed'		=> TRUE,
      'quizzler_form' => array(
        '#markup'	=> "<div id='quizzler-form'>" . t("Please save this content before adding quiz settings.") . "</div>"
      )
    );

    return;
  }

  $quiz   = quizzler_quiz($form['#node']);
  $fields = quizzler_form_collect_fields($form_state);

  if (!$quiz) {
    $quiz = new stdClass();
    $quiz->qid = 'new';
  }

  if ($fields) {
    $quiz->fields = $fields;
  }

  if (isset($form_state['clicked_button'])) {
    $field_input = quizzler_form_clicked_field($form_state);
    if ($field_input) {
      // They're adding a new question, so create a dummy field
      $delta = (isset($quiz->fields))? count($quiz->fields) : 0;
      $field = (object) array(
        'fid'		    => 'new-' . $delta,
        'qid'		    => $quiz->qid,
        'delta'     => $delta,
        'module'    => $field_input,
        'question'	=> '',
        'required'	=> FALSE
      );
      module_invoke('quizzler', 'quizzler_field_load', $field);

      $quiz->fields[$delta] = $field;
    }
  }

  $form['quizzler'] = quizzler_quiz_form($form_state, $quiz);
  $form['#validate'][] = 'quizzler_form_validate';
  $form['#submit'][]   = 'quizzler_form_save';
}

/**
 *
 * Determine which of our new field buttons was clicked, if any.
 *
 * @param $form_state
 *
 * @return
 * A known plugin value, or an empty string
 */
function quizzler_form_clicked_field($form_state) {
  if (!isset($form_state['clicked_button'])) {
    return "";
  }

  $plugins = quizzler_fields();
  foreach ($plugins as $name) {
    if (!isset($form_state['values'][$name])) {
      continue;
    }

    if ($form_state['clicked_button']['#value'] == $form_state['values'][$name]) {
      return $name;
    }
  }

  return "";
}

/**
 * Validate form data for a quiz.
 *
 * Since fields can also set up AJAX triggers that will come through here, make sure we ignore those
 * AJAX calls, and only validate our own data.
 */
function quizzler_form_validate($form, &$form_state) {
  $validate = FALSE;
  // validate on submitting the whole form
  if ($form_state['clicked_button']['#value'] == $form_state['values']['submit']) {
    $validate = TRUE;
  }
  // validate for adding a new field element
  if (quizzler_form_clicked_field($form_state)) {
    $validate = TRUE;
  }

  if (!$validate) {
    return;
  }

  $fields = quizzler_form_collect_fields($form_state);
  foreach ($fields as $delta => $field) {
    if (!$field->question && !$field->delete) {
      unset($form_state['quizzler_element']);
      form_set_error($field->module . "_" . $field->delta . "_question", t("You must supply a question."));
    }

    module_invoke_all('quizzler_field_validate', $field);
  }
}

/**
 * Store form data for a quiz
 */
function quizzler_form_save($form, &$form_state) {
  if (!isset($form_state['values']['quizzler_qid'])) {
    return;
  }

  $qid = $form_state['values']['quizzler_qid'];
  if (!(int) $qid) {
    $quiz = quizzler_create_quiz($form_state['values']['nid']);
  }
  else {
    $quiz = quizzler_load_quiz($qid);
  }

  $fields = quizzler_form_collect_fields($form_state);

  foreach ($fields as $delta => $field) {
    $field->qid = $quiz->qid;
    quizzler_save_field($field);
    module_invoke_all('quizzler_field_save', $field);
  }
}

/**
 *
 * Store field values to the database.
 *
 * @param $field
 * A field object.  If this object does not have an fid property, a new record will be created.
 */
function quizzler_save_field(&$field) {
  if (!isset($field->fid) || !(int) $field->fid) {
    quizzler_insert_field($field);
  }
  else {
    quizzler_update_field($field);
  }
}

/**
 *
 * Insert a new field record into the database.
 *
 * @param $field
 */
function quizzler_insert_field($field) {
  if ($field->delete) {
    return;
  }

  $field->fid = db_insert('quizzler_field')
  ->fields(array(
      'qid'	      => $field->qid,
      'delta'	    => $field->delta,
      'module'	  => $field->module,
      'question'	=> $field->question,
      'required'	=> $field->required,
  ))
  ->execute();
}

/**
 *
 * Update an existing field record in the database.
 *
 * @param $field
 */
function quizzler_update_field($field) {
  if ($field->delete) {
    quizzler_delete_field($field);
    return;
  }

  db_update('quizzler_field')
    ->fields(array(
      'fid'				=> $field->fid,
      'qid'	      => $field->qid,
      'delta'	    => $field->delta,
      'module'	  => $field->module,
      'question'	=> $field->question,
      'required'	=> $field->required
      )
    )
    ->condition('fid', $field->fid, '=')
    ->execute();
}

/**
 *
 * Store an answer to the database.
 *
 * @param $field
 * The field that this is an answer to.
 *
 * @param $answer
 * The answer value.  The meaning of this value is determined by the field implementation.
 *
 * @param $correct
 * A boolean indicating whether this answer is correct or not.
 */
function quizzler_save_answer($field, $answer, $correct) {
  global $user;

  db_insert('quizzler_answer')
    ->fields(array(
      'fid'	      => $field->fid,
      'uid'	      => $user->uid,
      'timestamp'	=> $field->timestamp,
      'answer'	  => $answer,
      'correct'	  => ($correct)? 1 : 0
  ))
  ->execute();
}

/**
 *
 * Generate a fieldset definition array for a quiz
 *
 * @param $form_state
 * @param $quiz = NULL
 *
 * @return
 * A fieldset definition
 */
function quizzler_quiz_form($form_state, $quiz = NULL) {
  $fieldset = array(
    '#type'		=> 'fieldset',
    '#title'  => t("Quizzler configuration"),
    '#collapsible'	=> TRUE,
    '#collapsed'		=> TRUE,
    'quizzler_form' => array(
      '#prefix'	=> "<div id='quizzler-form'>",
      '#suffix'	=> "</div>"
      )
    );

  if ($quiz && isset($quiz->qid) && isset($quiz->fields) && $quiz->fields) {
    $fieldset['quizzler_form']['quizzler_qid'] = array(
      '#type'	 => 'hidden',
      '#value' => $quiz->qid
    );

    foreach ($quiz->fields as $delta => $field) {
      $fieldset['quizzler_form']['quizzler_fields'][$field->delta] = quizzler_field_element($field->module, $form_state, $field);
    }
  }
  else {
    $fieldset['quizzler_form']['no_quiz'] = array(
    	'#markup' => "<div class='messages'>" . t("No quiz is currently attached to this content.  You may add some questions with the buttons below to create a new quiz.") . "</div>"
    );
  }

  $fieldset['quizzler_form']['quizzler_buttons'] = array(
    '#type'		      => 'fieldset',
    '#weight'				=> 100,
    '#title'				=> t("Add a new question"),
    '#collapsible'	=> TRUE,
    '#collapsed'		=> TRUE
  );

  $fields = quizzler_fields();
  foreach ($fields as $plugin) {
    $fn_info     = $plugin . "_quizzler_field_info";
    if (!function_exists($fn_info)) {
      continue;
    }

    $info = $fn_info();
    $fieldset['quizzler_form']['quizzler_buttons'][$plugin] = array(
    '#type'		=> 'button',
    '#value'	=> $info['name'],
    '#ajax'		=> array(
      'callback'	=> 'quizzler_ajax',
      'wrapper'	=> 'quizzler-form'
      )
    );
  }

  if (!$fields) {
    $fieldset['quizzler_form']['quizzler_buttons'] = array(
  	'#markup' => "
    	<p>" . t("You must enable some Quizzler field modules before you can add questions to this node.") . "</p>
    	<p>" . l(t("Enable modules now"), "admin/modules") . "</p>
    "
    );
  }

  return $fieldset;
}

/**
 *
 * Create a field element for a given plugin.  Returns a default element if the plugin
 * doesn't implement a custom function, or if the passed plugin value isn't known.
 *
 * @param $plugin
 * @param $form_state
 * @param $field
 *
 * @return
 * A renderable array
 */
function quizzler_field_element($plugin, $form_state, $field) {
  $key = $field->module . '_' . $field->delta;

  $title = ((int) $field->fid)? $field->question : t("New question");
  if ($field->required) {
    $title .= " (" . t("required") . ")";
  }

  $fieldset = array(
    '#type'	  => 'fieldset',
    '#title'	=> $title,
    '#collapsible'	=> TRUE,
    '#collapsed'		=> ((int) $field->fid),
    $key . '_fid' => array(
      '#type'		=> 'hidden',
      '#value'	=> ($field)? $field->fid : "new"
    )
  );

  if (isset($field->delete) && $field->delete) {
    drupal_set_message(t("You have deleted the question '!question'.  This question will be permanently deleted when you save this form.", array('!question' => $field->question)), 'warning');
    $fieldset[$key . "_delete"] = array(
    	'#type'		=> 'hidden',
    	'#value'	=> 1
    );
  }
  else {
    $fieldset[$key . '_title'] = array(
    '#markup'	=> '<h5>' . $field->name . '</h5>'
    );
    $fieldset[$key . '_question'] = array(
      '#type'		        => 'textfield',
      '#title'	        => t("Question"),
      '#default_value'	=> ($field)? $field->question : "",
      '#weight'					=> 1
    );
    $fieldset[$key . '_required'] = array(
      '#type'		        => 'checkbox',
      '#title'	        => t("Required"),
      '#default_value'	=> ($field)? $field->required : "",
      '#weight'					=> 2
    );
    $fieldset[$key . '_delta'] = array(
      '#type'		        => 'weight',
      '#title'	        => t("Weight"),
      '#default_value'	=> ($field)? $field->delta : 0,
      '#description'		=> t('Optional. In the quiz form, the heavier items will sink and the lighter items will be positioned nearer the top.'),
      '#weight'					=> 3
    );
    $fieldset[$key . '_delete'] = array(
      '#type'		        => 'checkbox',
      '#title'	        => t("Delete"),
      '#default_value'	=> ($field && isset($field->delete))? $field->delete : FALSE,
      '#description'		=> t('Check this box to delete this question when the form is saved.'),
      '#weight'					=> 100
    );
  }

  foreach (module_implements('quizzler_field_element_alter') as $module) {
    $fieldset = module_invoke($module, 'quizzler_field_element_alter', $fieldset, $form_state, $field);
  }

  return $fieldset;
}

/**
 *
 * Collect submitted form data and create an array of field objects from it.
 *
 * @param $form_state
 *
 * @return
 * An array of field objects.
 */
function quizzler_form_collect_fields($form_state) {
  $plugins = quizzler_fields();
  $fields  = array();
  foreach ($plugins as $name) {
    $delta = 0;
    while (TRUE) {
      $key = $name . "_" . $delta;

      $in_field    = $key . "_fid";
      $in_question = $key . "_question";
      $in_required = $key . "_required";
      $in_delta    = $key . "_delta";
      $in_delete   = $key . "_delete";
      if (!isset($form_state['values'][$in_field])) {
        break;
      }

      $field = (object) array(
        'fid'		    => $form_state['values'][$in_field],
        'qid'		    => $form_state['values']['quizzler_qid'],
        'delta'     => $form_state['values'][$in_delta],
        'module'    => $name,
        'question'	=> $form_state['values'][$in_question],
        'required'	=> $form_state['values'][$in_required],
        'delete'		=> $form_state['values'][$in_delete]
      );
      module_invoke('quizzler', 'quizzler_field_load', $field);
      module_invoke_all('quizzler_field_submit', $field, $form_state);

      $fields[$field->delta] = $field;
      $delta++;
    }
  }

  return $fields;
}

/**
 *
 * AJAX callback for the node edit form.
 *
 * @param $form
 * @param $form_state
 */
function quizzler_ajax($form, $form_state) {
  // if this callback was triggered by a field, get the form section to return
  list($plugin, $remains) = explode("-", $form_state['clicked_button']['#ajax']['wrapper']);
  $element = module_invoke($plugin, 'quizzler_ajax_callback', $form, $form_state);
  if (is_array($element)) {
    return $element;
  }
  else {
    // our own add field button was clicked
    return $form['quizzler']['quizzler_form'];
  }
}

/**
 *
 * Add our config section to the node_type form.
 */
function quizzler_form_node_type_form_alter(&$form, &$form_state) {
  if (!user_access('administer quizzler')) {
    return;
  }

  $view_modes     = field_view_mode_settings('node', $form['#node_type']->type);
  // field_view_mode_settings doesn't return the default settings, which many may use for
  // the full view mode, so add that here
  $view_options   = array('full' => 'full');
  foreach ($view_modes as $mode => $default) {
    $view_options[$mode] = $mode;
  }

  $form['quizzler'] = array(
    '#type'		=> 'fieldset',
    '#title'  => t("Quizzler settings"),
    '#group'  => 'additional_settings',
    'quizzler_active' => array(
      '#type'	          => 'checkbox',
      '#title'	        => t("Activate Quizzler for this content type?"),
      '#default_value'	=> quizzler_active_type($form['#node_type']->type),
      '#description'		=> t("Check this box to allow users with the 'configure quizzler' permission to add a quiz to nodes of this type.")
    ),
    'quizzler_quiz_view' => array(
      '#type'	          => 'checkboxes',
      '#title'	        => t("Where should Quizzler quizzes be displayed?"),
      '#options'				=> $view_options,
      '#default_value'	=> quizzler_quiz_view_modes($form['#node_type']->type),
      '#description'		=> t("Select which view modes the quizzes will appear on.")
    ),
    'quizzler_quiz_view_weight' => array(
      '#type'	          => 'textfield',
      '#size'           => 3,
      '#title'	        => t("Quizzler fieldset weight"),
      '#default_value'	=> quizzler_quiz_view_weight($form['#node_type']->type),
      '#description'		=> t("Indicate the weight of this element in relation to other elements on the page.")
    ),
    'quizzler_results_view' => array(
      '#type'	          => 'checkboxes',
      '#title'	        => t("Where should Quizzler quiz results be displayed?"),
      '#options'				=> $view_options,
      '#default_value'	=> quizzler_results_view_modes($form['#node_type']->type),
      '#description'		=> t("Select which view modes the quiz results will appear on.  Quiz results will only ever be visible to administrators and the person who took the quiz.")
    ),
    'quizzler_quiz_results_view_weight' => array(
      '#type'	          => 'textfield',
      '#size'           => 3,
      '#title'	        => t("Quizzler results fieldset weight"),
      '#default_value'	=> quizzler_quiz_results_view_weight($form['#node_type']->type),
      '#description'		=> t("Indicate the weight of this element in relation to other elements on the page.")
    ),
    'quizzler_profile_view' => array(
      '#type'	          => 'checkbox',
      '#title'	        => t("Display results on user profile?"),
      '#default_value'	=> quizzler_profile_view($form['#node_type']->type),
      '#description'		=> t("Check the box to also make a quiz report available to individual users on their profile page.")
    ),
    'quizzler_quiz_user_view_weight' => array(
      '#type'	          => 'textfield',
      '#size'           => 3,
      '#title'	        => t("Quizzler results fieldset weight on the user profile"),
      '#default_value'	=> quizzler_quiz_user_view_weight(),
      '#description'		=> t("Indicate the weight of this element in relation to other elements on the user profile page.  Note that this setting is global for ALL Quizzler quizzes, regardless of the content type it is associated with.")
    ),
  );

  $form['#submit'][] = 'quizzler_save_node_type';
}

/**
 *
 * Add our config section on the node_type form.
 */
function quizzler_save_node_type($form, &$form_state) {
  $active = variable_get("quizzler_active_types", array());
  $active[$form_state['values']['type']] = $form_state['values']['quizzler_active'];
  variable_set("quizzler_active_types", $active);

  $profile = variable_get("quizzler_profile_view", array());
  $profile[$form_state['values']['type']] = $form_state['values']['quizzler_profile_view'];
  variable_set("quizzler_profile_view", $profile);

  $modes = variable_get("quizzler_quiz_view", array());
  $modes[$form_state['values']['type']] = array();
  foreach ($form_state['values']['quizzler_quiz_view'] as $key => $value) {
    if ($value) {
      $modes[$form_state['values']['type']][] = $value;
    }
  }
  variable_set("quizzler_quiz_view", $modes);

  $modes = variable_get("quizzler_results_view", array());
  $modes[$form_state['values']['type']] = array();
  foreach ($form_state['values']['quizzler_results_view'] as $key => $value) {
    if ($value) {
      $modes[$form_state['values']['type']][] = $value;
    }
  }
  variable_set("quizzler_results_view", $modes);

  $weights = variable_get("quizzler_view_weight", array());
  $weights[$form_state['values']['type']] = $form_state['values']['quizzler_quiz_view_weight'];
  variable_set("quizzler_view_weight", $weights);

  $weights = variable_get("quizzler_results_view_weight", array());
  $weights[$form_state['values']['type']] = $form_state['values']['quizzler_quiz_results_view_weight'];
  variable_set("quizzler_results_view_weight", $weights);

  variable_set("quizzler_user_view_weight", $form_state['values']['quizzler_quiz_user_view_weight']);
}

/**
 *
 * Retrieve a quiz object for the given node or nid
 *
 * @param $node
 * A fully populated node object, or else a node id
 *
 * @return
 * A fully populated quiz object, or NULL
 */
function quizzler_quiz($node) {
  $nid = NULL;
  if (is_object($node)) {
    $nid = $node->nid;
  }
  else {
    $nid = (int) $node;
  }

  if (!$nid) {
    return NULL;
  }


  $quiz = db_query("SELECT * FROM {quizzler_quiz} WHERE nid = :nid", array(':nid' => $nid))->fetchAssoc();
  if ($quiz) {
    $quiz = (object) $quiz;
    module_invoke_all('quizzler_load', $quiz);
  }

  return $quiz;
}

/**
 *
 * Retrieve a quiz from a quiz id.
 *
 * @param $qid
 *
 * @return
 * A fully populated quiz object, or NULL
 */
function quizzler_load_quiz($qid) {
  $quiz = db_query("SELECT * FROM {quizzler_quiz} WHERE qid = :qid", array(':qid' => $qid))->fetchAssoc();
  if ($quiz) {
    $quiz = (object) $quiz;
    module_invoke_all('quizzler_load', $quiz);
  }

  return $quiz;
}

/**
 *
 * Insert a new quiz into the database.
 *
 * @param $nid
 *
 * @return
 * A quiz object
 */
function quizzler_create_quiz($nid) {
  $quiz = (object) array(
    'nid' => $nid
  );

  $quiz->qid = db_insert("quizzler_quiz")
  ->fields(array('nid' => $nid))
  ->execute();

  return $quiz;
}

/**
 *
 * Retrieve a field from a field id.
 *
 * @param $fid
 *
 * @param $quiz
 * If you pass in a quiz object, the field will be returned from the quiz object, not retrieved from the database.
 *
 * @param $clear_cache = FALSE
 * Clear the in memory cache and load this field fresh from the database.  This will also update the field in the passed
 * quiz object.
 *
 * @return
 * A field object
 */
function quizzler_field($fid, $quiz = NULL, $clear_cache = FALSE) {
  static $fields;

  if (!$clear_cache && $fields && isset($fields[$fid])) {
    return $fields[$fid];
  }

  $field = NULL;
  if (!$quiz || $clear_cache) {
    $field = db_query("SELECT * FROM {quizzler_field} WHERE fid = :fid", array(':fid' => $fid))->fetchAssoc();
    if ($field) {
      $field = (object) $field;
    }

    module_invoke_all('quizzler_field_load', $field);
  }

  if ($quiz) {
    if (!is_array($quiz->fields)) {
      $quiz->fields = array();
    }

    $index = 0;
    foreach ($quiz->fields as $test) {
      if ($test->fid == $fid) {
        break;
      }

      $index++;
    }

    if ($field) {
      $quiz->fields[$index] = $field;
    }
    else {
      $field = $quiz->fields[$index];
    }
  }

  $fields[$fid] = $field;
  return $field;
}


/**
 *
 * Implement hook_quizzler_load()
 *
 * Load the associated fields for this quiz
 */
function quizzler_quizzler_load($quiz) {
  $fields = db_query("SELECT * FROM {quizzler_field} WHERE qid = :qid ORDER BY delta", array(':qid' => $quiz->qid))->fetchAllAssoc('fid');

  $quiz->fields = array();
  foreach ($fields as $field) {
    module_invoke_all('quizzler_field_load', $field);

    $quiz->fields[] = $field;
  }
}

/**
 * Implement hook_quizzler_field_load()
 */
function quizzler_quizzler_field_load($field) {
  $fn_info = $field->module . "_quizzler_field_info";
  if (function_exists($fn_info)) {
    $info = $fn_info();
    $field->name = $info['name'];
    $field->info = $info;
  }
}

/**
 *
 * Returns TRUE if this content type is active for quizzes.
 *
 * @param $bundle
 *
 * @return
 * TRUE or FALSE
 */
function quizzler_active_type($bundle) {
  $active = variable_get("quizzler_active_types", array());
  return (isset($active[$bundle]) && $active[$bundle] == 1);
}

/**
 *
 * Returns TRUE if we are configured to show quiz results on the profile page.
 *
 * @return
 * TRUE or FALSE
 */
function quizzler_active_profile() {
  return variable_get("quizzler_profile_view", FALSE);
}

/**
 *
 * Returns TRUE if this content type is active for quizzes in this node view.
 *
 * @param $bundle
 *
 * @param $view_mode
 *
 * @return
 * TRUE or FALSE
 */
function quizzler_active_quiz_view_mode($bundle, $view_mode) {
  $active = quizzler_quiz_view_modes($bundle);
  return (in_array($view_mode, $active));
}

/**
 *
 * Returns TRUE if this content type is active for results in this node view.
 *
 * @param $bundle
 *
 * @param $view_mode
 *
 * @return
 * TRUE or FALSE
 */
function quizzler_active_results_view_mode($bundle, $view_mode) {
  $active = quizzler_results_view_modes($bundle);
  return (in_array($view_mode, $active));
}

/**
 *
 * Returns TRUE if user results should be displayed on the user profile page for this content type.
 *
 * @param $bundle
 *
 * @return
 * TRUE or FALSE
 */
function quizzler_profile_view($bundle) {
  $profile = variable_get("quizzler_profile_view", array());
  return (isset($profile[$bundle]) && $profile[$bundle] == 1);
}

/**
 *
 * Return an array of view modes the administrator wants the quiz to show up on.
 *
 * @param $bundle
 *
 * @return
 * An array of view modes (ie. 'full', 'teaser', etc.)
 */
function quizzler_quiz_view_modes($bundle) {
  $modes = variable_get("quizzler_quiz_view", array('full' => 'full'));
  return (isset($modes[$bundle]))? $modes[$bundle] : array();
}

/**
 *
 * Return an array of view modes the administrator wants the quiz results to show up on.
 *
 * @param $bundle
 *
 * @return
 * An array of view modes (ie. 'full', 'teaser', etc.)
 */
function quizzler_results_view_modes($bundle) {
  $modes = variable_get("quizzler_results_view", array());
  return (isset($modes[$bundle]))? $modes[$bundle] : array();
}

/**
 * Get the weight of the form element for this bundle for the quiz fieldset.
 *
 * @param $bundle
 *
 * @return
 * A number
 */
function quizzler_quiz_view_weight($bundle) {
  $weights = variable_get("quizzler_view_weight", array());
  return (isset($weights[$bundle]))? $weights[$bundle] : 0;
}

/**
 * Get the weight of the form element for this bundle for the quiz results fieldset.
 *
 * @param $bundle
 *
 * @return
 * A number
 */
function quizzler_quiz_results_view_weight($bundle) {
  $weights = variable_get("quizzler_results_view_weight", array());
  return (isset($weights[$bundle]))? $weights[$bundle] : 0;
}

/**
 * Get the weight of the form element for this bundle for the quiz results fieldset on the user profile.
 *
 * @return
 * A number
 */
function quizzler_quiz_user_view_weight() {
  return variable_get("quizzler_user_view_weight", 0);
}

/**
 *
 * Retrieve an array of known field plugins.
 *
 * @return
 * An array of plugin names.
 */
function quizzler_fields() {
  static $plugins;

  if ($plugins) {
    return $plugins;
  }

  $path = drupal_get_path('module', 'quizzler') . '/modules';
  $dirs = scandir($path);

  $plugins = array();
  foreach ($dirs as $filename) {
    if (preg_match("/^\./", $filename)) {
      continue;
    }

    if (is_dir($path . "/" . $filename) && module_exists($filename)) {
      $plugins[] = $filename;
    }
  }

  return $plugins;
}

/**
 *
 * Gather results for a given user over all quizzes taken by this user, sorted by timestamp.
 *
 * @see quizzler_user_results()
 *
 * @param $account
 * A fully loaded user account, an account uid, or NULL to retrieve results for the current
 * user.
 *
 * @return
 * An array of results, where every entry is an array containing the following information:
 *   - node: 						the node attached to this quiz
 *   - attempts:				the number of times the quiz was taken
 *   - correct:					the highest count of correct answers
 *   - total_questions: the count of questions for this quiz
 *   - percent:					the highest percent scored on this quiz
 *   - grade:						the highest letter grade scored on this quiz
 *   - quizzes:					a keyed array containing a record for each quiz attempt, where each key is the timestamp for that attempt
 */
function quizzler_user_stats($account = NULL) {
	if (is_null($account)) {
		global $user;
		$account = $user;
	}

	$uid = NULL;
	if (is_numeric($account)) {
		$uid = (int) $account;
	}
	if (is_object($account)) {
		$uid = $account->uid;
	}

	if (!$uid) {
		return array();
	}

	$sql = "
		SELECT DISTINCT
			nid
		FROM
			{quizzler_quiz} qq
		INNER JOIN
			{quizzler_field} qf
			ON qq.qid = qf.qid
		INNER JOIN
			{quizzler_answer} qa
			ON qa.fid = qf.fid
		WHERE
			qa.uid = :uid
	";

	$result  = db_query($sql, array(':uid' => $uid));
	$results = array();

	foreach ($result as $record) {
		$r = quizzler_user_results($record->nid, $uid);

		// $r contains an entry for each time this user took this quiz, so collapse that down into
		// stats for each quiz
		$quiz_record = array(
			'node'						=> node_load($record->nid),
			'attempts'				=> count($r['results'][$uid]),
			'correct'					=> 0,
			'total_questions'	=> 0,
			'percent'					=> 0,
			'grade'						=> 'F',
			'quizzes'					=> array()
		);

		foreach ($r['results'][$uid] as $timestamp => $data) {
			$quiz_record['quizzes'][$timestamp] = $data;

			if (!$quiz_record['total_questions']) {
				$quiz_record['total_questions'] = $data['total_questions'];
			}

			if ($data['correct'] > $quiz_record['correct']) {
				foreach (array('correct', 'percent', 'grade') as $k) {
					$quiz_record[$k] = $data[$k];
				}
			}
		}

		$results[] = $quiz_record;
	}

	return $results;
}

/**
 *
 * Gather the results for a given quiz, sorted by timestamp and user.
 *
 * @param $nid
 * The nid of the content associated with the quiz.
 *
 * @param $uid = NULL
 * If supplied, restrict results to the given user uid.
 *
 * @return
 * An array of results, containing the following entries -
 *   total_questions: number of questions appearing on this quiz
 *   correct:     		average correct over all results
 *   answered:        average answered over all results
 *   percent:					average percent over all results
 *   grade:      			average letter grade over all results
 *
 *   results:  an array of arrays containing individual quiz results, keyed by user id
 *             each result array is itself an array, keyed on timestamp containing arrays with the individual
 *             quiz results.
 *
 *             Each quiz result contains the following entries
 *   					 total_questions: total questions appearing on this quiz
 *   `				 correct:     		number correct
 *   					 answered:        number answered
 *   					 percent:					percent correct
 *   					 grade:      			grade
 */
function quizzler_user_results($nid, $uid = NULL) {
  $correct     = 0;
  $answered    = 0;
  $total       = 0;
  $quiz_total  = quizzler_question_count($nid);
  if (!$quiz_total) {
    return array();
  }

  $results = array('results' => array());
  $answers = quizzler_answers($nid, $uid);
  foreach ($answers as $answer) {
    $u = $answer->uid;
    $t = $answer->timestamp;

    if (!isset($results['results'][$u])) {
      $results['results'][$u] = array();
    }
    if (!isset($results['results'][$u][$t])) {
      $results['results'][$u][$t] = array(
        'total_questions' => $quiz_total,
        'correct'					=> 0,
        'answered'				=> 0,
        'percent'					=> 0,
        'grade'						=> 0
      );
    }

    if ($answer->correct) {
      $results['results'][$u][$t]['correct']++;
      $correct++;
    }
    if ($answer->answer) {
      $results['results'][$u][$t]['answered']++;
      $answered++;
    }

    $total++;

    $this_percent = $results['results'][$u][$t]['correct'] / $quiz_total;
    $results['results'][$u][$t]['percent'] = sprintf("%02d", $this_percent * 100) . "%";
    $results['results'][$u][$t]['grade']   = quizzler_grade($this_percent);
  }

  // no answers for this quiz
  if (!$total) {
    return array();
  }

  $percent = $correct / $total;
  $grade   = quizzler_grade($percent);

  $results['total_questions'] = $total;
  $results['correct']         = $correct ;
  $results['answered']        = $answered;
  $results['percent']         = sprintf("%02d", $percent * 100) . '%';
  $results['grade']           = $grade;

  return $results;
}

/**
 *
 * Retrieve all of the answers associated with a given node nid.
 *
 * @param $nid
 *
 * @param $uid = NULL
 * If supplied, restrict results to the given user uid.
 *
 * @return
 * An array of answer database objects
 */
function quizzler_answers($nid, $uid = NULL) {
  $sql = "
  	SELECT
  		qa.*
  	FROM
  		{quizzler_answer} qa
  	INNER JOIN
  		{quizzler_field} qf
  		ON qf.fid = qa.fid
  	INNER JOIN
  		{quizzler_quiz} qq
  		ON qq.qid = qf.qid
  	WHERE
  		qq.nid = :nid
  ";
  $params = array(':nid' => $nid);

  if ((int) $uid) {
    $sql .= " AND qa.uid = :uid";
    $params[':uid'] = $uid;
  }

  $result = db_query($sql, $params);

  $answers = array();
  foreach ($result as $record) {
    $answers[] = $record;
  }

  return $answers;
}

/**
 *
 * Returns the number of questions associated with this node's quiz.
 *
 * @param $nid
 *
 * @return
 * An integer
 */
function quizzler_question_count($nid) {
  $count = db_query("
  	SELECT
  		count(fid)
  	FROM
 			{quizzler_field} qf
 		INNER JOIN
 			{quizzler_quiz} qq
 		ON
 			qq.qid = qf.qid
 	  WHERE
 	  	qq.nid = :nid
 ", array(':nid' => $nid))
    ->fetchField();

  return $count;
}

/**
 *
 * Return a letter grade for the given percentage value.
 *
 * A+ = 95%+
 * A = 90 - 95%
 * B = 80 - 90%
 * C = 70 - 80%
 * D = 60 - 70%
 * F = < 60%
 *
 * @param $percent
 * An integer between 0 and 1
 */
function quizzler_grade($percent) {
  if ($percent > .95) {
    return "A+";
  }
  if ($percent > .9) {
    return "A";
  }
  if ($percent > .8) {
    return "B";
  }
  if ($percent > .7) {
    return "C";
  }
  if ($percent > .6) {
    return "D";
  }

  return "F";
}
