<?php

/**
* @see Phergie_Plugin_Abstract_Command
*/
require_once 'Phergie/Plugin/Abstract/Command.php';

/**
* @see Phergie_Plugin_Users
*/
require_once 'Phergie/Plugin/Users.php';

/**
* Logs channel events and handles requests for logged data pertaining to the
* last actions taken or messages sent by a given user or posts sent
* containing a given search phrase.
*/
class Phergie_Plugin_Logging extends Phergie_Plugin_Abstract_Command
{
    /**
    * Indicates that a local directory is required for this plugin
    *
    * @var bool
    */
    protected $needsDir = true;

    /**
    * Indicates a JOIN event in the type column of the logs table
    *
    * @const int
    */
    const JOIN = 1;

    /**
    * Indicates a PART event in the type column of the logs table
    *
    * @const int
    */
    const PART = 2;

    /**
    * Indicates a QUIT event in the type column of the logs table
    *
    * @const int
    */
    const QUIT = 3;

    /**
    * Indicates a PRIVMSG event in the type column of the logs table
    *
    * @const int
    */
    const PRIVMSG = 4;

    /**
    * Indicates a CTCP ACTION event in the type column of the logs table
    *
    * @const int
    */
    const ACTION = 5;

    /**
    * Indicates a NICK event in the type column of the logs table
    *
    * @const int
    */
    const NICK = 6;

    /**
    * Indicates a KICK event in the type column of the logs table
    *
    * @const int
    */
    const KICK = 7;

    /**
    * Indicates a MODE event in the type column of the logs table
    *
    * @const int
    */
    const MODE = 8;

    /**
    * Indicates a TOPIC event in the type column of the logs table
    *
    * @const int
    */
    const TOPIC = 9;

    /**
    * PDO instance for the database
    *
    * @var PDO
    */
    protected $db = null;

    /**
    * Prepared statement for searching for the last logged action in which a
    * particular user was mentioned
    *
    * @var PDOStatement
    */
    protected $search;

    /**
    * Prepared statement for searching for the last logged action originating
    * from a particular user
    *
    * @var PDOStatement
    */
    protected $seen;

    /**
    * Prepared statement for searching for the last logged PRIVMSG action or
    * CTCP ACTION command originating from a particular user
    *
    * @var PDOStatement
    */
    protected $heard;

    /**
    * PRepared statement for searching for one or more time ranges during 
    * which a particular user is most likely to be present in a particular 
    * channel 
    *
    * @var PDOStatement
    */
    protected $willsee;

    /**
    * Prepared statement for inserting new log entries
    *
    * @var PDOStatement
    */
    protected $insert;

    /**
    * Answers for seen when the user is present
    *
    * @var array
    */
    protected $present = array
    (
        'Open your eyes already!',
        'Are you blind?',
        'Behind you!',
        'Over there!',
    );

    /**
    * Answers for when record of a user cannot be found
    *
    * @var array
    */
    protected $missing = array
    (
        'must be mute, or one of your imaginary friends?',
        'was never heard from... a first time.',
        'was never heard from... to begin with.',
        'must have eluded my ubiquity.',
    );

    /**
    * Action descriptions corresponding to event constants
    *
    * @var array
    */
    protected $actions = array
    (
        self::JOIN => 'joining this channel',
        self::PART => 'leaving this channel because',
        self::QUIT => 'quitting',
        self::PRIVMSG => 'saying',
        self::ACTION => 'saying',
        self::NICK => 'changing nick to',
        self::KICK => 'being kicked off because',
    );

    /**
    * Initializes the database.
    *
    * @return void
    */
    public function init()
    {
        try {
            // Initialize the database connection
            $db = $this->dir . 'logging.db';
            $create = !file_exists($db);
            $this->db = new PDO('sqlite:' . $db);

            // Create database tables if necessary
            if ($create) {
                $result = $this->db->exec('
                    CREATE TABLE logs (
                        tstamp VARCHAR(19),
                        type SHORTINT,
                        chan VARCHAR(45),
                        nick VARCHAR(25),
                        message VARCHAR(255)
                    );
                    CREATE INDEX channicktype ON logs (tstamp, type, chan, nick);
                    CREATE INDEX channick ON logs (tstamp, chan, nick);
                ');
            }

            // Initialize prepared statements for common operations
            $this->search = $this->db->prepare('
                SELECT tstamp, type, chan, nick, message
                FROM logs
                WHERE nick LIKE :phrase
                OR message LIKE :phrase
                ORDER BY tstamp DESC
                LIMIT :limit
            ');

            $this->seen = $this->db->prepare('
                SELECT tstamp, type, message
                FROM logs
                WHERE nick = :name
                AND chan = :chan
                ORDER BY tstamp DESC
                LIMIT 1
            ');

            $this->heard = $this->db->prepare('
                SELECT tstamp, type, message
                FROM logs
                WHERE type IN (' . self::PRIVMSG . ', ' . self::ACTION . ')
                AND nick = :nick
                AND chan = :chan
                ORDER BY tstamp DESC
                LIMIT 1
            ');

            $this->willsee = $this->db->prepare('
                SELECT strftime("%H", tstamp) post_hour, COUNT(*) post_count
                FROM logs
                WHERE type IN (' . self::PRIVMSG . ', ' . self::ACTION . ')
                AND nick = :nick
                AND chan = :chan
                GROUP BY strftime("%H", tstamp)
                ORDER BY 2 DESC, 1
                LIMIT 1
            ');

            $this->insert = $this->db->prepare('
                INSERT INTO logs (
                    tstamp,
                    type,
                    chan,
                    nick,
                    message
                )
                VALUES (
                    :tstamp,
                    :type,
                    :chan,
                    :nick,
                    :message
                )
            ');
        } catch (PDOException $e) { }
    }

    /**
    * Returns whether or not the plugin's dependencies are met.
    *
    * @param Phergie_Driver_Abstract $client Client instance
    * @param array $plugins List of short names for plugins that the
    *                       bootstrap file intends to instantiate
    * @see Phergie_Plugin_Abstract_Base::checkDependencies()
    * @return bool TRUE if dependencies are met, FALSE otherwise
    */
    public static function checkDependencies(Phergie_Driver_Abstract $client, array $plugins)
    {
        if (! in_array('Users', $plugins)
            || ! Phergie_Plugin_Users::checkDependencies($client, $plugins)) {
            return false;
        }

        if (!extension_loaded('PDO') 
            || !extension_loaded('pdo_sqlite')) {
            return false;
        }

        return true;
    }

    /**
    * Formats a timestamp for display purposes.
    *
    * @param string $timestamp Timestamp to format
    * @return string Formatted timestamp
    */
    protected function formatTimestamp($timestamp)
    {
        return preg_replace(
            '#^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$#',
            '$1-$2-$3 @ $4:$5:$6',
            $timestamp
        );
    }

    /**
    * Returns a random message used when a user is currently present.
    *
    * @return string
    */
    protected function randomPresent()
    {
        return $this->present[array_rand($this->present)];
    }

    /**
    * Returns a random message used when no logged record of a user exists.
    *
    * @return string
    */
    protected function randomMissing()
    {
        return $this->missing[array_rand($this->missing)];
    }

    /**
    * Inserts a new entry in the log database.
    *
    * @param int $type Class constant representing the event type
    * @param string $chan Name of the channel in which the event occurs
    * @param string $nick Nick of the user from which the event originates
    * @param string $message Message associated with the event if applicable
    *                        (optional)
    * @return void
    */
    protected function insertEvent($type, $chan, $nick, $message = null)
    {
        if (!$this->db) {
            return;
        }

        $params = array(
            ':tstamp' => date('Y-m-d H:i:s'),
            ':type' => $type,
            ':chan' => $chan,
            ':nick' => $nick,
            ':message' => trim($message)
        );

        $result = $this->insert->execute($params);
    }

    /**
    * Logs incoming messages.
    *
    * @return void
    */
    public function onPrivmsg()
    {
        // Allow the Command plugin to process command calls
        parent::onPrivmsg();

        if ($this->event->isInChannel()) {
            $this->insertEvent(
                self::PRIVMSG,
                $this->event->getSource(),
                $this->event->getNick(),
                $this->event->getArgument(1)
            );
        }
    }

    /**
    * Logics incoming actions.
    *
    * @return void
    */
    public function onAction()
    {
        if ($this->event->isInChannel()) {
            $this->insertEvent(
                self::ACTION,
                $this->event->getSource(),
                $this->event->getNick(),
                $this->event->getArgument(1)
            );
        }
    }

    /**
    * Tracks users joining.
    *
    * @return void
    */
    public function onJoin()
    {
        $this->insertEvent(
            self::JOIN,
            $this->event->getSource(),
            $this->event->getNick()
        );
    }

    /**
    * Tracks users parting.
    *
    * @return void
    */
    public function onPart()
    {
        $this->insertEvent(
            self::PART,
            $this->event->getSource(),
            $this->event->getNick(),
            $this->event->getArgument(1)
        );
    }

    /**
    * Tracks users being kicked.
    *
    * @return void
    */
    public function onKick()
    {
        $this->insertEvent(
            self::KICK,
            $this->event->getSource(),
            $this->event->getNick(),
            $this->event->getArgument(1)
        );
    }

    /**
    * Tracks users changing modes.
    *
    * @return void
    */
    public function onMode()
    {
        $this->insertEvent(
            self::MODE,
            $this->event->getSource(),
            $this->event->getNick(),
            implode(' ', array_slice($this->event->getArguments(), 1))
        );
    }

    /**
    * Tracks channel topic changes.
    *
    * @return void
    */
    public function onTopic()
    {
        $this->insertEvent(
            self::TOPIC,
            $this->event->getSource(),
            $this->event->getNick(),
            $this->event->getArgument(1)
        );
    }

    /**
    * Tracks users quitting.
    *
    * @return void
    */
    public function onQuit()
    {
        $nick = $this->event->getNick();

        foreach (Phergie_Plugin_Users::getChannels($nick) as $chan) {
            $this->insertEvent(
                self::QUIT,
                $chan,
                $this->event->getNick(),
                $this->event->getArgument(0)
            );
        }
    }

    /**
    * Tracks users changing nicks.
    *
    * @return void
    */
    public function onNick()
    {
        $nick = $this->event->getNick();

        foreach (Phergie_Plugin_Users::getChannels($nick) as $chan) {
            $this->insertEvent(
                self::NICK,
                $chan,
                $this->event->getNick(),
                $this->event->getArgument(0)
            );
        }
    }

    /**
    * Responds to requests for logged messages containing a particular search
    * phrase.
    *
    * @param string $phrase Phrase to search for
    * @return void
    */
    public function onDoSearch($phrase)
    {
        if (!$this->db) {
            return;
        }

        $source = $this->event->getSource();

        $params = array(
            ':phrase' => '%' . $phrase . '%',
            ':limit' => ($source[0] == '#' ? 1 : 6)
        );

        $this->search->execute($params);

        foreach($this->search as $row) {
            $this->doPrivmsg(
                $source,
                sprintf(
                    '%s was seen %s: %s on %s (on %s)',
                    $row['nick'],
                    $this->actions[$row['type']],
                    $row['message'],
                    $row['chan'],
                    $this->formatTimestamp($row['tstamp'])
                )
            );
        }
    }

    /**
    * Responds to requests for the last logged action originating from a
    * particular user.
    *
    * @param string $user Nick of the user to search for
    * @return void
    */
    public function onDoSeen($user)
    {
        if (!$this->db) {
            return;
        }

    	// Don't match if user has a space (obviously it's not a nick)
    	if (strpos($user, ' ') !== false) {
    		return;
    	}

        $source = $this->event->getSource();
        $target = $this->event->getNick();

        // Handle 'me' alias
        if ($user == 'me') {
            $user = $target;
        }

        // Person is online, send a random prank answer
        if (Phergie_Plugin_Users::isIn($user, $source)) {
            $this->doPrivmsg(
                $source,
                sprintf(
                    '%s: %s',
                    $target,
                    $this->randomPresent()
                )
            );

        // Person is offline
        } else {
            $params = array(
                ':name' => $user,
                ':chan' => $source
            );

            $this->seen->execute($params);
            $row = $this->seen->fetch(PDO::FETCH_ASSOC);

            // Send the last action if available
            if ($row) {
                $this->doPrivmsg(
                    $source,
                    sprintf(
                        '%s: %s was last seen %s: %s (on %s)',
                        $target,
                        $user,
                        $this->actions[$row['type']],
                        $row['message'],
                        $this->formatTimestamp($row['tstamp'])
                    )
                );

            // Send a prank answer if no last action is found
            } else {
                $this->doPrivmsg(
                    $source,
                    sprintf(
                        '%s: %s %s',
                        $target,
                        $user,
                        $this->randomMissing()
                    )
                );
            }
        }
    }

    /**
    * Responds to requests for the last logged PRIVMSG action or CTCP ACTION
    * command originating from a particular user.
    *
    * @param string $user Nick of the user to search for
    * @return void
    */
    public function onDoHeard($user)
    {
        if (!$this->db) {
            return;
        }

    	// Don't match if user has a space (obviously it's not a nick)
    	if (strpos($user, ' ') !== false) {
    		return;
    	}

        $source = $this->event->getSource();
        $target = $this->event->getNick();

        // Handle 'me' alias
        if ($user == 'me') {
            $user = $target;
        }

        // Search for the last recorded action
        $params = array(
            ':nick' => $user,
            ':chan' => $source
        );

        $this->heard->execute($params);
        $row = $this->heard->fetch(PDO::FETCH_ASSOC);

        // Send the last action if available
        if($row) {
            $this->doPrivmsg(
                $source,
                sprintf(
                    '%s: %s\'s last words were: %s (on %s)',
                    $target,
                    $user,
                    $row['message'],
                    $this->formatTimestamp($row['tstamp'])
                )
            );

        // Send a prank answer if no last action is found
        } else {
            $this->doPrivmsg(
                $source,
                sprintf(
                    '%s: %s %s',
                    $target,
                    $user,
                    $this->randomMissing()
                )
            );
        }
    }

    /**
    * Responds to requests for a time range during which a particular user 
    * is most likely to be present in the channel from which the request 
    * originates.
    *
    * @param string $user Nick of the user to search for
    * @return void
    */
    public function onDoWillsee($user)
    {
        if (!$this->db) {
            return;
        }
        
        // Check to make sure the request came from a channel
        $source = $this->event->getSource();
        if ($source[0] != '#') {
            return;
        }

        // Handle cases where the bot is the subject
        if ($user == $this->getIni('nick')) {
            $this->doPrivmsg($source, 'What are you talking about? I\'m always here!');
            return;
        }

        // Handle 'me' alias
        if ($user == 'me') {
            $user = $this->event->getNick();
        }

        // Perform the search
        $params = array(
            ':nick' => $user,
            ':chan' => $source
        );

        $this->willsee->execute($params);
        $prediction = $this->willsee->fetchColumn();

        // Return if no results are found
        if ($prediction === false) {
            $this->doPrivmsg($source, $user . ' ' . $this->randomMissing());
            return;
        }

        // Calculate a predicted time of arrival 
        $hour = date('H');
        if ($hour > $prediction) {
            $prediction = 24 - ($hour - $prediction);
        } else {
            $prediction = $prediction - $hour;
        }

        // Return with a message including the prediction
        $message = $target . ': ' . $user . ' is most likely to be online ';
        if ($prediction == 0) {
            $message .= 'now!';
        } elseif ($prediction == 1) {
            $message .= 'in 1 hour.';
        } else {
            $message .= 'in ' . $prediction . ' hours.';
        }
        $this->doPrivmsg($source, $message);
    }
}
