import socket
import pickle


class IServer:
    def __init__(
        self,
        addr=('0.0.0.0', 0),
        buffer_size=1024*1024,
        listen_n=1,
        debug=False
    ):
        self.buffer_size = buffer_size
        self.debug = debug

        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.bind(addr)
        self.addr = self.server.getsockname()
        print('Server will be started at', self.addr)
        self.server.listen(listen_n)
        self.func_name = dict()
        self.add_func(eval, 'remote_eval')
        self.add_func(exec, 'remote_exec')

    def _eval_from_data(self, b):
        func, args, kwargs = pickle.loads(b)
        assert isinstance(func, str)
        assert isinstance(args, tuple)
        assert isinstance(kwargs, dict)
        assert func in self.func_name
        if self.debug:
            print('_eval', func, args, kwargs)
        try:
            res = self.func_name[func](*args, **kwargs)
        except Exception as e:
            res = e
        return pickle.dumps(res)

    def add_func(self, func, name=None, ignore_err=False):
        if name is None:
            if hasattr(func, '__name__'):
                name = func.__name__
            elif ignore_err:
                return
            else:
                raise ValueError('name must be specified')
        assert name not in self.func_name
        print('_add_func', name, func)
        self.func_name[name] = func

    def add_funcs(self, *args):
        for fs in args:
            if isinstance(fs, (list, tuple)):
                for f in fs:
                    self.add_func(f)
            else:
                self.add_func(fs)

    def join_client(self):
        s, addr = self.server.accept()
        print('Connected by', addr)
        while True:
            data = s.recv(self.buffer_size)
            if not data:
                break
            res = self._eval_from_data(data)
            s.sendall(res)
        s.close()
        print('Disconnected by', addr)

    def join(self):
        while True:
            self.join_client()
        self.close()

    def run(self):
        self.join()

    def close(self):
        self.server.close()

    def gen_import_code(self, p='to_import.py', addr=None):
        server_ip, server_port = self.addr
        if addr is None:
            ip = server_ip
            port = server_port
        else:
            ip, port = addr
            if port == 0:
                port = server_port
        if ip == '0.0.0.0':
            ip = '127.0.0.1'

        s = '''# Generated by `inincompatibility`.
# But **not** depend on `incompatibility`. :)
# Please use this file instead of the original file to `import`.
import socket
import pickle

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(''' + str((ip, port)) + ''')

BUFFER_SIZE = ''' + str(self.buffer_size) + '''


def _func_eval(func, args, kwargs):
    data = pickle.dumps((func, args, kwargs))
    client.sendall(data)
    return pickle.loads(client.recv(BUFFER_SIZE))
'''
        for name in self.func_name:
            s += '''

def ''' + name + '''(*args, **kwargs):
    return _func_eval("''' + name + '''", args, kwargs)
'''

        import os
        if os.path.exists(p):
            # raise FileExistsError('File already exists')
            pass
        else:
            os.makedirs(os.path.dirname(os.path.abspath(p)), exist_ok=True)
        with open(p, 'wb') as f:
            f.write(s.encode('utf-8'))
        print('Generated at "%s"' % p)
        return p
