#!/usr/bin/env python3

""" AD1459, an IRC Client

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.

  Handling for networks and their rooms.
"""

import asyncio
import logging
import time

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib

from .widgets.room_row import RoomRow
from .widgets.message_row import MessageRow
from .client import Client
from .commands import Commands
from .formatting import Parser
from .room import Room

class Network:
    """An IRC network to connect to and chat on.

    Attributes:
        rooms (list of :obj:`Room`): A list of the joined rooms on this network.
        name (str): The user-defined name for this network.
        auth (str): The user-authentication for this network. 'sasl', 'pass', 
            or 'none'.
        host (str): The hostname for the server to connect to.
        port (int): The port number to use.
        tls (bool): Wheter the connection uses TLS encryption.
        nickname (str): The user's nickname on this network.
        realname (str): The user's Real Name on this network.
        password (str): The authentication for the password on this network.
        app (:obj:`Ad1459Application`): The app we're running on.
    """
    cmd = Commands()
    commands = {
        '/me': cmd.me
    }

    def __init__(self, app, window):
        self.log = logging.getLogger('ad1459.network')
        self.log.debug('Creating network')
        self.app = app
        self.parser = Parser()
        self.window = window
        self.rooms = []
        self._config = {
            'name': 'New Network',
            'auth': 'sasl',
            'host': 'chat.freenode.net',
            'port': 6697,
            'tls': True,
            'nickname': 'ad1459-user',
            'username': 'ad1459-user',
            'realname': 'AD1459 User',
            'password': 'hunter2'
        }
        self.client = None

    # Synchronous Methods for this object.
    def connect(self):
        """ Connect to the network, disconnecting first if already connected. """
        if self.auth == 'sasl':
            self.client = Client(
                self.nickname, 
                self, 
                sasl_password=self.password, 
                sasl_username=self.username
            )
        else:
            self.client = Client(self.nickname, self)
        
        self.client.username = self.username

        self.log.debug('Spinning up async connection to %s', self.host)

        self.server_room = Room(self.app, self, self.window, self.name)
        self.server_room.kind = "server"
        self.add_room(self.server_room)

        if self.auth == 'pass':
            self.log.debug('Using password authentication')
            asyncio.run_coroutine_threadsafe(
                self.client.connect(
                    hostname=self.host,
                    port=self.port,
                    tls=self.tls,
                    password=self.password
                ),
                loop=asyncio.get_event_loop()
            )

        else:
            self.log.debug('Using SASL authentication (or none)')
            asyncio.run_coroutine_threadsafe(
                self.client.connect(
                    hostname=self.host,
                    port=self.port,
                    tls=self.tls
                ),
                loop=asyncio.get_event_loop()
            )
    
    def disconnect(self):
        """Disconnect from this network."""
        self.log.debug('Disconnecting from %s', self.name)
        asyncio.run_coroutine_threadsafe(
            self.client.quit(message='AD1459 Quit'),
            loop=asyncio.get_event_loop()
        )
    
    def change_nick(self, new_nick):
        """ Changes the user's nick."""
        asyncio.run_coroutine_threadsafe(
            self.client.set_nickname(new_nick),
            loop=asyncio.get_event_loop()
        )

    def join_channel(self, channel):
        """ Joins a room on the network."""
        asyncio.run_coroutine_threadsafe(
            self.client.join(channel), loop=asyncio.get_event_loop()
        )

    def add_room(self, room):
        """Adds a room to the window for this network."""
        self.log.debug('Adding room %s to the window', room.name)
        self.window.switcher.add_row(room.row)
        self.window.message_stack.add_named(room.buffer, room.id)
        self.window.topic_stack.add_named(room.topic_pane, room.id)
        self.window.show_all()
        self.window.switcher.invalidate_sort()
        self.rooms.append(room)
    
    def get_room_for_name(self, name):
        """ Gets a room object given the name."""
        for room in self.rooms:
            if room.name == name:
                return room

        if not name.startswith('#'):
            new_room = Room(self.app, self, self.window, name)
            new_room.kind = 'dialog'
            new_room.name = name
            new_room.topic_pane.update_topic()
            self.add_room(new_room)
            self.window.switcher.invalidate_sort()
            return new_room
    
    def send_message(self, room, message):
        """ Sends a message to the entity for room."""
        message_text = self.parser.format_text(message)
        for command in self.commands:
            if not message.startswith(command):
                asyncio.run_coroutine_threadsafe(
                    self.client.message(room.name, message_text),
                    loop=asyncio.get_event_loop()
                )
            else:
                self.commands[command](room, self.client, message_text)

    
    # Asynchronous Callbacks
    async def on_connected(self):
        """ Called upon connection to IRC."""
        self.log.info('Connected to %s', self.name)
        GLib.idle_add(self.do_connected)
    
    def do_connected(self):
        popup = self.window.header.server_popup
        popup.reset_all_text()
        popup.layout_grid.set_sensitive(True)
        self.window.header.spinner.stop()
        self.window.switcher.invalidate_sort()
    
    async def on_nick_change(self, old, new):
        self.log.debug('Nick %s changed to %s', old, new)
        GLib.idle_add(self.do_nick_change, old, new)
    
    def do_nick_change(self, old, new):
        if old == self.nickname or old == '<unregistered>':
            self.nickname = new
            self.window.nick_button.set_label(self.nickname)
        
        else:
            for room in self.rooms:
                if new in room.users:
                    room.add_message(f'{old} is now {new}.', kind='server')
                    room.window.show_all()
                    room.update_users()
    
    async def on_join(self, channel, user):
        self.log.debug('%s has joined %s', user, channel)
        GLib.idle_add(self.do_join, channel, user)
    
    def do_join(self, channel, user):
        if user == self.nickname:
            new_channel = Room(self.app, self, self.window, channel)
            new_channel.kind = 'channel'
            new_channel.name = channel
            new_channel.topic_pane.update_topic()
            self.add_room(new_channel)
            new_channel.update_users()
            self.window.switcher.invalidate_sort()
        
        else:
            room = self.get_room_for_name(channel)
            room.add_message(f'{user} has joined.', kind='server')
            self.window.show_all()
            room.update_users()
    
    async def on_part(self, channel, user, message=None):
        self.log.debug('%s has left %s, (%s)', user, channel, message)
        GLib.idle_add(self.do_part, channel, user, message)
    
    def do_part(self, channel, user, message=None):
        room = self.get_room_for_name(channel)
        if user == self.nickname:
            room.leave()
            room.network.server_room.add_message(
                f'You have left {room.name}', kind='server'
            )
            self.rooms.remove(room)
            self.window.switcher.invalidate_sort()
        
        else:
            room.update_users()
            room.add_message(f'{user} has left ({message}).', kind='server')
            self.window.show_all()
    
    async def on_kick(self, channel, target, by, reason=None):
        GLib.idle_add(self.do_kick, channel, target, by, reason)
    
    def do_kick(self, channel, target, by, reason=None):
        room = self.get_room_for_name(channel)
        if target == self.nickname:
            room.leave()
            room.network.server_room.add_message(
                f'You were kicked from {room.name} by {by}, reason: "{reason}"', 
                kind='server'
            )
            room.network.server_room.row.set_icon('emblem-important-symbolic')
            self.rooms.remove(room)
            self.window.switcher.invalidate_sort()
        
        else:
            room.update_users()
            room.add_message(
                f'{target} was kicked by {by}, reason: "{reason}".', kind='server'
            )
            self.window.show_all()

    async def on_quit(self, user, message=None):
        self.log.debug('%s has quit! (%s)', user, message)
        GLib.idle_add(self.do_quit, user, message)
    
    def do_quit(self, user, message=None):
        qmessage = f'{user} has quit ({message}).'
        for room in self.rooms:
            if user in room.old_users:
                room.add_message(qmessage, kind='server')
                room.update_users()
    
    async def on_kill(self, target, by, reason=None):
        GLib.idle_add(self.do_kill, target, by, reason)
    
    def do_kill(self, target, by, reason=None):
        qmessage = f'{target} was killed by {by}, reason: "{reason}".'
        for room in self.rooms:
            if target in room.old_users:
                room.add_message(qmessage, kind='server')
                room.update_users()
    
    async def on_message(self, target, source, message):
        GLib.idle_add(self.do_message, target, source, message)
    
    def do_message(self, target, source, message):
        if target.startswith('#'):
            room = self.get_room_for_name(target)
            self.log.debug('Adding message to %s', room.id)
            room.add_message(message, source)
            room.update_tab_complete(source)
            self.window.show_all()

    async def on_notice(self, target, source, message):
        self.log.debug('%s noticed to %s: %s', source, target, message)
        GLib.idle_add(self.do_notice, target, source, message)
    
    def do_notice(self, target, source, message):
        if target.startswith('#'):
            room = self.get_room_for_name(target)
            self.log.debug('Adding notice to %s', room.id)
            room.add_message(message, source, kind='notice')
            room.update_tab_complete(source)
            self.window.show_all()
    
    async def on_private_message(self, target, source, message):
        self.log.debug('PM to %s from %s: %s', target, source, message)
        GLib.idle_add(self.do_private_message, target, source, message)

    def do_private_message(self, target, source, message):
        if target == self.nickname:
            room = self.get_room_for_name(source)
            self.log.debug('Adding message from %s', room.id)
        else:
            room = self.get_room_for_name(target)
            self.log.debug('Adding message to %s', room.id)
        
        room.add_message(message, source)
        self.window.show_all()
    
    async def on_private_notice(self, target, source, message):
        self.log.debug('Private Notice to %s from %s: %s', target, source, message)
        GLib.idle_add(self.do_private_notice, target, source, message)
        
    def do_private_notice(self, target, source, message):
        room = self.get_room_for_name(source)
        self.log.debug('Adding notice from %s', room.id)
        room.add_message(message, source, kind='notice')
        self.window.show_all()
    
    async def on_topic_change(self, channel, message, by):
        GLib.idle_add(self.do_topic_change, channel, message, by)
    
    def do_topic_change(self, channel, message, by):
        self.log.debug('Changing topic in %s', channel)
        room = self.get_room_for_name(channel)
        room.topic_pane.update_topic()
        room.add_message(
            f'{by} has changed the channel topic, "{message}"', kind='server'
        )
    
    async def on_mode_change(self, channel, modes, by):
        GLib.idle_add(self.do_mode_change, channel, modes, by)

    def do_mode_change(self, channel, modes, by):
        room = self.get_room_for_name(channel)
        mode_codes = modes[0]
        mode_index = 1
        change = None
        mode_msg = f'{by} '
        try:
            for code in mode_codes:
                if code == '+':
                    change = 'set '
                
                elif code == '-':
                    change = 'removed '
                
                elif code == 'b':
                    mode_msg += f'{change} a ban on {modes[mode_index]}, '
                    mode_index += 1
                
                elif code == 'c':
                    mode_msg += f'{change} colour filter, '
                
                elif code == 'e':
                    mode_msg += f'{change} a ban exemption on {modes[mode_index]}, '
                    mode_index += 1
                
                elif code == 'C':
                    mode_msg += f'{change} CTCP blocking, '
                
                elif code == 'i':
                    mode_msg += f'{change} invite-only, '

                elif code == 'o':
                    mode_msg += f'{change} op on {modes[mode_index]}, '
                    mode_index += 1
                
                elif code == 'v':
                    mode_msg += f'{change} voice on {modes[mode_index]}, '
                    mode_index += 1
                
                elif code == 'q':
                    mode_msg += f'{change} quiet on {modes[mode_index]}, '
                    mode_index += 1
                
                elif code == 'm':
                    mode_msg += f'{change} moderated, '
        
        except IndexError:
            print('No index %s', mode_index)
        
        mode_msg = mode_msg.strip()
        mode_msg = mode_msg.strip(',')
        room.add_message(f'{mode_msg}.', kind='server')

    async def on_user_invite(self, target, channel, by):
        GLib.idle_add(self.do_user_invite, target, channel, by)
    
    def do_user_invite(self, target, channel, by):
        room = self.get_room_for_name(channel)
        room.add_message(f'{by} invited {target}', kind='server')

    async def on_invite(self, channel, by):
        GLib.idle_add(self.do_invite, channel, by)
    
    def do_invite(self, channel, by):
        self.server_room.add_message(
            f'You were invited to {channel} by {by}', kind='server'
        )
        self.server_room.row.set_icon('emblem-important-symbolic')


    async def on_ctcp_action(self, target, source, action):
        self.log.debug('Action in %s from %s: %s %s', target, source, source, action )
        GLib.idle_add(self.do_ctcp_action, target, source, action)
        
    def do_ctcp_action(self, target, source, action):
        self.log.debug('Adding action from %s to %s', source, target)
        if target.startswith('#'):
            room = self.get_room_for_name(target)
        elif target == self.nickname:
            room = self.get_room_for_name(source)
        
        message = f'{source} {action}'
        room.add_message(message, kind='action')
        self.window.show_all()
        room.update_tab_complete(source)

    # Data for this object.
    @property
    def name(self):
        """str: The name of this network (and its room)."""
        return self._config['name']
    
    @name.setter
    def name(self, name):
        """This is actually tracked by the room."""
        self.log.debug('Setting name to %s', name)
        self._config['name'] = name
    
    @property
    def auth(self):
        """str: One of 'sasl', 'pass', or 'none'."""
        return self._config['auth']
    
    @auth.setter
    def auth(self, auth):
        """Only set if it's a valid value."""
        if auth == 'sasl' or auth == 'pass' or auth == 'none':
            self._config['auth'] = auth

    @property
    def host(self):
        """str: The hostname of the server to connect to."""
        return self._config['host']
    
    @host.setter
    def host(self, host):
        self._config['host'] = host
    
    @property
    def port(self):
        return self._config['port']
    
    @port.setter
    def port(self, port):
        """ Only set a port that is within the valid range."""
        if port > 0 and port <= 65535:
            self._config['port'] = int(port)

    @property
    def tls(self):
        """bool: Whether or not to use TLS"""
        return self._config['tls']
    
    @tls.setter
    def tls(self, tls):
        self._config['tls'] = tls

    @property
    def nickname(self):
        """str: The user's nickname"""
        return self._config['nickname']
    
    @nickname.setter
    def nickname(self, nickname):
        self.log.debug('Setting nickname to %s', nickname)
        self._config['nickname'] = nickname

    @property
    def username(self):
        """str: The username to use for the connection"""
        return self._config['username']
    
    @username.setter
    def username(self, username):
        self.log.debug('Setting username to %s', username)
        self._config['username'] = username

    @property
    def realname(self):
        """str: The user's real name"""
        return self._config['realname']
    
    @realname.setter
    def realname(self, realname):
        self._config['realname'] = realname

    @property
    def password(self):
        """str: The user's password."""
        return self._config['password']
    
    @password.setter
    def password(self, password):
        self.log.debug('Setting password')
        self._config['password'] = password
