<?php
/**
 * Phergie
 *
 * PHP version 5
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.
 * It is also available through the world-wide-web at this URL:
 * http://phergie.org/license
 *
 * @category  Phergie
 * @package   Phergie_Plugin_Remind
 * @author    Phergie Development Team <team@phergie.org>
 * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
 * @license   http://phergie.org/license New BSD License
 * @link      http://pear.phergie.org/package/Phergie_Plugin_Remind
 */

/**
 * Parses and logs messages that should be relayed to other users the next time
 * the recipient is active on the same channel.
 *
 * @category Phergie
 * @package  Phergie_Plugin_Remind
 * @author   Phergie Development Team <team@phergie.org>
 * @license  http://phergie.org/license New BSD License
 * @link     http://pear.phergie.org/package/Phergie_Plugin_Remind
 * @uses     Phergie_Plugin_Command pear.phergie.org
 * @uses     Phergie_Plugin_Time pear.phergie.org
 * @uses     extension PDO
 * @uses     extension pdo_sqlite
 */
class Phergie_Plugin_Remind extends Phergie_Plugin_Abstract
{
    /**
     * Number of reminders to show in public.
     */
    protected $publicReminders = 3;

    /**
     * PDO resource for a SQLite database containing the reminders.
     *
     * @var resource
     */
    protected $db;

    /**
     * Flag that indicates whether or not to use an in-memory reminder list.
     *
     * @var bool
     */
    protected $keepListInMemory = true;

    /**
     * In-memory store for pending reminders.
     */
    protected $msgStorage = array();

    /**
     * Check for dependencies.
     *
     * @return void
     */
    public function onLoad()
    {
        $plugins = $this->getPluginHandler();
        $plugins->getPlugin('Command');
        $plugins->getPlugin('Time');

        if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
            $this->fail('PDO and pdo_sqlite extensions must be installed');
        }

        $dir = dirname(__FILE__) . '/' . $this->getName();
        $path = $dir . '/reminder.db';
        if (!file_exists($dir)) {
            mkdir($dir);
        }

        if (isset($this->config['remind.use_memory'])) {
            $this->keepListInMemory = (bool) $this->config['remind.use_memory'];
        }

        if (isset($this->config['remind.public_reminders'])) {
            $this->publicReminders = (int) $this->config['remind.public_reminders'];
            $this->publicReminders = max($this->publicReminders, 0);
        }

        try {
            $this->db = new PDO('sqlite:' . $path);
            $this->createTables();
        } catch (PDO_Exception $e) {
            throw new Phergie_Plugin_Exception($e->getMessage());
        }
    }

    /**
     * Intercepts a message and processes any contained recognized commands.
     *
     * @return void
     */
    public function onPrivmsg()
    {
        $source = $this->getEvent()->getSource();
        $nick = $this->getEvent()->getNick();

        $this->deliverReminders($source, $nick);
    }

    /**
     * Handle reminder requests
     *
     * @param string $recipient recipient of the message
     * @param string $message   message to tell the recipient
     *
     * @return void
     * @see handleRemind()
     */
    public function onCommandTell($recipient, $message)
    {
        $this->handleRemind($recipient, $message);
    }

    /**
     * Handle reminder requests
     *
     * @param string $recipient recipient of the message
     * @param string $message   message to tell the recipient
     *
     * @return void
     * @see handleRemind()
     */
    public function onCommandAsk($recipient, $message)
    {
        $this->handleRemind($recipient, $message);
    }

    /**
     * Handle reminder requests
     *
     * @param string $recipient recipient of the message
     * @param string $message   message to tell the recipient
     *
     * @return void
     * @see handleRemind()
     */
    public function onCommandRemind($recipient, $message)
    {
        $this->handleRemind($recipient, $message);
    }

    /**
     * Handles the tell/remind command (stores the message)
     *
     * @param string $recipient name of the recipient
     * @param string $message   message to store
     *
     * @return void
     */
    protected function handleRemind($recipient, $message)
    {
        $source = $this->getEvent()->getSource();
        $nick = $this->getEvent()->getNick();

        if (!$this->getEvent()->isInChannel()) {
            $this->doPrivmsg($source, 'Reminders must be requested in-channel.');
            return;
        }

        $q = $this->db->prepare(
            'INSERT INTO remind
                (
                    time,
                    channel,
                    recipient,
                    sender,
                    message
                )
            VALUES
                (
                    :time,
                    :channel,
                    :recipient,
                    :sender,
                    :message
               )'
        );
        try {
            $q->execute(
                array(
                    'time' => date(DATE_RFC822),
                    'channel' => $source,
                    'recipient' => strtolower($recipient),
                    'sender' => strtolower($nick),
                    'message' => $message
                )
            );
        } catch (PDOException $e) {
        }

        if ($rowid = $this->db->lastInsertId()) {
            $this->doPrivmsg($source, 'ok, ' . $nick . ', message stored');
        } else {
            $this->doPrivmsg(
                $source,
                $nick . ': bad things happened. Message not saved.'
            );
            return;
        }

        if ($this->keepListInMemory) {
            $this->msgStorage[$source][strtolower($recipient)] = $rowid;
        }
    }

    /**
     * Determines if the user has pending reminders, and if so, delivers them.
     *
     * @param string $channel channel to check
     * @param string $nick    nick to check
     *
     * @return void
     */
    protected function deliverReminders($channel, $nick)
    {
        if ($channel[0] != '#') {
            // private message, not a channel, so don't check
            return;
        }

        // short circuit if there's no message in memory (if allowed)
        if ($this->keepListInMemory
            && !isset($this->msgStorage[$channel][strtolower($nick)])
        ) {
            return;
        }

        // fetch and deliver messages
        $reminders = $this->fetchMessages($channel, $nick);
        if (count($reminders) > $this->publicReminders) {
            $msgs = array_slice($reminders, 0, $this->publicReminders);
            $privmsgs = array_slice($reminders, $this->publicReminders);
        } else {
            $msgs = $reminders;
            $privmsgs = false;
        }

        foreach ($msgs as $msg) {
            $ts = $this->plugins->time->getCountdown($msg['time']);
            $formatted = sprintf(
                '%s: (from %s, %s ago) %s',
                $nick, $msg['sender'], $ts, $msg['message']
            );
            $this->doPrivmsg($channel, $formatted);
            $this->deleteMessage($msg['rowid'], $channel, $nick);
        }

        if ($privmsgs) {
            foreach ($privmsgs as $msg) {
                $ts = $this->plugins->time->getCountdown($msg['time']);
                $formatted = sprintf(
                    'from %s, %s ago: %s',
                    $msg['sender'], $ts, $msg['message']
                );
                $this->doPrivmsg($nick, $formatted);
                $this->deleteMessage($msg['rowid'], $channel, $nick);
            }
            $formatted = sprintf(
                '%s: (%d more messages sent in private.)',
                $nick, count($privmsgs)
            );
            $this->doPrivmsg($channel, $formatted);
        }
    }

    /**
     * Get pending messages (for a specific channel/recipient)
     *
     * @param string $channel   channel on which to check for pending messages
     * @param string $recipient user for which to check pending messages
     *
     * @return array of records
     */
    protected function fetchMessages($channel = null, $recipient = null)
    {
        if ($channel) {
            $qClause = 'WHERE channel = :channel AND recipient LIKE :recipient';
            $params = compact('channel', 'recipient');
        } else {
            $qClause = '';
            $params = array();
        }
        $q = $this->db->prepare(
            'SELECT rowid, channel, sender, recipient, time, message
            FROM remind ' . $qClause
        );
        $q->execute($params);
        return $q->fetchAll();
    }

    /**
     * Deletes a delivered message
     *
     * @param int    $rowid   ID of the message to delete
     * @param string $channel message's channel
     * @param string $nick    message's recipient
     *
     * @return void
     */
    protected function deleteMessage($rowid, $channel, $nick)
    {
        $nick = strtolower($nick);
        $q = $this->db->prepare('DELETE FROM remind WHERE rowid = :rowid');
        $q->execute(array('rowid' => $rowid));

        if ($this->keepListInMemory) {
            if (isset($this->msgStorage[$channel][$nick])
                && $this->msgStorage[$channel][$nick] == $rowid
            ) {
                unset($this->msgStorage[$channel][$nick]);
            }
        }
    }

    /**
     * Determines if a table exists
     *
     * @param string $name Table name
     *
     * @return bool
     */
    protected function haveTable($name)
    {
        $sql = 'SELECT COUNT(*) FROM sqlite_master WHERE name = '
            . $this->db->quote($name);
        return (bool) $this->db->query($sql)->fetchColumn();
    }

    /**
     * Creates the database table(s) (if they don't exist)
     *
     * @return void
     */
    protected function createTables()
    {
        if (!$this->haveTable('remind')) {
            $this->db->exec(
                'CREATE TABLE
                    remind
                    (
                        time INTEGER,
                        channel TEXT,
                        recipient TEXT,
                        sender TEXT,
                        message TEXT
                    )'
            );
        }
    }

    /**
     * Populates the in-memory cache of pending reminders
     *
     * @return void
     */
    protected function populateMemory()
    {
        if (!$this->keepListInMemory) {
            return;
        }
        foreach ($this->fetchMessages() as $msg) {
            $this->msgStorage[$msg['channel']][$msg['recipient']] = $msg['rowid'];
        }
    }
}
