# -*- coding: utf-8 -*-
"""
This module contains the server socket definition.
"""
import argparse
import inspect
import logging
import json
import struct
import sys
import traceback


try:
    import socketserver
    PY33 = True
except ImportError:
    import SocketServer as socketserver
    PY33 = False


def _logger():
    """ Returns the module's logger """
    return logging.getLogger(__name__)


def import_class(klass):
    """
    Imports a class from a fully qualified name string.

    :param klass: class string, e.g.
        "pyqode.core.backend.workers.CodeCompletionWorker"
    :return: The corresponding class

    """
    path = klass.rfind(".")
    class_name = klass[path + 1: len(klass)]
    try:
        module = __import__(klass[0:path], globals(), locals(), [class_name])
        klass = getattr(module, class_name)
    except ImportError as e:
        raise ImportError('%s: %s' % (klass, str(e)))
    except AttributeError:
        raise ImportError(klass)
    else:
        return klass


class JsonServer(socketserver.TCPServer):
    """
    A server socket based on a json messaging system.
    """
    class _Handler(socketserver.BaseRequestHandler):
        """
        Our custom request handler. There will always be only 1 request that
        establish the communication, this is a 1 to 1.

        Once the connection has been establish will loop forever waiting for
        pending command or for the shutdown signal.

        The handler also implements all the logic for packing/unpacking
        messages and calling the requested worker instance.
        """

        def read_bytes(self, size):
            """
            Read x bytes

            :param size: number of bytes to read.

            """
            if not PY33:
                data = ''
            else:
                data = bytes()
            while len(data) < size:
                tmp = self.request.recv(size - len(data))
                data += tmp
                if tmp == '':
                    raise RuntimeError("socket connection broken")
            return data

        def get_msg_len(self):
            """ Gets message len """
            data = self.read_bytes(4)
            payload = struct.unpack('=I', data)
            return payload[0]

        def read(self):
            """ Reads a json string from socket and load it. """
            size = self.get_msg_len()
            data = self.read_bytes(size).decode('utf-8')
            return json.loads(data)

        def send(self, obj):
            """
            Sends a python obj as a json string on the socket.

            :param obj: The object to send, must be Json serializable.
            """
            msg = json.dumps(obj).encode('utf-8')
            _logger().debug('sending %d bytes for the payload', len(msg))
            header = struct.pack('=I', len(msg))
            self.request.sendall(header)
            self.request.sendall(msg)

        def handle(self):
            """
            Hanlde the request and keep it alive while shutdown signal
            has not been received
            """
            data = self.read()
            self._handle(data)

        def _handle(self, data):
            """
            Handles a work request.
            """
            try:
                _logger().debug('handling request %r', data)
                assert data['worker']
                assert data['request_id']
                assert data['data'] is not None
                response = {'request_id': data['request_id'], 'results': []}
                try:
                    worker = import_class(data['worker'])
                except ImportError:
                    _logger().exception('Failed to import worker class')
                else:
                    if inspect.isclass(worker):
                        worker = worker()
                    _logger().debug('worker: %r', worker)
                    _logger().debug('data: %r', data['data'])
                    try:
                        ret_val = worker(data['data'])
                    except Exception:
                        _logger().exception(
                            'something went bad with worker %r(data=%r)',
                            worker, data['data'])
                        ret_val = None
                    if ret_val is None:
                        ret_val = []
                    response = {'request_id': data['request_id'],
                                'results': ret_val}
                finally:
                    _logger().debug('sending response: %r', response)
                    try:
                        self.send(response)
                    except ConnectionAbortedError:
                        pass
            except:
                _logger().debug('error with data=%r', data)
                exc1, exc2, exc3 = sys.exc_info()
                traceback.print_exception(exc1, exc2, exc3, file=sys.stderr)

    def __init__(self, args=None):
        """
        :param args: Argument parser args. If None, the server will setup and
            use its own argument parser (using
            :meth:`pyqode.core.backend.default_parser`)
        """
        if not args:
            args = default_parser().parse_args()
        self.port = args.port
        self._Handler.srv = self
        self._running = True
        # print('server running on port %s' % args.port)
        socketserver.TCPServer.__init__(
            self, ('127.0.0.1', int(args.port)), self._Handler)
        _logger().debug('started on 127.0.0.1:%d' % int(args.port))
        _logger().debug('running with python %d.%d.%d' %
                        (sys.version_info[:3]))


def default_parser():
    """
    Configures and return the default argument parser. You should use this
    parser as a base if you want to add custom arguments.

    The default parser only has one argument, the tcp port used to start the
    server socket. *(CodeEdit picks up a free port and use it to run
    the server and connect its client socket)*

    :returns: The default server argument parser.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("port", help="the local tcp port to use to run "
                        "the server")
    return parser


def serve_forever(args=None):
    """
    Creates the server and serves forever

    :param args: Optional args if you decided to use your own
        argument parser. Default is None to let the JsonServer setup its own
        parser and parse command line arguments.
    """
    server = JsonServer(args=args)
    server.serve_forever()


# Server script example
if __name__ == '__main__':
    from pyqode.core import backend
    backend.CodeCompletionWorker.providers.append(
        backend.DocumentWordsProvider())
    serve_forever()
