#!/usr/bin/python
# -*- coding: utf-8 -*-
### BEGIN LICENSE
# Copyright (C) 2009 Jono Bacon <jono@ubuntu.com>
# Copyright (C) 2010 Michael Budde <mbudde@gmail.com>
# Copyright (c) 2011 John S Gruber <johnsgruber@gmail.com>
#
#This program is free software: you can redistribute it and/or modify it
#under the terms of the GNU General Public License version 3, as published
#by the Free Software Foundation.
#
#This program is distributed in the hope that it will be useful, but
#WITHOUT ANY WARRANTY; without even the implied warranties of
#MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.
### END LICENSE

import glib
import os
import gobject
import random
import dbus.mainloop.glib
dbus.mainloop.glib.DBusGMainLoop(set_as_default = True)

import telepathy
from telepathy.interfaces import *
from telepathy.constants import *

import logging
from lernid.LernidOptions import Options
from lernid.lernidconfig import VERSION
from lernid.Statusbar import Statusbar

DBUS_PROPERTIES = dbus.PROPERTIES_IFACE

def ignore(*args): pass


_server_instances = {}

class Server(gobject.GObject):

    __gsignals__ = {
        'connected': (
            gobject.SIGNAL_RUN_LAST, None, ()
        ),
        'disconnected': (
            gobject.SIGNAL_RUN_LAST, None, ()
        ),
        'identified': (
            gobject.SIGNAL_RUN_LAST, None, ()
        ),
    }

    DISCONNECTED, CONNECTING, CONNECTED = range(3)

    def __init__(self, server, nick, eventman):
        gobject.GObject.__init__(self)
        self._eventman = eventman
        self._nick = nick
        self._temp_nick = nick[0:9] + str(random.randint(0,999999))
        self._server = server
        self._channels = {}
        self._private_channels = {}
        self._conn = None
        self._state = self.DISCONNECTED
        self._unconnected_channels = {}
        self._unconnected_private_channels = {}
        self._nickidentifier = None
        self._load_manager()
        self._request_connection(server, self._temp_nick)
        self._conn_timeout = None

    @property
    def nick(self):
        return self._nick

    @nick.setter
    def nick(self, val):
        self._nick = val

    @classmethod
    def get_server(cls, server, nick, eventman):
        logging.debug('getting server')
        global _server_instances
        if server in _server_instances:
            server_obj = _server_instances[server]
            server_obj.nick = nick
            server_obj._request_connection(server, server_obj._temp_nick)
            return server_obj
        else:
            obj = cls(server, nick, eventman)
            _server_instances[server] = obj
            return obj

    def get_channel(self, name):
        if name in self._channels:
            return self._channels[name]
        obj = Channel(name)
        self._channels[name] = obj
        self._connect_channel(obj)
        return obj

    def get_private_channel(self, target):
        if target in self._private_channels:
            return self._private_channels[target]
        obj = PrivateChannel(target)
        self._private_channels[target] = obj
        self._connect_private_channel(obj)
        return obj

    def identify_nick(self, password):
        self._nickidentifier = NickServIdentifier(self.nick, password)
        self._connect_private_channel(self._nickidentifier)
        return self._nickidentifier

    def connection_ready_timeout(self):
        logging.debug('Connection timed out')
        self.disconnect()
        self._eventman.emit('event-interrupted', self._eventman._event)
        return False

    def _load_manager(self):
        logging.debug('loading manager')
        reg = telepathy.client.ManagerRegistry()
        reg.LoadManagers()
        self._cm = reg.GetManager('idle') # IRC

    def _request_connection(self, server, nick):
        logging.debug('requesting connection: %s %s' % (server, nick))
        if self._state == self.DISCONNECTED:
            self._state = self.CONNECTING
            self._cm[CONNECTION_MANAGER].RequestConnection('irc',
                {'account': nick, 'server': server, 'fullname': VERSION},
                reply_handler=self._got_connection,
                error_handler=self.error)
        else:
            logging.debug('ignoring request')

    def _got_connection(self, bus_name, object_path):
        logging.debug('got connection')
        self._conn = telepathy.client.Connection(
            bus_name, object_path,
            ready_handler=self._ready)
        self._conn._acquired_nick = ''
        self._conn[CONNECTION].connect_to_signal('StatusChanged',
                                                 self._status_changed)
        self._conn[CONNECTION].Connect(reply_handler=ignore,
                                       error_handler=self.error)
        if self._conn_timeout:
            glib.source_remove(self._conn_timeout)
        self._conn_timeout = glib.timeout_add_seconds(90,self.connection_ready_timeout)

    def _ready(self, connection):
        logging.debug('server ready')
        self._state = self.CONNECTED
        self._conn[CONNECTION_INTERFACE_RENAMING].RequestRename(self._nick)
        logging.debug('nick potentially changed in _ready handler to %s' % \
            self._nick)
        glib.source_remove(self._conn_timeout)
        self._conn_timeout = None
        for channel in self._unconnected_channels.keys():
            self._connect_channel(channel)
        self._unconnected_channels = {}
        for channel in self._unconnected_private_channels.keys():
            self._connect_private_channel(channel)
        self._unconnected_private_channels = {}

    def _connect_channel(self, channel):
        if self._state != self.CONNECTED:
            self._unconnected_channels[channel] = True
            return
        def setup_channel(yours, object_path, properties):
            channel.do_connect(self._conn, object_path, properties)
        self._conn[CONNECTION_INTERFACE_REQUESTS].EnsureChannel({
                CHANNEL + ".ChannelType": CHANNEL_TYPE_TEXT,
                CHANNEL + ".TargetHandleType": HANDLE_TYPE_ROOM,
                CHANNEL + ".TargetID": channel.name,
            },
            reply_handler = setup_channel,
            error_handler = self.error)

    def _connect_private_channel(self, channel):
        if self._state != self.CONNECTED:
            self._unconnected_private_channels[channel] = True
            return
        def setup_channel(yours, object_path, properties):
            channel.do_connect(self._conn, object_path, properties)
        self._conn[CONNECTION_INTERFACE_REQUESTS].EnsureChannel({
                CHANNEL + ".ChannelType": CHANNEL_TYPE_TEXT,
                CHANNEL + ".TargetHandleType": HANDLE_TYPE_CONTACT,
                CHANNEL + ".TargetID": channel.target,
            },
            reply_handler = setup_channel,
            error_handler = self.error)

    def _status_changed(self, status, reason):
        if status == CONNECTION_STATUS_CONNECTED:
            logging.debug('connected to server')
            self.emit('connected')
        elif status == CONNECTION_STATUS_DISCONNECTED:
            logging.debug('disconnected from server')
            if self._conn_timeout:
                glib.source_remove(self._conn_timeout)
                self._conn_timeout = None
            for chan in self._channels.values():
                chan._c_disconnect()
                self._unconnected_channels[chan] = True
            for chan in self._private_channels.values():
                self._unconnected_private_channels[chan] = True
            if self._nickidentifier:
                self._unconnected_private_channels[self._nickidentifier] = True
            if self._state == self.CONNECTED:
                # Only emit signal if we had a connection before
                self.emit('disconnected')
            self._state = self.DISCONNECTED
            if reason == CONNECTION_STATUS_REASON_NAME_IN_USE:
                self._nick = '{0}{1}'.format(self._nick, random.randint(10, 99))
                self._request_connection(self._server, self._nick)
            if reason == CONNECTION_STATUS_REASON_NETWORK_ERROR:
                self._eventman.emit('event-interrupted', self._eventman._event)

    def disconnect(self):
        if not self._conn:
            return
        def disconnect_cb():
            logging.debug("Server Disconnect Done")
        def disconnect_error_cb(error):
            pass
        if self._conn_timeout:
            glib.source_remove(self._conn_timeout)
            self._conn_timeont = None
        self._conn[CONNECTION].Disconnect(reply_handler=disconnect_cb,
                                          error_handler=disconnect_error_cb)
        self._state = self.DISCONNECTED
        self._conn = None
        self._nickidentifier = None

    def error(self, e):
        logging.debug("ERROR:Server:%s:%s", e.get_dbus_name(), e.args[0])
        self._state = self.DISCONNECTED
        if e.get_dbus_name() == 'org.freedesktop.DBus.Error.ServiceUnknown':
            self._load_manager()
            self._request_connection(self._server, self._temp_nick)
        else:
            self.disconnect()


class Channel(gobject.GObject):

    TYPE_NORMAL = 0
    TYPE_ACTION = 1

    __gsignals__ = {
        'joined': (
            gobject.SIGNAL_RUN_LAST, None, ()
        ),
        'message-received': (
            gobject.SIGNAL_RUN_LAST, None, (str, str, int)
        ),
        'message-sent': (
            gobject.SIGNAL_RUN_LAST, None, (str,)
        ),
        'members-changed': (
            gobject.SIGNAL_RUN_LAST, None, (object,)
        ),
    }

    def __init__(self, name):
        """Channel objects should not be instancted directly, use
        Server.get_channel function."""
        logging.debug('channel created')
        gobject.GObject.__init__(self)
        self._name = name
        self._signal_matches = []
        self._my_handle = None

    def do_connect(self, connection, object_path, properties):
        """Should only be called by Server"""
        logging.debug('connecting channel')
        self._members = {}
        self._conn = connection
        self._chan = telepathy.client.Channel(
            connection.service_name,
            object_path,
            ready_handler=self._ready)

    @property
    def name(self):
        return self._name

    def get_members(self):
        return self._members.values()

    def send_message(self, message):
        if not self._chan:
            return
        def ack():
            logging.debug('message sent to %s' % self.name)
            self.emit('message-sent', message)
        self._chan[CHANNEL_TYPE_TEXT].Send(0, message,
                                           reply_handler=ack,
                                           error_handler=self._conn.error)

    def is_moderated(self):
        try:
            return bool(self._chan[PROPERTIES_INTERFACE].GetProperties([3])[0][1])
        except Exception, e:
            logging.debug('could not acess "moderated" property: {0}'.format(e))
            return False

    def _ready(self, channel):
        """We have joined the channel."""
        logging.debug('connected channel %s' % self.name)
        match = self._chan[CHANNEL_INTERFACE_GROUP].connect_to_signal('MembersChanged',
                                     self._members_changed)
        self._signal_matches.append(match)
        self._chan[CHANNEL_INTERFACE_GROUP].GetMembers(reply_handler=self._update_members,
                                                       error_handler=self._conn.error)
        match = self._chan[CHANNEL_TYPE_TEXT].connect_to_signal('Received', self._received_message)
        self._signal_matches.append(match)
        self.emit('joined')

    def _c_disconnect(self):
        for match in self._signal_matches:
            match.remove()
        self._signal_matches = []

    def _update_members(self, handles):
        def update_cb(ids):
            self._members.update(zip(handles, ids))
            if Options.get('more_verbose'):
                logging.debug('members updated')
            self.emit('members-changed', self.get_members())
            if self._my_handle:
                try:
                    my_alias = self._members[self._my_handle]
                except:
                    return
                if not my_alias == self._conn._acquired_nick:
                    self._conn._acquired_nick = my_alias
                    Statusbar.push_message(_('Nick assigned by server: %s' % my_alias), duration=20)
                    logging.debug('Nick assigned by server: %s' % my_alias)
        def update_my_handle(handle):
            self._my_handle = handle
        # If this first call doesn't return before the Aliases come back
        # we can set our alias name the next time membership in a room changes
        self._my_handle = self._conn[CONNECTION].GetSelfHandle(reply_handler=update_my_handle, error_handler=lambda x: None)
        self._conn[CONNECTION_INTERFACE_ALIASING].RequestAliases(
             handles,
             reply_handler=update_cb,
             error_handler=ignore)

    def _members_changed(self, message, added, removed,
                         local_pending, remote_pending, actor, reason):
        if Options.get('more_verbose'):
            logging.debug('members changed in %s' % self.name)
        for h in removed:
            if h in self._members:
                del self._members[h]
        self._update_members(added)

    def _received_message(self, id, timestamp, sender_handle, msgtype, flags, text):
        if Options.get('more_verbose'):
            logging.debug('message received in %s' % self.name)
        if msgtype == CHANNEL_TEXT_MESSAGE_TYPE_ACTION:
            msgtype = self.TYPE_ACTION
        else:
            msgtype = self.TYPE_NORMAL
        if not sender_handle in self._members:
            sender = self._conn[CONNECTION_INTERFACE_ALIASING].RequestAliases(
             [sender_handle])[0] # Need to backstop other alias mechanisms
            self._members[sender_handle] = sender
        else:
            sender = self._members[sender_handle]
        self.emit('message-received', sender, text, msgtype)


class PrivateChannel(gobject.GObject):

    __gsignals__ = {
        'joined': (
            gobject.SIGNAL_RUN_LAST, None, ()
        ),
        'message-received': (
            gobject.SIGNAL_RUN_LAST, None, (str, str)
        ),
        'message-sent': (
            gobject.SIGNAL_RUN_LAST, None, (str,)
        ),
    }

    @property
    def target(self):
        return self._target

    def __init__(self, target):
        """PrivateChannel objects should not be instanced directly, use
        Server.get_private_channel function."""
        logging.debug('private channel created')
        gobject.GObject.__init__(self)
        self._target = target
        self._conn = None
        self._chan = None

    def do_connect(self, connection, object_path, properties):
        """Should only be called by Server"""
        logging.debug('connecting private channel')
        self._conn = connection
        self._chan = telepathy.client.Channel(
            connection.service_name,
            object_path,
            ready_handler=self._ready)

    def send_message(self, message):
        if not self._chan:
            return False
        def ack():
            logging.debug('message sent to %s' % self.target)
            self.emit('message-sent', message)
        self._chan[CHANNEL_TYPE_TEXT].Send(0, message,
                                           reply_handler=ack,
                                           error_handler=self._conn.error)
        return True

    def _ready(self, channel):
        """We have joined the channel."""
        logging.debug('connected private channel %s' % self.target)
        self._chan[CHANNEL_TYPE_TEXT].connect_to_signal('Received', self._received_message)
        self.emit('joined')

    def _received_message(self, id, timestamp, sender_handle, msgtype, flags, text):
        logging.debug('private message by %s is %s' % (self.target,text))
        self.emit('message-received', self.target, text)


class NickServIdentifier(PrivateChannel):
    """A class for identifying with NickServ.
    Should only be instanciated by Server object
    """

    __gsignals__ = {
        'identified': (
            gobject.SIGNAL_RUN_LAST, None, ()
        ),
        'invalid-password': (
            gobject.SIGNAL_RUN_LAST, None, ()
        ),
    }

    def __init__(self, nick, password):
        PrivateChannel.__init__(self, 'NickServ')
        self._nick = nick
        self.connect('joined', self._joined, password)
        self.connect('message-received', self._msg_received)

    def retry(self, password):
        """Try indentifying with NickServ again.
        To be used if initial identification fails because of invalid password.
        """
        self._identify(password)

    def _joined(self, chan, password):
        self._identify(password)

    def _identify(self, password):
        self.send_message('identify {0} {1}'.format(self._nick, password))

    def _msg_received(self, chan, sender, msg):
        if msg.startswith('You are now identified'):
            logging.debug('identified nick')
            handle = self._conn[CONNECTION].GetSelfHandle()
            logging.debug('our handle before the rename is %s' %  self._conn[CONNECTION_INTERFACE_ALIASING].RequestAliases([handle])[0])
            self._conn[CONNECTION_INTERFACE_RENAMING].RequestRename(self._nick)
            logging.debug('nick potentially changed in ident. handler to %s'% \
                self._nick)
            self.emit('identified')
        elif msg.startswith('Invalid password'):
            logging.debug('invalid password')
            self.emit('invalid-password')
