class SingletonApp:

    kernel_app = None
    display_data_hook = DisplayDataHook()

    def __init__(self, browser_name=None):
        SingletonApp.singleton_ids = []
        SingletonApp.client_started = False

        self.notebook_dir = tempfile.mkdtemp(prefix='jupyter_singleton_')
        self.browser_name = browser_name

    def open_singleton(self, debug_redirect=False):
        """
        Opens a new browser-window running a single jupyter output-cell.
        After calling this function output printed via 'print' or displayed via 'IPython.display.display' will be shown
        in the output cell.

        NOTE: this function blocks until the browser window is started and ready to handle output

        :return:
        """

        # first register displayhook, so that the correct message parent is set in messages send to the browser
        thread_local_data = threading.local()
        if not hasattr(
                thread_local_data,
                'has_displayhook') or not thread_local_data.has_displayhook:
            InteractiveShell.instance().display_pub.register_hook(
                self.display_data_hook)
            thread_local_data.has_displayhook = True

        server_info_path = os.path.join(self.notebook_dir, 'server_info.json')
        with open(server_info_path, "r") as f:
            server_info = json.load(f)
        singleton_id = str(uuid.uuid4())

        url_parts = [
            server_info['url'], 'singletonnotebooks/', 'Untitled.ipynb',
            '?token=', server_info['token'], '&singletonid=', singleton_id
        ]
        url = ''.join(url_parts)

        if debug_redirect:
            url_parts = [
                'file://',
                os.path.abspath(
                    os.path.join(os.path.abspath(jupyter_singleton.__file__),
                                 '..', 'debugredirect.html')), '?', 'url=',
                urllib.parse.quote(url)
            ]
            url = ''.join(url_parts)

        browser = webbrowser.get(self.browser_name)
        browser.open(url, new=1)

        # poll until output-cell active
        started = False
        for _ in range(6000):
            if singleton_id in SingletonApp.singleton_ids:
                started = True
                break
            time.sleep(0.1)
        if not started:
            raise IOError('display did not seem to start ... timed out')

    def _poll_and_read_kernel_info(self):
        path = os.path.join(self.notebook_dir, 'kernel_info.json')
        for _ in range(6000):
            if os.path.isfile(path):
                try:
                    with open(path, 'r') as f:
                        result = json.load(f)
                        return result
                except RuntimeError:
                    pass
            time.sleep(0.1)
        return None

    def _launch_server(self, server_parameters=None):
        if server_parameters is None:
            server_parameters = dict()
        parameter_path = os.path.join(self.notebook_dir, 'parameters.json')
        with open(parameter_path, 'w') as parameter_fd:
            json.dump(server_parameters, parameter_fd)

        # start server
        cmd = [
            sys.executable,
            '-m',
            'jupyter_singleton.serverlauncher',
            '--notebook-dir',
            self.notebook_dir,
        ]
        env = os.environ.copy()
        env.pop('PYTHONEXECUTABLE', None)
        launch_kernel(
            cmd, env=env
        )  # this function originally starts a kernel but can also be used to start the server

        # poll for the connection file to be written by server
        kernel_info = self._poll_and_read_kernel_info()

        return kernel_info

    def _launch_client(self, kernel_id):
        # we have to create the event-loop explicitly here since IOLoop.current() is prohibited outside of the main
        # thread
        AsyncIOLoop(make_current=True)

        # prepare parameters
        kernel_file = 'kernel-' + kernel_id + '.json'
        code_to_run = 'from jupyter_singleton.singletonapp import SingletonApp\n' + \
                      'SingletonApp.client_started=True'
        kernel_class = 'jupyter_singleton.singletonipkernel.SingletonIPythonKernel'

        parameters = {
            'connection_file': kernel_file,
            'code_to_run': code_to_run,
            'quiet': False,
            'kernel_class': kernel_class
        }

        # start jupyter client
        self.kernel_app = IPKernelApp(**parameters)
        self.kernel_app.initialize([])
        if hasattr(self.kernel_app.kernel, 'set_displaydatahook'):
            self.kernel_app.kernel.set_displaydatahook(self.display_data_hook)
        self.kernel_app.start()

    def _launch(self, server_parameters):
        kernel_info = self._launch_server(server_parameters)
        if kernel_info:
            threading.Thread(target=self._launch_client,
                             args=(kernel_info['kernel_id'], )).start()
        else:
            raise IOError(
                'kernel connection file was not written before timeout')

    def launch(self, server_parameters):
        """
        Launches the jupyter server and afterwards the jupyter client in a new thread
        The server will be launched in a new subprocess while the client will be launched in a new thread in the current
        process.
        NOTE: this function returns immediately. It does NOT block until jupyter server and jupyter client are ready to
              display something
        :param server_parameters: parameters to pass to the jupyter server
        :return:
        """
        threading.Thread(target=self._launch,
                         args=(server_parameters, )).start()