<?php

/**
 * @file
 * Allows users to send private messages to other users.
 */

/**
 * Status constant for read messages.
 */
define('PRIVATEMSG_READ', 0);
/**
 * Status constant for unread messages.
 */
define('PRIVATEMSG_UNREAD', 1);
/**
 * Show unlimited messages in a thread.
 */
define('PRIVATEMSG_UNLIMITED', 'unlimited');

/**
 * Implements hook_perm().
 */
function privatemsg_perm() {
  return array(
    'read privatemsg',
    'read all private messages',
    'administer privatemsg settings',
    'write privatemsg',
    'delete privatemsg',
  );
}

/**
 * Generate aray of user objects based on a string.
 *
 *
 * @param $userstring
 *   A string with user id, for example 1,2,4. Returned by the list query
 *
 * @return
 *   Array with user objects.
 */
function _privatemsg_generate_user_array($userstring, $slice = NULL) {
  static $user_cache = array();

  // Convert user uid list (uid1,uid2,uid3) into an array. If $slice is not NULL
  // pass that as argument to array_slice(). For example, -4 will only load the
  // last four users.
  // This is done to avoid loading user objects that are not displayed, for
  // obvious performance reasons.
  $users = explode(',', $userstring);
  if (!is_null($slice)) {
    $users = array_slice($users, $slice);
  }
  $participants = array();
  foreach ($users as $uid) {
    if (!array_key_exists($uid, $user_cache)) {
      $user_cache[$uid] = user_load($uid);
    }
    if (is_object($user_cache[$uid])) {
      $participants[$uid] = $user_cache[$uid];
    }
  }
  return $participants;
}

/**
 * Format an array of user objects.
 *
 * @param $part_array
 *   Array with user objects, for example the one returnd by
 *   _privatemsg_generate_user_array.
 *
 * @param $limit
 *   Limit the number of user objects which should be displayed.
 * @param $no_text
 *   When TRUE, don't display the Participants/From text.
 * @return
 *   String with formated user objects, like user1, user2.
 */
function _privatemsg_format_participants($part_array, $limit = NULL, $no_text = FALSE) {
  if (count($part_array) > 0) {
    $to = array();
    $limited = FALSE;
    foreach ($part_array as $account) {
      if (is_int($limit) && count($to) >= $limit) {
        $limited = TRUE;
        break;
      }
      $to[] = theme('username', $account);
    }

    $limit_string = '';
    if ($limited) {
      $limit_string = t(' and others');
    }


    if ($no_text) {
      return implode(', ', $to) . $limit_string;
    }

    $last = array_pop($to);
    if (count($to) == 0) { // Only one participant
      return t("From !last", array('!last' => $last));
    }
    else { // Multipe participants..
      $participants = implode(', ', $to);
      return t('Participants: !participants and !last', array('!participants' => $participants, '!last' => $last));
    }
  }
  return '';
}

/**
 * Implements hook_menu().
 */
function privatemsg_menu() {
  $items['messages'] = array(
    'title'            => 'Messages',
    'title callback'  => 'privatemsg_title_callback',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('privatemsg_list', 'list'),
    'access callback'  => 'privatemsg_user_access',
    'type'             => MENU_NORMAL_ITEM,
  );
  $items['messages/list'] = array(
    'title'            => 'Messages',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('privatemsg_list', 'list'),
    'access callback'  => 'privatemsg_user_access',
    'type'             => MENU_DEFAULT_LOCAL_TASK,
    'weight'           => -10,
  );
  $items['messages/view/%privatemsg_thread'] = array(
    'title'            => 'Read message',
    // Set the third argument to TRUE so that we can show access denied instead
    // of not found.
    'load arguments'   => array(NULL, NULL, TRUE),
    'page callback'    => 'privatemsg_view',
    'page arguments'   => array(2),
    'access callback'  => 'privatemsg_view_access',
    'access arguments' => array(2),
    'type'             => MENU_LOCAL_TASK,
    'weight'           => -5,
  );
  $items['messages/delete/%privatemsg_thread/%privatemsg_message'] = array(
    'title'            => 'Delete message',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('privatemsg_delete', 2, 3),
    'access callback'  => 'privatemsg_user_access',
    'access arguments' => array('delete privatemsg'),
    'type'             => MENU_CALLBACK,
  );
  $items['messages/new'] = array(
    'title'            => 'Write new message',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('privatemsg_new', 2, 3, NULL),
    'access callback'  => 'privatemsg_user_access',
    'access arguments' => array('write privatemsg'),
    'type'             => MENU_LOCAL_TASK,
    'weight'           => -3,
  );
  // Auto-completes available user names & removes duplicates.
  $items['messages/user-name-autocomplete'] = array(
    'page callback'    => 'privatemsg_user_name_autocomplete',
    'access callback'  => 'privatemsg_user_access',
    'access arguments' => array('write privatemsg'),
    'type'             => MENU_CALLBACK,
    'weight'           => -10,
  );
  $items['admin/settings/messages'] = array(
    'title'            => 'Private messages',
    'description'      => 'Configure private messaging settings.',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('private_message_settings'),
    'access arguments' => array('administer privatemsg settings'),
    'type'             => MENU_NORMAL_ITEM,
  );
  $items['admin/settings/messages/default'] = array(
    'title'            => 'Private messages',
    'description'      => 'Configure private messaging settings.',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('private_message_settings'),
    'access arguments' => array('administer privatemsg settings'),
    'type'             => MENU_DEFAULT_LOCAL_TASK,
    'weight'           => -10,
  );
  $items['messages/undo/action'] = array(
    'title'            => 'Private messages',
    'description'      => 'Undo last thread action',
    'page callback'    => 'privatemsg_undo_action',
    'access arguments' => array('read privatemsg'),
    'type'             => MENU_CALLBACK,
  );
  $items['user/%/messages'] = array(
    'title' => 'Messages',
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('privatemsg_list', 'list', 1),
    'access callback'  => 'privatemsg_user_access',
    'access arguments' => array('read all private messages'),
    'type' => MENU_LOCAL_TASK,
  );

  return $items;
}

/**
 * Privatemsg  wrapper for user_access.
 *
 * Never allows anonymous user access as that doesn't makes sense.
 *
 * @param $permission
 *   Permission string, defaults to read privatemsg
 *
 * @return
 *   TRUE if user has access, FALSE if not
 *
 * @ingroup api
 */
function privatemsg_user_access($permission = 'read privatemsg', $account = NULL) {
  if ( $account === NULL ) {
    global $user;
    $account = $user;
  }
  if (!$account->uid) { // Disallow anonymous access, regardless of permissions
    return FALSE;
  }
  if (!user_access($permission, $account)) {
    return FALSE;
  }
  return TRUE;
}

/**
 * Check access to the view messages page.
 *
 * Function to restrict the access of the view messages page to just the
 * messages/view/% pages and not to leave tabs artifact on other lower
 * level pages such as the messages/new/%.
 * 
 * @param $thread
 *   A array containing all information about a specific thread, generated by
 *   privatemsg_thread_load().
 *
 * @ingroup api
 */
function privatemsg_view_access($thread = array()) {
  // Do not allow access to threads without messages.
  if (empty($thread['messages'])) {
    // Count all messages, if there
    return FALSE;
  }
  if (privatemsg_user_access('read privatemsg') && arg(1) == 'view') {
    return TRUE;
  }
  return FALSE;
}

/**
 * Load a thread with all the messages and participants.
 *
 * This function is called by the menu system through the %privatemsg_thread
 * wildcard.
 *
 * @param $thread_id
 *   Thread id, pmi.thread_id or pm.mid of the first message in that thread.
 * @param $account
 *   User object for which the thread should be loaded, defaults to
 *   the current user.
 * @param $start
 *   Message offset from the start of the thread.
 *
 * @param $useAccessDenied
 *   Set to TRUE if the function should forward to the access denied page
 *   instead of not found. This is used by the menu system because that does
 *   load arguments before access checks are made. Defaults to FALSE.
 *
 * @return
 *   $thread object, with keys messages, participants, title and user. messages
 *   contains an array of messages, participants an array of user, subject the
 *   subject of the thread and user the user viewing the thread.
 *
 *   If no messages are found, or the thread_id is invalid, the function returns
 *   FALSE.

 * @ingroup api
 */
function privatemsg_thread_load($thread_id, $account = NULL, $start = NULL, $useAccessDenied = FALSE) {
  static $threads = array();
  if ((int)$thread_id > 0) {
    $thread = array('thread_id' => $thread_id);

    if (is_null($account)) {
      global $user;
      $account = drupal_clone($user);
    }

    if (!isset($threads[$account->uid])) {
      $threads[$account->uid] = array();
    }

    if (!array_key_exists($thread_id, $threads[$account->uid])) {
      // Load the list of participants.
      $query = _privatemsg_assemble_query('participants', $thread_id);
      $participants = db_query($query['query']);
      $thread['participants'] = array();
      while ($participant = db_fetch_object($participants)) {
        $thread['participants'][$participant->uid] = $participant;
      }
      $thread['read_all'] = FALSE;
      if (!array_key_exists($account->uid, $thread['participants']) && privatemsg_user_access('read all private messages', $account)) {
        $thread['read_all'] = TRUE;
      }

      // Load messages returned by the messages query with privatemsg_message_load_multiple().
      $query = _privatemsg_assemble_query('messages', array($thread_id), $thread['read_all'] ? NULL : $account);
      $thread['message_count'] = $thread['to'] = db_result(db_query($query['count']));
      $thread['from'] = 1;
      // Check if we need to limit the messages.
      $max_amount = variable_get('privatemsg_view_max_amount', 20);

      // If there is no start value, select based on get params.
      if (is_null($start)) {
        if (isset($_GET['start']) && $_GET['start'] < $thread['message_count']) {
          $start = $_GET['start'];
        }
        elseif (!variable_get('privatemsg_view_use_max_as_default', FALSE) && $max_amount == PRIVATEMSG_UNLIMITED) {
          $start = PRIVATEMSG_UNLIMITED;
        }
        else {
          $start = $thread['message_count'] - (variable_get('privatemsg_view_use_max_as_default', FALSE) ? variable_get('privatemsg_view_default_amount', 10) : $max_amount);
        }
      }

      if ($start != PRIVATEMSG_UNLIMITED) {
        if ($max_amount == PRIVATEMSG_UNLIMITED) {
          $last_page = 0;
          $max_amount = $thread['message_count'];
        }
        else {
          // Calculate the number of messages on the "last" page to avoid
          // message overlap.
          // Note - the last page lists the earliest messages, not the latest.
          $paging_count = variable_get('privatemsg_view_use_max_as_default', FALSE) ? $thread['message_count'] - variable_get('privatemsg_view_default_amount', 10) : $thread['message_count'];
          $last_page = $paging_count % $max_amount;
        }

        // Sanity check - we cannot start from a negative number.
        if ($start < 0) {
          $start = 0;
        }
        $thread['start'] = $start;

        //If there are newer messages on the page, show pager link allowing to go to the newer messages.
        if (($start + $max_amount + 1) < $thread['message_count']) {
          $thread['to'] = $start + $max_amount;
          $thread['newer_start'] = $start + $max_amount;
        }
        if ($start - $max_amount >= 0) {
          $thread['older_start'] = $start - $max_amount;
        }
        elseif ($start > 0) {
          $thread['older_start'] = 0;
        }

        // Do not show messages on the last page that would show on the page
        // before. This will only work when using the visual pager.
        if ($start < $last_page && $max_amount != PRIVATEMSG_UNLIMITED && $max_amount < $thread['message_count']) {
          unset($thread['older_start']);
          $thread['to'] = $thread['newer_start'] = $max_amount = $last_page;
          // Start from the first message - this is a specific hack to make sure
          // the message display has sane paging on the last page.
          $start = 0;
        }
        // Visual counts start from 1 instead of zero, so plus one.
        $thread['from'] = $start + 1;
        $conversation = db_query_range($query['query'], $start, $max_amount);
      }
      else {
        $conversation = db_query($query['query']);
      }
      $mids = array();
      while ($result = db_fetch_array($conversation)) {
        $mids[] = $result['mid'];
      }
      // Load messages returned by the messages query.
      $thread['messages'] = privatemsg_message_load_multiple($mids, $thread['read_all'] ? NULL : $account);

      // If there are no messages, don't allow access to the thread.
      if (empty($thread['messages'])) {
        if ($useAccessDenied) {
          // Generate new query with read all to see if the thread does exist.
          $query = _privatemsg_assemble_query('messages', array($thread_id), NULL);
          $exists = db_result(db_query($query['count']));
          if (!$exists) {
            // Thread does not exist, display 404.
            $thread = FALSE;
          }
        }
        else {
          $thread = FALSE;
        }
      }
      else {
        // General data, assume subject is the same for all messages of that thread.
        $thread['user'] = $account;
        $message = current($thread['messages']);
        $thread['subject'] = $message['subject'];
      }
      $threads[$account->uid][$thread_id] = $thread;
    }

    return $threads[$account->uid][$thread_id];
  }
  return FALSE;
}

function private_message_view_options() {
  $options = module_invoke_all('privatemsg_view_template');
  return $options;
}

/**
 * Implements hook_privatemsg_view_template().
 *
 * Allows modules to define different message view template.
 *
 * This hook returns information about available themes for privatemsg viewing.
 *
 * array(
 *  'machine_template_name' => 'Human readable template name',
 *  'machine_template_name_2' => 'Human readable template name 2'
 * };
 */
function privatemsg_privatemsg_view_template() {
  return array(
    'privatemsg-view' => 'Default view',
  );
}

function private_message_settings() {
  $form = array();

  $form['theming_settings'] = array(
    '#type'        => 'fieldset',
    '#collapsible' => TRUE,
    '#collapsed'   => TRUE,
    '#title'       => t('Theming settings'),
  );
  $form['theming_settings']['private_message_view_template'] = array(
    '#type'          => 'radios',
    '#title'         => t('Private message display template'),
    '#default_value' => variable_get('private_message_view_template', 'privatemsg-view'),
    '#options'       => private_message_view_options(),
  );
  $form['privatemsg_display_loginmessage'] = array(
    '#type' => 'checkbox',
    '#title' => t('Inform the user about new messages on login'),
    '#default_value' => variable_get('privatemsg_display_loginmessage', TRUE),
    '#description' => t('This option can safely be disabled if the "New message indication" block is used instead.'),
  );

  $form['flush_deleted'] = array(
    '#type'        => 'fieldset',
    '#collapsible' => TRUE,
    '#collapsed'   => TRUE,
    '#title'       => t('Flush deleted messages'),
    '#description' => t('By default, deleted messages are only hidden from the user but still stored in the database. These settings control if and when messages should be removed.'),
  );

  $form['flush_deleted']['privatemsg_flush_enabled'] = array(
    '#type'          => 'checkbox',
    '#title'         => t('Flush deleted messages'),
    '#default_value' => variable_get('privatemsg_flush_enabled', FALSE),
    '#description'   => t('Enable the flushing of deleted messages. Requires that cron is enabled'),
  );

  $form['flush_deleted']['privatemsg_flush_days'] = array(
    '#type' => 'select',
    '#title' => t('Flush messages after they have been deleted for more days than'),
    '#default_value' => variable_get('privatemsg_flush_days', 30),
    '#options' => drupal_map_assoc(array(0, 1, 2, 5, 10, 30, 100)),
  );

  $form['flush_deleted']['privatemsg_flush_max'] = array(
    '#type' => 'select',
    '#title' => t('Maximum number of messages to flush per cron run'),
    '#default_value' => variable_get('privatemsg_flush_max', 200),
    '#options' => drupal_map_assoc(array(50, 100, 200, 500, 1000)),
  );

  $form['privatemsg_listing'] = array(
    '#type' => 'fieldset',
    '#title' => t('Configure listings'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );

  $form['privatemsg_listing']['privatemsg_per_page'] = array(
    '#type' => 'select',
    '#title' => t('Threads per page'),
    '#default_value' => variable_get('privatemsg_per_page', 25),
    '#options' => drupal_map_assoc(array(10, 25, 50, 75, 100)),
    '#description' => t('Choose the number of conversations that should be listed per page.'),
  );

  $form['privatemsg_listing']['privatemsg_display_fields'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Configure fields'),
    '#description' => t('Select which columns/fields should be displayed in the message listings. Subject and Last updated cannot be disabled.'),
    '#options' => array(
      'participants'    => t('Participants'),
      'thread_started'  => t('Started'),
      'count'           => t('Messages'),
    ),
    '#default_value' => variable_get('privatemsg_display_fields', array('participants')),
  );

  $amounts = drupal_map_assoc(array(5, 10, 20, 30, 50, 70, 90, 150, 200, 250, 300));
  $form['privatemsg_listing']['privatemsg_view_max_amount'] = array(
    '#type'          => 'select',
    '#title'         => t('Number of messages on thread pages'),
    '#options'       => $amounts + array(PRIVATEMSG_UNLIMITED => t('Unlimited')),
    '#default_value' => variable_get('privatemsg_view_max_amount', 20),
    '#description'   => t('Threads will not show more than this number of messages on a single page.'),
    '#weight'        => 10,
  );

  $form['privatemsg_listing']['privatemsg_view_use_max_as_default'] = array(
    '#type'          => 'checkbox',
    '#title'         => t('Display different amount of messages on first thread page'),
    '#default_value' => variable_get('privatemsg_view_use_max_as_default', FALSE),
    '#description'   => t('By default, the first thread page shows the maximally allowed amount of messages. Enable this checkbox to set a different value.'),
    '#weight'        => 15,
  );

  $form['privatemsg_listing']['privatemsg_view_default_amount'] = array(
    '#prefix'        => '<div id="privatemsg-view-default-button">',
    '#suffix'        => '</div>',
    '#type'          => 'select',
    '#title'         => t('Number of messages on first thread page'),
    '#default_value' => variable_get('privatemsg_view_default_amount', 10),
    '#description'   => t('The number of messages to be displayed on first thread page. Displays the newest messages.'),
    '#options'       => $amounts,
    '#weight'        => 20,
  );
  drupal_add_js(drupal_get_path('module', 'privatemsg') .'/privatemsg-admin.js');

  $form['#submit'][] = 'private_message_settings_submit';
  return system_settings_form($form);
}

function private_message_settings_submit() {
  drupal_rebuild_theme_registry();
}

/**
 * Implements hook_cron().
 *
 * If the flush feature is enabled, a given amount of deleted messages that are
 * old enough are flushed.
 */
function privatemsg_cron() {
  if (variable_get('privatemsg_flush_enabled', FALSE)) {
    $query = _privatemsg_assemble_query('deleted', variable_get('privatemsg_flush_days', 30));
    $result = db_query($query['query']);

    $flushed = 0;
    while (($row = db_fetch_array($result)) && ($flushed < variable_get('privatemsg_flush_max', 200))) {
      $message = privatemsg_message_load($row['mid']);
      module_invoke_all('privatemsg_message_flush', $message);

      // Delete recipients of the message.
      db_query('DELETE FROM {pm_index} WHERE mid = %d', $row['mid']);
      // Delete message itself.
      db_query('DELETE FROM {pm_message} WHERE mid = %d', $row['mid']);
      $flushed++;
    }
  }
}

function privatemsg_theme() {
  return array(
    'privatemsg_view'    => array(
      'arguments'        => array('message' => NULL),
      'template'         => variable_get('private_message_view_template', 'privatemsg-view'), // 'privatemsg',
    ),
    'privatemsg_from'    => array(
      'arguments'        => array('author' => NULL),
      'template'         => 'privatemsg-from',
    ),
    'privatemsg_recipients' => array(
      'arguments'        => array('message' => NULL),
      'template'         => 'privatemsg-recipients',
    ),
    'privatemsg_between' => array(
      'arguments'        => array('recipients' => NULL),
      'template'         => 'privatemsg-between',
    ),
    'privatemsg_list'    => array(
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
      'arguments'        => array('form'),
    ),
    // Define pattern for header/field templates. The theme system will register all
    // theme functions that start with the defined pattern.
    'privatemsg_list_header'  => array(
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
      'pattern'               => 'privatemsg_list_header__',
      'arguments'             => array(),
    ),
    'privatemsg_list_field'   => array(
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
      'pattern'               => 'privatemsg_list_field__',
      'arguments'             => array('thread'),
    ),
    'privatemsg_new_block'  => array(
      'file'                  => 'privatemsg.theme.inc',
      'path'                  => drupal_get_path('module', 'privatemsg'),
      'arguments'             => array('count'),
    ),
  );
}

function template_preprocess_privatemsg_view(&$vars) {
//  drupal_set_message('<pre>'. print_r($vars,1 ) . '</pre>');

  $message = $vars['message'];
  $vars['mid'] = isset($message['mid']) ? $message['mid'] : NULL;
  $vars['thread_id'] = isset($message['thread_id']) ? $message['thread_id'] : NULL;
  $vars['author_picture'] = theme('user_picture', $message['author']);
  $vars['author_name_link'] = theme('username', $message['author']);
  /**
   * @todo perhaps make this timestamp configurable via admin UI?
   */
  $vars['message_timestamp'] = format_date($message['timestamp'], 'small');
  $vars['message_body'] = check_markup($message['body'], $message['format'], FALSE);
  if (isset($vars['mid']) && isset($vars['thread_id']) && privatemsg_user_access('delete privatemsg')) {
    $vars['message_actions'][] = array('title' => t('Delete message'), 'href' => 'messages/delete/' . $vars['thread_id'] . '/' . $vars['mid']);
  }
  $vars['message_anchors'][] = 'privatemsg-mid-' . $vars['mid'];
  if (!empty($message['is_new'])) {
    $vars['message_anchors'][] = 'new';
    $vars['new'] = drupal_ucfirst(t('new'));
  }

  // call hook_privatemsg_message_view_alter
  drupal_alter('privatemsg_message_view', $vars);

  $vars['message_actions'] = !empty($vars['message_actions']) ? theme('links', $vars['message_actions'], array('class' => 'message-actions')) : '';

  $vars['anchors'] = '';
  foreach ($vars['message_anchors'] as $anchor) {
    $vars['anchors'] .= '<a name="' . $anchor . '"></a>';
  }
}

function template_preprocess_privatemsg_recipients(&$vars) {
  $vars['participants'] = ''; // assign a default empty value
  if (isset($vars['message']['participants'])) {
    $vars['participants'] = _privatemsg_format_participants($vars['message']['participants']);
  }
}

/**
 * List messages.
 *
 * @param $form_state
 *   Form state array
 * @param $argument
 *   An argument to pass through to the query builder.
 * @param $uid
 *   User id messages of another user should be displayed
 *
 * @return
 *   Form array
 */
function privatemsg_list(&$form_state, $argument = 'list', $uid = NULL) {
  global $user;

  // Setting default behavior...
  $account = $user;
  // Because uid is submitted by the menu system, it's a string not a integer.
  if ((int)$uid > 0 && $uid != $user->uid) {
    // Trying to view someone else's messages...
    if (!privatemsg_user_access('read all private messages')) {
      drupal_set_message(t("You do not have sufficient rights to view someone else's messages"), 'warning');
    }
    elseif ($account_check = user_load(array('uid' => $uid))) {
      // Has rights and user_load return an array so user does exist
      $account = $account_check;
    }
  }
  // By this point we have figured out for which user we are listing messages and now it is safe to use $account->uid in the listing query.

  $query = _privatemsg_assemble_query('list', $account, $argument);
  $result = pager_query($query['query'], variable_get('privatemsg_per_page', 25), 0, $query['count']);

  $threads = array();
  $form['#data'] = array();
  while ($row = db_fetch_array($result)) {
    // Store the raw row data.
    $form['#data'][$row['thread_id']] = $row;
    // store thread id for the checkboxes array
    $threads[$row['thread_id']] = '';
  }
  if (!empty($form['#data'])) {
    $form['actions'] = _privatemsg_action_form();
  }

  // Save the currently active account, used for actions.
  $form['account'] = array('#type' => 'value', '#value' => $account);

  // Define checkboxes, pager and theme
  $form['threads'] = array('#type' => 'checkboxes', '#options' => $threads);
  $form['pager'] = array('#value' => theme('pager'), '#weight' => 20);
  $form['#theme'] = 'privatemsg_list';

  // Store the account for which the threads are displayed.
  $form['#account'] = $account;
  return $form;
}

/**
 * Changes the read/new status of a single message.
 *
 * @param $pmid
 *   Message id
 * @param $status
 *   Either PRIVATEMSG_READ or PRIVATEMSG_UNREAD
 * @param $account
 *   User object, defaults to the current user
 */
function privatemsg_message_change_status($pmid, $status, $account = NULL) {
  if (!$account) {
    global $user;
    $account = $user;
  }
  $query = "UPDATE {pm_index} SET is_new = %d WHERE mid = %d AND uid = %d";
  db_query($query, $status, $pmid, $account->uid);
}

/**
 * Return number of unread messages for an account.
 *
 * @param $account
 *   Specifiy the user for which the unread count should be loaded.
 *
 * @ingroup api
 */
function privatemsg_unread_count($account = NULL) {
  static $counts = array();
  if (!$account || $account->uid == 0) {
    global $user;
    $account = $user;
  }
  if ( !isset($counts[$account->uid])) {
    $query = _privatemsg_assemble_query('unread_count', $account);
    $counts[$account->uid] = db_result(db_query($query['query']));
  }
  return $counts[$account->uid];
}

/**
 * Menu callback for viewing a thread.
 *
 * @param $thread
 *   A array containing all information about a specific thread, generated by
 *   privatemsg_thread_load().
 * @return
 *   The page content.
 * @see privatemsg_thread_load()
 */
function privatemsg_view($thread) {
  drupal_set_title(check_plain($thread['subject']));

  // Generate paging links.
  $older = '';
  if (isset($thread['older_start'])) {
    $options = array(
      'query' => array('start' => $thread['older_start']),
      'title' => t('Display older messages'),
    );
     $older = l(t('<<'), 'messages/view/' . $thread['thread_id'], $options);
  }
  $newer = '';
  if (isset($thread['newer_start'])) {
    $options = array(
      'query' => array('start' => $thread['newer_start']),
      'title' => t('Display newer messages'),
    );
    $newer = l(t('>>'), 'messages/view/' . $thread['thread_id'], $options);
  }
  $substitutions = array('@from' => $thread['from'], '@to' => $thread['to'], '@total' => $thread['message_count'], '!previous_link' => $older, '!newer_link' => $newer);
  $title = t('!previous_link Displaying messages @from - @to of @total !newer_link', $substitutions);
  $content['pager_top'] = array(
    '#value'  => trim($title),
    '#prefix' => '<div class="privatemsg-view-pager">',
    '#suffix' => '</div>',
    '#weight' => -10,
  );

  // Display a copy at the end.
  $content['pager_bottom'] = $content['pager_top'];
  $content['pager_bottom']['#weight'] = 3;

  // Render the participants.
  $content['participants']['#value'] = theme('privatemsg_recipients', $thread);
  $content['participants']['#weight'] = -5;

  // Render the messages.
  $output = '';
  foreach ($thread['messages'] as $pmid => $message) {
    // Set message as read and theme it.
    if (!empty($message['is_new'])) {
      privatemsg_message_change_status($pmid, PRIVATEMSG_READ, $thread['user']);
    }
    $output .= theme('privatemsg_view', $message);
  }
  $content['messages']['#value'] = $output;
  $content['messages']['#weight'] = 0;

  // Display the reply form if user is allowed to use it.
  if (privatemsg_user_access('write privatemsg')) {
    $content['reply']['#value'] = drupal_get_form('privatemsg_new', $thread['participants'], $thread['subject'], $thread['thread_id'], $thread['read_all']);
    $content['reply']['#weight'] = 5;
  }

  // Check after calling the privatemsg_new form so that this message is only
  // displayed when we are not sending a message.
  if ($thread['read_all']) {
    // User has permission to read all messages AND is not a participant of the current thread.
    drupal_set_message(t('This conversation is being viewed with escalated priviledges and may not be the same as shown to normal users.'), 'warning');
  }

  // Allow other modules to hook into the $content array and alter it.
  drupal_alter('privatemsg_view_messages', $content, $thread);
  return drupal_render($content);
}


function privatemsg_new(&$form_state, $recipients = array(), $subject = '', $thread_id = NULL, $read_all = FALSE) {
  global $user;

  $recipients_string = '';
  $body      = '';

  // convert recipients to array of user objects
  if (!empty($recipients) && is_string($recipients) || is_int($recipients)) {
    $recipients = _privatemsg_generate_user_array($recipients);
  }
  elseif (is_object($recipients)) {
    $recipients = array($recipients);
  }
  elseif (empty($recipients) && is_string($recipients)) {
    $recipients = array();
  }

  $usercount = 0;
  $to = array();
  $to_themed = array();
  $blocked = FALSE;
  foreach ($recipients as $recipient) {
    if (in_array($recipient->name, $to)) {
      // We already added the recipient to the list, skip him.
      continue;
    }
    // Check if another module is blocking the sending of messages to the recipient by current user.
    $user_blocked = module_invoke_all('privatemsg_block_message', $user, array($recipient->uid => $recipient));
    if (!count($user_blocked) <> 0 && $recipient->uid) {
      if ($recipient->uid == $user->uid) {
        $usercount++;
        // Skip putting author in the recipients list for now.
        continue;
      }
      $myuser = user_load(array('uid' => $recipient->uid));
      $myname = preg_replace("/ /", "\xc2\xa0", $myuser->boincuser_name);
      $to[] = $myname . "_" . $myuser->boincuser_id;
      $to_themed[$recipient->uid] = $myname;
    }
    else {
      // Recipient list contains blocked users.
      $blocked = TRUE;
    }
  }

  if (empty($to) && $usercount >= 1 && !$blocked) {
    // Assume the user sent message to own account as if the usercount is one or less, then the user sent a message but not to self.
    $myuser = user_load(array('uid' => $user->uid));
    $myname = preg_replace("/ /", "\xc2\xa0", $myuser->boincuser_name);
    $to[] = $myname . "_" . $myuser->boincuser_id;
    $to_themed[$user->uid] = $myname;
  }
  if (!empty($to)) {
    $recipients_string = implode(', ', $to);
    $recipients_string .= ', ';
  }
  if (isset($form_state['values'])) {
    if (isset($form_state['values']['recipient'])) {
      $recipients_string = $form_state['values']['recipient'];

    }
    $subject   = $form_state['values']['subject'];
    $body      = $form_state['values']['body'];
  }
  if (!$thread_id && !empty($recipients_string)) {
    drupal_set_title(t('Write new message to %recipient', array('%recipient' => $recipients_string)));
  }
  elseif (!$thread_id) {
    drupal_set_title(t('Write new message'));
  }
  $form = array();
  if (isset($form_state['privatemsg_preview'])) {
    $form['message_header'] = array(
      '#type' => 'fieldset',
      '#attributes' => array('class' => 'preview'),
    );
    $form['message_header']['message_preview'] = array(
      '#value'  => $form_state['privatemsg_preview'],
    );
  }
  $form['privatemsg'] = array(
    '#type'               => 'fieldset',
    '#access'             => privatemsg_user_access('write privatemsg'),
  );
  $form['privatemsg']['author'] = array(
    '#type' => 'value',
    '#value' => $user,
  );
  if (is_null($thread_id)) {
    $form['privatemsg']['recipient'] = array(
      '#type'               => 'textfield',
      '#title'              => t('To'),
      '#description'        => t('Separate multiple names with a comma followed by one space. The number appearing in the suffix is the BOINC id. You can find a user\'s BOINC id on their user profile page.'),
      '#default_value'      => $recipients_string,
      '#required'           => TRUE,
      '#weight'             => -10,
      '#size'               => 50,
      '#autocomplete_path'  => 'messages/user-name-autocomplete',
      // Do not hardcode #maxlength, make it configurable by number of recipients, not their name length.
    );
  }
  $form['privatemsg']['subject'] = array(
    '#type'               => 'textfield',
    '#title'              => t('Subject'),
    '#size'               => 50,
    '#maxlength'          => 255,
    '#default_value'      => $subject,
    '#weight'             => -5,
  );
  $form['privatemsg']['body'] = array(
    '#type'               => 'textarea',
    '#title'              => t('Message'),
    '#rows'               => 6,
    '#weight'             => 0,
    '#default_value'      => $body,
    '#resizable'          => TRUE,
  );
  $format = FILTER_FORMAT_DEFAULT;
  // The input filter widget looses the format during preview, specify it
  // explicitly.
  if (isset($form_state['values']) && array_key_exists('format', $form_state['values'])) {
    $format = $form_state['values']['format'];
  }
  $form['privatemsg']['format'] = filter_form($format);
  $form['privatemsg']['preview'] = array(
    '#type'               => 'submit',
    '#value'              => t('Preview message'),
    '#submit'             => array('pm_preview'),
    '#validate'           => array('pm_send_validate'),
    '#weight'             => 10,
  );
  $form['privatemsg']['submit'] = array(
    '#type'               => 'submit',
    '#value'              => t('Send message'),
    '#submit'             => array('pm_send'),
    '#validate'           => array('pm_send_validate'),
    '#weight'             => 15,
  );
  $url = 'messages';
  $title = t('Cancel');
  if (isset($_REQUEST['destination'])) {
    $url = $_REQUEST['destination'];
  }
  elseif (!is_null($thread_id)) {
    $url = $_GET['q'];
    $title = t('Clear');
  }

  $form['privatemsg']['cancel'] = array(
    '#value'              => l($title, $url, array('attributes' => array('id' => 'edit-cancel'))),
    '#weight'             => 20,
  );

  if (!is_null($thread_id)) {
    $form['privatemsg']['thread_id'] = array(
      '#type' => 'value',
      '#value' => $thread_id,
    );
    $form['privatemsg']['subject'] = array(
          '#type' => 'value',
          '#default_value' => $subject,
    );
    $recipients_string_themed = implode(', ', $to_themed);
    $form['privatemsg']['recipient_display'] = array(
      '#value' =>  '<p>'. t('<strong>Reply to thread</strong>:<br /> Recipients: !to', array('!to' => $recipients_string_themed)) .'</p>',
      '#weight' => -10,
    );
    if (empty($recipients_string)) {
      // If there are no valid recipients, unset the message reply form.
      $form['privatemsg']['#access'] = FALSE;
    }
  }
  $form['privatemsg']['read_all'] = array(
    '#type'  => 'value',
    '#value' => $read_all,
  );
  return $form;
}

function pm_send_validate($form, &$form_state) {
  // The actual message that is being sent, we create this during validation and pass to submit to send out.
  $message = $form_state['values'];
  $message['timestamp'] = time();
  // Avoid subjects which only consist of a space as these can not be clicked.
  $message['subject'] = trim($message['subject']);


  $trimed_body = trim(truncate_utf8(strip_tags($message['body']), 50, TRUE, TRUE));
  if (empty($message['subject']) && !empty($trimed_body)) {
    $message['subject'] = $trimed_body;
  }
  // Only parse the user string for a new thread.
  if (!isset($message['thread_id'])) {
    list($message['recipients'], $invalid) = _privatemsg_parse_userstring($message['recipient']);
  }
  else {
    // Load participants.
    $message['recipients'] = _privatemsg_load_thread_participants($message['thread_id']);
    // Remove author.
    if (isset($message['recipients'][$message['author']->uid]) && count($message['recipients']) > 1) {
      unset($message['recipients'][$message['author']->uid]);
    }
  }
  $validated = _privatemsg_validate_message($message, TRUE);
  foreach ($validated['messages'] as $type => $text) {
    drupal_set_message($text, $type);
  }
  $form_state['validate_built_message'] = $message;
  if (!empty($invalid)) {
    drupal_set_message(t('The following users will not receive this private message: @invalid', array('@invalid' => implode(", ", $invalid))), 'error');
  }
}

/**
 * Load all participants of a thread, optionally without author.
 *
 * @param $thread_id
 *   Thread ID for wich the participants should be loaded.
 */
function _privatemsg_load_thread_participants($thread_id) {
  $query = _privatemsg_assemble_query('participants', $thread_id);
  $result = db_query($query['query']);
  $participants = array();
  while ($uid = db_fetch_object($result)) {
    if (($recipient = user_load($uid->uid))) {
      $participants[$recipient->uid] = $recipient;
    }
  }
  return $participants;
}

/**
 * Extract the valid usernames of a string and loads them.
 *
 * This function is used to parse a string supplied by a username autocomplete
 * field and load all user objects.
 *
 * @param $string
 *   A string in the form "usernameA, usernameB, ...".
 * @return
 *   Array, first element is an array of loaded user objects, second an array
 *   with invalid names.
 */
function _privatemsg_parse_userstring($input) {
  if (is_string($input)) {
    $input = explode(', ', $input);
  }

  // Start working through the input array.
  $invalid = array();
  $recipients = array();
  foreach ($input as $string) {
    $string = trim($string);
    if (!empty($string)) { // We don't care about white space names.

      // First, check if another module is able to resolve the string into an
      // user object.
      foreach (module_implements('privatemsg_name_lookup') as $module) {
        $function = $module . '_privatemsg_name_lookup';
        if (($recipient = $function($string)) && is_object($recipient)) {
          // If there is a match, continue with the next input string.
          $recipients[$recipient->uid] = $recipient;
          continue 2;
        }
      }
      // Fall back to the default username lookup.
      if (!$error = module_invoke('user', 'validate_name', $string)) {
        // String is a valid username, look it up.
        if ($recipient = user_load(array('name' => $string))) {
          $recipients[$recipient->uid] = $recipient;
          continue;
        }
      }
      $invalid[$string] = $string;
    }
  }
  return array($recipients, $invalid);
}

/**
 * Submit callback for the privatemsg_new form.
 */
function pm_send($form, &$form_state) {
  $status = _privatemsg_send($form_state['validate_built_message']);
  // Load usernames to which the message was sent to.
  $recipient_names = array();
  foreach ($form_state['validate_built_message']['recipients'] as $recipient) {
    $recipient_names[] = theme('username', $recipient);
  }
  if ($status !== FALSE )  {
    drupal_set_message(t('A message has been sent to !recipients.', array('!recipients' => implode(', ', $recipient_names))));
  }
  else {
    drupal_set_message(t('An attempt to send a message <em>may have failed</em> when sending to !recipients.', array('!recipients' => implode(', ', $recipient_names))), 'error');
  }
}

function pm_preview($form, &$form_state) {

    drupal_validate_form($form['form_id']['#value'], $form, $form_state);
    if (!form_get_errors()) {
      $form_state['privatemsg_preview'] = theme('privatemsg_view', $form_state['validate_built_message']);
    }

  $form_state['rebuild'] = TRUE; // this forces our form to be rebuilt instead of being submitted.
}

/**
 * @addtogroup sql
 * @{
 */

/**
 * Query definition to load a list of threads.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $account
 *   User object for which the messages are being loaded.
 * @param $argument
 *   String argument which can be used in the query builder to modify the
 *   thread listing.
 */

function privatemsg_sql_list(&$fragments, $account, $argument = 'list') {
  $fragments['primary_table'] = '{pm_message} pm';

  // Load enabled columns.
  $fields = array_filter(variable_get('privatemsg_display_fields', array('participants')));

  // Required columns.
  $fragments['select'][]      = 'pmi.thread_id';
  // We have to use MIN as the subject might not be the same in some threads.
  // MIN() does not have a useful meaning except that it helps to correctly
  // aggregate the thread on PostgreSQL.
  $fragments['select'][]      = 'MIN(pm.subject) as subject';
  $fragments['select'][]      = 'MAX(pm.timestamp) as last_updated';
  // We use SUM so that we can count the number of unread messages.
  $fragments['select'][]      = 'SUM(pmi.is_new) as is_new';

  // Select number of messages in the thread if the count is
  // set to be displayed.
  if (in_array('count', $fields)) {
    $fragments['select'][]      = 'COUNT(distinct pmi.mid) as count';
  }
  if (in_array('participants', $fields)) {
    // Query for a string with uid's, for example "1,6,7".
    // @todo: Replace this with a single query similiar to the tag list.
    if ($GLOBALS['db_type'] == 'pgsql') {
      // PostgreSQL does not know GROUP_CONCAT, so a subquery is required.
      $fragments['select'][]      = "array_to_string(array(SELECT DISTINCT textin(int4out(pmia.uid))
                                                            FROM {pm_index} pmia
                                                            WHERE pmia.thread_id = pmi.thread_id), ',') AS participants";
    }
    else {
      $fragments['select'][]      = '(SELECT GROUP_CONCAT(DISTINCT pmia.uid SEPARATOR ",")
                                                            FROM {pm_index} pmia
                                                            WHERE pmia.thread_id = pmi.thread_id) AS participants';
    }
  }
  if (in_array('thread_started', $fields)) {
    $fragments['select'][]      = 'MIN(pm.timestamp) as thread_started';
  }

  $fragments['inner_join'][]  = 'INNER JOIN {pm_index} pmi ON pm.mid = pmi.mid';

  // Only load undeleted messages of the current user and group by thread.
  $fragments['where'][]       = 'pmi.uid = %d';
  $fragments['query_args']['where'][]  = $account->uid;
  $fragments['where'][]       = 'pmi.deleted = 0';
  $fragments['group_by'][]    = 'pmi.thread_id';

  $order_by_first = 'MAX(pmi.is_new) DESC, ';
  // MySQL 4.1 does not allow to order by aggregate functions. MAX() is used
  // to avoid a ordering bug with multiple new messages.
  if ($GLOBALS['db_type'] != 'pgsql' && version_compare(db_version(), '5.0.0') < 0) {
    $order_by_first = 'is_new DESC, ';
  }

  // tablesort_sql() generates a ORDER BY string. However, the "ORDER BY " part 
  // is not needed and added by the query builder. Discard the first 9
  // characters of the string.
  $order_by = drupal_substr(tablesort_sql(_privatemsg_list_headers( FALSE, array_merge(array('subject', 'last_updated'), $fields)), $order_by_first), 9);
  $fragments['order_by'][]  = $order_by;
}

/**
 * Query function for loading a single or multiple messages.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $pmids
 *   Array of pmids.
 * @param $account
 *   Account for which the messages should be loaded.
 */
function privatemsg_sql_load(&$fragments, $pmids, $account = NULL) {
  $fragments['primary_table'] = '{pm_message} pm';

  $fragments['select'][]      = "pm.mid";
  $fragments['select'][]      = "pm.author";
  $fragments['select'][]      = "pm.subject";
  $fragments['select'][]      = "pm.body";
  $fragments['select'][]      = "pm.timestamp";
  $fragments['select'][]      = "pm.format";
  $fragments['select'][]      = "pmi.is_new";
  $fragments['select'][]      = "pmi.thread_id";

  $fragments['inner_join'][]  = 'INNER JOIN {pm_index} pmi ON pm.mid = pmi.mid';
  // Use IN() to load multiple messages at the same time.
  $fragments['where'][]       = 'pmi.mid IN (' . db_placeholders($pmids) . ')';
  $fragments['query_args']['where']  += $pmids;
  if ($account) {
    $fragments['where'][]       = 'pmi.uid = %d';
    $fragments['query_args']['where'][]  = $account->uid;
  }
  $fragments['order_by'][] = 'pm.timestamp ASC';
  $fragments['order_by'][] = 'pm.mid ASC';
}

/**
 * Query definition to load messages of one or multiple threads.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $threads
 *   Array with one or multiple thread id's.
 * @param $account
 *   User object for which the messages are being loaded.
 * @param $load_all
 *   Deleted messages are only loaded if this is set to TRUE.
 */
function privatemsg_sql_messages(&$fragments, $threads, $account = NULL, $load_all = FALSE) {
  $fragments['primary_table'] = '{pm_index} pmi';

  $fragments['select'][]      = 'pmi.mid';
  $fragments['where'][]       = 'pmi.thread_id IN ('. db_placeholders($threads) .')';
  $fragments['query_args']['where']   += $threads;
  $fragments['inner_join'][]  = 'INNER JOIN {pm_message} pm ON (pm.mid = pmi.mid)';
  if ($account) {
    // Only load the user's messages.
    $fragments['where'][]     = 'pmi.uid = %d';
    $fragments['query_args']['where'][]  = $account->uid;
  }
  if (!$load_all) {
    // Also load deleted messages when requested.
    $fragments['where'][]     = 'pmi.deleted = 0';
  }
  // Only load each mid once.
  $fragments['group_by'][]    = 'pmi.mid';
  $fragments['group_by'][]    = 'pm.timestamp';

  // Order by timestamp first.
  $fragments['order_by'][]    = 'pm.timestamp ASC';
  // If there are multiple inserts during the same second (tests, for example)
  // sort by mid second to have them in the same order as they were saved.
  $fragments['order_by'][]    = 'pmi.mid ASC';

}

/**
 * Load all participants of a thread.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $thread_id
 *   Thread id from which the participants should be loaded.
 */
function privatemsg_sql_participants(&$fragments, $thread_id) {
  $fragments['primary_table'] = '{pm_index} pmi';

  // Only load each participant once since they are listed as recipient for
  // every message of that thread.
  $fragments['select'][]      = 'DISTINCT(pmi.uid) AS uid';
  $fragments['select'][]      = 'u.name AS name';

  $fragments['inner_join'][]  = 'INNER JOIN {users} u ON (u.uid = pmi.uid)';
  $fragments['where'][]       = 'pmi.thread_id = %d';
  $fragments['query_args']['where'][]  = $thread_id;
}

/**
 * Query definition to count unread messages.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $account
 *   User object for which the messages are being counted.
 */
function privatemsg_sql_unread_count(&$fragments, $account) {
  $fragments['primary_table'] = '{pm_index} pmi';

  $fragments['select'][]      = 'COUNT(DISTINCT thread_id) as unread_count';

  // Only count new messages that have not been deleted.
  $fragments['where'][]       = 'pmi.deleted = 0';
  $fragments['where'][]       = 'pmi.is_new = 1';
  $fragments['where'][]       = 'pmi.uid = %d';
  $fragments['query_args']['where'][]  = $account->uid;
}

/**
 * Query definition to search for username autocomplete suggestions.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $search
 *   Which search string is currently searched for.
 * @param $names
 *   Array of names not to be used as suggestions.
 */
function privatemsg_sql_autocomplete(&$fragments, $search, $names) {
  // Select users from BOINC user table by name and id. The id is
  // unique.
  $fragments['primary_table'] = '{user} u';
  $fragments['select'][] = 'u.name,u.id';
  // Escape the % to get it through the placeholder replacement.
  $fragments['where'][] = "u.name LIKE '%s'";
  $fragments['query_args']['where'][] = $search .'%%';
  if (!empty($names)) {
    // If there are already names selected, exclude them from the
    // suggestions. Extract the BOINC ids from the names array, and
    // use to remove query results.
    $ids = array();
    foreach ($names as $name) {
      $boincid = substr($name, strrpos($name, '_') + 1);
      $ids[$boincid] = $boincid;
    }
    $fragments['where'][] = "u.id NOT IN (". db_placeholders($ids, 'int') .")";
    $fragments['query_args']['where'] += $ids;
  }
  // Only load active users and sort them by name.
  $fragments['order_by'][] = 'u.name ASC';
}

/**
 * Query Builder function to load all messages that should be flushed.
 *
 * @param $fragments
 *   Query fragments array.
 * @param $days
 *   Select messages older than x days.
 */
function privatemsg_sql_deleted(&$fragments, $days) {
  $fragments['primary_table'] = '{pm_message} pm';

  $fragments['select'][] = 'pm.mid';
  // The lowest value is higher than 0 if all recipients have deleted a message.
  $fragments['select'][] = 'MIN(pmi.deleted) as is_deleted';
  // The time the most recent deletion happened.
  $fragments['select'][] = 'MAX(pmi.deleted) as last_deleted';

  $fragments['inner_join'][] = 'INNER JOIN {pm_index} pmi ON (pmi.mid = pm.mid)';

  $fragments['group_by'][] = 'pm.mid';

  // Ignore messages that have not been deleted by all users.
  $fragments['having'][] = 'MIN(pmi.deleted) > 0';

  // Only select messages that have been deleted more than n days ago.
  $fragments['having'][] = 'MAX(pmi.deleted) < %d';
  $fragments['query_args']['having'][] = time() - $days * 86400;
}

/**
 * @}
 */

/**
 * Return autocomplete results for usernames.
 *
 * Prevents usernames from being used and/or suggested twice.
 */
function privatemsg_user_name_autocomplete($string) {
  $names = array();
  // 1: Parse $string and build list of valid user names.
  $fragments = explode(', ', $string);
  foreach ($fragments as $index => $name) {
    if ($name = trim($name)) {
      $names[$name] = $name;
    }
  }
  // By using user_validate_user we can ensure that names included in $names are at least logisticaly possible.
  // 2: Find the next user name suggestion.
  $fragment = array_pop($names);
  $matches = array();
  if (!empty($fragment)) {
    $query = _privatemsg_assemble_query('autocomplete', $fragment, $names);
    db_set_active('boinc');
    $result = db_query_range($query['query'], $fragment, 0, 10);
    db_set_active('default');
    $prefix = count($names) ? implode(', ', $names) . ', ' : '';
    // 3: Build proper suggestions and print.
    while ($user = db_fetch_object($result)) {
      $myname = preg_replace("/ /", "\xc2\xa0", $user->name);
      $matches[$prefix . $myname . "_" . $user->id . ', '] = htmlentities($myname) . ' (' . $user->id . ')';
    }
  }
  // convert to object to prevent drupal bug, see http://drupal.org/node/175361
  drupal_json((object)$matches);
}

function privatemsg_user($op, &$edit, &$account, $category = NULL) {
  global $user;

  switch ($op) {
    case 'view':
      if ($url = privatemsg_get_link(array($account))) {
        $account->content['privatemsg_send_new_message'] = array(
          '#type'   => 'markup',
          '#value'  => l(t('Send this user a message'), $url, array('query' => drupal_get_destination())),
          '#weight' => 10,
        );
      }
      break;
    case 'login':
      if (variable_get('privatemsg_display_loginmessage', TRUE) && privatemsg_user_access()) {
        $count = privatemsg_unread_count();
        if ($count) {
          drupal_set_message(format_plural($count, 'You have <a href="@messages">1 unread message</a>.', 'You have <a href="@messages">@count unread messages</a>', array('@messages' => url('messages'))));
        }
      }
      break;
    case 'delete':

      // Load all mids of the messages the user wrote.
      $result = db_query("SELECT mid FROM {pm_message} WHERE author = %d", $account->uid);
      $mids = array();
      while ($row = db_fetch_array($result)) {
        $mids[] = $row['mid'];
      }

      // Delete messages the user wrote.
      db_query('DELETE FROM {pm_message} WHERE author = %d', $account->uid);

      if (!empty($mids)) {
        // Delete recipient entries in {pm_index} of the messages the user wrote.
        db_query('DELETE FROM {pm_index} WHERE mid IN (' . db_placeholders($mids) . ')', $mids);
      }

      // Delete recipient entries of that user.
      db_query('DELETE FROM {pm_index} WHERE uid = %d', $account->uid);
      break;
  }
}

function privatemsg_block($op = 'list', $delta = 0, $edit = array()) {
  if ('list' == $op) {
    $blocks = array();
    $blocks['privatemsg-menu'] = array(
      'info' => t('Privatemsg links'),
      'cache' => BLOCK_NO_CACHE,
    );
    $blocks['privatemsg-new'] = array(
      'info' => t('New message indication'),
      'cache' => BLOCK_NO_CACHE,
    );

    return $blocks;
  }
  elseif ('view' == $op) {
    $block = array();
    switch ($delta) {
      case 'privatemsg-menu':
        $block = _privatemsg_block_menu();
        break;
      case 'privatemsg-new':
        $block = _privatemsg_block_new();
        break;
    }
    return $block;
  }
}

function privatemsg_title_callback($title = NULL) {
  $count = privatemsg_unread_count();

  if ($count > 0) {
    return format_plural($count, 'Messages (1 new)', 'Messages (@count new)');
  }
  return t('Messages');
}


function _privatemsg_block_new() {
  $block = array();

  if (!privatemsg_user_access()) {
    return $block;
  }

  $count = privatemsg_unread_count();
  if ($count) {
    $block = array(
      'subject' => format_plural($count, 'New message', 'New messages'),
      'content' => theme('privatemsg_new_block', $count),
    );
    return $block;
  }
  return array();
}

function _privatemsg_block_menu() {
  $block = array();

  $links = array();
  if (privatemsg_user_access('write privatemsg')) {
    $links[] = l(t('Write new message'), 'messages/new');
  }
  if (privatemsg_user_access('read privatemsg') || privatemsg_user_access('read all private messages') ) {
    $links[] = l(privatemsg_title_callback(), 'messages');
  }
  if ( count( $links ) ) {
    $block = array(
      'subject' => t('Private messages'),
      'content' => theme('item_list', $links),
    );
  }
  return $block;
}

function privatemsg_delete($form_state, $thread, $message) {
  $form['pmid'] = array(
    '#type' => 'value',
    '#value' => $message['mid'],
  );
  $form['delete_destination'] = array(
    '#type' => 'value',
    '#value' => count($thread['messages']) > 1 ? 'messages/view/' . $message['thread_id'] : 'messages',
  );

  if (privatemsg_user_access('read all private messages')) {
    $form['delete_options'] = array(
      '#type' => 'checkbox',
      '#title' => t('Delete this message for all users?'),
      '#description' => t('Tick the box to delete the message for all users.'),
      '#default_value' => FALSE,
    );
  }
  return confirm_form($form,
    t('Are you sure you want to delete this message?'),
    isset($_GET['destination']) ? $_GET['destination'] : 'messages/view/'. $message['thread_id'],
    t('This action cannot be undone.'),
    t('Delete'),
    t('Cancel')
  );
}

function privatemsg_delete_submit($form, &$form_state) {
  global $user;
  $account = drupal_clone($user);

  if ($form_state['values']['confirm']) {
    if (isset($form_state['values']['delete_options']) && $form_state['values']['delete_options']) {
      privatemsg_message_change_delete($form_state['values']['pmid'], 1);
      drupal_set_message(t('Message has been deleted for all users.'));
    }
    else {
      privatemsg_message_change_delete($form_state['values']['pmid'], 1, $account);
      drupal_set_message(t('Message has been deleted.'));
    }
  }
  $form_state['redirect'] = $form_state['values']['delete_destination'];
}

/**
 * Delete or restore a message.
 *
 * @param $pmid
 *   Message id, pm.mid field.
 * @param $delete
 *   Either deletes or restores the thread (1 => delete, 0 => restore)
 * @param $account
 *   User acccount for which the delete action should be carried out - Set to
 *   NULL to delete for all users.
 *
 * @ingroup api
 */
function privatemsg_message_change_delete($pmid, $delete, $account = NULL) {
  $delete_value = 0;
  if ($delete == TRUE) {
    $delete_value = time();
  }

  if ($account) {
    db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = %d AND uid = %d', $delete_value, $pmid, $account->uid);
  }
  else {
    // Mark deleted for all users.
    db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = %d', $delete_value, $pmid);
  }
}

/**
 * Send a new message.
 *
 * This functions does send a message in a new thread.
 * Example:
 * @code
 * privatemsg_new_thread(array(user_load(5)), 'The subject', 'The body text');
 * @endcode
 *
 * @param $recipients
 *   Array of recipients (user objects)
 * @param $subject
 *   The subject of the new message
 * @param $body
 *   The body text of the new message
 * @param $options
 *   Additional options, possible keys:
 *     author => User object of the author
 *     timestamp => Time when the message was sent
 *
 * @return
 *   An array with a key success. If TRUE, it also contains a key 'message' with
 *   the created $message array, the same that is passed to the insert hook.
 *   If FALSE, it contains a key 'messages'. This key contains an array where
 *   the key is the error type (error, warning, notice) and an array with
 *   messages of that type.
 *
 *   It is theoretically possible for success to be TRUE and message to be
 *   FALSE. For example if one of the privatemsg database tables become
 *   corrupted. When testing for success of message being sent it is always
 *   best to see if ['message'] is not FALSE as well as ['success'] is TRUE.
 *
 *   Example:
 *   @code
 *   array('error' => array('A error message'))
 *   @endcode
 *
 * @ingroup api
 */
function privatemsg_new_thread($recipients, $subject, $body = NULL, $options = array()) {
  global $user;
  $author = drupal_clone($user);

  $message = array();
  $message['subject'] = $subject;
  $message['body'] = $body;
  // Make sure that recipients are keyed by user id and are not added
  // multiple times.
  foreach ($recipients as $recipient) {
    $message['recipients'][$recipient->uid] = $recipient;
  }

  // Set custom options, if any.
  if (!empty($options)) {
    $message += $options;
  }
  // Apply defaults - this will not overwrite existing keys.
  $message += array(
    'author' => $author,
    'timestamp' => time(),
    'format' => filter_resolve_format(FILTER_FORMAT_DEFAULT),
  );

  $validated = _privatemsg_validate_message($message);
  if ($validated['success']) {
    $validated['message'] = _privatemsg_send($message);
  }

  return $validated;
}

/**
 * Send a reply message
 *
 * This functions replies on an existing thread.
 *
 * @param $thread_id
 *   Thread id
 * @param $body
 *   The body text of the new message
 * @param $options
 *   Additional options, possible keys:
 *     author => User object of the author
 *     timestamp => Time when the message was sent
 *
 * @return
 *   An array with a key success and messages. This key contains an array where
 *   the key is the error type (error, warning, notice) and an array with
 *   messages of that type.. If success is TRUE, it also contains a key $message
 *   with the created $message array, the same that is passed to
 *   hook_privatemsg_message_insert().
 *
 *   It is theoretically possible for success to be TRUE and message to be
 *   FALSE. For example if one of the privatemsg database tables become
 *   corrupted. When testing for success of message being sent it is always
 *   best to see if ['message'] is not FALSE as well as ['success'] is TRUE.
 *
 *   Example messages values:
 *   @code
 *   array('error' => array('A error message'))
 *   @endcode
 *
 * @ingroup api
 */
function privatemsg_reply($thread_id, $body, $options = array()) {
  global $user;
  $author = drupal_clone($user);

  $message = array();
  $message['body'] = $body;

  // set custom options, if any
  if (!empty($options)) {
    $message += $options;
  }
  // apply defaults
  $message += array(
    'author' => $author,
    'timestamp' => time(),
    'format' => filter_resolve_format(FILTER_FORMAT_DEFAULT),
  );

  // We don't know the subject and the recipients, so we need to load them..
  // thread_id == mid on the first message of the thread
  $first_message = privatemsg_message_load($thread_id, $message['author']);
  if (!$first_message) {
    return array(t('Thread %thread_id not found, unable to answer', array('%thread_id' => $thread_id)));
  }

  $message['thread_id'] = $thread_id;
  // Load participants.
  $message['recipients'] = _privatemsg_load_thread_participants($thread_id);
  // Remove author.
  if (isset($message['recipients'][$message['author']->uid]) && count($message['recipients']) > 1) {
    unset($message['recipients'][$message['author']->uid]);
  }
  $message['subject'] = $first_message['subject'];

  $validated = _privatemsg_validate_message($message);
  if ($validated['success']) {
    $validated['message'] = _privatemsg_send($message);
  }
  return $validated;
}

function _privatemsg_validate_message(&$message, $form = FALSE) {
  $messages = array('error' => array(), 'warning' => array());
  if (!privatemsg_user_access('write privatemsg', $message['author'])) {
    // no need to do further checks in this case...
    if ($form) {
      form_set_error('author', t('User @user is not allowed to write messages', array('@user' => $message['author']->name)));
      return array(
        'success'  => FALSE,
        'messages'   => $messages,
      );
    }
    else {
      $messages['error'][] = t('User @user is not allowed to write messages', array('@user' => $message['author']->name));
      return array(
         'success'  => FALSE,
         'messages'   => $messages,
      );
    }
  }

  // Prevent subjects which only consist of a space as these can not be clicked.
  $message['subject'] = trim($message['subject']);
  if (empty($message['subject'])) {
    if ($form) {
      form_set_error('subject', t('Disallowed to send a message without subject'));
    }
    else {
      $messages['error'][] = t('Disallowed to send a message without subject');
    }
  }

  // Don't allow replies without a body.
  if (!empty($message['thread_id']) && ($message['body'] === NULL || $message['body'] === '') ) {
    if ($form) {
      form_set_error('body', t('Disallowed to send reply without a message.'));
    }
    else {
      $messages['error'][] = t('Disallowed to send reply without a message.');
    }
  }
  // Check if an allowed format is used. global $user needs to be changed since
  // it is not possible to do the check for a specific user.
  global $user;
  $original_user = drupal_clone($user);
  session_save_session(FALSE);
  $user = $message['author'];

  if (!filter_access($message['format'])) {
    if ($form) {
      form_set_error('format', t('You are not allowed to use the specified input format.'));
    }
    else {
      $messages['error'][] = t('User @user is not allowed to use the specified input format.', array('@user' => $message['author']->name));
    }
  }

  $user = $original_user;
  session_save_session(TRUE);

  if (empty($message['recipients']) || !is_array($message['recipients'])) {
    if ($form) {
      form_set_error('to', t('Disallowed to send a message without at least one valid recipient'));
    }
    else {
      $messages['error'][] = t('Disallowed to send a message without at least one valid recipient');
    }
  }

  if (!empty($message['recipients']) && is_array($message['recipients'])) {
    foreach (module_invoke_all('privatemsg_block_message', $message['author'], $message['recipients']) as $blocked) {
      unset($message['recipients'][$blocked['uid']]);
      if ($form) {
        drupal_set_message($blocked['message'], 'warning');
      }
      else {
        $messages['warning'][] = $blocked['message'];
      }
    }
  }

  // Check again, give another error message if all recipients are blocked
  if (empty($message['recipients'])) {
    if ($form) {
      form_set_error('to', t('Disallowed to send message because all recipients are blocked'));
    }
    else {
      $messages['error'][] = t('Disallowed to send message because all recipients are blocked');
    }
  }

  $messages = array_merge_recursive(module_invoke_all('privatemsg_message_validate', $message, $form), $messages);

  // Check if there are errors in $messages or if $form is TRUE, there are form errors.
  $success = empty($messages['error']) || ($form && count((array)form_get_errors()) > 0);
  return array(
    'success'  => $success,
    'messages'   => $messages,
  );
}

/**
 * Internal function to save a message.
 *
 * @param $message
 *   A $message array with the data that should be saved. If a thread_id exists
 *   it will be created as a reply to an existing thread. If not, a new thread
 *   will be created.
 *
 * @return
 *   The updated $message array.
 */
function _privatemsg_send($message) {

  drupal_alter('privatemsg_message_presave', $message);

  $index_sql = "INSERT INTO {pm_index} (mid, thread_id, uid, is_new, deleted) VALUES (%d, %d, %d, %d, 0)";
  if (isset($message['read_all']) && $message['read_all']) {
    // The message was sent in read all mode, add the author as recipient to all
    // existing messages.
    $query_messages = _privatemsg_assemble_query('messages', array($message['thread_id']), NULL);
    $conversation = db_query($query_messages['query']);
    while ($result = db_fetch_array($conversation)) {
      if (!db_query($index_sql, $result['mid'], $message['thread_id'], $message['author']->uid, 0)) {
        return FALSE;
      }
    }
  }

  // 1) Save the message body first.
  $args = array();
  $args[] = $message['subject'];
  $args[] = $message['author']->uid;
  $args[] = $message['body'];
  $args[] = $message['format'];
  $args[] = $message['timestamp'];
  $message_sql = "INSERT INTO {pm_message} (subject, author, body, format, timestamp) VALUES ('%s', %d, '%s', %d, %d)";
  db_query($message_sql, $args);
  $mid = db_last_insert_id('pm_message', 'mid');
  $message['mid'] = $mid;

  // Thread ID is the same as the mid if it's the first message in the thread.
  if (!isset($message['thread_id'])) {
    $message['thread_id'] = $mid;
  }

  // 2) Save message to recipients.
  // Each recipient gets a record in the pm_index table.
  foreach ($message['recipients'] as $recipient) {
    if (!db_query($index_sql, $mid, $message['thread_id'], $recipient->uid, 1) ) {
      // We assume if one insert failed then the rest may fail too against the
      // same table.
      return FALSE;
    }
  }

  // When author is also the recipient, we want to set message to UNREAD.
  // All other times the message is set to READ.
  $is_new = isset($message['recipients'][$message['author']->uid]) ? 1 : 0;

  // Also add a record for the author to the pm_index table.
  if (!db_query($index_sql, $mid, $message['thread_id'], $message['author']->uid, $is_new)) {
    return FALSE;
  }

  module_invoke_all('privatemsg_message_insert', $message);

  // If we reached here that means we were successful at writing all messages to db.
  return $message;
}

/**
 * Returns a link to send message form for a specific users.
 *
 * Contains permission checks of author/recipient, blocking and
 * if a anonymous user is involved.
 *
 * @param $recipient
 *   Recipient of the message
 * @param $account
 *   Sender of the message, defaults to the current user
 *
 * @return
 *   Either FALSE or a URL string
 *
 * @ingroup api
 */
function privatemsg_get_link($recipients, $account = array(), $subject = NULL) {
  if ($account == NULL) {
    global $user;
    $account = $user;
  }

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

  if (!privatemsg_user_access('write privatemsg', $account) || $account->uid == 0) {
    return FALSE;
  }

  $validated = array();
  foreach ($recipients as $recipient) {
    if (!privatemsg_user_access('read privatemsg', $recipient)) {
      continue;
    }
    if (count(module_invoke_all('privatemsg_block_message', $account, array($recipient->uid => $recipient))) > 0) {
      continue;
    }
    $validated[] = $recipient->uid;
  }
  if (empty($validated)) {
    return FALSE;
  }
  $url = 'messages/new/'. implode(',', $validated);
  if (!is_null($subject)) {
    $url .= '/'. $subject;
  }
  return $url;
}

/**
 * Load a single message.
 *
 * @param $pmid
 *   Message id, pm.mid field
 * @param $account
 *   For which account the message should be loaded.
 *   Defaults to the current user.
 *
 * @ingroup api
 */
function privatemsg_message_load($pmid, $account = NULL) {
  $messages = privatemsg_message_load_multiple(array($pmid), $account);
  return current($messages);
}

/**
 * Load multiple messages.
 *
 * @param $pmids
 *   Array of Message ids, pm.mid field
 * @param $account
 *   For which account the message should be loaded.
 *   Defaults to the current user.
 *
 * @ingroup api
 */
function privatemsg_message_load_multiple($pmids, $account = NULL) {
  // Avoid SQL error that would happen with an empty pm.mid IN () clause.
  if (empty($pmids)) {
    return array();
  }

  $query = _privatemsg_assemble_query('load', $pmids, $account);
  $result = db_query($query['query']);
  $messages = array();
  while ($message = db_fetch_array($result)) {
    // Load author of message.
    if (!($message['author'] = user_load($message['author']))) {
      // If user does not exist, load anonymous user.
      $message['author'] = user_load(array('uid' => 0));
    }
    $returned = module_invoke_all('privatemsg_message_load', $message);
    if (!empty($returned)) {
      $message = array_merge_recursive($returned, $message);
    }
    $messages[$message['mid']] = $message;
  }
  return $messages;
}

/**
 * Generates a query based on a query id.
 *
 * @param $query
 *   Either be a string ('some_id') or an array('group_name', 'query_id'),
 *   if a string is supplied, group_name defaults to 'privatemsg'.
 *
 * @return
 *    Array with the keys query and count. count can be used to count the
 *    elements which would be returned by query. count can be used together
 *    with pager_query().
 *
 * @ingroup sql
 */
function _privatemsg_assemble_query($query) {

  // Modules will be allowed to choose the prefix for the querybuilder, but if there is not one supplied, 'privatemsg' will be taken by default.
  if (is_array($query)) {
    $query_id = $query[0];
    $query_group = $query[1];
  }
  else {
    $query_id = $query;
    $query_group = 'privatemsg';
  }

  $SELECT = array();
  $INNER_JOIN = array();
  $WHERE = array();
  $GROUP_BY = array();
  $HAVING   = array();
  $ORDER_BY = array();
  $QUERY_ARGS = array('select' => array(), 'where' => array(), 'join' => array(), 'having' => array());
  $primary_table = '';

  $fragments = array(
    'select'      => $SELECT,
    'inner_join'  => $INNER_JOIN,
    'where'       => $WHERE,
    'group_by'    => $GROUP_BY,
    'having'      => $HAVING,
    'order_by'    => $ORDER_BY,
    'query_args'  => $QUERY_ARGS,
    'primary_table'  => $primary_table,
  );

  /**
   * Begin: dynamic arguments
   */
  $args = func_get_args();
  unset($args[0]);
  // we do the merge because we call call_user_func_array and not drupal_alter
  // this is necessary because otherwise we would not be able to use $args correctly (otherwise it doesnt unfold)
  $alterargs = array(&$fragments);
  $query_function = $query_group .'_sql_'. $query_id;
  if (!empty($args)) {
    $alterargs = array_merge($alterargs, $args);
  }
  /**
   * END: Dynamic arguments
   */
  if (!function_exists($query_function)) {
    drupal_set_message(t('Query function %function does not exist', array('%function' => $query_function)), 'error');
    return FALSE;
  }
  call_user_func_array($query_function, $alterargs);

  array_unshift($alterargs, $query_function);
  call_user_func_array('drupal_alter', $alterargs);

  $SELECT = $fragments['select'];
  $INNER_JOIN = $fragments['inner_join'];
  $WHERE = $fragments['where'];
  $GROUP_BY = $fragments['group_by'];
  $HAVING   = $fragments['having'];
  $ORDER_BY = $fragments['order_by'];
  $QUERY_ARGS = $fragments['query_args'];
  $primary_table = $fragments['primary_table'];

  // pgsql has a case sensitive LIKE - replace it with ILIKE. see http://drupal.org/node/462982
  if ($GLOBALS['db_type'] == 'pgsql') {
    $WHERE = str_replace('LIKE', 'ILIKE', $WHERE);
  }

  if (empty($primary_table)) {
    $primary_table = '{privatemsg} pm';
  }

  // Perform the whole query assembly only if we have something to select.
  if (!empty($SELECT)) {
    $str_select = implode(", ", $SELECT);
    $query = "SELECT {$str_select} FROM ". $primary_table;

    // Also build a count query which can be passed to pager_query to get a "page count" as that does not play well with queries including "GROUP BY".
    // In most cases,  "COUNT(*)" is enough to get the count query, but in queries involving a GROUP BY, we want a count of the number of groups we have, not the count of elements inside each group.
    // So we test if there is GROUP BY and if there is, count the number of distinct groups. If not, we go the normal wal and do a plain COUNT(*).
    if (!empty($GROUP_BY)) {
      // PostgreSQL does not support COUNT(sometextfield, someintfield), so I'm only using the first one
      // Works fine for thread_id/list but may generate an error when a more complex GROUP BY is used.
      $str_group_by_count = current($GROUP_BY);
      $count = "SELECT COUNT(DISTINCT {$str_group_by_count}) FROM ". $primary_table;
    }
    else {
      $count = "SELECT COUNT(*) FROM ". $primary_table;
    }

    if (!empty($INNER_JOIN)) {
      $str_inner_join = implode(' ', $INNER_JOIN);
      $query .= " {$str_inner_join}";
      $count .= " {$str_inner_join}";
    }
    if (!empty($WHERE)) {
      $str_where = '('. implode(') AND (', $WHERE) .')';
      $query .= " WHERE {$str_where}";
      $count .= " WHERE {$str_where}";
    }
    if (!empty($GROUP_BY)) {
      $str_group_by = ' GROUP BY '. implode(", ", $GROUP_BY) ;
      $query .= " {$str_group_by}";
    }
    if (!empty($HAVING)) {
      $str_having = '('. implode(') AND (', $HAVING) .')';
      $query .= " HAVING {$str_having}";
      // queries containing a HAVING break the count query on pgsql.
      // In this case, use the subquery method as outlined in http://drupal.org/node/303087#comment-1370752 .
      // The subquery method will work for all COUNT queries, but it is thought to be much slower, so we are only using it where other cross database approaches fail.
      $count = 'SELECT COUNT(*) FROM ('. $query .') as count';
    }
    if (!empty($ORDER_BY)) {
      $str_order_by = ' ORDER BY '. implode(", ", $ORDER_BY) ;
      $query .= " {$str_order_by}";
    }
    $QUERY_ARGS = array_merge($QUERY_ARGS['select'], $QUERY_ARGS['join'], $QUERY_ARGS['where'], $QUERY_ARGS['having']);
    if (!empty($QUERY_ARGS)) {
      _db_query_callback($QUERY_ARGS, TRUE);
      $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query);
      _db_query_callback($QUERY_ARGS, TRUE);
      $count = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $count);
    }
    return array('query' => $query, 'count' => $count);
  }
  return FALSE;
}

/**
 * Returns a form which handles and displays thread actions.
 *
 * Additional actions can be added with the privatemsg_thread_operations hook.
 * It is also possible to extend this form with additional buttons or other
 * elements, in that case, the definitions in the above hook need no label tag,
 * instead, the submit button key needs to match with the key of the operation.
 *
 * @see hook_privatemsg_thread_operations()
 *
 * @return
 *   The FAPI definitions for the thread action form.
 */
function _privatemsg_action_form() {
  $form = array(
      '#type'        => 'fieldset',
      '#title'       => t('Actions'),
      '#prefix'      => '<div class="container-inline">',
      '#suffix'      => '</div>',
      '#collapsible' => TRUE,
      '#collapsed'   => FALSE,
      '#weight'      => 15,
  );
  if (privatemsg_user_access('delete privatemsg')) {
    $form['delete'] = array(
        '#type'   => 'submit',
        '#value'  => t('Delete'),
    );
  }
  // Display all operations which have a label.
  $options = array(0 => t('More actions...'));
  foreach (module_invoke_all('privatemsg_thread_operations') as $operation => $array) {
    if (isset($array['label'])) {
      $options[$operation] = $array['label'];
    }
  }
  $form['operation'] = array(
      '#type'          => 'select',
      '#options'       => $options,
      '#default_value' => 0,
  );
  $form['submit'] = array(
      '#prefix'     => '<div class="privatemsg-op-button">',
      '#suffix'    => '</div>',
      '#type'       => 'submit',
      '#value'      => t('Execute'),
      '#submit'     => array('privatemsg_list_submit'),
      '#attributes' => array('class' => 'privatemsg-action-button'),
  );
  // JS for hiding the execute button.
  drupal_add_js(drupal_get_path('module', 'privatemsg') .'/privatemsg-list.js');
  return $form;
}

/**
 * Marks one or multiple threads as (un)read.
 *
 * @param $threads
 *   Array with thread id's or a single thread id.
 * @param $status
 *   Either PRIVATEMSG_READ or PRIVATEMSG_UNREAD, sets the new status.
 * @param $account
 *   User object for which the threads should be deleted, defaults to the
 *   current user.
 */
function privatemsg_thread_change_status($threads, $status, $account = NULL) {
  if (!is_array($threads)) {
    $threads = array($threads);
  }
  if (empty($account)) {
    global $user;
    $account = drupal_clone($user);
  }
  // Merge status and uid with the threads list. array_merge() will not overwrite/ignore thread_id 1.
  $params = array_merge(array($status, $account->uid), $threads);
  db_query('UPDATE {pm_index} SET is_new = %d WHERE uid = %d AND thread_id IN ('. db_placeholders($threads) .')', $params);

  if ($status == PRIVATEMSG_UNREAD) {
    drupal_set_message(format_plural(count($threads), 'Marked 1 thread as unread.', 'Marked @count threads as unread.'));
  }
  else {
    drupal_set_message(format_plural(count($threads), 'Marked 1 thread as read.', 'Marked @count threads as read.'));
  }
}
/**
 * Returns a table header definition based on the submitted keys.
 *
 * Uses @link theming theme patterns @endlink to theme single headers.
 *
 * @param $has_posts
 *   TRUE when there is at least one row. Decides if the select all checkbox
 *   should be displayed.
 * @param $keys
 *   Array with the keys which are present in the query/should be displayed.
 * @return
 *   Array with header defintions for tablesort_sql and theme('table').
 */
function _privatemsg_list_headers($has_posts, $keys) {
  $select_header = $has_posts ? theme('table_select_header_cell') : '';
  $select_header['#weight'] = -50;

  // theme() doesn't include the theme file for patterns, we need to do it manually.
  include_once drupal_get_path('module', 'privatemsg') .'/privatemsg.theme.inc';

  $header = array($select_header);
  foreach ($keys as $key) {
    // First, try to load a specific theme for that header, if not present, use the default.
    if ($return = theme(array('privatemsg_list_header__'. $key, 'privatemsg_list_header'))) {
      // The default theme returns nothing, only store the value if we have something.
      $header[$key] = $return;
    }
  }
  if (count($header) == 1) {
    // No header definition returned, fallback to the default.
    $header += _privatemsg_list_headers_fallback($keys);
  }
  return $header;
}

/**
 * Table header definition for themes that don't support theme patterns.
 *
 * @return
 *   Array with the correct headers.
 */
function _privatemsg_list_headers_fallback($keys) {
  $header = array();
  foreach ($keys as $key) {
    $theme_function = 'phptemplate_privatemsg_list_header__' . $key;
    if (function_exists($theme_function)) {
      $header[$key] = $theme_function();
    }
  }

  return $header;
}

/**
 * Formats a row in the message list.
 *
 * Uses @link theming theme patterns @endlink to theme single fields.
 *
 * @param $thread
 *   Array with the row data returned by the database.
 * @return
 *   Row definition for use with theme('table')
 */
function _privatemsg_list_thread($thread) {
  $row = array('data' => array());

  if (!empty($thread['is_new'])) {
    // Set the css class in the tr tag.
    $row['class'] = 'privatemsg-unread';
  }
  foreach ($thread as $key => $data) {
    // First, try to load a specific theme for that field, if not present, use the default.
    if ($return = theme(array('privatemsg_list_field__'. $key, 'privatemsg_list_field'), $thread)) {
      // The default theme returns nothing, only store the value if we have something.
      $row['data'][$key] = $return;
    }
  }
  if (empty($row['data'])) {
    $row['data'] = _privatemsg_list_thread_fallback($thread);
  }
  return $row;
}

/**
 * Table row definition for themes that don't support theme patterns.
 *
 * @return
 *   Array with row data.
 */
function _privatemsg_list_thread_fallback($thread) {
  $row_data = array();
  foreach ($thread as $key => $data) {
    $theme_function = 'phptemplate_privatemsg_list_field__' . $key;
    if (function_exists($theme_function)) {
      $row_data[$key] = $theme_function($thread);
    }
  }

  return $row_data;
}

/**
 * Menu callback for messages/undo/action.
 *
 * This function will test if an undo callback is stored in SESSION and
 * execute it.
 */
function privatemsg_undo_action() {
  // Check if a undo callback for that user exists.
  if (isset($_SESSION['privatemsg']['undo callback']) && is_array($_SESSION['privatemsg']['undo callback'])) {
    $undo = $_SESSION['privatemsg']['undo callback'];
    // If the defined undo callback exists, execute it
    if (isset($undo['function']) && isset($undo['args'])) {
      // Load the user object.
      if (isset($undo['args']['account']) && $undo['args']['account'] > 0) {
        $undo['args']['account'] = user_load((int)$undo['args']['account']);
      }
      call_user_func_array($undo['function'], $undo['args']);
    }
    // Return back to the site defined by the destination GET param.
    drupal_goto();
  }
}

/**
 * Process privatemsg_list form submissions.
 *
 * Execute the chosen action on the selected messages. This function is
 * based on node_admin_nodes_submit().
 */
function privatemsg_list_submit($form, &$form_state) {
  // Load all available operation definitions.
  $operations = module_invoke_all('privatemsg_thread_operations');

  // Default "default" operation, which won't do anything.
  $operation = array('callback' => 0);

  // Check if a valid operation has been submitted.
  if (isset($form_state['values']['operation']) && isset($operations[$form_state['values']['operation']])) {
    $operation = $operations[$form_state['values']['operation']];
  }

  // Load all keys where the value is the current op.
  $keys = array_keys($form_state['values'], $form_state['values']['op']);

  // The first one is op itself, we need to use the second.
  if (isset($keys[1]) && isset($operations[$keys[1]])) {
    $operation = $operations[$keys[1]];
  }

  // Only execute something if we have a valid callback and at least one checked thread.
  if (!empty($operation['callback'])) {
    privatemsg_operation_execute($operation, $form_state['values']['threads'], $form_state['values']['account']);
  }
}

/**
 * Execute an operation on a number of threads.
 *
 * @param $operation
 *   The operation that should be executed.
 *   @see hook_privatemsg_thread_operations()
 * @param $threads
 *   An array of thread ids. The array is filtered before used, a checkboxes
 *   array can be directly passed to it.
 */
function privatemsg_operation_execute($operation, $threads, $account = null) {
  // Filter out unchecked threads, this gives us an array of "checked" threads.
  $threads = array_filter($threads);

  if (empty($threads)) {
    // Do not execute anything if there are no checked threads.
    return;
  }
  // Add in callback arguments if present.
  if (isset($operation['callback arguments'])) {
    $args = array_merge(array($threads), $operation['callback arguments']);
  }
  else {
    $args = array($threads);
  }

  // Add the user object to the arguments.
  if ($account) {
    $args[] = $account;
  }

  // Execute the chosen action and pass the defined arguments.
  call_user_func_array($operation['callback'], $args);

  // Check if that operation has defined an undo callback.
  if (isset($operation['undo callback']) && $undo_function = $operation['undo callback']) {
    // Add in callback arguments if present.
    if (isset($operation['undo callback arguments'])) {
      $undo_args = array_merge(array($threads), $operation['undo callback arguments']);
    }
    else {
      $undo_args = array($threads);
    }

    // Avoid saving the complete user object in the session.
    if ($account) {
      $undo_args['account'] = $account->uid;
    }
    // Store the undo callback in the session and display a "Undo" link.
    // @todo: Provide a more flexible solution for such an undo action, operation defined string for example.
    $_SESSION['privatemsg']['undo callback'] = array('function' => $undo_function, 'args' => $undo_args);
    $undo = url('messages/undo/action', array('query' => drupal_get_destination()));

    drupal_set_message(t('The previous action can be <a href="!undo">undone</a>.', array('!undo' => $undo)));
  }
}

/**
 * Delete or restore one or multiple threads.
 *
 * @param $threads
 *   Array with thread id's or a single thread id.
 * @param $delete
 *   Indicates if the threads should be deleted or restored.
 *   1 => delete, 0 => restore.
 * @param $account
 *   User object for which the threads should be deleted,
 *   defaults to the current user.
 */
function privatemsg_thread_change_delete($threads, $delete, $account = NULL) {
  if (!is_array($threads)) {
    $threads = array($threads);
  }
  if (empty($account)) {
    global $user;
    $account = drupal_clone($user);
  }

  // Merge status and uid with the threads list. array_merge() will not overwrite/ignore thread_id 1.
  $params = array_merge(array($delete, $account->uid), $threads);

  // Load all messages of those threads including the deleted.
  $query = _privatemsg_assemble_query('messages', $threads, $account, TRUE);
  $result = db_query($query['query']);

  // Delete each message. We need to do that to trigger the delete hook.
  while ($row = db_fetch_array($result)) {
    privatemsg_message_change_delete($row['mid'], $delete, $account);
  }

  if ($delete) {
    drupal_set_message(format_plural(count($threads), 'Deleted 1 thread.', 'Deleted @count threads.'));
  }
  else {
    drupal_set_message(format_plural(count($threads), 'Restored 1 thread.', 'Restored @count threads.'));
  }
}

/**
 * Implements hook_privatemsg_thread_operations().
 */
function privatemsg_privatemsg_thread_operations() {
  $operations = array(
    'mark as read' => array(
      'label' => t('Mark as read'),
      'callback' => 'privatemsg_thread_change_status',
      'callback arguments' => array('status' => PRIVATEMSG_READ),
      'undo callback' => 'privatemsg_thread_change_status',
      'undo callback arguments' => array('status' => PRIVATEMSG_UNREAD),
    ),
    'mark as unread' => array(
      'label' => t('Mark as unread'),
      'callback' => 'privatemsg_thread_change_status',
      'callback arguments' => array('status' => PRIVATEMSG_UNREAD),
      'undo callback' => 'privatemsg_thread_change_status',
      'undo callback arguments' => array('status' => PRIVATEMSG_READ),
    ),
  );
  if (privatemsg_user_access('delete privatemsg')) {
    $operations['delete'] = array(
      'callback' => 'privatemsg_thread_change_delete',
      'callback arguments' => array('delete' => 1),
      'undo callback' => 'privatemsg_thread_change_delete',
      'undo callback arguments' => array('delete' => 0),
    );
  }
  return $operations;
}

/**
 * Implementation of hook_views_api().
 */
function privatemsg_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'privatemsg') . '/views',
  );
}
