import datetime
import importlib
import io
import re
import select
import socket
import ssl
import time

from pyromancer import utils


class Pyromancer(object):

    def __init__(self, settings_path):
        self.settings = Settings(settings_path)
        self.setup_database()
        self.find_commands()
        self.find_timers()

    def run(self):
        self.connect()
        self.listen()

    def connect(self):
        self.connection = Connection(
            self.settings.host, self.settings.port, self.settings.encoding,
            self.settings.ssl)

        self.online = True
        self.connect_time = datetime.datetime.now()
        self.connection.write('NICK {}\n'.format(self.settings.nick))
        self.connection.write('USER {0} {1} {1} :{2}\n'.format(
            self.settings.nick, self.settings.host, self.settings.real_name))
        self.connection.me.nick = self.settings.nick

    def listen(self):
        self.online = True
        ticks = self.settings.ticks

        while self.online:
            self.connection.read()

            for line in self.connection.buffer.lines():
                self.process(line)

            for timer in self.timers:
                timer.match(self.timers, self.connect_time, self.connection,
                            self.settings)

            time.sleep(1.0 / ticks)

    def process(self, line):
        line = Line(line, self.connection)

        if line[0] == 'PING':
            self.connection.write('PONG {}\n'.format(line[1]))

        for c in self.commands:
            c.command.match(line, self.timers, self.connection, self.settings)

    def find_commands(self):
        self.commands = []

        utils.find_functions(
            self.settings.packages, self.commands, 'commands',
            'disabled_commands', when=lambda f: hasattr(f, 'command'))

    def find_timers(self):
        self.timers = []

        utils.find_functions(
            self.settings.packages, self.timers, 'timers', 'disabled_timers',
            when=lambda f: hasattr(f, 'timer'), ret=lambda f: f.timer)

    def setup_database(self):
        if self.settings.database:
            from sqlalchemy import create_engine

            from pyromancer.database import Session, Base

            engine = create_engine(self.settings.database)
            Session.configure(bind=engine)

            for package in self.settings.packages:
                if isinstance(package, tuple):
                    package = package[0]

                module_name = '{}.models'.format(package)

                try:
                    importlib.import_module(module_name)
                except ImportError:
                    continue

            Base.metadata.create_all(bind=engine)


class Settings(object):

    def __init__(self, path):
        main_settings = importlib.import_module(path)
        self.packages = getattr(main_settings, 'packages', [])
        self.package_settings = {}
        self.package_name, _ = path.split('.', 1)

        if self.package_name not in self.packages:
            self.packages.insert(0, self.package_name)

        for package in self.packages:
            if isinstance(package, tuple):
                package = package[0]

            if package == self.package_name:
                module = main_settings
            else:
                module = importlib.import_module('{}.settings'.format(package))

            self.package_settings[package] = module

        self.global_settings = None
        if 'pyromancer' not in self.packages:
            self.global_settings = importlib.import_module(
                'pyromancer.settings')

    def __getattr__(self, item):
        for package in self.packages:
            if isinstance(package, tuple):
                package = package[0]

            if hasattr(self.package_settings[package], item):
                return getattr(self.package_settings[package], item)

        if hasattr(self.global_settings, item):
            return getattr(self.global_settings, item)

        raise AttributeError('No such setting "{}" found in any of the '
                             'installed packages'.format(item))


class LineBuffer(object):
    """Line buffer based on irc library's DecodingLineBuffer.

    See https://bitbucket.org/jaraco/irc
    """
    line_sep_exp = re.compile(b'\r?\n')

    def __init__(self, encoding='utf-8'):
        self.buffer = b''
        self.encoding = encoding

    def feed(self, bytes):
        self.buffer += bytes

    def lines(self):
        lines = self.line_sep_exp.split(self.buffer)
        # save the last, unfinished, possibly empty line
        self.buffer = lines.pop()

        for line in lines:
            yield line.decode(self.encoding, 'strict')

    def __iter__(self):
        return self.lines()

    def __len__(self):
        return len(self.buffer)


class Connection(object):

    def __init__(self, host, port, encoding='utf8', use_ssl=False):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((host, port))

        # We need non-blocking so it won't hang when there is no data.
        self.socket.setblocking(False)

        if use_ssl:
            self.socket = ssl.wrap_socket(
                self.socket, do_handshake_on_connect=False)

            # We need to do the handshake manually, because we use a
            # non-blocking socket.
            # https://docs.python.org/2/library/ssl.html#ssl-nonblocking
            while True:
                try:
                    self.socket.do_handshake()
                    break
                except ssl.SSLWantReadError:
                    select.select([self.socket], [], [])
                except ssl.SSLWantWriteError:
                    select.select([], [self.socket], [])

        self.encoding = encoding
        self.buffer = LineBuffer(self.encoding)

        self.me = User('')

    @property
    def users(self):
        users = [self.me]
        for chan in self.me.channels:
            users.extend(chan.users)

        return list(set(users))

    @property
    def channels(self):
        return self.me.channels

    def write(self, data):
        self.socket.send('{}\n'.format(data).encode(self.encoding))

    def read(self, bytes=4096):
        try:
            self.buffer.feed(self.socket.recv(bytes))
        except (io.BlockingIOError, ssl.SSLWantReadError):
            pass

    def msg(self, target, msg):
        self.write('PRIVMSG {} :{}'.format(target,  msg))


class User(object):
    name = None
    host = None
    auth = None

    def __init__(self, str):
        if '@' in str:
            self.nick, self.name, self.host = self.split_user_str(str)
        else:
            self.nick = str

        self.channels = []

    def __repr__(self):
        return '{0.nick}@{0.host}'.format(self) if self.nick else self.host

    @staticmethod
    def split_user_str(str):
        nick, parts = str.split('!', 1)
        name, host = parts.split('@', 1)
        return nick.lstrip(':'), name, host

    @classmethod
    def get(cls, user_str, pool):
        if isinstance(pool, Match):
            pool = pool.connection

        if isinstance(pool, Connection):
            pool = pool.users

        if '@' in user_str:
            nick, _, _ = cls.split_user_str(user_str)
        else:
            nick = user_str

        for user in pool:
            if user.nick == nick:
                return user


class Channel(object):

    def __init__(self, name):
        self.name = name.lstrip(':')
        self.users = []

    def __repr__(self):
        return self.name

    @classmethod
    def get(cls, name, pool):
        if isinstance(pool, Match):
            pool = pool.connection

        if isinstance(pool, Connection):
            pool = pool.channels

        name = name.lstrip(':')

        for chan in pool:
            if chan.name == name:
                return chan


class Match(object):
    """
    This class acts as a layer between the code in commands and the other
    objects and should have multiple uses and shortcut functions. An object is
    created for every matching command and is passed on as an argument to the
    command to use. It provides easy access to any captured groups of the
    matching regex.

    Later on, it should provide some utility functions for messaging and other
    things a command may like to do.
    """

    def __init__(self, match, line, connection, settings=None):
        self.match = match
        self.line = line
        self.connection = connection
        self.settings = settings

    def __getitem__(self, item):
        try:
            return self.match.group(item)
        except IndexError:
            return ''

    def msg(self, message, *args, **kwargs):
        """Shortcut to send a message through the connection.

        This function sends the input message through the connection. A target
        can be defined, else it will send it to the channel or user from the
        input Line, effectively responding on whatever triggered the command
        which calls this function to be called. If raw has not been set to
        True, formatting will be applied using the standard Python Formatting
        Mini-Language, using the additional given args and kwargs, along with
        some additional kwargs, such as the match object to easily access Regex
        matches, color codes and other things.

        http://docs.python.org/3.3/library/string.html#format-string-syntax
        """

        target = kwargs.pop('target', None)
        raw = kwargs.pop('raw', False)

        if not target:
            target = self.line.sender.nick if self.line.pm else \
                self.line.target

        if not raw:
            kw = {
                'm': self,
                'b': chr(2),
                'c': chr(3),
                'u': chr(31),
            }
            kw.update(kwargs)

            try:
                message = message.format(*args, **kw)
            except IndexError:
                if len(args) == 1 and isinstance(args[0], list):
                    # Message might be: msg, [arg1, arg2], kwargs
                    message = message.format(*args[0], **kw)
                else:
                    raise

        self.connection.msg(target, message)

CODE_PATTERN = re.compile(r'\d{3}')
COMMAND_PATTERN = re.compile(r'[A-Z]{4,5}')


class Line(object):

    def __init__(self, data, connection):
        self.raw = data.lstrip(':')
        self.datetime = datetime.datetime.now()
        self.connection = connection
        self.parse()

    def __getitem__(self, item):
        try:
            if self.usermsg:
                return self.full_msg.split(' ')[item]
            else:
                return self.parts[item]
        except IndexError:
            return ''

    def __repr__(self):
        return '{0.sender}: {0.full_msg}'.format(self) if self.usermsg else \
            self.raw

    def parse(self):
        self.parts = self.raw.split()
        self.privmsg = self.parts[1] == 'PRIVMSG'
        self.notice = self.parts[1] == 'NOTICE'
        self.usermsg = self.privmsg or self.notice

        if self.usermsg:
            # ":Nick!Name@Host PRIVMSG #Chan :Hello world!"
            self.target = self.parts[2]
            self.pm = self.target[0] != '#'
            self.full_msg = ' '.join(self.parts[3:])[1:]
        elif CODE_PATTERN.match(self.parts[1]):
            self.code = int(self.parts[1])
        elif COMMAND_PATTERN.match(self.parts[1]):
            self.command = self.parts[1]

        if self.usermsg or getattr(self, 'command', None):
            self.sender = User.get(self.parts[0], self.connection)

            if not self.sender:
                self.sender = User(self.parts[0])

            self.channel = None
            if not getattr(self, 'pm', False):
                self.channel = Channel.get(self.parts[2], self.connection)

                if not self.channel:
                    self.channel = Channel(self.parts[2])


class Timer(object):

    def __init__(self, scheduled, msg_or_command=None, *args, **kwargs):
        self.scheduled = scheduled
        self.direct = kwargs.pop('direct', False)
        self.remaining = kwargs.pop('count', 0)
        target = kwargs.pop('target', None)

        self.msg_tuple = None
        self.function = None

        if callable(msg_or_command):
            self.function = msg_or_command
        else:
            if target is not None and msg_or_command is not None:
                self.msg_tuple = (target, msg_or_command, args, kwargs,)

    def __eq__(self, other):
        return (self.scheduled == other.scheduled and
                self.function is other.function and
                self.msg_tuple == other.msg_tuple)

    def match(self, timers, connect_time, connection, settings):
        if self.matches(connect_time):
            self.last_time = datetime.datetime.now()
            match = Match(None, None, connection, settings)

            if self.function is not None:
                result = self.function(match)

                if result is not None:
                    self.send_messages(result, match, timers)

            if self.msg_tuple is not None:
                self.send_messages(self.msg_tuple, match, timers)

            if self.remaining > 0:
                self.remaining -= 1

                if self.remaining == 0:
                    timers.remove(self)

    def matches(self, connect_time):
        if self.direct:
            self.direct = False
            return True

        if isinstance(self.scheduled, datetime.datetime):
            next_time = self.scheduled

        if isinstance(self.scheduled, datetime.timedelta):
            if hasattr(self, 'last_time'):
                next_time = self.last_time + self.scheduled
            else:
                next_time = connect_time + self.scheduled

        return datetime.datetime.now() >= next_time

    def send_messages(self, result, match, timers):
        for r in utils.process_messages(result, with_target=True):
            if isinstance(r, Timer):
                timers.append(r)
            else:
                match.msg(r[1], *r[2], target=r[0], **r[3])

import datetime
import importlib
import io
import re
import select
import socket
import ssl
import time

from pyromancer import utils


class Pyromancer(object):

    def __init__(self, settings_path):
        self.settings = Settings(settings_path)
        self.setup_database()
        self.find_commands()
        self.find_timers()

    def run(self):
        self.connect()
        self.listen()

    def connect(self):
        self.connection = Connection(
            self.settings.host, self.settings.port, self.settings.encoding,
            self.settings.ssl)

        self.online = True
        self.connect_time = datetime.datetime.now()
        self.connection.write('NICK {}\n'.format(self.settings.nick))
        self.connection.write('USER {0} {1} {1} :{2}\n'.format(
            self.settings.nick, self.settings.host, self.settings.real_name))
        self.connection.me.nick = self.settings.nick

    def listen(self):
        self.online = True
        ticks = self.settings.ticks

        while self.online:
            self.connection.read()

            for line in self.connection.buffer.lines():
                self.process(line)

            for timer in self.timers:
                timer.match(self.timers, self.connect_time, self.connection,
                            self.settings)

            time.sleep(1.0 / ticks)

    def process(self, line):
        line = Line(line, self.connection)

        if line[0] == 'PING':
            self.connection.write('PONG {}\n'.format(line[1]))

        for c in self.commands:
            c.command.match(line, self.timers, self.connection, self.settings)

    def find_commands(self):
        self.commands = []

        utils.find_functions(
            self.settings.packages, self.commands, 'commands',
            'disabled_commands', when=lambda f: hasattr(f, 'command'))

    def find_timers(self):
        self.timers = []

        utils.find_functions(
            self.settings.packages, self.timers, 'timers', 'disabled_timers',
            when=lambda f: hasattr(f, 'timer'), ret=lambda f: f.timer)

    def setup_database(self):
        if self.settings.database:
            from sqlalchemy import create_engine

            from pyromancer.database import Session, Base

            engine = create_engine(self.settings.database)
            Session.configure(bind=engine)

            for package in self.settings.packages:
                if isinstance(package, tuple):
                    package = package[0]

                module_name = '{}.models'.format(package)

                try:
                    importlib.import_module(module_name)
                except ImportError:
                    continue

            Base.metadata.create_all(bind=engine)


class Settings(object):

    def __init__(self, path):
        main_settings = importlib.import_module(path)
        self.packages = getattr(main_settings, 'packages', [])
        self.package_settings = {}
        self.package_name, _ = path.split('.', 1)

        if self.package_name not in self.packages:
            self.packages.insert(0, self.package_name)

        for package in self.packages:
            if isinstance(package, tuple):
                package = package[0]

            if package == self.package_name:
                module = main_settings
            else:
                module = importlib.import_module('{}.settings'.format(package))

            self.package_settings[package] = module

        self.global_settings = None
        if 'pyromancer' not in self.packages:
            self.global_settings = importlib.import_module(
                'pyromancer.settings')

    def __getattr__(self, item):
        for package in self.packages:
            if isinstance(package, tuple):
                package = package[0]

            if hasattr(self.package_settings[package], item):
                return getattr(self.package_settings[package], item)

        if hasattr(self.global_settings, item):
            return getattr(self.global_settings, item)

        raise AttributeError('No such setting "{}" found in any of the '
                             'installed packages'.format(item))


class LineBuffer(object):
    """Line buffer based on irc library's DecodingLineBuffer.

    See https://bitbucket.org/jaraco/irc
    """
    line_sep_exp = re.compile(b'\r?\n')

    def __init__(self, encoding='utf-8'):
        self.buffer = b''
        self.encoding = encoding

    def feed(self, bytes):
        self.buffer += bytes

    def lines(self):
        lines = self.line_sep_exp.split(self.buffer)
        # save the last, unfinished, possibly empty line
        self.buffer = lines.pop()

        for line in lines:
            yield line.decode(self.encoding, 'strict')

    def __iter__(self):
        return self.lines()

    def __len__(self):
        return len(self.buffer)


class Connection(object):

    def __init__(self, host, port, encoding='utf8', use_ssl=False):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((host, port))

        # We need non-blocking so it won't hang when there is no data.
        self.socket.setblocking(False)

        if use_ssl:
            self.socket = ssl.wrap_socket(
                self.socket, do_handshake_on_connect=False)

            # We need to do the handshake manually, because we use a
            # non-blocking socket.
            # https://docs.python.org/2/library/ssl.html#ssl-nonblocking
            while True:
                try:
                    self.socket.do_handshake()
                    break
                except ssl.SSLWantReadError:
                    select.select([self.socket], [], [])
                except ssl.SSLWantWriteError:
                    select.select([], [self.socket], [])

        self.encoding = encoding
        self.buffer = LineBuffer(self.encoding)

        self.me = User('')

    @property
    def users(self):
        users = [self.me]
        for chan in self.me.channels:
            users.extend(chan.users)

        return list(set(users))

    @property
    def channels(self):
        return self.me.channels

    def write(self, data):
        self.socket.send('{}\n'.format(data).encode(self.encoding))

    def read(self, bytes=4096):
        try:
            self.buffer.feed(self.socket.recv(bytes))
        except (io.BlockingIOError, ssl.SSLWantReadError):
            pass

    def msg(self, target, msg):
        self.write('PRIVMSG {} :{}'.format(target,  msg))


class User(object):
    name = None
    host = None
    auth = None

    def __init__(self, str):
        if '@' in str:
            self.nick, self.name, self.host = self.split_user_str(str)
        else:
            self.nick = str

        self.channels = []

    def __repr__(self):
        return '{0.nick}@{0.host}'.format(self) if self.nick else self.host

    @staticmethod
    def split_user_str(str):
        nick, parts = str.split('!', 1)
        name, host = parts.split('@', 1)
        return nick.lstrip(':'), name, host

    @classmethod
    def get(cls, user_str, pool):
        if isinstance(pool, Match):
            pool = pool.connection

        if isinstance(pool, Connection):
            pool = pool.users

        if '@' in user_str:
            nick, _, _ = cls.split_user_str(user_str)
        else:
            nick = user_str

        for user in pool:
            if user.nick == nick:
                return user


class Channel(object):

    def __init__(self, name):
        self.name = name.lstrip(':')
        self.users = []

    def __repr__(self):
        return self.name

    @classmethod
    def get(cls, name, pool):
        if isinstance(pool, Match):
            pool = pool.connection

        if isinstance(pool, Connection):
            pool = pool.channels

        name = name.lstrip(':')

        for chan in pool:
            if chan.name == name:
                return chan


class Match(object):
    """
    This class acts as a layer between the code in commands and the other
    objects and should have multiple uses and shortcut functions. An object is
    created for every matching command and is passed on as an argument to the
    command to use. It provides easy access to any captured groups of the
    matching regex.

    Later on, it should provide some utility functions for messaging and other
    things a command may like to do.
    """

    def __init__(self, match, line, connection, settings=None):
        self.match = match
        self.line = line
        self.connection = connection
        self.settings = settings

    def __getitem__(self, item):
        try:
            return self.match.group(item)
        except IndexError:
            return ''

    def msg(self, message, *args, **kwargs):
        """Shortcut to send a message through the connection.

        This function sends the input message through the connection. A target
        can be defined, else it will send it to the channel or user from the
        input Line, effectively responding on whatever triggered the command
        which calls this function to be called. If raw has not been set to
        True, formatting will be applied using the standard Python Formatting
        Mini-Language, using the additional given args and kwargs, along with
        some additional kwargs, such as the match object to easily access Regex
        matches, color codes and other things.

        http://docs.python.org/3.3/library/string.html#format-string-syntax
        """

        target = kwargs.pop('target', None)
        raw = kwargs.pop('raw', False)

        if not target:
            target = self.line.sender.nick if self.line.pm else \
                self.line.target

        if not raw:
            kw = {
                'm': self,
                'b': chr(2),
                'c': chr(3),
                'u': chr(31),
            }
            kw.update(kwargs)

            try:
                message = message.format(*args, **kw)
            except IndexError:
                if len(args) == 1 and isinstance(args[0], list):
                    # Message might be: msg, [arg1, arg2], kwargs
                    message = message.format(*args[0], **kw)
                else:
                    raise

        self.connection.msg(target, message)

CODE_PATTERN = re.compile(r'\d{3}')
COMMAND_PATTERN = re.compile(r'[A-Z]{4,5}')


class Line(object):

    def __init__(self, data, connection):
        self.raw = data.lstrip(':')
        self.datetime = datetime.datetime.now()
        self.connection = connection
        self.parse()

    def __getitem__(self, item):
        try:
            if self.usermsg:
                return self.full_msg.split(' ')[item]
            else:
                return self.parts[item]
        except IndexError:
            return ''

    def __repr__(self):
        return '{0.sender}: {0.full_msg}'.format(self) if self.usermsg else \
            self.raw

    def parse(self):
        self.parts = self.raw.split()
        self.privmsg = self.parts[1] == 'PRIVMSG'
        self.notice = self.parts[1] == 'NOTICE'
        self.usermsg = self.privmsg or self.notice

        if self.usermsg:
            # ":Nick!Name@Host PRIVMSG #Chan :Hello world!"
            self.target = self.parts[2]
            self.pm = self.target[0] != '#'
            self.full_msg = ' '.join(self.parts[3:])[1:]
        elif CODE_PATTERN.match(self.parts[1]):
            self.code = int(self.parts[1])
        elif COMMAND_PATTERN.match(self.parts[1]):
            self.command = self.parts[1]

        if self.usermsg or getattr(self, 'command', None):
            self.sender = User.get(self.parts[0], self.connection)

            if not self.sender:
                self.sender = User(self.parts[0])

            self.channel = None
            if not getattr(self, 'pm', False):
                self.channel = Channel.get(self.parts[2], self.connection)

                if not self.channel:
                    self.channel = Channel(self.parts[2])


class Timer(object):

    def __init__(self, scheduled, msg_or_command=None, *args, **kwargs):
        self.scheduled = scheduled
        self.direct = kwargs.pop('direct', False)
        self.remaining = kwargs.pop('count', 0)
        target = kwargs.pop('target', None)

        self.msg_tuple = None
        self.function = None

        if callable(msg_or_command):
            self.function = msg_or_command
        else:
            if target is not None and msg_or_command is not None:
                self.msg_tuple = (target, msg_or_command, args, kwargs,)

    def __eq__(self, other):
        return (self.scheduled == other.scheduled and
                self.function is other.function and
                self.msg_tuple == other.msg_tuple)

    def match(self, timers, connect_time, connection, settings):
        if self.matches(connect_time):
            self.last_time = datetime.datetime.now()
            match = Match(None, None, connection, settings)

            if self.function is not None:
                result = self.function(match)

                if result is not None:
                    self.send_messages(result, match, timers)

            if self.msg_tuple is not None:
                self.send_messages(self.msg_tuple, match, timers)

            if self.remaining > 0:
                self.remaining -= 1

                if self.remaining == 0:
                    timers.remove(self)

    def matches(self, connect_time):
        if self.direct:
            self.direct = False
            return True

        if isinstance(self.scheduled, datetime.datetime):
            next_time = self.scheduled

        if isinstance(self.scheduled, datetime.timedelta):
            if hasattr(self, 'last_time'):
                next_time = self.last_time + self.scheduled
            else:
                next_time = connect_time + self.scheduled

        return datetime.datetime.now() >= next_time

    def send_messages(self, result, match, timers):
        for r in utils.process_messages(result, with_target=True):
            if isinstance(r, Timer):
                timers.append(r)
            else:
                match.msg(r[1], *r[2], target=r[0], **r[3])

Line buffer based on irc library's DecodingLineBuffer.      See https://bitbucket.org/jaraco/irc # save the last, unfinished, possibly empty line # We need non-blocking so it won't hang when there is no data. # We need to do the handshake manually, because we use a # non-blocking socket. # https://docs.python.org/2/library/ssl.html#ssl-nonblocking This class acts as a layer between the code in commands and the other     objects and should have multiple uses and shortcut functions. An object is     created for every matching command and is passed on as an argument to the     command to use. It provides easy access to any captured groups of the     matching regex.      Later on, it should provide some utility functions for messaging and other     things a command may like to do. Shortcut to send a message through the connection.          This function sends the input message through the connection. A target         can be defined, else it will send it to the channel or user from the         input Line, effectively responding on whatever triggered the command         which calls this function to be called. If raw has not been set to         True, formatting will be applied using the standard Python Formatting         Mini-Language, using the additional given args and kwargs, along with         some additional kwargs, such as the match object to easily access Regex         matches, color codes and other things.          http://docs.python.org/3.3/library/string.html#format-string-syntax # Message might be: msg, [arg1, arg2], kwargs # ":Nick!Name@Host PRIVMSG #Chan :Hello world!"