# -*- coding: utf-8 -*-

"""
Bot core

@author Riku 'Shrike' Lindblad (shrike@addiktit.net)
@copyright Copyright (c) 2004 Riku Lindblad
@license New-Style BSD

"""

from __future__ import print_function, division
# twisted imports
from twisted.words.protocols import irc
from twisted.internet import reactor, threads
from twisted.python import rebuild

from types import FunctionType

import string
import logging
from util import pyfiurl

# line splitting
import textwrap

__pychecker__ = 'unusednames=i, classattr'

log = logging.getLogger("bot")


class CoreCommands(object):
    def command_echo(self, user, channel, args):
        self.say(channel, "%s: %s" % (user, args))

    def command_ping(self, user, channel, args):
        self.say(channel, "%s: My current ping is %.0fms" %
                 (self.factory.getNick(user), self.pingAve * 100.0))

    def command_rehash(self, user, channel, args):
        """Reload modules and optionally the configuration file. Usage: rehash [conf]"""

        if self.factory.isAdmin(user):
            try:
                # rebuild core & update
                log.info("rebuilding %r" % self)
                rebuild.updateInstance(self)

                # reload config file
                if args == 'conf':
                    self.factory.reload_config()
                    self.say(channel, 'Configuration reloaded.')

                # unload removed modules
                self.factory._unload_removed_modules()
                # reload modules
                self.factory._loadmodules()
            except Exception, e:
                self.say(channel, "Rehash error: %s" % e)
                log.error("Rehash error: %s" % e)
            else:
                self.say(channel, "Rehash OK")
                log.info("Rehash OK")

    def say(self, channel, message, length=None):
        """Must be implemented by the inheriting class"""
        raise NotImplementedError

    def command_join(self, user, channel, args):
        """Usage: join <channel>[@network] [password] - Join the specified channel"""

        if not self.factory.isAdmin(user):
            return

        password = None
        # see if we have multiple arguments
        try:
            args, password = args.split(' ', 1)
        except ValueError:
            pass

        # see if the user specified a network
        try:
            newchannel, network = args.split('@', 1)
        except ValueError:
            newchannel, network = args, self.network.alias

        try:
            bot = self.factory.allBots[network]
        except KeyError:
            self.say(channel, "I am not on that network.")
        else:
            log.debug("Attempting to join channel %s on ", (newchannel, network))
            if newchannel in bot.network.channels:
                self.say(channel, "I am already in %s on %s." % (newchannel, network))
                log.debug("Already on channel %s" % channel)
                log.debug("Channels I'm on this network: %s" % bot.network.channels)
            else:
                if password:
                    bot.join(newchannel, key=password)
                    log.debug("Joined with password")
                else:
                    bot.join(newchannel)
                    log.debug("Joined")

    # alias of part
    def command_leave(self, user, channel, args):
        """Usage: leave <channel>[@network] - Leave the specified channel"""
        self.command_part(user, channel, args)

    def command_part(self, user, channel, args):
        """Usage: part <channel>[@network] - Leave the specified channel"""

        if not self.factory.isAdmin(user):
            return

        # part what and where?
        try:
            newchannel, network = args.split('@', 1)
        except ValueError:
            newchannel, network = args, self.network.alias

        # get the bot instance for this chat network
        try:
            bot = self.factory.allBots[network]
        except KeyError:
            self.say(channel, "I am not on that network.")
        else:
            # no arguments, attempt to part current channel
            if not newchannel:
                log.debug("Parted channel %s" % channel)
                bot.network.channels.remove(channel)
                bot.part(channel)
                return

            if newchannel in bot.network.channels:
                log.debug("Parted channel %s" % newchannel)
                bot.network.channels.remove(newchannel)
                bot.part(newchannel)
            else:
                log.debug("Attempted to part channel I am not on: %s@%s" % (newchannel, network))
                log.debug("Channels on network: %s" % bot.network.channels)

    def command_quit(self, user, channel, args):
        """Usage: logoff - Leave this network"""

        if not self.factory.isAdmin(user):
            return

        self.quit("Working as programmed")
        self.hasQuit = 1

    def command_channels(self, user, channel, args):
        """Usage: channels <network> - List channels the bot is on"""
        if not args:
            self.say(channel, "Please specify a network: %s" % ", ".join(self.factory.allBots.keys()))
            return

        self.say(channel, "I am on %s" % self.network.channels)

    def command_help(self, user, channel, cmnd):
        """Get help on all commands or a specific one. Usage: help [<command>]"""

        commands = []
        for module, env in self.factory.ns.items():
            myglobals, mylocals = env
            commands += [(c.replace("command_", ""), ref) for c, ref in mylocals.items() if c.startswith("command_%s" % cmnd)]
        # Help for a specific command
        if len(cmnd) > 0:
            for cname, ref in commands:
                if cname == cmnd:
                    helptext = ref.__doc__.split("\n", 1)[0]
                    self.say(channel, "Help for %s: %s" % (cmnd, helptext))
                    return
        # Generic help
        else:
            commandlist = ", ".join([c for c, ref in commands])
            self.say(channel, "Available commands: %s" % commandlist)


class PyFiBot(irc.IRCClient, CoreCommands):
    """PyFiBot"""

    nickname = "pyfibot"
    realname = "https://github.com/lepinkainen/pyfibot"
    password = None

    # send 2 msgs per second max
    lineRate = 0.5
    hasQuit = False

    # Rolling ping time average
    pingAve = 0.0

    def __init__(self, config, network):
        self.cmdchar = config.get('cmdchar', '.')
        self.network = network
        self.nickname = self.network.nickname
        self.lineRate = self.network.linerate
        self.password = self.network.password
        # Text wrapper to clip overly long answers
        self.tw = textwrap.TextWrapper(width=400, break_long_words=True)
        log.info("bot initialized")

    def __repr__(self):
        return 'PyFiBot(%r, %r)' % (self.nickname, self.network.address)

    # Core
    def printResult(self, msg, info):
        # Don't print results if there is nothing to say (usually non-operation on module)
        if msg:
            log.debug("Result %s %s" % (msg, info))

    def printError(self, msg, info):
        log.error("ERROR %s %s" % (msg, info))

    def connectionMade(self):
        irc.IRCClient.connectionMade(self)
        self.repeatingPing(300)
        log.info("connection made")

    def connectionLost(self, reason):
        irc.IRCClient.connectionLost(self, reason)
        log.info("connection lost: %s", reason)

    def signedOn(self):
        """Called when bot has succesfully connected to a server."""
        log.info("Connected to network")

        network_conf = self.factory.config['networks'][self.network.alias]

        # Name used for authentication
        authname = network_conf.get('authname', None)
        # Pass used for authentication
        authpass = network_conf.get('authpass', None)

        # If authentication is used
        if authname and authpass:
            # QuakeNet specific auth and IP-address masking
            if self.network.alias.lower() == "quakenet":
                log.info("I'm on Quakenet, authenticating...")
                self.mode(self.nickname, '+', 'x')  # Hide ident
                log.info("Authenticating...")
                self.say("Q@CServe.quakenet.org", "AUTH %s %s" % (authname, authpass))
            # more generic authentication
            else:
                # Get authentication service
                # Default: None
                # None as default, so the user must be sure who he's authenticating to
                authservice = network_conf.get('authservice', None)
                if authservice:
                    # Command used for authentication
                    # Default: "IDENTIFY authname authpass"
                    authcommand = network_conf.get('authcommand', 'IDENTIFY %(authname)s %(authpass)s')
                    authcommand = authcommand % {'authname': authname, 'authpass': authpass}

                    log.info('Authenticating to "%s"' % authservice)
                    log.debug('Authentication command used: "%s"' % authcommand)
                    self.say(authservice, authcommand)
                else:
                    log.info('authservice not set, authentication not attempted')
        else:
            log.debug('authname or authpass not found, authentication not attempted')

        authdelay = network_conf.get('authdelay', None)
        if authdelay:
            # allowing the connection to establish and authentication to happen before joining
            log.info("Joining channels after %s second delay" % (authdelay))
            reactor.callLater(authdelay, self.joinChannels)
        else:
            self.joinChannels()

    # separate function to allow timing the joins
    def joinChannels(self):
        for chan in self.network.channels:
            # Defined as a tuple, channel has a key
            if type(chan) == list:
                self.join(chan[0], key=chan[1])
            else:
                self.join(chan)

        log.info("joined %d channel(s): %s" % (len(self.network.channels), ", ".join(self.network.channels)))
        self._runEvents("signedon")

    def pong(self, user, secs):
        self.pingAve = ((self.pingAve * 5) + secs) / 6.0

    def repeatingPing(self, delay):
        reactor.callLater(delay, self.repeatingPing, delay)
        self.ping(self.nickname)

    # TODO: Move the function here completely
    def get_url(self, url, nocache=False, params=None, headers=None, cookies=None):
        return self.factory.getUrl(url, nocache, params, headers, cookies)

    def getUrl(self, url, nocache=False, params=None, headers=None, cookies=None):
        return self.factory.getUrl(url, nocache, params, headers, cookies)

    def log(self, message):
        botId = "%s@%s" % (self.nickname, self.network.alias)
        log.info("%s: %s", botId, message)

    def callLater(self, delay, callable):
        self.callLater(delay, callable)

    # Communication
    def privmsg(self, user, channel, msg):
        """This will get called when the bot receives a message.
        @param user: nick!user@host
        @param channel: Channel where the message originated from
        @param msg: The actual message
        """

        channel = channel.lower()
        lmsg = msg.lower()
        lnick = self.nickname.lower()
        nickl = len(lnick)
        if channel == lnick:
            # Turn private queries into a format we can understand
            if not msg.startswith(self.cmdchar):
                msg = self.cmdchar + msg
            elif lmsg.startswith(lnick):
                msg = self.cmdchar + msg[nickl:].lstrip()
            elif lmsg.startswith(lnick) and len(lmsg) > nickl and lmsg[nickl] in string.punctuation:
                msg = self.cmdchar + msg[nickl + 1:].lstrip()
        else:
            # Turn 'nick:' prefixes into self.cmdchar prefixes
            if lmsg.startswith(lnick) and len(lmsg) > nickl and lmsg[nickl] in string.punctuation:
                msg = self.cmdchar + msg[len(self.nickname) + 1:].lstrip()
        reply = (channel == lnick) and user or channel

        if msg.startswith(self.cmdchar):
            cmnd = msg[len(self.cmdchar):]
            self._command(user, reply, cmnd)

        # Run privmsg handlers
        self._runhandler("privmsg", user, reply, self.factory.to_unicode(msg))

        # run URL handlers
        urls = pyfiurl.grab(msg)
        if urls:
            for url in urls:
                self._runhandler("url", user, reply, url, self.factory.to_unicode(msg))

    def _runhandler(self, handler, *args, **kwargs):
        """Run a handler for an event"""
        handler = "handle_%s" % handler
        # module commands
        for module, env in self.factory.ns.items():
            myglobals, mylocals = env
            # find all matching command functions
            handlers = [(h, ref) for h, ref in mylocals.items() if h == handler and type(ref) == FunctionType]

            for hname, func in handlers:
                # defer each handler to a separate thread, assign callbacks to see when they end
                d = threads.deferToThread(func, self, *args, **kwargs)
                d.addCallback(self.printResult, "handler %s completed" % hname)
                d.addErrback(self.printError, "handler %s error" % hname)

    def _runEvents(self, eventname, *args, **kwargs):
        """Run funtions on events named by eventname parameter"""
        eventname = "event_%s" % eventname
        for module, env in self.factory.ns.items():
            myglobals, mylocals = env

            # find all matching events
            events = [(h, ref) for h, ref in mylocals.items() if h == eventname and type(ref) == FunctionType]

            for ename, func in events:
                # defer each handler to a separate thread, assign callbacks to see when they end
                d = threads.deferToThread(func, self, *args, **kwargs)
                d.addCallback(self.printResult, "%s %s event completed" % (module, ename))
                d.addErrback(self.printError, "%s %s event error" % (module, ename))

    def _command(self, user, channel, cmnd):
        """Handles bot commands.

        This function calls the appropriate method for the given command.

        The command methods are formatted as "command_<commandname>"
        """
        # Split arguments from the command part
        try:
            cmnd, args = cmnd.split(" ", 1)
        except ValueError:
            args = ""

        # core commands
        method = getattr(self, "command_%s" % cmnd, None)
        if method is not None:
            log.info("internal command %s called by %s (%s) on %s" % (cmnd, user, self.factory.isAdmin(user), channel))
            method(user, channel, args)
            return

        # module commands
        for module, env in self.factory.ns.items():
            myglobals, mylocals = env
            # find all matching command functions
            commands = [(c, ref) for c, ref in mylocals.items() if c == "command_%s" % cmnd]

            for cname, command in commands:
                log.info("module command %s called by %s (%s) on %s" % (cname, user, self.factory.isAdmin(user), channel))
                # Defer commands to threads
                d = threads.deferToThread(command, self, user, channel, self.factory.to_unicode(args.strip()))
                d.addCallback(self.printResult, "command %s completed" % cname)
                d.addErrback(self.printError, "command %s error" % cname)

    ### Overrides for twisted.words.irc core commands ###
    def say(self, channel, message, length=None):
        """Override default say to make replying to private messages easier"""

        # Encode channel
        # (for cases where channel is specified in code instead of "answering")
        channel = self.factory.to_utf8(channel)
        # Encode all outgoing messages to UTF-8
        message = self.factory.to_utf8(message)

        # Change nick!user@host -> nick, since all servers don't support full hostmask messaging
        if "!" and "@" in channel:
            channel = self.factory.getNick(channel)

        # wrap long text into suitable fragments
        msg = self.tw.wrap(message)
        cont = False

        for m in msg:
            if cont:
                m = "..." + m
            self.msg(channel, m, length)
            cont = True

        return ('botcore.say', channel, message)

    def act(self, channel, message, length=None):
        """Use act instead of describe for actions"""
        return super(PyFiBot, self).describe(channel, message)

    def mode(self, chan, set, modes, limit=None, user=None, mask=None):
        chan = self.factory.to_utf8(chan)
        _set = self.factory.to_utf8(set)
        modes = self.factory.to_utf8(modes)
        return super(PyFiBot, self).mode(chan, _set, modes, limit, user, mask)

    def kick(self, channel, user, reason=None):
        reason = self.factory.to_utf8(reason)
        return super(PyFiBot, self).kick(channel, user, reason)

    def join(self, channel, key=None):
        channel = self.factory.to_utf8(channel)
        return super(PyFiBot, self).join(channel, key)

    def leave(self, channel, key=None):
        channel = self.factory.to_utf8(channel)
        return super(PyFiBot, self).leave(channel, key)

    def quit(self, message=''):
        message = self.factory.to_utf8(message)
        return super(PyFiBot, self).quit(message)

    ### Overrides for twisted.words.irc internal commands ###
    def XXregister(self, nickname, hostname='foo', servername='bar'):
        nickname = self.factory.to_utf8(nickname)
        hostname = self.factory.to_utf8(hostname)
        servername = self.factory.to_utf8(servername)
        return super(PyFiBot, self).register(nickname, hostname, servername)

        #self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
        #self.register(nickname, hostname, servername)

    ### LOW-LEVEL IRC HANDLERS ###

    def irc_JOIN(self, prefix, params):
        """override the twisted version to preserve full userhost info"""
        nick = self.factory.getNick(prefix)
        channel = params[-1]

        if nick == self.nickname:
            self.joined(channel)
        else:
            self.userJoined(prefix, channel)

        if nick.lower() != self.nickname.lower():
            pass
        elif channel not in self.network.channels:
            self.network.channels.append(channel)
            self.factory.setNetwork(self.network)

    def irc_PART(self, prefix, params):
        """override the twisted version to preserve full userhost info"""

        nick = self.factory.getNick(prefix)
        channel = params[0]

        if nick == self.nickname:
            self.left(channel)
        else:
            # some clients don't send a part message at all, compensate
            if len(params) == 1:
                params.append("")
            self.userLeft(prefix, channel, params[1])

    def irc_NICK(self, prefix, params):
        """override the twisted version to preserve full userhost info"""
        newnick = params[0]
        self.userRenamed(prefix, newnick)

    def irc_QUIT(self, prefix, params):
        """QUIT-handler.

        Twisted IRCClient doesn't handle this at all.."""

        nick = self.factory.getNick(prefix)
        if nick == self.nickname:
            self.left(params)
        else:
            self.userLeft(prefix, None, params[0])

    ###### HANDLERS ######

    ## ME
    def joined(self, channel):
        """I joined a channel"""
        self._runhandler("joined", channel)

    def left(self, channel):
        """I left a channel"""
        self._runhandler("left", channel)

    def noticed(self, user, channel, message):
        """I received a notice"""
        self._runhandler("noticed", user, channel, self.factory.to_unicode(message))

    def modeChanged(self, user, channel, set, modes, args):
        """Mode changed on user or channel"""
        self._runhandler("modeChanged", user, channel, set, modes, args)

    def kickedFrom(self, channel, kicker, message):
        """I was kicked from a channel"""
        self._runhandler("kickedFrom", channel, kicker, self.factory.to_unicode(message))

    def nickChanged(self, nick):
        """I changed my nick"""
        self._runhandler("nickChanged", nick)

    ## OTHER PEOPLE
    def userJoined(self, user, channel):
        """Someone joined"""
        self._runhandler("userJoined", user, channel)

    def userLeft(self, user, channel, message):
        """Someone left"""
        self._runhandler("userLeft", user, channel, self.factory.to_unicode(message))

    def userKicked(self, kickee, channel, kicker, message):
        """Someone got kicked by someone"""
        self._runhandler("userKicked", kickee, channel, kicker, self.factory.to_unicode(message))

    def action(self, user, channel, data):
        """An action"""
        self._runhandler("action", user, channel, self.factory.to_unicode(data))

    def topicUpdated(self, user, channel, topic):
        """Save topic to maindb when it changes"""
        self._runhandler("topicUpdated", user, channel, self.factory.to_unicode(topic))

    def userRenamed(self, oldnick, newnick):
        """Someone changed their nick"""
        self._runhandler("userRenamed", oldnick, newnick)

    def receivedMOTD(self, motd):
        """MOTD"""
        self._runhandler("receivedMOTD", self.factory.to_unicode(motd))

    ## SERVER INFORMATION

    ## Network = Quakenet -> do Q auth
    def isupport(self, options):
        log.info(self.network.alias + " SUPPORTS: " + ",".join(options))

    def created(self, when):
        log.info(self.network.alias + " CREATED: " + when)

    def yourHost(self, info):
        log.info(self.network.alias + " YOURHOST: " + info)

    def myInfo(self, servername, version, umodes, cmodes):
        log.info(self.network.alias + " MYINFO: %s %s %s %s" % (servername, version, umodes, cmodes))

    def luserMe(self, info):
        log.info(self.network.alias + " LUSERME: " + info)
