"""A client for in-process kernels."""

# -----------------------------------------------------------------------------
#  Copyright (C) 2012  The IPython Development Team
#
#  Distributed under the terms of the BSD License.  The full license is in
#  the file COPYING, distributed as part of this software.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------

import asyncio

from jupyter_client.client import KernelClient
from jupyter_client.clientabc import KernelClientABC

try:
    from jupyter_client.utils import run_sync  # requires 7.0+
except ImportError:
    run_sync = None  # type:ignore

# IPython imports
from traitlets import Instance, Type, default

# Local imports
from .channels import InProcessChannel, InProcessHBChannel

# -----------------------------------------------------------------------------
# Main kernel Client class
# -----------------------------------------------------------------------------


class InProcessKernelClient(KernelClient):
    """A client for an in-process kernel.

    This class implements the interface of
    `jupyter_client.clientabc.KernelClientABC` and allows
    (asynchronous) frontends to be used seamlessly with an in-process kernel.

    See `jupyter_client.client.KernelClient` for docstrings.
    """

    # The classes to use for the various channels.
    shell_channel_class = Type(InProcessChannel)
    iopub_channel_class = Type(InProcessChannel)
    stdin_channel_class = Type(InProcessChannel)
    control_channel_class = Type(InProcessChannel)
    hb_channel_class = Type(InProcessHBChannel)

    kernel = Instance("ipykernel.inprocess.ipkernel.InProcessKernel", allow_none=True)

    # --------------------------------------------------------------------------
    # Channel management methods
    # --------------------------------------------------------------------------

    @default("blocking_class")
    def _default_blocking_class(self):
        from .blocking import BlockingInProcessKernelClient

        return BlockingInProcessKernelClient

    def get_connection_info(self):
        d = super().get_connection_info()
        d["kernel"] = self.kernel
        return d

    def start_channels(self, *args, **kwargs):
        super().start_channels()
        self.kernel.frontends.append(self)

    @property
    def shell_channel(self):
        if self._shell_channel is None:
            self._shell_channel = self.shell_channel_class(self)
        return self._shell_channel

    @property
    def iopub_channel(self):
        if self._iopub_channel is None:
            self._iopub_channel = self.iopub_channel_class(self)
        return self._iopub_channel

    @property
    def stdin_channel(self):
        if self._stdin_channel is None:
            self._stdin_channel = self.stdin_channel_class(self)
        return self._stdin_channel

    @property
    def control_channel(self):
        if self._control_channel is None:
            self._control_channel = self.control_channel_class(self)
        return self._control_channel

    @property
    def hb_channel(self):
        if self._hb_channel is None:
            self._hb_channel = self.hb_channel_class(self)
        return self._hb_channel

    # Methods for sending specific messages
    # -------------------------------------

    def execute(
        self, code, silent=False, store_history=True, user_expressions=None, allow_stdin=None
    ):
        if allow_stdin is None:
            allow_stdin = self.allow_stdin
        content = dict(
            code=code,
            silent=silent,
            store_history=store_history,
            user_expressions=user_expressions or {},
            allow_stdin=allow_stdin,
        )
        msg = self.session.msg("execute_request", content)
        self._dispatch_to_kernel(msg)
        return msg["header"]["msg_id"]

    def complete(self, code, cursor_pos=None):
        if cursor_pos is None:
            cursor_pos = len(code)
        content = dict(code=code, cursor_pos=cursor_pos)
        msg = self.session.msg("complete_request", content)
        self._dispatch_to_kernel(msg)
        return msg["header"]["msg_id"]

    def inspect(self, code, cursor_pos=None, detail_level=0):
        if cursor_pos is None:
            cursor_pos = len(code)
        content = dict(
            code=code,
            cursor_pos=cursor_pos,
            detail_level=detail_level,
        )
        msg = self.session.msg("inspect_request", content)
        self._dispatch_to_kernel(msg)
        return msg["header"]["msg_id"]

    def history(self, raw=True, output=False, hist_access_type="range", **kwds):
        content = dict(raw=raw, output=output, hist_access_type=hist_access_type, **kwds)
        msg = self.session.msg("history_request", content)
        self._dispatch_to_kernel(msg)
        return msg["header"]["msg_id"]

    def shutdown(self, restart=False):
        # FIXME: What to do here?
        raise NotImplementedError("Cannot shutdown in-process kernel")

    def kernel_info(self):
        """Request kernel info."""
        msg = self.session.msg("kernel_info_request")
        self._dispatch_to_kernel(msg)
        return msg["header"]["msg_id"]

    def comm_info(self, target_name=None):
        """Request a dictionary of valid comms and their targets."""
        if target_name is None:
            content = {}
        else:
            content = dict(target_name=target_name)
        msg = self.session.msg("comm_info_request", content)
        self._dispatch_to_kernel(msg)
        return msg["header"]["msg_id"]

    def input(self, string):
        if self.kernel is None:
            raise RuntimeError("Cannot send input reply. No kernel exists.")
        self.kernel.raw_input_str = string

    def is_complete(self, code):
        msg = self.session.msg("is_complete_request", {"code": code})
        self._dispatch_to_kernel(msg)
        return msg["header"]["msg_id"]

    def _dispatch_to_kernel(self, msg):
        """Send a message to the kernel and handle a reply."""
        kernel = self.kernel
        if kernel is None:
            raise RuntimeError("Cannot send request. No kernel exists.")

        stream = kernel.shell_stream
        self.session.send(stream, msg)
        msg_parts = stream.recv_multipart()
        if run_sync:
            dispatch_shell = run_sync(kernel.dispatch_shell)
            dispatch_shell(msg_parts)
        else:
            loop = asyncio.get_event_loop()
            loop.run_until_complete(kernel.dispatch_shell(msg_parts))
        idents, reply_msg = self.session.recv(stream, copy=False)
        self.shell_channel.call_handlers_later(reply_msg)

    def get_shell_msg(self, block=True, timeout=None):
        return self.shell_channel.get_msg(block, timeout)

    def get_iopub_msg(self, block=True, timeout=None):
        return self.iopub_channel.get_msg(block, timeout)

    def get_stdin_msg(self, block=True, timeout=None):
        return self.stdin_channel.get_msg(block, timeout)

    def get_control_msg(self, block=True, timeout=None):
        return self.control_channel.get_msg(block, timeout)


# -----------------------------------------------------------------------------
# ABC Registration
# -----------------------------------------------------------------------------

KernelClientABC.register(InProcessKernelClient)
