"""
Achievements - gamifying IRC
Copyright (C) 2011  Brian S. Stephan

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import re
import thread
import time

import MySQLdb as mdb

from Module import Module

from extlib import irclib

__author__ = "Brian S. Stephan"
__copyright__ = "Copyright 2011, Brian S. Stephan"
__credits__ = ["Brian S. Stephan", "#lh"]
__license__ = "GPL"
__version__ = "0.1"
__maintainer__ = "Brian S. Stephan"
__email__ = "bss@incorporeal.org"
__status__ = "Development"

class Achievements(Module):

    """Give out achievements for doing stuff on IRC, because why not."""

    class AchievementsSettings():

        """Track system settings."""

        pass

    def __init__(self, irc, config, server):
        """Set up trigger regexes."""

        # TODO
        joinpattern = '^!achievements\s+join$'
        leavepattern = '^!achievements\s+leave$'
        infopattern = '^!achievements\s+info\s+(.*)$'
        rankpattern = '^!achievements\s+rank\s+(.*)$'

        self.joinre = re.compile(joinpattern)
        self.leavere = re.compile(leavepattern)
        self.infore = re.compile(infopattern)
        self.rankre = re.compile(rankpattern)

        Module.__init__(self, irc, config, server)

        self.connection = None
        self.next_achievements_scan = 0
        thread.start_new_thread(self.thread_do, ())

    def db_init(self):
        """Set up the database tables, if they don't exist."""

        version = self.db_module_registered(self.__class__.__name__)
        if (version == None):
            # have to create the database tables
            db = self.get_db()
            try:
                version = 1
                cur = db.cursor(mdb.cursors.DictCursor)
                cur.execute('''
                    CREATE TABLE achievements_player (
                        id SERIAL,
                        nick VARCHAR(64) NOT NULL UNIQUE,
                        userhost VARCHAR(256) NOT NULL DEFAULT '',
                        is_playing INTEGER NOT NULL DEFAULT 0,
                        last_seen_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                    ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
                    ''')
                cur.execute('''
                    CREATE TABLE achievements_event (
                        id SERIAL,
                        player_id BIGINT(20) UNSIGNED NOT NULL,
                        event VARCHAR(64) NOT NULL,
                        target VARCHAR(64),
                        msg_len INTEGER,
                        event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                        FOREIGN KEY(player_id) REFERENCES achievements_player(id)
                    ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
                    ''')
                cur.execute('''
                    CREATE TABLE achievements_achievement (
                        id SERIAL,
                        name VARCHAR(256) NOT NULL,
                        description VARCHAR(256) NOT NULL,
                        query VARCHAR(1024) NOT NULL
                    ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
                    ''')
                cur.execute('''
                    CREATE TABLE achievements_log (
                        id SERIAL,
                        player_id BIGINT(20) UNSIGNED NOT NULL,
                        achievement_id BIGINT(20) UNSIGNED NOT NULL,
                        event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                        FOREIGN KEY(player_id) REFERENCES achievements_player(id),
                        FOREIGN KEY(achievement_id) REFERENCES achievements_achievement(id)
                    ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
                    ''')
                cur.execute('''
                    CREATE TABLE achievements_config (
                        channel TEXT NOT NULL
                    ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
                    ''')
                cur.execute('''
                    CREATE TABLE achievements_filter (
                        id SERIAL,
                        filter VARCHAR(256) NOT NULL
                    ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
                    ''')
                cur.execute('''
                    CREATE TABLE achievements_filter_log (
                        id SERIAL,
                        filter_id BIGINT(20) UNSIGNED NOT NULL,
                        event_id BIGINT(20) UNSIGNED NOT NULL,
                        FOREIGN KEY(filter_id) REFERENCES achievements_filter(id),
                        FOREIGN KEY(event_id) REFERENCES achievements_event(id)
                    ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin
                    ''')
                db.commit()
                self.db_register_module_version(self.__class__.__name__, version)
            except mdb.Error as e:
                db.rollback()
                self.log.error("database error trying to create tables")
                self.log.exception(e)
                raise
            finally: cur.close()

    def register_handlers(self):
        """Handle all sorts of things to track."""

        self.server.add_global_handler('pubmsg', self.track_irc_event)
        self.server.add_global_handler('pubnotice', self.track_irc_event)
        self.server.add_global_handler('privmsg', self.track_irc_event)
        self.server.add_global_handler('privnotice', self.track_irc_event)
        self.server.add_global_handler('join', self.track_irc_event)
        self.server.add_global_handler('kick', self.track_irc_event)
        self.server.add_global_handler('mode', self.track_irc_event)
        self.server.add_global_handler('part', self.track_irc_event)
        self.server.add_global_handler('quit', self.track_irc_event)
        self.server.add_global_handler('invite', self.track_irc_event)
        self.server.add_global_handler('action', self.track_irc_event)
        self.server.add_global_handler('topic', self.track_irc_event)

        self.server.add_global_handler('pubmsg', self.on_pub_or_privmsg)
        self.server.add_global_handler('privmsg', self.on_pub_or_privmsg)

    def unregister_handlers(self):
        self.server.remove_global_handler('pubmsg', self.track_irc_event)
        self.server.remove_global_handler('pubnotice', self.track_irc_event)
        self.server.remove_global_handler('privmsg', self.track_irc_event)
        self.server.remove_global_handler('privnotice', self.track_irc_event)
        self.server.remove_global_handler('join', self.track_irc_event)
        self.server.remove_global_handler('kick', self.track_irc_event)
        self.server.remove_global_handler('mode', self.track_irc_event)
        self.server.remove_global_handler('part', self.track_irc_event)
        self.server.remove_global_handler('quit', self.track_irc_event)
        self.server.remove_global_handler('invite', self.track_irc_event)
        self.server.remove_global_handler('action', self.track_irc_event)
        self.server.remove_global_handler('topic', self.track_irc_event)

        self.server.remove_global_handler('pubmsg', self.on_pub_or_privmsg)
        self.server.remove_global_handler('privmsg', self.on_pub_or_privmsg)

    def track_irc_event(self, connection, event):
        """Put events in the database."""

        self.connection = connection

        if event.source():
            if event.source().find('!') >= 0:
                nick = irclib.nm_to_n(event.source())
                userhost = irclib.nm_to_uh(event.source())
                self.log.debug('good: ' + nick + ' ' + userhost + ' ' + event.eventtype() + ' ' + str(event.target()))
                if event.arguments():
                    msg = event.arguments()[0]
                    msg_len = len(event.arguments()[0])
                else:
                    msg = ''
                    msg_len = 0

                player_id = self._get_or_add_player(nick, userhost)
                self._add_event(player_id, event.eventtype(), event.target(), msg, msg_len)
            else:
                self.log.debug('bad: ' + event.source() + ' ' + event.eventtype())
        else:
            self.log.debug('really bad: ' + event.eventtype())

    def do(self, connection, event, nick, userhost, what, admin_unlocked):
        """Do stuff when commanded."""

        if self.joinre.search(what):
            return self.reply(connection, event, self._join_system(nick))
        elif self.leavere.search(what):
            return self.reply(connection, event, self._leave_system(nick))
        elif self.infore.search(what):
            match = self.infore.search(what)
            achievement = match.group(1)
            desc = self._get_achievement_info(achievement)
            if desc:
                return self.reply(connection, event, achievement + ': ' + desc)
        elif self.rankre.search(what):
            match = self.rankre.search(what)
            player = match.group(1)
            achievements = self._get_player_achievements(player)
            if len(achievements):
                return self.reply(connection, event, player + ' has obtained ' + ', '.join(achievements))

    def thread_do(self):
        """Do the scan for achievements and other miscellaneous tasks."""

        while not self.is_shutdown:
            self._do_achievement_scan()
            time.sleep(1)

    def _get_or_add_player(self, nick, userhost):
        """Add a player to the database, or update the existing one, and return the id."""

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            statement = '''
                INSERT IGNORE INTO achievements_player (nick) VALUES (%s)'''
            cur.execute(statement, (nick,))
            statement = '''
                UPDATE achievements_player SET userhost = %s, last_seen_time = CURRENT_TIMESTAMP
                WHERE nick = %s'''
            cur.execute(statement, (userhost, nick))
            db.commit()
            statement = '''SELECT id FROM achievements_player WHERE nick = %s'''
            cur.execute(statement, (nick,))
            result = cur.fetchone()
            return result['id']
        except mdb.Error as e:
            self.log.error("database error getting or adding player")
            self.log.exception(e)
            raise
        finally: cur.close()

    def _add_event(self, player_id, event, target, msg, msg_len):
        """Add an event to the log."""

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            statement = '''
                INSERT INTO achievements_event (
                    player_id, event, target, msg_len
                ) VALUES (%s, %s, %s, %s)
                '''
            cur.execute(statement, (player_id, event, target, msg_len))
            db.commit()
            event_id = cur.lastrowid

            # now see if the event matched any filters
            query = '''
                SELECT id FROM achievements_filter WHERE %s REGEXP filter
                '''
            cur.execute(query, (msg,))
            results = cur.fetchall()
            for result in results:
                cur = db.cursor(mdb.cursors.DictCursor)
                statement = '''
                    INSERT INTO achievements_filter_log (filter_id, event_id)
                        VALUES (%s, %s)
                    '''
                cur.execute(statement, (result['id'], event_id))
                db.commit()

            return event_id
        except mdb.Error as e:
            self.log.error("database error adding event")
            self.log.exception(e)
            raise
        finally: cur.close()

    def _get_achievements_settings(self):
        """Get the report settings."""

        db = self.get_db()
        try:
            # get the settings
            cur = db.cursor(mdb.cursors.DictCursor)
            query = 'SELECT channel FROM achievements_config'
            cur.execute(query)
            result = cur.fetchone()
            if result:
                settings = self.AchievementsSettings()
                settings.channel = result['channel']

                return settings
            else:
                return None
        except mdb.Error as e:
            self.log.error("database error getting settings")
            self.log.exception(e)
            raise
        except AttributeError as e:
            self.log.error("could not get channel settings, probably unset")
            self.log.exception(e)
            return None
        finally: cur.close()

    def _join_system(self, nick):
        """Add the appropriate nick to the system."""

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            statement = 'UPDATE achievements_player SET is_playing = 1 WHERE nick = %s'
            cur.execute(statement, (nick,))
            db.commit()
            return nick + ' joined.'
        except mdb.Error as e:
            self.log.error("database error joining the system")
            self.log.exception(e)
            raise
        finally: cur.close()

    def _leave_system(self, nick):
        """Remove the appropriate nick from the system."""

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            statement = 'UPDATE achievements_player SET is_playing = 0 WHERE nick = %s'
            cur.execute(statement, (nick,))
            db.commit()
            return nick + ' left.'
        except mdb.Error as e:
            self.log.error("database error leaving the system")
            self.log.exception(e)
            raise
        finally: cur.close()

    def _add_player_to_achievement_log(self, player_id, achievement_id):
        """Log the success of a player."""

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            statement = 'INSERT INTO achievements_log (player_id, achievement_id) VALUES (%s, %s)'
            cur.execute(statement, (player_id, achievement_id))
            db.commit()
            return
        except mdb.Error as e:
            self.log.error("database error adding player to achievement log")
            self.log.exception(e)
            raise
        finally: cur.close()

    def _get_achievement_info(self, achievement):
        """Return the description of a given achievement."""

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            query = 'SELECT description FROM achievements_achievement WHERE name = %s'
            cur.execute(query, (achievement,))
            result = cur.fetchone()
            if result:
                return result['description']
        except mdb.Error as e:
            self.log.error("database error getting achievement info")
            self.log.exception(e)
            raise
        finally: cur.close()

    def _get_player_achievements(self, nick):
        """Return the achievements the nick has."""

        achievements = []

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            query = '''
                SELECT a.name FROM achievements_achievement a
                    INNER JOIN achievements_log l ON l.achievement_id = a.id
                    INNER JOIN achievements_player p ON p.id = l.player_id
                    WHERE p.nick = %s
                '''
            cur.execute(query, (nick,))
            results = cur.fetchall()
            for result in results:
                achievements.append(result['name'])

            return achievements
        except mdb.Error as e:
            self.log.error("database error getting player achievements")
            self.log.exception(e)
            raise
        finally: cur.close()

    def _do_achievement_scan(self):
        """Run the queries in the database, seeing if anyone new has an achievement."""

        # don't do anything the first time
        if self.next_achievements_scan == 0:
            self.next_achievements_scan = time.time() + 300

        if self.next_achievements_scan < time.time():
            self.next_achievements_scan = time.time() + 300

            if self.connection is None:
                return

            self.log.debug('in achievement scan')
            settings = self._get_achievements_settings()
            if settings is not None:
                channel = settings.channel
                achievers = self._query_for_new_achievers()
                for achiever in achievers:
                    self.sendmsg(self.connection, channel, achiever[0] + ' achieved ' + achiever[1] + '!')

    def _query_for_new_achievers(self):
        """Get new achievement earners for each achievement."""

        achievers = []
        self.log.debug('checking achievements')

        db = self.get_db()
        try:
            cur = db.cursor(mdb.cursors.DictCursor)
            query = 'SELECT id, name, query FROM achievements_achievement'
            cur.execute(query)
            achievements = cur.fetchall()

            for achievement in achievements:
                self.log.debug('checking achievement:[' + achievement['name'] + ']')
                query = '''
                    SELECT p.id, p.nick FROM achievements_player p WHERE
                        p.nick IN (
                    '''
                query = query + achievement['query']
                query = query + '''
                    ) AND p.is_playing = 1
                        AND p.id NOT IN (
                            SELECT player_id FROM achievements_log l
                                INNER JOIN achievements_achievement a
                                    ON a.id = l.achievement_id
                                WHERE a.name = %s
                        )
                    '''
                cur.execute(query, (achievement['name'],))
                ach_achievers = cur.fetchall()

                for ach_achiever in ach_achievers:
                    self.log.debug('name:[' + ach_achiever['nick'] + '] achievement:[' + achievement['name'] + ']')
                    self._add_player_to_achievement_log(ach_achiever['id'], achievement['id'])
                    achievers.append((ach_achiever['nick'], achievement['name']))

            return achievers
        except mdb.Error as e:
            self.log.error("database error scanning new achievers")
            self.log.exception(e)
            raise
        finally: cur.close()

# vi:tabstop=4:expandtab:autoindent