<?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_Karma
 * @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_Karma
 */

/**
 * Handles requests for incrementation or decrementation of a maintained list
 * of counters for specified terms.
 *
 * @category Phergie
 * @package  Phergie_Plugin_Karma
 * @author   Phergie Development Team <team@phergie.org>
 * @license  http://phergie.org/license New BSD License
 * @link     http://pear.phergie.org/package/Phergie_Plugin_Karma
 * @uses     extension PDO
 * @uses     extension pdo_sqlite
 * @uses     Phergie_Plugin_Command pear.phergie.org
 */
class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract
{
    /**
     * SQLite object
     *
     * @var resource
     */
    protected $db = null;

    /**
     * Prepared statement to add a new karma record
     *
     * @var PDOStatement
     */
    protected $insertKarma;

    /**
     * Prepared statement to update an existing karma record
     *
     * @var PDOStatement
     */
    protected $updateKarma;

    /**
     * Retrieves an existing karma record
     *
     * @var PDOStatement
     */
    protected $fetchKarma;

    /**
     * Retrieves an existing fixed karma record
     *
     * @var PDOStatement
     */
    protected $fetchFixedKarma;

    /**
     * Retrieves a positive answer for a karma comparison
     *
     * @var PDOStatement
     */
    protected $fetchPositiveAnswer;

    /**
     * Retrieves a negative answer for a karma comparison
     *
     * @var PDOStatement
     */
    protected $fetchNegativeAnswer;

    /**
     * Check for dependencies and initializes a database connection and
     * prepared statements.
     *
     * @return void
     */
    public function onLoad()
    {
        $plugins = $this->getPluginHandler();
        $plugins->getPlugin('Command');
        $this->getDb();
    }

    /**
     * Initializes prepared statements used by the plugin.
     *
     * @return void
     */
    protected function initializePreparedStatements()
    {
        $this->fetchKarma = $this->db->prepare('
            SELECT karma
            FROM karmas
            WHERE term = :term
            LIMIT 1
        ');

        $this->insertKarma = $this->db->prepare('
            INSERT INTO karmas (term, karma)
            VALUES (:term, :karma)
        ');

        $this->updateKarma = $this->db->prepare('
            UPDATE karmas
            SET karma = :karma
            WHERE term = :term
        ');

        $this->fetchFixedKarma = $this->db->prepare('
            SELECT karma
            FROM fixed_karmas
            WHERE term = :term
            LIMIT 1
        ');

        $this->fetchPositiveAnswer = $this->db->prepare('
            SELECT answer
            FROM positive_answers
            ORDER BY RANDOM()
            LIMIT 1
        ');

        $this->fetchNegativeAnswer = $this->db->prepare('
            SELECT answer
            FROM negative_answers
            ORDER BY RANDOM()
            LIMIT 1
        ');
    }

    /**
     * Returns a connection to the plugin database, initializing one if none
     * is explicitly set.
     *
     * @return PDO Database connection
     */
    public function getDb()
    {
        if (empty($this->db)) {
            $this->db = new PDO('sqlite:' . dirname(__FILE__) . '/Karma/karma.db');
            $this->initializePreparedStatements();
        }
        return $this->db;
    }

    /**
     * Sets the connection to the plugin database, mainly intended for unit
     * testing.
     *
     * @param PDO $db Database connection
     *
     * @return Phergie_Plugin_Karma Provides a fluent interface
     */
    public function setDb(PDO $db)
    {
        $this->db = $db;
        $this->initializePreparedStatements();
        return $this;
    }

    /**
     * Get the canonical form of a given term.
     *
     * In the canonical form all sequences of whitespace
     * are replaced by a single space and all characters
     * are lowercased.
     *
     * @param string $term Term for which a canonical form is required
     *
     * @return string Canonical term
     */
    protected function getCanonicalTerm($term)
    {
        $canonicalTerm = strtolower(preg_replace('|\s+|', ' ', trim($term, '()')));
        switch ($canonicalTerm) {
            case 'me':
                $canonicalTerm = strtolower($this->event->getNick());
                break;
            case 'all':
            case '*':
            case 'everything':
                $canonicalTerm = 'everything';
                break;
        }
        return $canonicalTerm;
    }

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

        $termPattern = '\S+?|\([^<>]+?\)+';
        $actionPattern = '(?P<action>\+\+|--)';

        $modifyPattern = <<<REGEX
		{^
		(?J) # allow overwriting capture names
		\s*  # ignore leading whitespace

		(?:  # start with ++ or -- before the term
            $actionPattern
            (?P<term>$termPattern)
		|   # follow the term with ++ or --
            (?P<term>$termPattern)
			$actionPattern # allow no whitespace between the term and the action
		)
		$}ix
REGEX;

        $versusPattern = <<<REGEX
        {^
        	(?P<term0>$termPattern)
        		\s+(?P<method><|>)\s+
        	(?P<term1>$termPattern)$#
        $}ix
REGEX;

        $match = null;

        if (preg_match($modifyPattern, $message, $match)) {
            $action = $match['action'];
            $term = $this->getCanonicalTerm($match['term']);
            $this->modifyKarma($term, $action);
        } elseif (preg_match($versusPattern, $message, $match)) {
            $term0 = trim($match['term0']);
            $term1 = trim($match['term1']);
            $method = $match['method'];
            $this->compareKarma($term0, $term1, $method);
        }
    }

    /**
     * Get the karma rating for a given term.
     *
     * @param string $term Term for which the karma rating needs to be
     *        retrieved
     *
     * @return void
     */
    public function onCommandKarma($term)
    {
        $source = $this->getEvent()->getSource();
        $nick = $this->getEvent()->getNick();

        $canonicalTerm = $this->getCanonicalTerm($term);

        $fixedKarma = $this->fetchFixedKarma($canonicalTerm);
        if ($fixedKarma) {
            $message = $nick . ': ' . $term . ' ' . $fixedKarma;
            $this->doPrivmsg($source, $message);
            return;
        }

        $karma = $this->fetchKarma($canonicalTerm);

        $message = $nick . ': ';

        if ($term == 'me') {
            $message .= 'You have';
        } else {
            $message .= $term . ' has';
        }

        $message .= ' ';

        if ($karma) {
            $message .= 'karma of ' . $karma;
        } else {
            $message .= 'neutral karma';
        }

        $message .= '.';

        $this->doPrivmsg($source, $message);
    }

    /**
     * Resets the karma for a term to 0.
     *
     * @param string $term Term for which to reset the karma rating
     *
     * @return void
     */
    public function onCommandReincarnate($term)
    {
        $data = array(
            ':term' => $term,
            ':karma' => 0
        );
        $this->updateKarma->execute($data);
    }

    /**
     * Compares the karma between two terms. Optionally increases/decreases
     * the karma of either term.
     *
     * @param string $term0  First term
     * @param string $term1  Second term
     * @param string $method Comparison method (< or >)
     *
     * @return void
     */
    protected function compareKarma($term0, $term1, $method)
    {
        $event = $this->getEvent();
        $nick = $event->getNick();
        $source = $event->getSource();

        $canonicalTerm0 = $this->getCanonicalTerm($term0);
        $canonicalTerm1 = $this->getCanonicalTerm($term1);

        $fixedKarma0 = $this->fetchFixedKarma($canonicalTerm0);
        $fixedKarma1 = $this->fetchFixedKarma($canonicalTerm1);

        if ($fixedKarma0 || $fixedKarma1) {
            return;
        }

        if ($canonicalTerm0 == 'everything') {
            $change = $method == '<' ? '++' : '--';
            $karma0 = 0;
            $karma1 = $this->modifyKarma($canonicalTerm1, $change);
        } elseif ($canonicalTerm1 == 'everything') {
            $change = $method == '<' ? '--' : '++';
            $karma0 = $this->modifyKarma($canonicalTerm0, $change);
            $karma1 = 0;
        } else {
            $karma0 = $this->fetchKarma($canonicalTerm0);
            $karma1 = $this->fetchKarma($canonicalTerm1);
        }

        // Combining the first and second branches here causes an odd
        // single-line lapse in code coverage, but the lapse disappears if
        // they're separated
        if ($method == '<' && $karma0 < $karma1) {
            $replies = $this->fetchPositiveAnswer;
        } elseif ($method == '>' && $karma0 > $karma1) {
            $replies = $this->fetchPositiveAnswer;
        } else {
            $replies = $this->fetchNegativeAnswer;
        }
        $replies->execute();
        $reply = $replies->fetchColumn();

        if (max($karma0, $karma1) == $karma1) {
            list($canonicalTerm0, $canonicalTerm1) =
                array($canonicalTerm1, $canonicalTerm0);
        }

        $message = str_replace(
            array('%owner%','%owned%'),
            array($canonicalTerm0, $canonicalTerm1),
            $reply
        );

        $this->doPrivmsg($source, $message);
    }

    /**
     * Modifes a term's karma.
     *
     * @param string $term   Term to modify
     * @param string $action Karma action (either ++ or --)
     *
     * @return int Modified karma rating
     */
    protected function modifyKarma($term, $action)
    {
        $karma = $this->fetchKarma($term);
        if ($karma !== false) {
            $statement = $this->updateKarma;
        } else {
            $statement = $this->insertKarma;
        }

        $karma += ($action == '++') ? 1 : -1;

        $args = array(
            ':term'  => $term,
            ':karma' => $karma
        );
        $statement->execute($args);

        return $karma;
    }

    /**
     * Returns the karma rating for a specified term for which the karma
     * rating can be modified.
     *
     * @param string $term Term for which to fetch the corresponding karma
     *        rating
     *
     * @return integer|boolean Integer value denoting the term's karma or
     *         FALSE if there is the specified term has no associated karma
     *         rating
     */
    protected function fetchKarma($term)
    {
        $this->fetchKarma->execute(array(':term' => $term));
        $result = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);

        if ($result === false) {
            return false;
        }

        return (int) $result['karma'];
    }

    /**
     * Returns a phrase describing the karma rating for a specified term for
     * which the karma rating is fixed.
     *
     * @param string $term Term for which to fetch the corresponding karma
     *        rating
     *
     * @return string Phrase describing the karma rating, which may be append
     *         to the term to form a complete response
     */
    protected function fetchFixedKarma($term)
    {
        $this->fetchFixedKarma->execute(array(':term' => $term));
        $result = $this->fetchFixedKarma->fetch(PDO::FETCH_ASSOC);

        if ($result === false) {
            return false;
        }

        return $result['karma'];
    }
}