class FlowIRCGateway(object):
    """A Flow-IRC gateway."""
    def __init__(self, options):
        """Arguments:
        options : optparse.OptionParser, with the following attributes:
            irc_ports, verbose, debug, show_timestamps, daemon,
            flowappglue, username, server, port, db, schema, uri
        """
        self.irc_ports = options.irc_ports
        self.verbose = options.verbose
        self.debug = options.debug
        self.show_timestamps = options.show_timestamps

        gateway_name_limit = 63  # From the RFC.
        self.name = socket.getfqdn()[:gateway_name_limit]

        self.channels = {}  # channelId --> Channel instance.
        self.clients = {}  # Socket --> IRCClient instance.
        self.organizations = {}  # orgId --> Organization Name
        self.pending_channels = {}  # channelId --> PendingChannel instance

        self.client_connected = True
        self.flow_service = None
        self.flow_initialized = False
        self.flow_username = ""
        self.flow_account_id = ""
        self.notification_handler = NotificationHandler(self)

    def terminate(self):
        """Terminates the Flow service."""
        self.flow_service.terminate()

    def get_member(self, irc_nickname):
        """Gets a channel member from its IRC nickname.
        Arguments:
        irc_nickname : string, IRC nickname of a 'ChannelMember'.
        Returns a ChannelMember instance or 'None' if not found.
        """
        for channel in self.channels.values():
            for member in channel.members:
                if member.get_irc_nickname() == irc_nickname:
                    return member
        return None

    def get_oid_from_name(self, org_name):
        """Returns an the orgId given an organization name.
        Arguments:
        org_name : string, organization name.
        Returns an empty string if not found.
        """
        for oid, oname in self.organizations.iteritems():
            if oname == org_name:
                return oid
        return ""

    def get_member_account_id(self, username):
        """Returns an accountId string from Flow given the username.
        Arguments:
        username : string, Flow username of the account.
        Returns an empty string if not found.
        """
        try:
            member_peer_data = self.flow_service.get_peer(username)
            if member_peer_data:
                return member_peer_data["accountId"]
        except Flow.FlowError as flow_err:
            self.print_debug("get_peer: '%s'" % str(flow_err))
        return ""

    def transmit_message_to_channel(self, channel, message_text):
        """Sends a message using Flow.send_message.
        Arguments:
        channel : Channel instance
        message_test : Text to be sent to the channel.
        Returns True if the message was sent successfully.
        """
        try:
            message_id = self.flow_service.send_message(
                channel.organization_id, channel.channel_id, message_text)
            return message_id != ""
        except Flow.FlowError as flow_err:
            self.print_debug("send_message: '%s'" % str(flow_err))
            return False

    def create_direct_channel(self, account_id, account_username, oid,
                              organization_name):
        """Creates a direct conversation channel.
        Arguments:
        account_id : string, receiver's accountId.
        account_username : string, receiver's username.
        oid : string, orgId the two members share.
        organization_name : Name of the Organization the two members share.
        Returns a 'Channel' instance.
        If there's an error in the channel creation, then 'None' is returned.
        """
        direct_conversation_channel = None
        try:
            direct_conversation_cid = \
                self.flow_service.new_direct_conversation(
                    oid,
                    account_id)
            if direct_conversation_cid:
                direct_conversation_channel = DirectChannel(
                    self, direct_conversation_cid, oid, organization_name,
                    True)
                current_user = ChannelMember(self.flow_username,
                                             self.flow_account_id,
                                             organization_name)
                other_member = ChannelMember(account_username, account_id,
                                             organization_name)
                direct_conversation_channel.add_member(current_user)
                direct_conversation_channel.add_member(other_member)
                self.add_channel(direct_conversation_channel)
        except Flow.FlowError as flow_err:
            self.print_debug("NewDirectConversation: '%s'" % str(flow_err))
        return direct_conversation_channel

    def get_channel(self, channel_id):
        """Returns a 'Channel' instance given a channelId.
        Returns 'None' if not found.
        """
        try:
            return self.channels[channel_id]
        except KeyError:
            return None

    def get_channel_from_irc_name(self, channel_irc_name):
        """Returns a 'Channel' instance given the IRC name.
        Returns 'None' if not found.
        """
        for channel in self.channels.values():
            if channel.get_irc_name() == channel_irc_name:
                return channel
        return None

    def check_channel_collision(self, channel):
        """Checks and sets if a 'Channel'
        instance IRC name collides with other channel.
        """
        if self.get_channel_from_irc_name(channel.get_irc_name()):
            channel.name_collides = True

    def get_channels(self, oid, org_name):
        """Loads all channels of a given organization
        Arguments:
        oid : string, orgId of the Organization
        org_name : string, Name of the Organization
        """
        channels = self.flow_service.enumerate_channels(oid)
        for channel in channels:
            channel_name = channel["name"]
            direct_channel = channel["purpose"] == "direct message"
            if direct_channel:
                irc_channel = DirectChannel(self, channel["id"], oid, org_name)
            else:
                irc_channel = Channel(self, channel["id"], channel_name, oid,
                                      org_name)
                self.check_channel_collision(irc_channel)
            self.get_channel_members(irc_channel)
            self.add_channel(irc_channel)

    def get_orgs_and_channels(self):
        """Loads all Organizations and Channels the account is member of."""
        self.organizations = {}
        self.channels = {}
        orgs = self.flow_service.enumerate_orgs()
        for org in orgs:
            oid = org["id"]
            org_name = org["name"]
            self.organizations[oid] = org_name
            self.get_channels(oid, org_name)

    def get_local_account(self):
        """Returns the first local Flow account on this device.
        It returns the username string.
        Returns 'None' if there is no account on the device.
        """
        local_accounts = self.flow_service.enumerate_local_accounts()
        if not local_accounts:
            return None
        return local_accounts[0]["username"]

    def register_callbacks(self):
        """Registers all the notification types this gateway supports."""
        self.flow_service.register_callback(
            Flow.ORG_NOTIFICATION, self.notification_handler.org_notification)
        self.flow_service.register_callback(
            Flow.CHANNEL_NOTIFICATION,
            self.notification_handler.channel_notification)
        self.flow_service.register_callback(
            Flow.MESSAGE_NOTIFICATION,
            self.notification_handler.message_notification)
        self.flow_service.register_callback(
            Flow.CHANNEL_MEMBER_NOTIFICATION,
            self.notification_handler.channel_member_notification)

    def unregister_callbacks(self):
        """Unregisters all the notification types this gateway supports."""
        self.flow_service.unregister_callback(Flow.ORG_NOTIFICATION)
        self.flow_service.unregister_callback(Flow.CHANNEL_NOTIFICATION)
        self.flow_service.unregister_callback(Flow.MESSAGE_NOTIFICATION)
        self.flow_service.unregister_callback(Flow.CHANNEL_MEMBER_NOTIFICATION)

    def initialize_flow_service(self, options):
        """Initializes the Flow Service by logging into the user
        account and starting up the Flow session.
        """
        self.flow_initialized = False
        try:
            self.flow_username = options.username
            self.flow_service = Flow("", "", options.flowappglue,
                                     options.debug, options.server,
                                     options.port, options.db, options.schema,
                                     options.attachment_dir)
            if not self.flow_username:
                self.flow_username = self.get_local_account()
            if not self.flow_username:
                raise Flow.FlowError("Local account not found.")
            self.flow_service.start_up(self.flow_username, options.uri)
            self.flow_initialized = True
        except Flow.FlowError as flow_err:
            self.print_error("Flow Initialization: '%s'" % str(flow_err))

    def add_channel(self, channel):
        """Adds a 'Channel' instance to the gateway's channel list."""
        self.channels[channel.channel_id] = channel

    def print_info(self, msg):
        """Prints a info msg string to stdout
        (if self.verbose is set to True).
        """
        if self.verbose:
            print(msg)
            sys.stdout.flush()

    def print_debug(self, msg):
        """Prints a debug msg string to stdout
        (if self.debug is set to True).
        """
        if self.debug:
            print(msg)
            sys.stdout.flush()

    @staticmethod
    def print_error(msg):
        """Prints an error msg string to stdout."""
        sys.stderr.write("%s\n" % msg)

    def remove_client(self, client):
        """Removes 'Client' instance 'client' from the clients map.
        The gateway stops processing notifications until reconnection.
        """
        del self.clients[client.client_socket]
        self.unregister_callbacks()
        self.client_connected = False

    def notify_clients(self, irc_msg):
        """Sends 'msg' string to all IRC client connections."""
        for client in self.clients.values():
            client.message(irc_msg)

    def get_username_from_id(self, account_id):
        """Returns the username of a given account.
        Arguments:
        account_id : string
        """
        peer_data = self.flow_service.get_peer_from_id(account_id)
        return peer_data["username"] if peer_data else ""

    def get_channel_members(self, channel):
        """Retrieves (via Flow.enumerate_channel_members) and sets
        all channel members on a given 'Channel' instance.
        """
        members = self.flow_service.enumerate_channel_members(
            channel.channel_id)
        for member in members:
            account_id = member["accountId"]
            account_username = self.get_username_from_id(account_id)
            assert account_username
            if account_username == self.flow_username:
                self.flow_account_id = account_id
            channel_member = ChannelMember(account_username, account_id,
                                           channel.organization_name)
            channel.add_member(channel_member)

    def start(self):
        """FlowIRCGateway initialization and main loop"""
        if not self.flow_initialized:
            self.terminate()
            return
        gatewaysockets = []
        for port in self.irc_ports:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            try:
                # Listen to local connections only
                sock.bind(("localhost", port))
            except socket.error as sock_err:
                self.print_error("Could not bind port %s: %s." %
                                 (port, sock_err))
                sys.exit(1)
            sock.listen(5)
            gatewaysockets.append(sock)
            del sock
            self.print_info("Listening on port %d." % port)
        last_aliveness_check = time.time()

        while True:
            # Process Flow notifications
            if self.client_connected:
                while self.flow_service.process_one_notification(0.05):
                    pass
            # Process IRC client socket connections
            (iwtd, owtd, _) = select.select(
                gatewaysockets +
                [client.client_socket for client in self.clients.values()], [
                    client.client_socket for client in self.clients.values()
                    if client.write_queue_size() > 0
                ], [], 0.05)
            for sock in iwtd:
                if sock in self.clients:
                    self.clients[sock].socket_readable_notification()
                else:
                    (conn, addr) = sock.accept()
                    try:
                        self.clients[conn] = IRCClient(self, conn)
                        self.print_info("Accepted connection from %s:%s." %
                                        (addr[0], addr[1]))
                    except socket.error:
                        try:
                            conn.close()
                        except:
                            pass
            for sock in owtd:
                if sock in self.clients:  # client may have been disconnected
                    self.clients[sock].socket_writable_notification()
            now = time.time()
            if last_aliveness_check + 10 < now:
                for client in self.clients.values():
                    client.check_aliveness()
                last_aliveness_check = now