<?php

/**
* @see Phergie_Plugin_Abstract_Base
*/
require_once 'Phergie/Plugin/Abstract/Base.php';

/**
* Handles requests for incrementation or decrementation of a maintained list
* of counters for specified terms and antithrottling to prevent extreme
* inflation or depression of counters by any single individual.
*/
class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract_Base
{
    /**
    * Indicates that a local directory is required for this plugin
    *
    * @var bool
    */
    protected $needsDir = true;

    /**
    * Stores the SQLite object
    *
    * @var resource
    */
    protected $db = null;

    /**
    * Retains the last garbage collection date
    *
    * @var array
    */
    protected $lastGc = null;

    /**
    * Logs the karma usages and limits users to one karma change per word
    * and per day
    *
    * @return void
    */
    protected $log = array();

    /**
    * Some fixed karma values, keys must be lowercase
    *
    * @var array
    */
    protected $fixedKarma;

    /**
     * Answers for correct assertions
     */
	protected $positiveAnswers;

	/**
	 * Answers for incorrect assertions
	 */
	protected $negativeAnswers;

    /**#@+
     * Prepared PDO statements
     *
     * @var PDOStatement
     */
    protected $insertKarma;
    protected $updateKarma;
    protected $fetchKarma;
    protected $insertComment;
    /**#@-*/

    /**
    * Connects to the database containing karma ratings and initializes
    * class properties.
    *
    * @return void
    */
    public function init()
    {
        $this->db = null;
        $this->lastGc = null;
        $this->log = null;

        $this->fixedKarma = array
        (
            'phergie' => '%s has karma of awesome',
            'pi' => 'pi has karma of ' . M_PI,
            'chucknorris' => '%s has karma of Warning: Integer out of range',
            'chuck norris' => '%s has karma of Warning: Integer out of range',
            'c' => '%s has karma of 299 792 458 m/s',
            'e' => '%s has karma of ' . M_E,
            'euler' => '%s has karma of ' . M_EULER,
            'mole' => '%s has karma of 6.02214e23 molecules',
            'avogadro' => '%s has karma of 6.02214e23 molecules',
            'spoon' => '%s has no karma. There is no spoon',
            'mc^2' => '%s has karma of e',
            'mc2' => '%s has karma of e',
            'mc²' => '%s has karma of e',
            'i' => 'I haz big karma',
            'karma' => 'The karma law says that all living creatures are responsible for their karma - their actions and the effects of their actions. You should watch yours.',
            'answer to life, the universe, and everything' => 'The answer is 42',
            'the answer to life, the universe, and everything' => 'The answer is 42',
        );

		$this->positiveAnswers = array
		(
			'No kidding, %owner% totally kicks %owned%\'s ass !',
			'True that.',
			'I concur.',
			'Yay, %owner% ftw !',
			'%owner% is made of WIN!',
			'Nothing can beat %owner%!',
		);

		$this->negativeAnswers = array
		(
			'No sir, not at all.',
			'You\'re wrong dude, %owner% wins.',
			'I\'d say %owner% is better than %owned%.',
			'You must be joking, %owner% ftw!',
			'%owned% is made of LOSE!',
			'%owned% = Epic Fail',
		);

        $static = $this->getPluginIni('static');
        if ($static) {
            $this->fixedKarma[strtolower($this->getIni('nick'))] = $static;
        }

        // Load or initialize the database
        $db = $this->dir . 'karma.db';
        if (!file_exists($db)) {
            $this->db = new PDO('sqlite:' . $db);
            $this->db->query('
                CREATE TABLE karmas ( word VARCHAR ( 255 ) , karma MEDIUMINT ) ;
                CREATE UNIQUE INDEX word ON karmas ( word ) ;
                CREATE INDEX karmaIndex ON karmas ( karma ) ;
            ');
            $this->db->query('
                CREATE TABLE comments ( wordid INT , comment VARCHAR ( 255 ) ) ;
                CREATE INDEX wordidIndex ON comments ( wordid ) ;
                CREATE UNIQUE INDEX commentUnique ON comments ( comment ) ;
            ');
        } else {
            $this->db = new PDO('sqlite:' . $db);
        }

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

        $this->insertComment = $this->db->prepare('
            INSERT INTO comments (
                wordid,
                comment
            )
            VALUES (
                :wordid,
                :comment
            )
        ');

        $this->fetchKarma = $this->db->prepare('
            SELECT karma, ROWID id FROM karmas WHERE word = :word LIMIT 1
        ');

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

    /**
    * 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 (!extension_loaded('PDO')
            || !extension_loaded('pdo_sqlite')) {
            return false;
        }

        return true;
    }

    /**
    * Handles requests for incrementation, decrementation, or lookup of karma
    * ratings sent via messages from users.
    *
    * @return void
    */
    public function onPrivmsg()
    {
        if ($this->db === null) {
            return;
        }
        $source = $this->event->getSource();
        $message = $this->event->getArgument(1);

        // Command prefix check
        $commandPrefix = preg_quote(trim($this->getIni('command_prefix')));
        $prefix = ($commandPrefix ? $commandPrefix . '|' : '').'(?:' . $this->getIni('nick') . '\s*:?\s+)?';

        // Karma status request
        if (preg_match('#^(?:'.$prefix.')karma\s+(.+)$#i', $message, $m)) {
            // Return user's value if "me" is requested
            if (strtolower($m[1]) === 'me') {
                $m[1] = $this->event->getNick();
            }
        	  // Clean the term
            $term = $this->doCleanWord($m[1]);
            // Return fixed value if set
            if (isset ($this->fixedKarma[$term])) {
                $this->doPrivmsg($source, sprintf($this->fixedKarma[$term], $m[1]).'.');
                return;
            }

            // Return current karma or neutral if not set yet
            $this->fetchKarma->execute(array(':word'=>$term));
            $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);

            // Sanity check if someone if someone prefixed their conversation with karma
            if (!$res && substr_count($term, ' ') > 1 && !(substr($m[1], 0, 1) === '(' && substr($m[1], -1) === ')'))
                return;

            // Clean the raw term if it was contained within brackets
            if(substr($m[1], 0, 1) === '(' && substr($m[1], -1) === ')')
                $m[1] = substr($m[1], 1, -1);

            if ($res && $res['karma'] != 0) {
                $this->doPrivmsg($source, $m[1].' has karma of '.$res['karma'].'.');
            } else {
                $this->doPrivmsg($source, $m[1].' has neutral karma.');
            }
        // Incrementation/decrementation request
        } elseif (preg_match('{^(?:'.$prefix.')?(\S+?|\(.+?\)+)(\+{2,}|-{2,})(?:\s+(.*))?$}i', $message, $m)) {
            $word = strtolower($m[1]);
            // Strip parenthesis grouping multiple words
            if(substr($word, 0, 1) === '(' && substr($word, -1) === ')') {
                $word = substr($word, 1, -1);
            } else { // Add trailing + or -'s
                if(strlen($m[2]) > 2) {
                    $word .= substr($m[2], 2);
                    $m[2] = substr($m[2], -2);
                }
            }
            // Replaces multiple spaces by one
            $word = preg_replace('#\s+#', ' ', $word);
            // Do nothing if the karma is fixed
            if (isset($this->fixedKarma[$word])) {
                return;
            }
            // Force a decrementation if someone tries to update his own karma
            if ($word == strtolower($this->event->getNick())) {
                $m[2] = '--';
            }
            // Antithrottling check
            $host = $this->event->getHost();
            $limit = $this->getPluginIni('limit');
            if (isset ($this->log[$host][$word])
                && $limit
                && $this->log[$host][$word] > $limit) {
                return;
            } else {
                $this->log[$host][$word] = 0;
            }
            $this->log[$host][$word]++;
            // Get the current value then update or create entry
            $this->fetchKarma->execute(array(':word'=>$word));

            $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
            if ($res) {
                $args = array(
                    ':karma' => ($res['karma'] + ($m[2]=='++' ? 1 : -1)),
                    ':word' => $word
                );
                $this->updateKarma->execute($args);
            } else {
                $args = array(
                    ':word' => $word,
                    ':karma' => ($m[2]=='++' ? '1' : '-1')
                );
                $this->insertKarma->execute($args);
                $this->fetchKarma->execute(array(':word'=>$word));
                $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
            }
            $id = $res['id'];

            // Add comment
            $comment = preg_replace('{(?:^//(.*)|^#(.*)|^/\*(.*?)\*/$)}', '$1$2$3', $m[3]);
            if (!empty($comment)) {
                $this->insertComment->execute(array(':wordid' => $id, ':comment' => $comment));
            }
            // Perform garbage collection on the antithrottling log if needed
            if (date('d') !== $this->lastGc) {
                $this->doGc();
            }
        // Assertion request
        } elseif (preg_match('#^([^><]+)(<|>)([^><]+)$#', $message, $m)) {
            // Trim words
            $word1 = strtolower(trim($m[1]));
            $word2 = strtolower(trim($m[3]));
            $operator = $m[2];

            // Fetch first word
			if ($word1 === '*' || $word1 === 'all' || $word1 === 'everything') {
           		$res = array('karma' => 0);
           		$word1 = 'everything';
			} else {
	            $this->fetchKarma->execute(array(':word'=>$word1));
    	        $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
			}
            // If it exists, fetch second word
            if ($res) {
				if ($word2 === '*' || $word2 === 'all' || $word2 === 'everything') {
            		$res2 = array('karma' => 0);
            		$word2 = 'everything';
				} else {
	                $this->fetchKarma->execute(array(':word'=>$word2));
    	            $res2 = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
				}
                // If it exists, compare and return value
                if ($res2 && $res['karma'] != $res2['karma']) {
	               	$assertion = ($operator === '<' && $res['karma'] < $res2['karma']) || ($operator === '>' && $res['karma'] > $res2['karma']);
                   	// Switch arguments if they are in the wrong order
                   	if ($operator === '<') {
		           		$tmp = $word2; $word2 = $word1; $word1 = $tmp;
                   	}
                   	$this->doPrivmsg($source, $assertion ? $this->fetchPositiveAnswer($word1, $word2) : $this->fetchNegativeAnswer($word1, $word2) );
                   	// If someone asserts that something is greater or lesser than everything, we increment/decrement that something at the same time
                   	if ($word2 === 'everything') {
                   		$this->event = clone $this->event;
                   		$this->event->setArguments(array($this->event->getArgument(0), $word1.'++'));
                   		$this->onPrivmsg();
                   	} elseif ($word1 === 'everything') {
                   		$this->event = clone $this->event;
                   		$this->event->setArguments(array($this->event->getArgument(0), $word2.'--'));
                   		$this->onPrivmsg();
                   	}
                }
            }
        }
    }

	protected function fetchPositiveAnswer($owner, $owned) {
		return str_replace(array('%owner%','%owned%'), array($owner, $owned), $this->positiveAnswers[array_rand($this->positiveAnswers,1)]);
	}

	protected function fetchNegativeAnswer($owned, $owner) {
		return str_replace(array('%owner%','%owned%'), array($owner, $owned), $this->negativeAnswers[array_rand($this->negativeAnswers,1)]);
	}

  protected function doCleanWord($word) {
    if(substr($word, 0, 1) === '(' && substr($word, -1) === ')')
       $word = substr($word, 1, -1);
    $word = preg_replace('#\s+#', ' ', strtolower(trim($word)));
    return $word;
  }
    /**
     * Performs garbage collection on the antithrottling log.
     *
     * @return void
     */
    public function doGc()
    {
        unset ($this->log);
        $this->log = array();
        $this->lastGc = date('d');
    }
}
