import threading

import atexit
import logging

from caproto.threading import pyepics_compat
from caproto.threading.pyepics_compat import PV as _PV, caput, caget  # noqa
from ._dispatch import _CallbackThread, EventDispatcher, wrap_callback


thread_class = threading.Thread
module_logger = logging.getLogger(__name__)
_dispatcher = None
name = "caproto"


def get_dispatcher():
    "The event dispatcher for the caproto control layer"
    return _dispatcher


class CaprotoCallbackThread(_CallbackThread):
    ...


class PV(_PV):
    def __init__(
        self,
        pvname,
        callback=None,
        form="time",
        verbose=False,
        auto_monitor=None,
        count=None,
        connection_callback=None,
        connection_timeout=None,
        access_callback=None,
        context=None,
    ):
        connection_callback = wrap_callback(
            _dispatcher, "metadata", connection_callback
        )
        callback = wrap_callback(_dispatcher, "monitor", callback)
        access_callback = wrap_callback(_dispatcher, "metadata", access_callback)

        super().__init__(
            pvname,
            form=form,
            verbose=verbose,
            auto_monitor=auto_monitor,
            count=count,
            connection_timeout=connection_timeout,
            connection_callback=connection_callback,
            callback=callback,
            access_callback=access_callback,
            context=context,
        )

    def add_callback(
        self, callback=None, index=None, run_now=False, with_ctrlvars=True, **kw
    ):
        if not self.auto_monitor:
            self.auto_monitor = True
        callback = wrap_callback(_dispatcher, "monitor", callback)
        return super().add_callback(
            callback=callback,
            index=index,
            run_now=run_now,
            with_ctrlvars=with_ctrlvars,
            **kw
        )

    def put(
        self,
        value,
        wait=False,
        timeout=30.0,
        use_complete=False,
        callback=None,
        callback_data=None,
    ):
        if callback:
            use_complete = True
        callback = wrap_callback(_dispatcher, "get_put", callback)
        return super().put(
            value,
            wait=wait,
            timeout=timeout,
            use_complete=use_complete,
            callback=callback,
            callback_data=callback_data,
        )

    # TODO: caproto breaks API compatibility in wait_for_connection, raising TimeoutError

    def get_all_metadata_blocking(self, timeout):
        if self._args["status"] is None:
            self.get_timevars(timeout=timeout)
        self.get_ctrlvars(timeout=timeout)
        md = self._args.copy()
        md.pop("value", None)
        return md

    def get_all_metadata_callback(self, callback, *, timeout):
        def get_metadata_thread(pvname):
            md = self.get_all_metadata_blocking(timeout=timeout)
            callback(pvname, md)

        _dispatcher.schedule_utility_task(get_metadata_thread, pvname=self.pvname)

    def clear_callbacks(self):
        super().clear_callbacks()
        self.access_callbacks.clear()
        self.connection_callbacks.clear()

    def clear_auto_monitor(self):
        # TODO move into caproto
        self.auto_monitor = False
        if self._auto_monitor_sub is not None:
            self._auto_monitor_sub.clear()
            self._auto_monitor_sub = None


def release_pvs(*pvs):
    for pv in pvs:
        pv.clear_callbacks()
        # pv.disconnect()


def get_pv(
    pvname,
    form="time",
    connect=False,
    context=None,
    timeout=5.0,
    connection_callback=None,
    access_callback=None,
    callback=None,
    **kwargs
):
    """Get a PV from PV cache or create one if needed.

    Parameters
    ---------
    form : str, optional
        PV form: one of 'native' (default), 'time', 'ctrl'
    connect : bool, optional
        whether to wait for connection (default False)
    context : int, optional
        PV threading context (defaults to current context)
    timeout : float, optional
        connection timeout, in seconds (default 5.0)
    """
    if context is None:
        context = PV._default_context

    pv = PV(
        pvname,
        form=form,
        connection_callback=connection_callback,
        access_callback=access_callback,
        callback=callback,
        **kwargs
    )
    pv._reference_count = 0
    if connect:
        pv.wait_for_connection(timeout=timeout)
    return pv


def setup(logger):
    """Setup ophyd for use

    Must be called once per session using ophyd
    """
    # It's important to use the same context in the callback _dispatcher
    # as the main thread, otherwise not-so-savvy users will be very
    # confused
    global _dispatcher

    if _dispatcher is not None:
        logger.debug("ophyd already setup")
        return

    pyepics_compat._get_pv = pyepics_compat.get_pv
    pyepics_compat.get_pv = get_pv

    def _cleanup():
        """Clean up the ophyd session"""
        global _dispatcher
        if _dispatcher is None:
            return

        pyepics_compat.get_pv = pyepics_compat._get_pv

        if _dispatcher.is_alive():
            _dispatcher.stop()

        _dispatcher = None

    logger.debug("Installing event dispatcher")
    context = PV._default_context.broadcaster
    _dispatcher = EventDispatcher(
        thread_class=CaprotoCallbackThread, context=context, logger=logger
    )
    atexit.register(_cleanup)
    return _dispatcher
