# -*- coding: utf-8 -*-
#
# Copyright © 2011 Pierre Raybaut
# Licensed under the terms of the MIT License
# (see spyderlib/__init__.py for details)

"""External shell's introspection and notification servers"""

# Standard library imports
import errno
import os
import socket
import threading

# Third party imports
from qtpy.QtCore import QThread, Signal

# Local imports
from spyderlib.config.base import get_conf_path, DEBUG
from spyderlib.utils.debug import log_last_error
from spyderlib.utils.bsdsocket import read_packet, write_packet
from spyderlib.utils.misc import select_port


LOG_FILENAME = get_conf_path('introspection.log')

DEBUG_INTROSPECTION = DEBUG >= 2

if DEBUG_INTROSPECTION:
    import logging
    logging.basicConfig(filename=get_conf_path('introspection_debug.log'),
                        level=logging.DEBUG)

SPYDER_PORT = 20128


class IntrospectionServer(threading.Thread):
    """Introspection server"""
    def __init__(self):
        threading.Thread.__init__(self)
        self.shells = {}
        self.setDaemon(True)
        global SPYDER_PORT
        self.port = SPYDER_PORT = select_port(default_port=SPYDER_PORT)
        SPYDER_PORT += 1
        
    def register(self, shell):
        """Register introspection server
        See notification server below"""
        shell_id = str(id(shell))
        self.shells[shell_id] = shell
    
    def send_socket(self, shell_id, sock):
        """Send socket to the appropriate object for later communication"""
        shell = self.shells[shell_id]
        shell.set_introspection_socket(sock)
        if DEBUG_INTROSPECTION:
            logging.debug('Introspection server: shell [%r] port [%r]'
                          % (shell, self.port))
        
    def run(self):
        """Start server"""
        sock = socket.socket(socket.AF_INET)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind( ("127.0.0.1", self.port) )
        
        while True:
            sock.listen(2)
            try:
                conn, _addr = sock.accept()
            except socket.error as e:
                # See Issue 1275 for details on why errno EINTR is
                # silently ignored here.
                eintr = errno.WSAEINTR if os.name == 'nt' else errno.EINTR
                if e.args[0] == eintr:
                    continue
                raise
            shell_id = read_packet(conn)
            if shell_id is not None:
                self.send_socket(shell_id, conn)

class NotificationServer(IntrospectionServer):
    """Notification server"""
    def __init__(self):
        IntrospectionServer.__init__(self)
        self.notification_threads = {}
        
    def register(self, shell):
        """Register notification server
        See pythonshell.ExternalPythonShell.create_process"""
        IntrospectionServer.register(self, shell)
        shell_id = str(id(shell))
        n_thread = self.notification_threads[shell_id] = NotificationThread()
        return n_thread
    
    def send_socket(self, shell_id, sock):
        """Send socket to the appropriate object for later communication"""
        n_thread = self.notification_threads[shell_id]
        n_thread.set_notify_socket(sock)
        n_thread.start()
        if DEBUG_INTROSPECTION:
            logging.debug('Notification server: shell [%r] port [%r]'
                          % (self.shells[shell_id], self.port))

INTROSPECTION_SERVER = None

def start_introspection_server():
    """
    Start introspection server (only one time)
    This server is dedicated to introspection features, i.e. Spyder is calling 
    it to retrieve informations on remote objects
    """
    global INTROSPECTION_SERVER
    if INTROSPECTION_SERVER is None:
        if DEBUG_INTROSPECTION:
            import time
            time_str = "Logging time: %s" % time.ctime(time.time())
            logging.debug("="*len(time_str))
            logging.debug(time_str)
            logging.debug("="*len(time_str))
        INTROSPECTION_SERVER = IntrospectionServer()
        INTROSPECTION_SERVER.start()
    return INTROSPECTION_SERVER

NOTIFICATION_SERVER = None

def start_notification_server():
    """
    Start notify server (only one time)
    This server is dedicated to notification features, i.e. remote objects 
    are notifying Spyder about anything relevant like debugging data (pdb) 
    or "this is the right moment to refresh variable explorer" (syshook)
    """
    global NOTIFICATION_SERVER
    if NOTIFICATION_SERVER is None:
        NOTIFICATION_SERVER = NotificationServer()
        NOTIFICATION_SERVER.start()
    return NOTIFICATION_SERVER


class NotificationThread(QThread):
    """Notification thread"""
    sig_process_remote_view = Signal(object)
    sig_pdb = Signal(str, int)
    open_file = Signal(str, int)
    new_ipython_kernel = Signal(str)
    refresh_namespace_browser = Signal()
    
    def __init__(self):
        QThread.__init__(self)
        self.notify_socket = None
        
    def set_notify_socket(self, notify_socket):
        """Set the notification socket"""
        self.notify_socket = notify_socket
        
    def run(self):
        """Start notification thread"""
        while True:
            if self.notify_socket is None:
                continue
            output = None
            try:
                try:
                    cdict = read_packet(self.notify_socket)
                except:
                    # This except statement is intended to handle a struct.error
                    # (but when writing 'except struct.error', it doesn't work)
                    # Note: struct.error is raised when the communication has 
                    # been interrupted and the received data is not a string 
                    # of length 8 as required by struct.unpack (see read_packet)
                    break
                if cdict is None:
                    # Another notification thread has just terminated and 
                    # then wrote 'None' in the notification socket
                    # (see the 'finally' statement below)
                    continue
                if not isinstance(cdict, dict):
                    raise TypeError("Invalid data type: %r" % cdict)
                command = cdict['command']
                data = cdict.get('data')
                if command == 'pdb_step':
                    fname, lineno = data
                    self.sig_pdb.emit(fname, lineno)
                    self.refresh_namespace_browser.emit()
                elif command == 'refresh':
                    self.refresh_namespace_browser.emit()
                elif command == 'remote_view':
                    self.sig_process_remote_view.emit(data)
                elif command == 'ipykernel':
                    self.new_ipython_kernel.emit(data)
                elif command == 'open_file':
                    fname, lineno = data
                    self.open_file.emit(fname, lineno)
                else:
                    raise RuntimeError('Unsupported command: %r' % command)
                if DEBUG_INTROSPECTION:
                    logging.debug("received command: %r" % command)
            except:
                log_last_error(LOG_FILENAME, "notification thread")
            finally:
                try:
                    write_packet(self.notify_socket, output)
                except:
                    # The only reason why it should fail is that Spyder is 
                    # closing while this thread is still alive
                    break
