import os
import signal
import socket

import matplotlib as mpl
from matplotlib import _api, cbook
from matplotlib._pylab_helpers import Gcf
from . import _macosx
from .backend_agg import FigureCanvasAgg
from matplotlib.backend_bases import (
    _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
    ResizeEvent, TimerBase)


class TimerMac(_macosx.Timer, TimerBase):
    """Subclass of `.TimerBase` using CFRunLoop timer events."""
    # completely implemented at the C-level (in _macosx.Timer)


class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase):
    # docstring inherited

    # Ideally this class would be `class FCMacAgg(FCAgg, FCMac)`
    # (FC=FigureCanvas) where FCMac would be an ObjC-implemented mac-specific
    # class also inheriting from FCBase (this is the approach with other GUI
    # toolkits).  However, writing an extension type inheriting from a Python
    # base class is slightly tricky (the extension type must be a heap type),
    # and we can just as well lift the FCBase base up one level, keeping it *at
    # the end* to have the right method resolution order.

    # Events such as button presses, mouse movements, and key presses are
    # handled in C and events (MouseEvent, etc.) are triggered from there.

    required_interactive_framework = "macosx"
    _timer_cls = TimerMac
    manager_class = _api.classproperty(lambda cls: FigureManagerMac)

    def __init__(self, figure):
        super().__init__(figure=figure)
        self._draw_pending = False
        self._is_drawing = False
        # Keep track of the timers that are alive
        self._timers = set()

    def draw(self):
        """Render the figure and update the macosx canvas."""
        # The renderer draw is done here; delaying causes problems with code
        # that uses the result of the draw() to update plot elements.
        if self._is_drawing:
            return
        with cbook._setattr_cm(self, _is_drawing=True):
            super().draw()
        self.update()

    def draw_idle(self):
        # docstring inherited
        if not (getattr(self, '_draw_pending', False) or
                getattr(self, '_is_drawing', False)):
            self._draw_pending = True
            # Add a singleshot timer to the eventloop that will call back
            # into the Python method _draw_idle to take care of the draw
            self._single_shot_timer(self._draw_idle)

    def _single_shot_timer(self, callback):
        """Add a single shot timer with the given callback"""
        # We need to explicitly stop and remove the timer after
        # firing, otherwise segfaults will occur when trying to deallocate
        # the singleshot timers.
        def callback_func(callback, timer):
            callback()
            self._timers.remove(timer)
            timer.stop()
        timer = self.new_timer(interval=0)
        timer.single_shot = True
        timer.add_callback(callback_func, callback, timer)
        self._timers.add(timer)
        timer.start()

    def _draw_idle(self):
        """
        Draw method for singleshot timer

        This draw method can be added to a singleshot timer, which can
        accumulate draws while the eventloop is spinning. This method will
        then only draw the first time and short-circuit the others.
        """
        with self._idle_draw_cntx():
            if not self._draw_pending:
                # Short-circuit because our draw request has already been
                # taken care of
                return
            self._draw_pending = False
            self.draw()

    def blit(self, bbox=None):
        # docstring inherited
        super().blit(bbox)
        self.update()

    def resize(self, width, height):
        # Size from macOS is logical pixels, dpi is physical.
        scale = self.figure.dpi / self.device_pixel_ratio
        width /= scale
        height /= scale
        self.figure.set_size_inches(width, height, forward=False)
        ResizeEvent("resize_event", self)._process()
        self.draw_idle()


class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):

    def __init__(self, canvas):
        data_path = cbook._get_data_path('images')
        _, tooltips, image_names, _ = zip(*NavigationToolbar2.toolitems)
        _macosx.NavigationToolbar2.__init__(
            self, canvas,
            tuple(str(data_path / image_name) + ".pdf"
                  for image_name in image_names if image_name is not None),
            tuple(tooltip for tooltip in tooltips if tooltip is not None))
        NavigationToolbar2.__init__(self, canvas)

    def draw_rubberband(self, event, x0, y0, x1, y1):
        self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1))

    def remove_rubberband(self):
        self.canvas.remove_rubberband()

    def save_figure(self, *args):
        directory = os.path.expanduser(mpl.rcParams['savefig.directory'])
        filename = _macosx.choose_save_file('Save the figure',
                                            directory,
                                            self.canvas.get_default_filename())
        if filename is None:  # Cancel
            return
        # Save dir for next time, unless empty str (which means use cwd).
        if mpl.rcParams['savefig.directory']:
            mpl.rcParams['savefig.directory'] = os.path.dirname(filename)
        self.canvas.figure.savefig(filename)


class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
    _toolbar2_class = NavigationToolbar2Mac

    def __init__(self, canvas, num):
        self._shown = False
        _macosx.FigureManager.__init__(self, canvas)
        icon_path = str(cbook._get_data_path('images/matplotlib.pdf'))
        _macosx.FigureManager.set_icon(icon_path)
        FigureManagerBase.__init__(self, canvas, num)
        self._set_window_mode(mpl.rcParams.get("macosx.window_mode", "system"))
        if self.toolbar is not None:
            self.toolbar.update()
        if mpl.is_interactive():
            self.show()
            self.canvas.draw_idle()

    def _close_button_pressed(self):
        Gcf.destroy(self)
        self.canvas.flush_events()

    def destroy(self):
        # We need to clear any pending timers that never fired, otherwise
        # we get a memory leak from the timer callbacks holding a reference
        while self.canvas._timers:
            timer = self.canvas._timers.pop()
            timer.stop()
        super().destroy()

    @classmethod
    def start_main_loop(cls):
        # Set up a SIGINT handler to allow terminating a plot via CTRL-C.
        # The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
        # docstring for details.  Parts are implemented by wake_on_fd_write in ObjC.

        old_sigint_handler = signal.getsignal(signal.SIGINT)
        if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
            _macosx.show()
            return

        handler_args = None
        wsock, rsock = socket.socketpair()
        wsock.setblocking(False)
        rsock.setblocking(False)
        old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
        _macosx.wake_on_fd_write(rsock.fileno())

        def handle(*args):
            nonlocal handler_args
            handler_args = args
            _macosx.stop()

        signal.signal(signal.SIGINT, handle)
        try:
            _macosx.show()
        finally:
            wsock.close()
            rsock.close()
            signal.set_wakeup_fd(old_wakeup_fd)
            signal.signal(signal.SIGINT, old_sigint_handler)
            if handler_args is not None:
                old_sigint_handler(*handler_args)

    def show(self):
        if not self._shown:
            self._show()
            self._shown = True
        if mpl.rcParams["figure.raise_window"]:
            self._raise()


@_Backend.export
class _BackendMac(_Backend):
    FigureCanvas = FigureCanvasMac
    FigureManager = FigureManagerMac
    mainloop = FigureManagerMac.start_main_loop
