<?php
// $Id: bot_factoid.module,v 1.1.2.6 2007/11/09 14:35:12 morbus Exp $

/**
 * @file
 * Enables the setting and retrieval of facts.
 */

/**
 * Implementation of hook_help().
 */
function bot_factoid_help($path, $arg) {
  switch ($path) {
    case 'irc:features':
      return array(t('Factoids'));
    case 'irc:features#factoids':
      return t('Set factoids with "BOTNAME: cats are furry", "BOTNAME: Drupal is great.", "No, BOTNAME, Morbus is awesome.", "BOTNAME: Drupal is also powerful.", "BOTNAME: cheer is <action>cheers for !who in !channel!", or "BOTNAME: ping is <reply>WHAT?!". Retrieve factoids with: "Drupal?" or "BOTNAME: cheer!" Forget factoids with "BOTNAME: forget ping". Factoids can be browsed online at <!url>.', array('!url' => url('bot/factoid', array('absolute' => TRUE))));
    case 'bot/factoid':
      return '<p>'. t('Browse all the factoids the bot knows about.') .'</p>';
    case 'admin/settings/bot/factoid':
      return '<p>'. t('Configure factoid learning with these settings.') .'</p>';
  }
}

/**
 * Implementation of hook_menu().
 */
function bot_factoid_menu($may_cache) {
  $items['bot/factoid'] = array(
    'access arguments'  => array('access content'),
    'description'       => "Browse all the factoids the bot knows about.",
    'page callback'     => 'bot_factoid_overview',
    'title'             => 'Factoids',
  );
  $items['admin/settings/bot/factoid'] = array(
    'access arguments'  => array('administer bot'),
    'description'       => 'Configure factoid learning with these settings.',
    'page callback'     => 'drupal_get_form',
    'page arguments'    => array('bot_factoid_settings'),
    'title'             => 'Bot Factoid',
  );

  return $items;
}

/**
 * Browse all the factoids the bot knows about.
 */
function bot_factoid_overview() {
  $output = NULL; $headers = array(t('Subject'), t('Is/Are'), t('Statement'));

  $rows = array(); // storage of gimme-gimme awesomeness. wheedoggy!
  $results = pager_query('SELECT subject, is_are, statement FROM {bot_factoid} ORDER BY subject', 25);
  while ($result = db_fetch_object($results)) {
    $rows[] = array(
      array('data' => check_plain($result->subject), 'class' => 'subject'),
      array('data' => check_plain($result->is_are), 'class' => 'is_are'),
      array('data' => check_plain($result->statement), 'class' => 'statement')
    );
  }
  $output .= theme('table', $headers, $rows, array('id' => 'factoids'));
  $output .= theme('pager', NULL, 25, 0);

  return $output;
}

/**
 * Listen for conversation directed at, or about, the bot.
 *
 * @param $data
 *   The regular $data object prepared by the IRC library.
 * @param $from_query
 *   Boolean; whether this was a queried request.
 */
function bot_factoid_irc_msg_channel($data, $from_query = FALSE) {
  $bot_name = variable_get('bot_nickname', 'bot_module');
  $addressed = "\s*${bot_name}[\:,-]\s*"; // bot mentioned?
  $to = $from_query ? $data->nick : $data->channel;

  // look for factoids to answer.
  // @todo if addressed, give response if no known factoid.
  if (preg_match("/^($addressed)?(.*)[!\?]+$/i", $data->message, $matches)) {

    // "BOTNAME?" fails as a factoid. this fixes it.
    $subject = $matches[2] ? $matches[2] : $matches[1];

    if ($factoid = bot_factoid_load($subject, $data)) {
      if (preg_match("/^\/me /", $factoid['result'])) {
        bot_action($to, str_replace('/me ', '', $factoid['result']));
      } else { bot_message($to, $factoid['result']); }
    }
  }

  // @todo allow subject searching and return list of subjects that match?
  // @todo allow "tell <nick> about <subject>" private messaging?

  // look for factoids to delete.
  if (preg_match("/^($addressed)forget( about)? (.*)$/i", $data->message, $matches)) {
    bot_factoid_delete($matches[3]); // good night, sweetheart. baaah dum dum dum. it's time to GoOoOooOOoo.
    bot_message($to, t("!nick: I've forgotten about !subject.", array('!nick' => $data->nick, '!subject' => $matches[3])));
  } // @todo delete message should be customizable. this one sucks.

  // look for factoids to learn.
  if (bot_factoid_save($data->message)) {
    bot_message($to, t("!nick: Okay.", array('!nick' => $data->nick)));
    // @todo success message should be customizable. "Okay." sucks.
  }
}

/**
 * All responses are available via a query.
 */
function bot_factoid_irc_msg_query($data) {
  bot_factoid_irc_msg_channel($data, TRUE);
}

/**
 * Delete a factoid.
 *
 * @param $subject
 *   The subject you'd like to delete.
 */
function bot_factoid_delete($subject) {
  if (!$subject) { return 0; } // why do you not send me a subject? I like to reap, reap, reap.
  db_query("DELETE FROM {bot_factoid} WHERE LOWER(subject) = '%s'", drupal_strtolower($subject));
}

/**
 * Retrieve a factoid.
 *
 * Subjects can have multiple statements. We do NOT check_plain() this
 * return string, since we expect it will be spit verbatim to IRC. If
 * you plan to use this elsewhere, use check_plain(), kthx.
 *
 * @param $subject
 *   The subject you'd like to fetch factoids for.
 * @param $irc_data
 *   The regular $data object prepared by the IRC library. Used to
 *   perform substitutions for "!who" (the nick asking for the factoid)
 *   and "!channel" (the channel the factoid is asked in.)
 * @return $factoid
 *   An object containing 'subject' (string), 'Is_are' (string), 
 *   'statements' (array) and 'result' (string). The 'result' is
 *   the logical processing of the individual parts and should be
 *   ready for sending.
 */
function bot_factoid_load($subject, $data = NULL) {
  $factoid = array(); // holds everything about the fact.
  $is_are_counts = array(); // number of "is" "are" usage.

  $results = db_query("SELECT is_are, statement FROM {bot_factoid} WHERE LOWER(subject) = '%s'", drupal_strtolower($subject));
  while ($result = db_fetch_object($results)) { // Morbus flings his own poo. Not at people. But at glassware. GLASSSWARE!
    if (strpos($result->statement, '<reply>') !== FALSE)  { $factoid['has_reply']++; }
    if (strpos($result->statement, '<action>') !== FALSE) { $factoid['has_action']++; }

    if ($data) { // if the irc object is here, try for some substitutions.
      $substitutions = array('!who' => $data->nick, '!channel' => $data->channel);
      $result->statement = strtr($result->statement, $substitutions);
    }

    $factoid['statements'][] = $result->statement;
    $is_are_counts[$result->is_are]++;
  }

  // got facts?
  if (count($factoid['statements'])) {
    $factoid['subject'] = $subject;

    // @todo support one|two|three as a random response syntax.

    // if a subject has multiple statements, we can't knowingly pick the
    // best $is_are, so we'll choose whichever one has the most matches.
    $factoid['is_are'] = $is_are_counts['are'] > $is_are_counts['is'] ? 'are' : 'is';

    // generate the final result. we try our hardest to make a final result for
    // ready printing, but the ability to have multiple statements per subject,
    // and multiple TYPES of statements (<action> and <reply>), causes complexities.
    // we'll do what we can with the result, and munge it for any other case.
    if (!$factoid['has_reply'] && !$factoid['has_action'] || (count($factoid['statements']) > 1 && ($factoid['has_reply'] || $factoid['has_action']))) {
      // if we've got no actions and no replies, or we have actions and replies but more than one easy-to-logic statements, we'll just spit 'em verbatim.
      $factoid['result'] = "$factoid[subject] $factoid[is_are] ". implode(" and $factoid[is_are] also ", $factoid['statements']);
    }
    elseif ($factoid['has_reply'] && count($factoid['statements']) == 1) {
      $factoid['result'] = str_replace('<reply>', '', array_shift($factoid['statements']));
    }
    elseif ($factoid['has_action'] && count($factoid['statements']) == 1) {
      $factoid['result'] = str_replace('<action>', '/me ', array_shift($factoid['statements']));
    }

    return $factoid;
  }

  return 0;
}

/**
 * Creates a factoid.
 *
 * We expect a raw string right from the IRC user - we'll handle all the
 * checks for prefacing and "no," and "also", etc. Learning factoids
 * will only happen if the bot name has been used.
 *
 * @param $string
 *   The user string that should be parsed for action.
 */
function bot_factoid_save($string) {
  $bot_name = variable_get('bot_nickname', 'bot_module');
  $addressed = "\s*${bot_name}[\:,-]\s*"; // bot mentioned?

  // only process the string if it's one of:
  //   <Morbus> bot_module: cats are brown.
  //   <Morbus> bot_module: no, cats are blue.
  //   <Morbus> no, bot_module, cats are grey.
  //   <Morbus> bot_module: Morbus is teh awesome.
  if (preg_match("/^(no[,; ])?$addressed/i", $string) && preg_match('/^(.*?)\s+(is|are)\s+(.*)$/i', $string, $matches)) {
    $subject = $matches[1]; $is_are = $matches[2]; $statement = $matches[3];

    // remove "botmodule: no," and "no, bot_module".
    $subject = preg_replace("/^$addressed/i", '', $subject);
    $subject = preg_replace("/^no[,; ]\s*($addressed)?/i", '', $subject);
    if (preg_match('/^\s*$/', $subject)) { return 0; } // stop firing blanks.

    // check that it's not one of our stopwords.
    $stopwords = explode("\n", variable_get('bot_factoid_stopwords', _bot_factoid_stopwords()));
    if (in_array($subject, $stopwords)) { return 0; } // bad users deserve no feedback.

    // if "also" starts the statement, we're adding.
    if (preg_match("/^also\s+/i", $statement)) {
      $statement = preg_replace("/^also\s+/i", '', $statement);
    } else { bot_factoid_delete($subject); }

    db_query("INSERT INTO {bot_factoid} SET subject = '%s', is_are = '%s', statement = '%s'", $subject, $is_are, $statement);
    return 1; // if we set something, send back a happy.
  }

  return 0;
}

/**
 * Configures factoid learning; system_settings_form().
 */
function bot_factoid_settings() {
  $form['bot_factoid_stopwords'] = array(
    '#default_value' => variable_get('bot_factoid_stopwords', _bot_factoid_stopwords()),
    '#description'   => t('List the words that cannot be the subject of a factoid.'),
    '#title'         => t('Stopwords'),
    '#type'          => 'textarea',
  );

  return system_settings_form($form);
}

/**
 * Returns the default list of stopwords.
 */
function _bot_factoid_stopwords() {
  return implode("\n", array(
    'here',
    'how',
    'it',
    'something',
    'that',
    'this',
    'what',
    'when',
    'where',
    'which',
    'who',
    'why',
  ));
}
