import os, select, sys, threading, time, traceback

import EventManager, IRCLineHandler, IRCLogging, IRCServer
import ModuleManager, Timer

class Bot(object):
    def __init__(self):
        self.lock = threading.Lock()
        self.args = None
        self.database = None
        self.config = None
        self.bot_directory = os.path.dirname(os.path.realpath(__file__))
        self.servers = {}
        self.running = True
        self.poll = select.epoll()
        self.modules = ModuleManager.ModuleManager(self)
        self.events = EventManager.EventHook(self)
        self.log = IRCLogging.Log(self)
        self.line_handler = IRCLineHandler.LineHandler(self)
        self.timers = []
        self.events.on("timer").on("reconnect").hook(self.reconnect)
        self.events.on("boot").on("done").hook(self.setup_timers)

    def add_server(self, id, hostname, port, password, ipv4, tls,
            nickname, username, realname, connect=False):
        new_server = IRCServer.Server(id, hostname, port, password,
             ipv4, tls, nickname, username, realname, self)
        if not new_server.get_setting("connect", True):
            return
        self.events.on("new").on("server").call(server=new_server)
        if connect and new_server.get_setting("connect", True):
            self.connect(new_server)
        return new_server
    def connect(self, server):
        try:
            server.connect()
        except:
            sys.stderr.write("Failed to connect to %s\n" % str(server))
            traceback.print_exc()
            return False
        self.servers[server.fileno()] = server
        self.poll.register(server.fileno(), select.EPOLLOUT)
        return True
    def setup_timers(self, event):
        for setting, value in self.find_settings("timer-%"):
            id = setting.split("timer-", 1)[1]
            self.add_timer(value["event-name"], value["delay"], value[
                "next-due"], id, **value["kwargs"])
    def timer_setting(self, timer):
        self.set_setting("timer-%s" % timer.id, {
            "event-name": timer.event_name, "delay": timer.delay,
            "next-due": timer.next_due, "kwargs": timer.kwargs})
    def timer_setting_remove(self, timer):
        self.timers.remove(timer)
        self.del_setting("timer-%s" % timer.id)
    def add_timer(self, event_name, delay, next_due=None, id=None, persist=True,
            **kwargs):
        timer = Timer.Timer(self, event_name, delay, next_due, **kwargs)
        if id:
            timer.id = id
        elif persist:
            self.timer_setting(timer)
        self.timers.append(timer)
    def next_timer(self):
        next = None
        for timer in self.timers:
            time_left = timer.time_left()
            if next == None or time_left < next:
                next = time_left

        if next == None:
            return None
        if next < 0:
            return 0
        return next
    def call_timers(self):
        for timer in self.timers[:]:
            if timer.due():
                timer.call()
                if timer.done():
                    self.timer_setting_remove(timer)
    def next_send(self):
        next = None
        for server in self.servers.values():
            timeout = server.send_throttle_timeout()
            if server.waiting_send() and (next == None or timeout < next):
                next = timeout
        return next

    def next_ping(self):
        timeouts = []
        for server in self.servers.values():
            timeouts.append(server.until_next_ping())
        return min(timeouts)
    def next_read_timeout(self):
        timeouts = []
        for server in self.servers.values():
            timeouts.append(server.until_read_timeout())
        return min(timeouts)

    def get_poll_timeout(self):
        timeouts = []
        timeouts.append(self.next_timer())
        timeouts.append(self.next_send())
        timeouts.append(self.next_ping())
        timeouts.append(self.next_read_timeout())
        return min([timeout for timeout in timeouts if not timeout == None])

    def register_read(self, server):
        self.poll.modify(server.fileno(), select.EPOLLIN)
    def register_write(self, server):
        self.poll.modify(server.fileno(), select.EPOLLOUT)
    def register_both(self, server):
        self.poll.modify(server.fileno(),
            select.EPOLLIN|select.EPOLLOUT)

    def disconnect(self, server):
        try:
            self.poll.unregister(server.fileno())
        except FileNotFoundError:
            pass
        del self.servers[server.fileno()]

    def reconnect(self, event):
        server_details = self.database.servers.get(event["server_id"])
        server = self.add_server(*(server_details + (False,)))
        if self.connect(server):
            self.servers[server.fileno()] = server
        else:
            event["timer"].redo()

    def set_setting(self, setting, value):
        self.database.bot_settings.set(setting, value)
    def get_setting(self, setting, default=None):
        return self.database.bot_settings.get(setting, default)
    def find_settings(self, pattern, default=[]):
        return self.database.bot_settings.find(pattern, default)
    def find_settings_prefix(self, prefix, default=[]):
        return self.database.bot_settings.find_prefix(
            prefix, default)
    def del_setting(self, setting):
        self.database.bot_settings.delete(setting)

    def run(self):
        while self.running:
            self.lock.acquire()
            events = self.poll.poll(self.get_poll_timeout())
            self.call_timers()
            for fd, event in events:
                if fd in self.servers:
                    server = self.servers[fd]
                    if event & select.EPOLLIN:
                        lines = server.read()
                        for line in lines:
                            if self.args.verbose:
                                self.log.info("<%s | %s", [str(server), line])
                            server.parse_line(line)
                    elif event & select.EPOLLOUT:
                        server._send()
                        self.register_read(server)
                    elif event & select.EPULLHUP:
                        print("hangup")
                        server.disconnect()

            for server in list(self.servers.values()):
                if server.read_timed_out():
                    print("pingout from %s" % str(server))
                    server.disconnect()
                elif server.ping_due() and not server.ping_sent:
                        server.send_ping()
                        server.ping_sent = True
                if not server.connected:
                    self.disconnect(server)

                    reconnect_delay = self.config.get("reconnect-delay", 10)
                    self.add_timer("reconnect", reconnect_delay, None, None, False,
                        server_id=server.id)

                    print("disconnected from %s, reconnecting in %d seconds" % (
                        str(server), reconnect_delay))
                elif server.waiting_send() and server.throttle_done():
                    self.register_both(server)
            self.lock.release()