class _PhaseViewer(wx.Frame):
    """This is a window for selecting the ROI for interferometry."""
    def __init__(self, parent, input_image, image_ft, RMS_error):
        super().__init__(parent, title="Phase View")
        self._panel = wx.Panel(self)

        wx_img_real = _np_grey_img_to_wx_image(input_image)
        wx_img_fourier = _np_grey_img_to_wx_image(image_ft)

        self._canvas = FloatCanvas(self._panel, size=wx_img_real.GetSize())
        self._real_bmp = self._canvas.AddBitmap(wx_img_real, (0, 0),
                                                Position="cc")
        self._fourier_bmp = self._canvas.AddBitmap(wx_img_fourier, (0, 0),
                                                   Position="cc")
        # By default, show real and hide the fourier transform.
        self._fourier_bmp.Hide()

        save_btn = wx.ToggleButton(self._panel, label="Show Fourier")
        save_btn.Bind(wx.EVT_TOGGLEBUTTON, self.OnToggleFourier)

        rms_txt = wx.StaticText(self._panel,
                                label="RMS difference: %.05f" % (RMS_error))

        panel_sizer = wx.BoxSizer(wx.VERTICAL)
        panel_sizer.Add(self._canvas)

        bottom_sizer = wx.BoxSizer(wx.HORIZONTAL)
        bottom_sizer.Add(save_btn, wx.SizerFlags().Center().Border())
        bottom_sizer.Add(rms_txt, wx.SizerFlags().Center().Border())
        panel_sizer.Add(bottom_sizer)

        self._panel.SetSizer(panel_sizer)

        frame_sizer = wx.BoxSizer(wx.VERTICAL)
        frame_sizer.Add(self._panel)
        self.SetSizerAndFit(frame_sizer)

    def OnToggleFourier(self, event: wx.CommandEvent) -> None:
        show_fourier = event.IsChecked()
        # These bmp are wx.lib.floatcanvas.FCObjects.Bitmap and not
        # wx.Bitmap.  Their Show method does not take show argument
        # and therefore we can't do `Show(show_fourier)`.
        if show_fourier:
            self._fourier_bmp.Show()
            self._real_bmp.Hide()
        else:
            self._real_bmp.Show()
            self._fourier_bmp.Hide()
        self._canvas.Draw(Force=True)
class _ROISelect(wx.Frame):
    """Display a window that allows the user to select a circular area.

    This is a window for selecting the ROI for interferometry.
    """
    def __init__(self,
                 parent,
                 input_image: np.ndarray,
                 initial_roi,
                 scale_factor=1) -> None:
        super().__init__(parent, title="ROI selector")
        self._panel = wx.Panel(self)
        self._img = _np_grey_img_to_wx_image(input_image)
        self._scale_factor = scale_factor

        # What, if anything, is being dragged.
        # XXX: When we require Python 3.8, annotate better with
        # `typing.Literal[None, "xy", "r"]`
        self._dragging: typing.Optional[str] = None

        # Canvas
        self.canvas = FloatCanvas(self._panel, size=self._img.GetSize())
        self.canvas.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
        self.bitmap = self.canvas.AddBitmap(self._img, (0, 0), Position="cc")

        self.circle = self.canvas.AddCircle(
            self.canvas.PixelToWorld(initial_roi[:2]),
            initial_roi[2] * 2,
            LineColor="cyan",
            LineWidth=2,
        )

        # Save button
        saveBtn = wx.Button(self._panel, label="Save ROI")
        saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)

        panel_sizer = wx.BoxSizer(wx.VERTICAL)
        panel_sizer.Add(self.canvas)
        panel_sizer.Add(saveBtn, wx.SizerFlags().Border())
        self._panel.SetSizer(panel_sizer)

        frame_sizer = wx.BoxSizer(wx.VERTICAL)
        frame_sizer.Add(self._panel)
        self.SetSizerAndFit(frame_sizer)

    @property
    def ROI(self):
        """Convert circle parameters to ROI x, y and radius"""
        roi_x, roi_y = self.canvas.WorldToPixel(self.circle.XY)
        roi_r = max(self.circle.WH)
        return (roi_x, roi_y, roi_r)

    def OnSave(self, event: wx.CommandEvent) -> None:
        del event
        roi = [x * self._scale_factor for x in self.ROI]
        userConfig.setValue("dm_circleParams", (roi[1], roi[0], roi[2]))

    def MoveCircle(self, pos: wx.Point, r) -> None:
        """Set position and radius of circle with bounds checks."""
        x, y = pos
        _x, _y, _r = self.ROI
        xmax, ymax = self._img.GetSize()
        if r == _r:
            x_bounded = min(max(r, x), xmax - r)
            y_bounded = min(max(r, y), ymax - r)
            r_bounded = r
        else:
            r_bounded = max(_ROI_MIN_RADIUS, min(xmax - x, x, ymax - y, y, r))
            x_bounded = min(max(r_bounded, x), xmax - r_bounded)
            y_bounded = min(max(r_bounded, y), ymax - r_bounded)
        self.circle.SetPoint(self.canvas.PixelToWorld((x_bounded, y_bounded)))
        self.circle.SetDiameter(2 * r_bounded)
        if any((x_bounded != x, y_bounded != y, r_bounded != r)):
            self.circle.SetColor("magenta")
        else:
            self.circle.SetColor("cyan")

    def OnMouse(self, event: wx.MouseEvent) -> None:
        pos = event.GetPosition()
        x, y, r = self.ROI
        if event.LeftDClick():
            # Set circle centre
            self.MoveCircle(pos, r)
        elif event.Dragging():
            # Drag circle centre or radius
            drag_r = np.sqrt((x - pos[0])**2 + (y - pos[1])**2)
            if self._dragging is None:
                # determine what to drag
                if drag_r < 0.5 * r:
                    # closer to center
                    self._dragging = "xy"
                else:
                    # closer to edge
                    self._dragging = "r"
            elif self._dragging == "r":
                # Drag circle radius
                self.MoveCircle((x, y), drag_r)
            elif self._dragging == "xy":
                # Drag circle centre
                self.MoveCircle(pos, r)

        if not event.Dragging():
            # Stop dragging
            self._dragging = None
            self.circle.SetColor("cyan")

        self.canvas.Draw(Force=True)