# --------------------------------------------------------------------------- #
# FLOATSPIN Control wxPython IMPLEMENTATION
# Python Code By:
#
# Andrea Gavana, @ 16 Nov 2005
# Latest Revision: 03 Jan 2014, 23.00 GMT
#
# Modifications for DisplayCAL:
# - Select text on focus
# - Always accept "," (comma) as decimal point
# - Only call SyncSpinToText if TextCtrl value has actually changed
# - Set value to min/max if not in range
# - Use a different method for tab traversal because the original code didn't
#   work under wxGTK
# - If CMD or CTRL are held, don't block keycodes that are not allowed (fixes
#   copy/paste/select all keyboard shortcuts not working)
# - Do default action if return key is pressed (currently only works if the
#   default item is a button)
# - Fix children not reflecting enabled state under wxMac
#
# TODO List/Caveats
#
# 1. Ay Idea?
#
# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please
# Write To Me At:
#
# andrea.gavana@gmail.com
# andrea.gavana@maerskoil.com
#
# Or, Obviously, To The wxPython Mailing List!!!
#
#
# End Of Comments
# --------------------------------------------------------------------------- #
""":class:`FloatSpin` implements a floating point :class:`SpinCtrl`.

Description
===========

:class:`FloatSpin` implements a floating point :class:`SpinCtrl`. It is built using a
custom :class:`PyControl`, composed by a :class:`TextCtrl` and a :class:`SpinButton`. In
order to correctly handle floating points numbers without rounding errors or non-exact
floating point representations, :class:`FloatSpin` uses the great :class:`FixedPoint`
class from Tim Peters.

What you can do:

- Set the number of representative digits for your floating point numbers;
- Set the floating point format (``%f``, ``%F``, ``%e``, ``%E``, ``%g``, ``%G``);
- Set the increment of every ``EVT_FLOATSPIN`` event;
- Set minimum, maximum values for :class:`FloatSpin` as well as its range;
- Change font and colour for the underline :class:`TextCtrl`.


Usage
=====

Usage example::

    import wx
    import wx.lib.agw.floatspin as FS

    class MyFrame(wx.Frame):

        def __init__(self, parent):

            wx.Frame.__init__(self, parent, -1, "FloatSpin Demo")

            panel = wx.Panel(self)

            floatspin = FS.FloatSpin(panel, -1, pos=(50, 50), min_val=0, max_val=1,
                                     increment=0.01, value=0.1, agw_style=FS.FS_LEFT)
            floatspin.SetFormat("%f")
            floatspin.SetDigits(2)


    # our normal wxApp-derived class, as usual

    app = wx.App(0)

    frame = MyFrame(None)
    app.SetTopWindow(frame)
    frame.Show()

    app.MainLoop()



Events
======

:class:`FloatSpin` catches 3 different types of events:

1) Spin events: events generated by spinning up/down the spinbutton;
2) Char events: playing with up/down arrows of the keyboard increase/decrease
   the value of :class:`FloatSpin`;
3) Mouse wheel event: using the wheel will change the value of :class:`FloatSpin`.

In addition, there are some other functionalities:

- It remembers the initial value as a default value, call meth:~FloatSpin.SetToDefaultValue`, or
  press ``Esc`` to return to it;
- ``Shift`` + arrow = 2 * increment        (or ``Shift`` + mouse wheel);
- ``Ctrl``  + arrow = 10 * increment       (or ``Ctrl`` + mouse wheel);
- ``Alt``   + arrow = 100 * increment      (or ``Alt`` + mouse wheel);
- Combinations of ``Shift``, ``Ctrl``, ``Alt`` increment the :class:`FloatSpin` value by the
  product of the factors;
- ``PgUp`` & ``PgDn`` = 10 * increment * the product of the ``Shift``, ``Ctrl``, ``Alt``
  factors;
- ``Space`` sets the control's value to it's last valid state.


Window Styles
=============

This class supports the following window styles:

=============== =========== ==================================================
Window Styles   Hex Value   Description
=============== =========== ==================================================
``FS_READONLY``         0x1 Sets :class:`FloatSpin` as read-only control.
``FS_LEFT``             0x2 Horizontally align the underlying :class:`TextCtrl` on the left.
``FS_CENTRE``           0x4 Horizontally align the underlying :class:`TextCtrl` on center.
``FS_RIGHT``            0x8 Horizontally align the underlying :class:`TextCtrl` on the right.
=============== =========== ==================================================


Events Processing
=================

This class processes the following events:

================= ==================================================
Event Name        Description
================= ==================================================
``EVT_FLOATSPIN`` Emitted when the user changes the value of :class:`FloatSpin`, either with the mouse or with the keyboard.
================= ==================================================


License And Version
===================

:class:`FloatSpin` control is distributed under the wxPython license.

Latest revision: Andrea Gavana @ 03 Jan 2014, 23.00 GMT

Version 0.9


Backward Incompatibilities
==========================

Modifications to allow `min_val` or `max_val` to be ``None`` done by:

James Bigler,
SCI Institute, University of Utah,
March 14, 2007

:note: Note that the changes I made will break backward compatibility,
 because I changed the constructor's parameters from `min` / `max` to
 `min_val` / `max_val` to be consistent with the other functions and to
 eliminate any potential confusion with the built in `min` and `max`
 functions.

You specify open ranges like this (you can equally do this in the
constructor)::

    SetRange(min_val=1, max_val=None)     # [1,  ]
    SetRange(min_val=None, max_val=0)     # [ , 0]

or no range::

    SetRange(min_val=None, max_val=None)  # [ , ]

"""  # noqa: E501

from __future__ import annotations

import locale
import re
from math import ceil, floor

import wx

# Set The Styles For The Underline wx.TextCtrl
FS_READONLY = 1
""" Sets :class:`FloatSpin` as read-only control. """
FS_LEFT = 2
""" Horizontally align the underlying :class:`TextCtrl` on the left. """
FS_CENTRE = 4
""" Horizontally align the underlying :class:`TextCtrl` on center. """
FS_RIGHT = 8
""" Horizontally align the underlying :class:`TextCtrl` on the right. """

# Define The FloatSpin Event
wxEVT_FLOATSPIN = wx.NewEventType()  # noqa: N816


EVT_FLOATSPIN = wx.PyEventBinder(wxEVT_FLOATSPIN, 1)
"""Emitted when the user changes the value of :class:`FloatSpin`, either with the mouse
or with the keyboard.
"""


class FloatSpinEvent(wx.PyCommandEvent):
    """This event will be sent when a ``EVT_FLOATSPIN`` event is mapped in the parent.

    Args:
        eventType (int): the event type;
        eventId (int, optional): The event identifier;
        n_old_sel (int, optional): The old selection.
        n_sel (int, optional): The current selection;
    """

    def __init__(
        self,
        eventType: int,  # noqa: N803
        eventId: int = 1,  # noqa: N803
        n_sel: int = -1,
        n_old_sel: int = -1,
    ) -> None:
        wx.PyCommandEvent.__init__(self, eventType, eventId)
        self._eventType = eventType

    def SetPosition(self, pos: int) -> None:  # noqa: N802
        """Set event position.

        Args:
            pos (int): An integer specifying the event position.
        """
        self._position = pos

    def GetPosition(self) -> int:  # noqa: N802
        """Return event position.

        Returns:
            int: The event position.
        """
        return self._position


class FloatTextCtrl(wx.TextCtrl):
    """A class which holds a :class:`TextCtrl`.

    This is one of the two building blocks of :class:`FloatSpin`.

    Args:
        parent: The :class:`FloatTextCtrl` parent.
        id: an identifier for the control: a value of -1 is taken to mean a
            default.
        value: default text value.
        pos: the control position. A value of (-1, -1) indicates a default
            position, chosen by either the windowing system or wxPython,
            depending on platform.
        size: the control size. A value of (-1, -1) indicates a default size,
            chosen by either the windowing system or wxPython, depending on
            platform.
        style: the window style;
        validator: the window validator;
        name: the window name.
    """

    def __init__(
        self,
        parent: wx.Window,
        id: id = wx.ID_ANY,  # noqa: A002
        value: None | str | float | FixedPoint = "",
        pos: wx.Point = wx.DefaultPosition,
        size: wx.Size = wx.DefaultSize,
        style: int = wx.TE_NOHIDESEL | wx.TE_PROCESS_ENTER,
        validator: wx.Validator = wx.DefaultValidator,
        name: str = wx.TextCtrlNameStr,
    ) -> None:
        wx.TextCtrl.__init__(self, parent, id, value, pos, size, style, validator, name)

        self._parent = parent
        self._selection = (-1, -1)
        self._value = value
        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_SET_FOCUS, self.OnFocus)
        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)

    def OnDestroy(self, event: wx.WindowDestroyEvent) -> int:  # noqa: N802
        """Handle the ``wx.EVT_WINDOW_DESTROY`` event for :class:`FloatTextCtrl`.

        Note: This method tries to correctly handle the control destruction
            under MSW.

        Args:
            event (wx.WindowDestroyEvent): A :class:`WindowDestroyEvent` event
                to be processed.

        Returns:
            int: Returns 0 to indicate that the event has been processed.
        """
        if self._parent:
            self._parent._textctrl = None
            self._parent = None
        return 0

    def OnKeyDown(self, event: wx.KeyEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_KEYDOWN`` event for :class:`FloatTextCtrl`.

        Args:
            event (wx.KeyEvent): A :class:`KeyEvent` event to be processed.
        """
        if self._parent:
            self._parent.OnKeyDown(event)

    def OnFocus(self, event: wx.FocusEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_SET_FOCUS`` event for :class:`FloatTextCtrl`.

        Args:
            event (wx.FocusEvent): A :class:`FocusEvent` event to be processed.
        """
        self._value = self.Value
        self.RestoreSelection()
        event.Skip()

    def OnKillFocus(self, event: wx.FocusEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_KILL_FOCUS`` event for :class:`FloatTextCtrl`.

        Args:
            event: A :class:`FocusEvent` event to be processed.

        Note:
            This method synchronizes the :class:`SpinButton` and the
            :class:`TextCtrl` when focus is lost.
        """
        self._selection = self.GetSelection()
        if self._parent and self.Value != self._value:
            self._parent.SyncSpinToText(True)
        self.SetSelection(0, 0)

        event.Skip()

    def RestoreSelection(self) -> None:  # noqa: N802
        """Restore the selection under Mac OS X.

        Select all under other platforms (consistent with default wx.TextCtrl
        behaviour).
        """
        if "__WXMAC__" in wx.PlatformInfo and self._selection != (-1, -1):
            self.SetSelection(*self._selection)
        else:
            self.SelectAll()


class FloatSpin(wx.PyControl):
    """Implements a floating point :class:`SpinCtrl`.

    This Is The Main Class Implementation

    It is built using a custom :class:`PyControl`, composed by a
    :class:`TextCtrl` and a :class:`SpinButton`. In order to correctly handle
    floating points numbers without rounding errors or non-exact floating point
    representations, :class:`FloatSpin` uses the great :class:`FixedPoint`
    class from Tim Peters.
    """

    _spinwidth = 0

    def __init__(
        self,
        parent: wx.Window,
        id: int = wx.ID_ANY,  # noqa: A002
        pos: wx.Point = wx.DefaultPosition,
        size: tuple[int, int] = (130, -1),
        style: int = 0,
        value: None | str | float | FixedPoint = 0.0,
        min_val: None | str | float | FixedPoint = None,
        max_val: None | str | float | FixedPoint = None,
        increment: float = 1.0,
        digits: int = -1,
        agw_style: int = FS_LEFT,
        name: str = "FloatSpin",
    ) -> None:
        """Default class constructor.

        Args:
            parent (FloatSpin): The :class:`FloatSpin` parent.
            id (int): An identifier for the control: a value of -1 is taken to
                mean a default.
            pos: The control position. A value of (-1, -1) indicates a default
                position, chosen by either the windowing system or wxPython,
                depending on platform.
            size: The control size. A value of (-1, -1) indicates a default
                size, chosen by either the windowing system or wxPython,
                depending on platform;
            style: The window style.
            value: The current value for :class:`FloatSpin`.
            min_val: The minimum value, ignored if ``None``.
            max_val: the maximum value, ignored if ``None``.
            increment: the increment for every :class:`FloatSpinEvent` event.
            digits: number of representative digits for your floating point numbers.
            agw_style: one of the following bits:

                =============== =========== ==================================================
                Window Styles   Hex Value   Description
                =============== =========== ==================================================
                ``FS_READONLY``         0x1 Sets :class:`FloatSpin` as read-only control.
                ``FS_LEFT``             0x2 Horizontally align the underlying :class:`TextCtrl` on the left.
                ``FS_CENTRE``           0x4 Horizontally align the underlying :class:`TextCtrl` on center.
                ``FS_RIGHT``            0x8 Horizontally align the underlying :class:`TextCtrl` on the right.
                =============== =========== ==================================================

            name (str): The window name.
        """  # noqa: E501
        wx.PyControl.__init__(
            self,
            parent,
            id,
            pos,
            size,
            style | wx.NO_BORDER | wx.NO_FULL_REPAINT_ON_RESIZE | wx.CLIP_CHILDREN,
            wx.DefaultValidator,
            name,
        )

        # Don't call SetRange here, because it will try to modify
        # self._value whose value doesn't exist yet.
        self.SetRangeDontClampValue(min_val, max_val)
        self._value = self.ClampValue(FixedPoint(str(value), 20))
        self._defaultvalue = self._value
        self._increment = FixedPoint(str(increment), 20)
        self._spinmodifier = FixedPoint(str(1.0), 20)
        self._digits = digits
        self._snapticks = False
        self._spinbutton = None
        self._ignore_spin_event = False
        self._textctrl = None
        self._spinctrl_bestsize = wx.Size(-999, -999)
        self._enabled = True

        # start Philip Semanchuk addition
        # The textbox & spin button are drawn slightly differently
        # depending on the platform. The difference is most pronounced
        # under OS X.
        if "__WXMAC__" in wx.PlatformInfo:
            self._gap = 8
            self._spin_top = 3
            self._text_left = 4
            self._text_top = 4
        elif "__WXMSW__" in wx.PlatformInfo:
            self._gap = 1
            self._spin_top = 0
            self._text_left = 0
            self._text_top = 0
        else:
            # GTK
            self._gap = 1 if "gtk3" in wx.PlatformInfo else -1
            self._spin_top = 0
            self._text_left = 0
            self._text_top = 0
        # end Philip Semanchuk addition

        self.SetLabel(name)
        self.SetForegroundColour(parent.GetForegroundColour())

        width = size[0]
        height = size[1]
        best_size = self.DoGetBestSize()

        width = best_size.GetWidth() if width == -1 else width
        height = best_size.GetHeight() if height == -1 else height

        self._validkeycode = [43, 44, 45, 46, 69, 101]
        self._validkeycode.extend(list(range(48, 58)))
        self._validkeycode.extend(
            [
                wx.WXK_BACK,
                wx.WXK_DELETE,
                wx.WXK_LEFT,
                wx.WXK_RIGHT,
                wx.WXK_NUMPAD0,
                wx.WXK_NUMPAD1,
                wx.WXK_NUMPAD2,
                wx.WXK_NUMPAD3,
                wx.WXK_NUMPAD4,
                wx.WXK_NUMPAD5,
                wx.WXK_NUMPAD6,
                wx.WXK_NUMPAD7,
                wx.WXK_NUMPAD8,
                wx.WXK_NUMPAD9,
                wx.WXK_NUMPAD_ADD,
                wx.WXK_NUMPAD_DECIMAL,
                wx.WXK_NUMPAD_SEPARATOR,
                wx.WXK_NUMPAD_SUBTRACT,
            ]
        )

        self._spinbutton = wx.SpinButton(
            self,
            wx.ID_ANY,
            wx.DefaultPosition,
            size=(-1, height),
            style=wx.SP_ARROW_KEYS | wx.SP_VERTICAL | wx.SP_WRAP,
        )
        self._spinbutton.SetRange(-(2**32) / 2.0, 2**32 / 2.0 - 1)
        self._spinbutton.AcceptsFocusFromKeyboard = lambda: False
        if "gtk3" in wx.PlatformInfo:
            if not FloatSpin._spinwidth:
                spin = wx.SpinCtrl(self, -1)
                text = wx.TextCtrl(self, -1)
                FloatSpin._spinwidth = spin.Size[0] - text.Size[0] + 11
                spin.Destroy()
                text.Destroy()
            self._spinbutton.MaxSize = FloatSpin._spinwidth, height

        txtstyle = wx.TE_NOHIDESEL | wx.TE_PROCESS_ENTER

        if agw_style & FS_RIGHT:
            txtstyle = txtstyle | wx.TE_RIGHT
        elif agw_style & FS_CENTRE:
            txtstyle = txtstyle | wx.TE_CENTER

        txtstyle = (
            (txtstyle | wx.TE_READONLY) if (agw_style & FS_READONLY) else txtstyle
        )

        self._textctrl = FloatTextCtrl(
            self,
            wx.ID_ANY,
            "",
            wx.DefaultPosition,
            (width - self._spinbutton.GetSize().GetWidth(), height),
            txtstyle,
        )

        # start Philip Semanchuk addition
        # Setting the textctrl's size in the ctor also sets its min size.
        # But the textctrl is entirely controlled by the parent floatspin
        # control and should accept whatever size its parent dictates, so
        # here we tell it to forget its min size.
        self._textctrl.SetMinSize(wx.DefaultSize)
        # Setting the spin buttons's size in the ctor also sets its min size.
        # Under OS X that results in a rendering artifact because spin buttons
        # are a little shorter than textboxes.
        # Setting the min size to the default allows OS X to draw the spin
        # button correctly. However, Windows and KDE take the call to
        # SetMinSize() as a cue to size the spin button taller than the
        # textbox, so we avoid the call there.
        if "__WXMAC__" in wx.PlatformInfo:
            self._spinbutton.SetMinSize(wx.DefaultSize)
        # end Philip Semanchuk addition

        self._mainsizer = wx.BoxSizer(wx.HORIZONTAL)
        # Ensure the spin button is shown, and the text widget takes
        # all remaining free space
        self._mainsizer.Add(self._textctrl, 1)
        if "gtk3" in wx.PlatformInfo:
            self._mainsizer.Add((self._gap, 1), 0)
        self._mainsizer.Add(self._spinbutton, 0)
        self.SetSizer(self._mainsizer)
        self._mainsizer.Layout()

        self.SetFormat()

        if not (agw_style & FS_READONLY):
            self.Bind(wx.EVT_SPIN_UP, self.OnSpinUp)
            self.Bind(wx.EVT_SPIN_DOWN, self.OnSpinDown)
            self._spinbutton.Bind(wx.EVT_LEFT_DOWN, self.OnSpinMouseDown)
            self._spinbutton.Bind(wx.EVT_LEFT_UP, self.OnSpinMouseUp)

        self._textctrl.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter)
        self._textctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
        self._spinbutton.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)

        self.Bind(wx.EVT_SET_FOCUS, self.OnFocus)

        if "gtk3" not in wx.PlatformInfo:
            self.Bind(wx.EVT_SIZE, self.OnSize)

        if hasattr(self, "SetBestSize"):
            # Not Phoenix
            # start Philip Semanchuk move
            self.SetBestSize((width, height))
            # end Philip Semanchuk move

    def OnDestroy(self, event: wx.WindowDestroyEvent) -> int:  # noqa: N802
        """Handle the ``wx.EVT_WINDOW_DESTROY`` event for :class:`FloatSpin`.

        Note: This method tries to correctly handle the control destruction
            under MSW.

        Args:
            event (wx.WindowDestroyEvent): A :class:`WindowDestroyEvent` event
                to be processed.

        Returns:
            int: Returns 0 to indicate that the event has been processed.
        """
        # Null This Since MSW Sends KILL_FOCUS On Deletion
        if self._textctrl:
            self._textctrl._parent = None
            self._textctrl.Destroy()
            self._textctrl = None

        self._spinbutton.Destroy()
        self._spinbutton = None

        return 0

    def DoGetBestSize(self) -> wx.Size:  # noqa: N802
        """Get the size which best suits the window: for a control.

        It would be the minimal size which doesn't truncate the control, for a
        panel - the same size as it would have after a call to `Fit()`.

        :note: Overridden from :class:`PyControl`.
        """
        if self._spinctrl_bestsize.x == -999:
            spin = wx.SpinCtrl(self, -1)
            self._spinctrl_bestsize = spin.GetBestSize()

            # oops something went wrong, set to reasonable value
            if self._spinctrl_bestsize.GetWidth() < 20:
                self._spinctrl_bestsize.SetWidth(95)
            if self._spinctrl_bestsize.GetHeight() < 10:
                self._spinctrl_bestsize.SetHeight(22)

            spin.Destroy()

        return self._spinctrl_bestsize

    def DoSendEvent(self) -> None:  # noqa: N802
        """Send the event to the parent."""
        event = wx.CommandEvent(wx.wxEVT_COMMAND_SPINCTRL_UPDATED, self.GetId())
        event.SetEventObject(self)
        event.SetInt(int(self._value + 0.5))

        if self._textctrl:
            event.SetString(self._textctrl.GetValue())

        self.GetEventHandler().ProcessEvent(event)

        event_out = FloatSpinEvent(wxEVT_FLOATSPIN, self.GetId())
        event_out.SetPosition(int(self._value + 0.5))
        event_out.SetEventObject(self)
        self.GetEventHandler().ProcessEvent(event_out)

    def Disable(self) -> None:  # noqa: N802
        """Disable the child controls. This is needed under wxMac."""
        self.Enable(False)

    def Enable(self, enable: bool = True) -> None:  # noqa: N802
        """Enable the child controls. This is needed under wxMac.

        Args:
            enable (bool, optional): If ``True``, the control is enabled,
                otherwise it is disabled. Defaults to ``True``.
        """
        wx.PyControl.Enable(self, enable)
        self._enabled = enable

        if self._textctrl:
            self._textctrl.Enable(enable)
        self._spinbutton.Enable(enable)

    @property
    def Enabled(self) -> bool:  # noqa: N802
        """Return whether the control is enabled or not.

        Returns:
            bool: ``True`` if the control is enabled, ``False`` otherwise.
        """
        return self._enabled

    @Enabled.setter
    def Enabled(self, enable: bool = True) -> None:  # noqa: N802
        """Set whether the control is enabled or not.

        Args:
            enable (bool, optional): If ``True``, the control is enabled,
                otherwise it is disabled.
        """
        self.Enable(enable)

    def IsEnabled(self) -> bool:  # noqa: N802
        """Return whether the control is enabled or not.

        Returns:
            bool: ``True`` if the control is enabled, ``False`` otherwise.
        """
        return self._enabled

    def OnSpinMouseDown(self, event: wx.MouseEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_LEFT_DOWN`` event for :class:`FloatSpin`.

        Note:
            This method works on the underlying :class:`SpinButton`.

        Args:
            event (wx.MouseEvent): a :class:`MouseEvent` event to be processed.

        """
        modifier = FixedPoint(str(1.0), 20)
        if event.ShiftDown():
            modifier = modifier * 2.0
        if event.ControlDown():
            modifier = modifier * 10.0
        if event.AltDown():
            modifier = modifier * 100.0

        self._spinmodifier = modifier

        self._ignore_spin_event = False
        event.Skip()

    def OnSpinMouseUp(self, event: wx.MouseEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_LEFT_UP`` event for :class:`FloatSpin`.

        Note:
            This method works on the underlying :class:`SpinButton`.

        Args:
            event (wx.MouseEvent): a :class:`MouseEvent` event to be processed.

        """
        if self._textctrl:
            if "__WXMSW__" in wx.PlatformInfo:
                self._textctrl.SetFocus()
                self._textctrl.SelectAll()
            elif "__WXMAC__" not in wx.PlatformInfo:
                self._textctrl.SetFocus()
                self._textctrl.SetSelection(0, 0)

        self._ignore_spin_event = False
        event.Skip()

    def OnSpinUp(self, event: wx.SpinEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_SPIN_UP`` event for :class:`FloatSpin`.

        Args:
            event (wx.SpinEvent): a :class:`SpinEvent` event to be processed.
        """
        if self._ignore_spin_event:
            self._ignore_spin_event = False
            return

        if self._textctrl and self._textctrl.IsModified():
            self.SyncSpinToText(False)

        if self.InRange(self._value + self._increment * self._spinmodifier):
            self._value = self._value + self._increment * self._spinmodifier
        elif self._max is not None:
            self._value = self._max
        self.SetValue(self._value)
        if "__WXMAC__" in wx.PlatformInfo:
            self._textctrl.SelectAll()
        self.DoSendEvent()

    def OnSpinDown(self, event: wx.SpinEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_SPIN_DOWN`` event for :class:`FloatSpin`.

        Args:
            event (wx.SpinEvent): a :class:`SpinEvent` event to be processed.
        """
        if self._ignore_spin_event:
            self._ignore_spin_event = False
            return

        if self._textctrl and self._textctrl.IsModified():
            self.SyncSpinToText(False)

        if self.InRange(self._value - self._increment * self._spinmodifier):
            self._value = self._value - self._increment * self._spinmodifier
        elif self._min is not None:
            self._value = self._min
        self.SetValue(self._value)
        if "__WXMAC__" in wx.PlatformInfo:
            self._textctrl.SelectAll()
        self.DoSendEvent()

    def OnTextEnter(self, event: wx.KeyEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_TEXT_ENTER`` event for :class:`FloatSpin`.

        Note:
            This method works on the underlying :class:`TextCtrl`.

        Args:
            event (wx.KeyEvent): a :class:`KeyEvent` event to be processed.
        """
        self.SyncSpinToText(True)
        event.Skip()

    def OnKeyDown(self, event: wx.KeyEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_KEYDOWN`` event for :class:`FloatSpin`.

        Note:
            This method works on the underlying :class:`TextCtrl`.

        Args:
            event (wx.KeyEvent): a :class:`KeyEvent` event to be processed.
        """
        modifier = FixedPoint(str(1.0), 20)
        if event.ShiftDown():
            modifier = modifier * 2.0
        if event.ControlDown():
            modifier = modifier * 10.0
        if event.AltDown():
            modifier = modifier * 100.0

        keycode = event.GetKeyCode()

        if keycode == wx.WXK_UP:
            self._handle_wxk_up_key(modifier)
        elif keycode == wx.WXK_DOWN:
            self._handle_wxk_down_key(modifier)
        elif keycode == wx.WXK_PAGEUP:
            self._handle_wxk_pageup_key(modifier)
        elif keycode == wx.WXK_PAGEDOWN:
            self._handle_wxk_pagedown_key(modifier)
        elif keycode == wx.WXK_SPACE:
            self._handle_wxk_space_key(event)
        elif keycode == wx.WXK_ESCAPE:
            self._handle_wxk_escape_key()
        elif keycode == wx.WXK_TAB:
            # The original event code doesn't work under wxGTK
            focus_next_keyboard_focusable_control(event.EventObject)
        elif keycode in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
            self._handle_wxk_return_key()
        else:
            self._handle_wxk_default_key(event, keycode)

    def _handle_wxk_up_key(self, modifier: FixedPoint) -> None:
        """Handle the ``wx.WXK_UP`` key event for :class:`FloatSpin`.

        Args:
            modifier (FixedPoint): The modifier to apply to the increment.
        """
        if self._textctrl and self._textctrl.IsModified():
            self.SyncSpinToText(False)

        self.SetValue(self._value + self._increment * modifier)
        self.DoSendEvent()

    def _handle_wxk_down_key(self, modifier: FixedPoint) -> None:
        """Handle the ``wx.WXK_DOWN`` key event for :class:`FloatSpin`.

        Args:
            modifier (FixedPoint): The modifier to apply to the increment.
        """
        if self._textctrl and self._textctrl.IsModified():
            self.SyncSpinToText(False)

        self.SetValue(self._value - self._increment * modifier)
        self.DoSendEvent()

    def _handle_wxk_pageup_key(self, modifier: FixedPoint) -> None:
        """Handle the ``wx.WXK_PAGEUP`` key event for :class:`FloatSpin`.

        Args:
            modifier (FixedPoint): The modifier to apply to the increment.
        """
        if self._textctrl and self._textctrl.IsModified():
            self.SyncSpinToText(False)

        self.SetValue(self._value + 10.0 * self._increment * modifier)
        self.DoSendEvent()

    def _handle_wxk_pagedown_key(self, modifier: FixedPoint) -> None:
        """Handle the ``wx.WXK_PAGEDOWN`` key event for :class:`FloatSpin`.

        Args:
            modifier (FixedPoint): The modifier to apply to the increment.
        """
        if self._textctrl and self._textctrl.IsModified():
            self.SyncSpinToText(False)

        self.SetValue(self._value - 10.0 * self._increment * modifier)
        self.DoSendEvent()

    def _handle_wxk_space_key(self, event: wx.KeyEvent) -> None:
        """Handle the ``wx.WXK_SPACE`` key event for :class:`FloatSpin`.

        Args:
            event (wx.KeyEvent): A :class:`KeyEvent` event to be processed.
        """
        self.SetValue(self._value)
        if self._textctrl:
            self._textctrl.SelectAll()
        event.Skip(False)

    def _handle_wxk_escape_key(self) -> None:
        """Handle the ``wx.WXK_ESCAPE`` key event for :class:`FloatSpin`."""
        self.SetToDefaultValue()
        if self._textctrl:
            self._textctrl.SelectAll()
        self.DoSendEvent()

    def _handle_wxk_return_key(self) -> None:
        """Handle the ``wx.WXK_RETURN`` key event for :class:`FloatSpin`."""
        default = self.TopLevelParent.DefaultItem
        if (
            default
            and default.Enabled
            and default.IsShownOnScreen()
            and isinstance(default, wx.Button)
        ):
            default.ProcessEvent(
                wx.PyCommandEvent(wx.EVT_BUTTON.typeId, default.GetId())
            )

    def _handle_wxk_default_key(self, event: wx.KeyEvent, keycode: int) -> None:
        """Handle the default key event for :class:`FloatSpin`.

        Args:
            event (wx.KeyEvent): A :class:`KeyEvent` event to be processed.
            keycode (int): The key code of the pressed key.
        """
        if (
            not event.CmdDown()
            and not event.ControlDown()
            and keycode not in self._validkeycode
        ):
            return
        event.Skip()

    def OnMouseWheel(self, event: wx.MouseEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_MOUSEWHEEL`` event for :class:`FloatSpin`.

        Args:
            event (wx.MouseEvent): a :class:`MouseEvent` event to be processed.
        """
        modifier = FixedPoint(str(1.0), 20)
        if event.ShiftDown():
            modifier = modifier * 2.0
        if event.ControlDown():
            modifier = modifier * 10.0
        if event.AltDown():
            modifier = modifier * 100.0

        if self._textctrl and self._textctrl.IsModified():
            self.SyncSpinToText(False)

        if event.GetWheelRotation() > 0:
            self.SetValue(self._value + self._increment * modifier)

        else:
            self.SetValue(self._value - self._increment * modifier)

        if self._textctrl:
            if "__WXMAC__" in wx.PlatformInfo:
                self._textctrl.SelectAll()
            else:
                self._textctrl.SetFocus()
                self._textctrl.SetSelection(0, 0)

        self.DoSendEvent()

    def OnSize(self, event: wx.SizeEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_SIZE`` event for :class:`FloatSpin`.

        Note:
            This method resizes the text control and reposition the spin button
                when resized.

        Args:
            event (wx.SizeEvent): a :class:`SizeEvent` event to be processed.
        """
        # start Philip Semanchuk addition
        event_width = event.GetSize().width

        self._textctrl.SetPosition((self._text_left, self._text_top))

        text_width, text_height = self._textctrl.Size

        spin_width, _ = self._spinbutton.Size

        text_width = event_width - (spin_width + self._gap + self._text_left)

        self._textctrl.SetSize(wx.Size(text_width, event.GetSize().height))

        # The spin button is always snug against the right edge of the
        # control.
        self._spinbutton.SetPosition((event_width - spin_width, self._spin_top))

        event.Skip()
        # end Philip Semanchuk addition

    def ReplaceDoubleZero(self, strs: str) -> str:  # noqa: N802
        """Replace the (somewhat) python ugly `+e000` with `+e00`.

        Args:
            strs (str): A string (possibly) containing a `+e00` substring.

        Returns:
            str: The modified string with `+e00` replaced by `+e0` or `-e00`
                replaced by `-e0`.
        """
        if self._textformat not in ["%g", "%e", "%E", "%G"]:
            return strs

        if strs.find("e+00") >= 0:
            strs = strs.replace("e+00", "e+0")
        elif strs.find("e-00") >= 0:
            strs = strs.replace("e-00", "e-0")
        elif strs.find("E+00") >= 0:
            strs = strs.replace("E+00", "E+0")
        elif strs.find("E-00") >= 0:
            strs = strs.replace("E-00", "E-0")

        return strs

    def SetValue(self, value: float | FixedPoint) -> None:  # noqa: N802
        """Set the :class:`FloatSpin` value.

        Args:
            value (float | FixedPoint): The new value.
        """
        if not self._textctrl or not self.InRange(value):
            return

        if self._snapticks and self._increment != 0.0:
            finite, snap_value = self.IsFinite(value)

            if not finite:  # FIXME What To Do About A Failure?
                if snap_value - floor(snap_value) < ceil(snap_value) - snap_value:
                    value = self._defaultvalue + floor(snap_value) * self._increment
                else:
                    value = self._defaultvalue + ceil(snap_value) * self._increment

        decimal = locale.localeconv()["decimal_point"]
        strs = ("%100." + str(self._digits) + self._textformat[1]) % value
        strs = strs.replace(".", decimal)
        strs = strs.replace(",", ".")
        strs = strs.strip()
        strs = self.ReplaceDoubleZero(strs)

        # Sync spinbutton so that events generated by it match up with the
        # actual spin direction.
        # This fixes an issue under GTK and Mac OS X where the EVT_SPIN event
        # generated by pressing the spin up/down buttons is dependent on the
        # spin button value (e.g. if the down button is pressed when the button
        # value is at its minimum, there will always be a EVT_SPIN_UP generated
        # under GTK, and no event at all under Mac OS X).
        min_val = self._min if self._min is not None else self._spinbutton.Min
        max_val = self._max if self._max is not None else self._spinbutton.Max
        # Scale the value to the spinbutton range
        spinvalue = round(
            (float(value - min_val) / float(max_val - min_val)) * self._spinbutton.Max
        )
        # Setting the spin button value causes a EVT_SPIN event to be generated
        # under GTK, which we need to ignore
        self._ignore_spin_event = True
        self._spinbutton.SetValue(spinvalue)

        if value != self._value or strs != self._textctrl.GetValue():
            self._textctrl.SetValue(strs)
            self._textctrl.DiscardEdits()
            self._value = value

    def GetValue(self) -> float:  # noqa: N802
        """Return the :class:`FloatSpin` value.

        Returns:
            float: The current value of :class:`FloatSpin`.
        """
        return float(self._value)

    def SetRangeDontClampValue(  # noqa: N802
        self,
        min_val: None | float | FixedPoint,
        max_val: None | float | FixedPoint,
    ) -> None:
        """Set the allowed range.

        Args:
            min_val (None | float | FixedPoint): The minimum value for
                :class:`FloatSpin`. If it is `None` it is ignored.
            max_val (None | float | FixedPoint): the maximum value for
                :class:`FloatSpin`. If it is `None` it is ignored.

        :note: This method doesn't modify the current value.
        """
        if min_val is not None:
            self._min = FixedPoint(str(min_val), 20)
        else:
            self._min = None
        if max_val is not None:
            self._max = FixedPoint(str(max_val), 20)
        else:
            self._max = None

    def SetRange(self, min_val: FixedPoint, max_val: FixedPoint) -> None:  # noqa: N802
        """Set the allowed range.

        Args:
            min_val (FixedPoint): The minimum value for :class:`FloatSpin`. If
                it is ``None`` it is ignored.
            max_val: The maximum value for :class:`FloatSpin`. If it is `None`
                it is ignored.

        :note:
            This method doesn't modify the current value.

        :note:
            You specify open ranges like this (you can equally do this in the
            constructor)::

                SetRange(min_val=1, max_val=None)
                SetRange(min_val=None, max_val=0)


            or no range::

                SetRange(min_val=None, max_val=None)
        """
        self.SetRangeDontClampValue(min_val, max_val)

        value = self.ClampValue(self._value)
        if value != self._value:
            self.SetValue(value)

    def ClampValue(self, var: FixedPoint) -> FixedPoint:  # noqa: N802
        """Clamp var between _min and _max depending on the range has been specified.

        Args:
            var (FixedPoint): The value to be clamped.

        Returns:
            FixedPoint: A clamped copy of `var`.
        """
        if self._min is not None and float(var) < float(self._min):
            return self._min

        if self._max is not None and float(var) > float(self._max):
            var = self._max

        return var

    def SetIncrement(self, increment: FixedPoint) -> None:  # noqa: N802
        """Set the increment for every ``EVT_FLOATSPIN`` event.

        Args:
            increment: A floating point number specifying the :class:`FloatSpin`
                increment.

        Raises:
            Exception: If `increment` is less than 1 / (10 ** digits) or
                if `increment` is not a valid :class:`FixedPoint` number.
        """
        if increment < 1.0 / (10.0**self._digits):
            raise Exception(
                "\nERROR: Increment Should Be Greater Or Equal To 1 / (10 ** digits)."
            )

        self._increment = FixedPoint(str(increment), 20)
        self.SetValue(self._value)

    def GetIncrement(self) -> FixedPoint:  # noqa: N802
        """Return the increment for every ``EVT_FLOATSPIN`` event.

        Returns:
            FixedPoint: The increment for every ``EVT_FLOATSPIN`` event.
        """
        return self._increment

    def SetDigits(self, digits: int = -1) -> None:  # noqa: N802
        """Set the number of digits to show.

        Args:
            digits (int): The number of digits to show. If `digits` < 0,
                :class:`FloatSpin` tries to calculate the best number of digits
                based on input values passed in the constructor.
        """
        if digits < 0:
            incr = str(self._increment)
            digits = 0 if incr.find(".") < 0 else len(incr[incr.find(".") + 1 :])

        self._digits = digits
        self.SetValue(self._value)

    def GetDigits(self) -> int:  # noqa: N802
        """Return the number of digits shown.

        Returns:
            int: The number of digits shown by :class:`FloatSpin`.
        """
        return self._digits

    def SetFormat(self, fmt: str = "%f") -> None:  # noqa: N802
        """Set the string format to use.

        Args:
            fmt (str): The new string format to use. One of the following
                strings:

                ====== =================================
                Format Description
                ====== =================================
                'e'    Floating point exponential format (lowercase)
                'E'    Floating point exponential format (uppercase)
                'f'    Floating point decimal format
                'F'    Floating point decimal format
                'g'    Floating point format. Uses lowercase exponential format
                       if exponent is less than -4 or not less than precision,
                       decimal format otherwise
                'G'    Floating point format. Uses uppercase exponential format
                       if exponent is less than -4 or not less than precision,
                       decimal format otherwise
                ====== =================================
        """  # noqa: E501
        if fmt not in ["%f", "%g", "%e", "%E", "%F", "%G"]:
            raise Exception(
                f"\nERROR: Bad Float Number Format: {fmt!r}. It Should Be "
                'One Of "%f", "%g", "%e", "%E", "%F", "%G"'
            )

        self._textformat = fmt

        if self._digits < 0:
            self.SetDigits()
        else:
            self.SetValue(self._value)

    def GetFormat(self) -> str:  # noqa: N802
        """Return the string format in use.

        :see: :meth:`~FloatSpin.SetFormat` for a list of valid string formats.
        """
        return self._textformat

    def SetDefaultValue(self, defaultvalue: float) -> None:  # noqa: N802
        """Set the :class:`FloatSpin` default value.

        Args:
            defaultvalue (float): A floating point value representing the new
                default value for :class:`FloatSpin`.
        """
        if self.InRange(defaultvalue):
            self._defaultvalue = FixedPoint(str(defaultvalue), 20)

    def GetDefaultValue(self) -> None | FixedPoint:  # noqa: N802
        """Return the :class:`FloatSpin` default value.

        Returns:
            None | FixedPoint: The default value or ``None`` if no default
                value has been set.
        """
        return self._defaultvalue

    def IsDefaultValue(self) -> bool:  # noqa: N802
        """Return whether the current value is the default value or not.

        Returns:
            bool: ``True`` if the current value is the default value, ``False``
                otherwise.
        """
        return self._value == self._defaultvalue

    def SetToDefaultValue(self) -> None:  # noqa: N802
        """Set :class:`FloatSpin` value to its default value."""
        self.SetValue(self._defaultvalue)

    def SetSnapToTicks(self, forceticks: bool = True) -> None:  # noqa: N802
        """Force the value to always be divisible by the increment.

        Initially `False`.

        :note:
          This uses the default value as the basis, you will get strange results
          for very large differences between the current value and default value
          when the increment is very small.

        Args:
            forceticks: True to force the snap to ticks option, False otherwise.
        """
        if self._snapticks != forceticks:
            self._snapticks = forceticks
            self.SetValue(self._value)

    def GetSnapToTicks(self) -> bool:  # noqa: N802
        """Return whether the snap to ticks option is active or not.

        Returns:
            bool: ``True`` if the snap to ticks option is active, ``False``
                otherwise.
        """
        return self._snapticks

    def OnFocus(self, event: wx.FocusEvent) -> None:  # noqa: N802
        """Handle the ``wx.EVT_SET_FOCUS`` event for :class:`FloatSpin`.

        Args:
            event (wx.FocusEvent): A :class:`FocusEvent` event to be processed.
        """
        if self._textctrl:
            self._textctrl.SetFocus()

        event.Skip()

    def SyncSpinToText(self, send_event: bool = True, force_valid: bool = True) -> None:  # noqa: N802
        """Synchronize the underlying :class:`TextCtrl` with :class:`SpinButton`.

        Args:
            send_event (bool): ``True`` to send a ``EVT_FLOATSPIN`` event,
                ``False`` otherwise.
            force_valid (bool): ``True`` to force a valid value (i.e. inside
                the provided range), ``False`` otherwise.
        """
        if not self._textctrl:
            return

        curr = self._textctrl.GetValue()
        curr = curr.strip()
        decimal = locale.localeconv()["decimal_point"]
        curr = curr.replace(decimal, ".")
        curr = curr.replace(",", ".")

        if curr:
            try:
                # curro = float(curr)
                curr = FixedPoint(curr, 20)
            except Exception:
                self.SetValue(self._value)
                return

            if force_valid or not self.HasRange() or self.InRange(curr):
                if force_valid and self.HasRange():
                    curr = self.ClampValue(curr)

                if self._value != curr:
                    self.SetValue(curr)

                    if send_event:
                        self.DoSendEvent()

        elif force_valid:
            # textctrl is out of sync, discard and reset
            self.SetValue(self.GetValue())

    def SetFont(self, font: None | wx.Font = None) -> bool:  # noqa: N802
        """Set the underlying :class:`TextCtrl` font.

        Args:
            font (None | wx.Font, optional): A valid instance of :class:`Font`.

        Returns:
            bool: ``True`` if the font was set successfully, ``False``
                otherwise.
        """
        if font is None:
            font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)

        if not self._textctrl:
            return False

        return self._textctrl.SetFont(font)

    def GetFont(self) -> wx.Font:  # noqa: N802
        """Return the underlying :class:`TextCtrl` font.

        Returns:
            wx.Font: The font used by the underlying :class:`TextCtrl`. If no
                :class:`TextCtrl` is present, the default font for the control
                is returned.
        """
        if not self._textctrl:
            return self.GetFont()

        return self._textctrl.GetFont()

    def GetMin(self) -> None | FixedPoint:  # noqa: N802
        """Return the minimum value.

        It can be a number or ``None`` if no minimum is present.

        Returns:
            None | FixedPoint: The minimum value or ``None`` if no minimum
                is present.
        """
        return self._min

    def GetMax(self) -> None | FixedPoint:  # noqa: N802
        """Return the maximum value.

        It can be a number or ``None`` if no minimum is present.

        Returns:
            None | FixedPoint: The maximum value or ``None`` if no maximum
                is present.
        """
        return self._max

    def HasRange(self) -> bool:  # noqa: N802
        """Return whether range has been set or not.

        Returns:
            bool: ``True`` if a range has been set, ``False`` otherwise.
        """
        return (self._min is not None) or (self._max is not None)

    def InRange(self, value: float | FixedPoint) -> bool:  # noqa: N802
        """Return whether a value is inside the range.

        Args:
            value (float | FixedPoint): The value to test.

        Returns:
            bool: ``True`` if the value is inside the range, ``False``
                otherwise.
        """
        if not self.HasRange():
            return True
        if self._min is not None and float(value) < float(self._min):
            return False
        return self._max is None or float(value) <= float(self._max)

    def GetTextCtrl(self) -> wx.TextCtrl:  # noqa: N802
        """Return the underlying :class:`wx.TextCtrl`.

        Returns:
            wx.TextCtrl: The underlying :class:`wx.TextCtrl` instance.
        """
        return self._textctrl

    def IsFinite(self, value: float | FixedPoint) -> tuple[bool, None | FixedPoint]:  # noqa: N802
        """Try to determine if a value is finite or infinite/NaN.

        Args:
            value: The value to test.

        Returns:
            tuple (bool, None | FixedPoint): A tuple containing a boolean
                indicating whether the value is finite or not, and a
                `FixedPoint` instance representing the snapped value if finite,
                or `None` if not finite.
        """
        try:
            snap_value = (value - self._defaultvalue) / self._increment
            finite = True
        except Exception:
            finite = False
            snap_value = None

        return finite, snap_value


if wx.VERSION >= (3,):
    # Use wx.SpinCtrlDouble
    EVT_FLOATSPIN = wx.EVT_SPINCTRLDOUBLE

    class FloatSpin(wx.SpinCtrlDouble):
        """A wx.SpinCtrlDouble supporting floating point values and a default."""

        _spinwidth = 0

        def __init__(
            self,
            parent: wx.Window,
            id: int = wx.ID_ANY,  # noqa: A002
            pos: wx.Point = wx.DefaultPosition,
            size: tuple[int, int] = (95, -1),
            style: int = wx.SP_ARROW_KEYS | wx.ALIGN_RIGHT,
            value: None | str | float | FixedPoint = 0.0,
            min_val: None | str | float | FixedPoint = None,
            max_val: None | str | float | FixedPoint = None,
            increment: float = 1.0,
            digits: int = -1,
            agw_style: int = FS_LEFT,
            name: str = "FloatSpin",
        ) -> None:
            if "gtk3" in wx.PlatformInfo:
                if not FloatSpin._spinwidth:
                    spin = wx.SpinCtrl(parent, -1)
                    text = wx.TextCtrl(parent, -1)
                    FloatSpin._spinwidth = spin.Size[0] - text.Size[0] + 11
                    spin.Destroy()
                    text.Destroy()
                size = size[0] + FloatSpin._spinwidth, size[1]

            if agw_style & FS_RIGHT:
                style = style | wx.TE_RIGHT
            elif agw_style & FS_CENTRE:
                style = style | wx.TE_CENTER

            if agw_style & FS_READONLY:
                style = style | wx.TE_READONLY

            wx.SpinCtrlDouble.__init__(
                self,
                parent,
                id,
                str(value),
                pos,
                size,
                style,
                min_val or 0,
                max_val or 100,
                value,
                increment,
                name,
            )
            if digits > -1:
                self.SetDigits(digits)


# Class FixedPoint, version 0.0.4.
# Released to the public domain 28-Mar-2001,
# by Tim Peters (tim.one@home.com).

# Provided as-is; use at your own risk; no warranty; no promises; enjoy!


# 28-Mar-01 ver 0.0,4
#     Use repr() instead of str() inside __str__, because str(long) changed
#     since this was first written (used to produce trailing "L", doesn't
#     now).
#
# 09-May-99 ver 0,0,3
#     Repaired __sub__(FixedPoint, string); was blowing up.
#     Much more careful conversion of float (now best possible).
#     Implemented exact % and divmod.
#
# 14-Oct-98 ver 0,0,2
#     Added int, long, frac.  Beefed up docs.  Removed DECIMAL_POINT
#     and MINUS_SIGN globals to discourage bloating this class instead
#     of writing formatting wrapper classes (or subclasses)
#
# 11-Oct-98 ver 0,0,1
#     posted to c.l.py

__version__ = 0, 0, 4

# The default value for the number of decimal digits carried after the
# decimal point.  This only has effect at compile-time.
DEFAULT_PRECISION = 2
# The default value for the number of decimal digits carried after the decimal point.
# This only has effect at compile-time.


class FixedPoint:
    """Decimal arithmetic with a fixed number of digits after the decimal point.

    The number of digits before the decimal point is variable & unbounded.

    The precision is user-settable on a per-object basis when a FixedPoint
    is constructed, and may vary across FixedPoint objects.  The precision
    may also be changed after construction via `FixedPoint.set_precision(p)`.
    Note that if the precision of a FixedPoint is reduced via
    :meth:`FixedPoint.set_precision() <FixedPoint.set_precision>`,
    information may be lost to rounding.

    Example::

        >>> x = FixedPoint("5.55")  # precision defaults to 2
        >>> print x
        5.55
        >>> x.set_precision(1)      # round to one fraction digit
        >>> print x
        5.6
        >>> print FixedPoint("5.55", 1)  # same thing setting to 1 in constructor
        5.6
        >>> repr(x) #  returns constructor string that reproduces object exactly
        "FixedPoint('5.6', 1)"
        >>>


    When :class:`FixedPoint` objects of different precision are combined via + - * /,
    the result is computed to the larger of the inputs' precisions, which also
    becomes the precision of the resulting :class:`FixedPoint` object. Example::

        >>> print FixedPoint("3.42") + FixedPoint("100.005", 3)
        103.425
        >>>


    When a :class:`FixedPoint` is combined with other numeric types (ints, floats,
    strings representing a number) via + - * /, then similarly the computation
    is carried out using -- and the result inherits -- the :class:`FixedPoint`'s
    precision. Example::

        >>> print FixedPoint(1) / 7
        0.14
        >>> print FixedPoint(1, 30) / 7
        0.142857142857142857142857142857
        >>>


    The string produced by `str(x)` (implictly invoked by `print`) always
    contains at least one digit before the decimal point, followed by a
    decimal point, followed by exactly `x.get_precision()` digits.  If `x` is
    negative, `str(x)[0] == "-"`.

    The :class:`FixedPoint` constructor can be passed an int, long, string, float,
    :class:`FixedPoint`, or any object convertible to a float via `float()` or to a
    long via `long()`. Passing a precision is optional; if specified, the
    precision must be a non-negative int. There is no inherent limit on
    the size of the precision, but if very very large you'll probably run
    out of memory.

    Note that conversion of floats to :class:`FixedPoint` can be surprising, and
    should be avoided whenever possible. Conversion from string is exact
    (up to final rounding to the requested precision), so is greatly
    preferred. Example::

        >>> print FixedPoint(1.1e30)
        1099999999999999993725589651456.00
        >>> print FixedPoint("1.1e30")
        1100000000000000000000000000000.00
        >>>

    Args:
        value (None | str | float | complex | FixedPoint): The initial value.
        precision (int): Must be an int >= 0, and defaults to
            ``DEFAULT_PRECISION``.

    Raises:
        TypeError: If `value` is not convertible to a float or long, or if
            `precision` is not convertible to an int.
        ValueError: If `precision` is less than 0.
        OverflowError: If the float value is too large to be represented
            exactly as a :class:`FixedPoint`.
    """

    # the exact value is self.n / (10 ** self.p)
    # self.n is a long; self.p is an int

    def __init__(
        self,
        value: None | str | float | FixedPoint = None,
        precision: int = DEFAULT_PRECISION,
    ) -> None:
        if value is None:
            value = 0

        self.n = self.p = 0
        self.set_precision(precision)
        p = self.p

        if isinstance(value, str):
            self._parse_str_value(value, p)
            return

        if isinstance(value, int):
            self.n = value * (10**p)
            return

        if isinstance(value, FixedPoint):
            temp = value.copy()
            temp.set_precision(p)
            self.n, self.p = temp.n, temp.p
            return

        if isinstance(value, float):
            self._parse_float_value(value, p)
            return

        if isinstance(value, complex):
            raise TypeError(f"can't convert complex to FixedPoint: {value!r}")

        # can we coerce to a float?
        yes = 1
        try:
            asfloat = float(value)
        except Exception:
            yes = 0
        if yes:
            self.__init__(asfloat, p)
            return

        # similarly for long
        yes = 1
        try:
            aslong = int(value)
        except Exception:
            yes = 0
        if yes:
            self.__init__(aslong, p)
            return

        raise TypeError(f"can't convert to FixedPoint: {value!r}")

    def _parse_str_value(self, value: str, p: int) -> None:
        """Parse a str value into the FixedPoint representation.

        Args:
            value (str): The string representation of the number.
            p (int): The precision to use for the FixedPoint representation.
        """
        n, exp = _string2exact(value)
        # exact value is n * (10 ** exp) = n * (10 ** (exp+p)) / (10 ** p)
        effective_exp = exp + p
        if effective_exp > 0:
            n = n * (10**effective_exp)
        elif effective_exp < 0:
            n = _roundquotient(n, (10 ** (-effective_exp)))
        self.n = n

    def _parse_float_value(self, value: str, p: int) -> None:
        """Parse a float value into the FixedPoint representation.

        Args:
            value (float): The float representation of the number.
            p (int): The precision to use for the FixedPoint representation.

        Raises:
            OverflowError: If the float value is too large to be represented
                exactly as a :class:`FixedPoint`.
        """
        # XXX ignoring infinities and NaNs and overflows for now
        import math

        f, e = math.frexp(abs(value))
        if f != 0 and not (0.5 <= f < 1.0):
            raise OverflowError(f"FixedPoint can't represent {value!r} exactly.")
        # |value| = f * 2 ** e exactly

        # Suck up chunk bits at a time; 28 is enough so that we suck
        # up all bits in 2 iterations for all known binary double-
        # precision formats, and small enough to fit in an int.
        chunk = 28
        top = 0
        # invariant: |value| = (top + f) * 2 ** e exactly
        while f:
            f = math.ldexp(f, chunk)
            digit = int(f)
            if digit >> chunk != 0:
                raise OverflowError(f"FixedPoint can't represent {value!r} exactly.")
            top = (top << chunk) | digit
            f = f - digit
            if not (0.0 <= f < 1.0):
                raise OverflowError(f"FixedPoint can't represent {value!r} exactly.")
            e = e - chunk

        # now |value| = top * 2 ** e exactly
        # want n such that n / 10 ** p = top * 2 ** e, or
        # n = top * 10 ** p * 2 ** e
        top = top * (10**p)
        n = top << e if e >= 0 else _roundquotient(top, 1 << -e)
        if value < 0:
            n = -n
        self.n = n

    def get_precision(self) -> int:
        """Return the precision of this :class:`FixedPoint`.

        Note:
            The precision is the number of decimal digits carried after the
            decimal point, and is an int >= 0.

        Returns:
            int: The precision of this :class:`FixedPoint`.
        """
        return self.p

    def set_precision(self, precision: int = DEFAULT_PRECISION) -> None:
        """Change the precision carried by this :class:`FixedPoint` to `precision`.

        Note:
            If `precision` is less than this :class:`FixedPoint`'s current
            precision, information may be lost to rounding.

        Args:
            precision (int, optional): must be an int >= 0, and defaults to
                ``DEFAULT_PRECISION``.

        """
        try:
            p = int(precision)
        except Exception as e:
            raise TypeError(f"precision not convertable to int: {precision!r}") from e
        if p < 0:
            raise ValueError(f"precision must be >= 0: {precision!r}")

        if p > self.p:
            self.n = self.n * (10 ** (p - self.p))
        elif p < self.p:
            self.n = _roundquotient(self.n, (10 ** (self.p - p)))
        self.p = p

    def __str__(self) -> str:
        """Return the string representation of this :class:`FixedPoint`.

        Returns:
            str: The string representation of this :class:`FixedPoint`.
        """
        n, p = self.n, self.p
        i, f = divmod(abs(n), (10**p))
        if p:
            frac = repr(f)[:-1]
            frac = "0" * (p - len(frac)) + frac
        else:
            frac = ""
        return "-"[: n < 0] + repr(i)[:-1] + "." + frac

    def __repr__(self) -> str:
        """Return the string representation of this :class:`FixedPoint`.

        Returns:
            str: The string representation of this :class:`FixedPoint`.
        """
        return f"FixedPoint{(str(self), self.p)!r}"

    def copy(self) -> FixedPoint:
        """Create a copy of the current :class:`FixedPoint`.

        Returns:
            FixedPoint: A copy of the current :class:`FixedPoint`.
        """
        return _make_fixed_point(self.n, self.p)

    __copy__ = __deepcopy__ = copy

    def __cmp__(self, other: str | float | FixedPoint) -> int:
        """Compare this :class:`FixedPoint` with `other`.

        Args:
            other (str | float | FixedPoint): The other number to compare with.

        Returns:
            int: -1 if this :class:`FixedPoint` is less than `other`, 0 if they are
                equal, 1 if this :class:`FixedPoint` is greater than `other`.
        """
        if other is None:
            return 1
        xn, yn, p = _norm(self, other)
        return cmp(xn, yn)

    def __hash__(self) -> int:
        """Return the hash of this :class:`FixedPoint`.

        Returns:
            int: The hash of this :class:`FixedPoint`.
        """
        # caution!  == values must have equal hashes, and a FixedPoint
        # is essentially a rational in unnormalized form.  There's
        # really no choice here but to normalize it, so hash is
        # potentially expensive.
        n, p = self.__reduce()

        # Obscurity: if the value is an exact integer, p will be 0 now,
        # so the hash expression reduces to hash(n).  So FixedPoints
        # that happen to be exact integers hash to the same things as
        # their int or long equivalents.  This is Good.  But if a
        # FixedPoint happens to have a value exactly representable as
        # a float, their hashes may differ.  This is a teensy bit Bad.
        return hash(n) ^ hash(p)

    def __bool__(self) -> bool:
        """Return whether this :class:`FixedPoint` is non-zero.

        Returns:
            bool: ``True`` if this :class:`FixedPoint` is non-zero, ``False`` otherwise.
        """
        return self.n != 0

    def __neg__(self) -> FixedPoint:
        """Return the negation of this :class:`FixedPoint`.

        Returns:
            FixedPoint: The negation of this :class:`FixedPoint`.
        """
        return _make_fixed_point(-self.n, self.p)

    def __abs__(self) -> FixedPoint:
        """Return the absolute value of this :class:`FixedPoint`.

        Returns:
            FixedPoint: The absolute value of this :class:`FixedPoint`.
        """
        if self.n >= 0:
            return self.copy()
        return -self

    def __add__(self, other: str | float | FixedPoint) -> FixedPoint:
        """Return the addition of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to add with.

        Returns:
            FixedPoint: The addition of this :class:`FixedPoint`.
        """
        n1, n2, p = _norm(self, other)
        # n1/10 ** p + n2 / 10 ** p = (n1+n2) / 10 ** p
        return _make_fixed_point(n1 + n2, p)

    __radd__ = __add__
    """Return the addition of this :class:`FixedPoint` and `other`."""

    def __sub__(self, other: str | float | FixedPoint) -> FixedPoint:
        """Return the subtraction of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to subtract with.

        Returns:
            FixedPoint: The subtraction of this :class:`FixedPoint`.
        """
        if not isinstance(other, FixedPoint):
            other = FixedPoint(other, self.p)
        return self.__add__(-other)

    def __rsub__(self, other: str | float | FixedPoint) -> FixedPoint:
        """Return the subtraction of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to subtract with.

        Returns:
            FixedPoint: The subtraction of this :class:`FixedPoint`.
        """
        return (-self) + other

    def __mul__(self, other: str | float | FixedPoint) -> FixedPoint:
        """Return the multiplication of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to multiply with.

        Returns:
            FixedPoint: The multiplication of this :class:`FixedPoint`.
        """
        n1, n2, p = _norm(self, other)
        # n1/10 ** p * n2 / 10 ** p = (n1 * n2 / 10 ** p) / 10 ** p
        return _make_fixed_point(_roundquotient(n1 * n2, (10**p)), p)

    __rmul__ = __mul__

    def __div__(self, other: str | float | FixedPoint) -> FixedPoint:
        """Return the division of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to divide with.

        Returns:
            FixedPoint: The division of this :class:`FixedPoint`.
        """
        n1, n2, p = _norm(self, other)
        if n2 == 0:
            raise ZeroDivisionError("FixedPoint division")
        if n2 < 0:
            n1, n2 = -n1, -n2
        # n1 / 10 ** p / (n2 / 10 ** p) = n1/n2 = (n1 * 10 ** p / n2) / 10 ** p
        return _make_fixed_point(_roundquotient(n1 * (10**p), n2), p)

    def __rdiv__(self, other: str | float | FixedPoint) -> FixedPoint:
        """Return the division of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to divide with.

        Returns:
            FixedPoint: The division of this :class:`FixedPoint`.
        """
        _, n2, p = _norm(self, other)
        return _make_fixed_point(n2, p) / self

    def __divmod__(self, other: str | float | FixedPoint) -> tuple[int, FixedPoint]:
        """Return the quotient and remainder of this :class:`FixedPoint` and `other`.

        Args:
            other (FixedPoint): The other number to divide with.

        Returns:
            tuple[int, FixedPoint]: The quotient and remainder of this
                :class:`FixedPoint`.
        """
        n1, n2, p = _norm(self, other)
        if n2 == 0:
            raise ZeroDivisionError("FixedPoint modulo")
        # floor((n1 / 10 ** p) / (n2 * 10 ** p)) = floor(n1/n2)
        q = n1 / n2
        # n1/10 ** p - q * n2 / 10 ** p = (n1 - q * n2) / 10 ** p
        return q, _make_fixed_point(n1 - q * n2, p)

    def __rdivmod__(self, other: float | FixedPoint) -> tuple[int, FixedPoint]:
        """Return the quotient and remainder of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to divide with.

        Returns:
            tuple[int, FixedPoint]: The quotient and remainder of this
                :class:`FixedPoint`.
        """
        _, n2, p = _norm(self, other)
        return divmod(_make_fixed_point(n2, p), self)

    def __mod__(self, other: float | FixedPoint) -> FixedPoint:
        """Return the modulo of this :class:`FixedPoint` and `other`.

        Args:
            other (float | FixedPoint): The other number to modulo with.

        Returns:
            FixedPoint: The modulo of this :class:`FixedPoint`.
        """
        return self.__divmod__(other)[1]

    def __rmod__(self, other: str | float | FixedPoint) -> FixedPoint:
        """Return the modulo of this :class:`FixedPoint` and `other`.

        Args:
            other (str | float | FixedPoint): The other number to modulo with.

        Returns:
            FixedPoint: The modulo of this :class:`FixedPoint`.
        """
        _, n2, p = _norm(self, other)
        return _make_fixed_point(n2, p).__mod__(self)

    def __float__(self) -> float:
        """Return the float portion of this :class:`FixedPoint`.

        Returns:
            float: The float portion of this :class:`FixedPoint`.
        """
        # caution! float can lose precision
        n, p = self.__reduce()
        return float(n) / float(10**p)

    def __long__(self) -> int:
        """Return the long portion of this :class:`FixedPoint`.

        Returns:
            int: The long portion of this :class:`FixedPoint.
        """
        # XXX should this round instead?
        # XXX note e.g. long(-1.9) == -1L and long(1.9) == 1L in Python
        # XXX note that __int__ inherits whatever __long__ does,
        # XXX and .frac() is affected too
        answer = abs(self.n) / (10**self.p)
        if self.n < 0:
            answer = -answer
        return answer

    def __int__(self) -> int:
        """Return the integer portion of this :class:`FixedPoint`.

        Returns:
            int: The integer portion of this :class:`FixedPoint`.
        """
        return int(self.__long__())

    def frac(self) -> FixedPoint:
        """Return fractional portion as a :class:`FixedPoint`.

        Note:
            In :class:`FixedPoint`, this equality holds true::

                x = x.frac() + long(x)

        Returns:
            FixedPoint: The fractional portion of this :class:`FixedPoint`.
        """
        return self - int(self)

    def __reduce(self) -> tuple[int, int]:
        """Return numerator and precision.

        Returns:
            tuple[int, int]: A tuple containing the numerator and precision.
        """
        n, p = self.n, self.p
        if n == 0:
            p = 0
        while p and n % 10 == 0:
            p = p - 1
            n = n / 10
        return n, p


def _norm(x: FixedPoint, y: str | float | FixedPoint) -> tuple[int, int, int]:
    """Normalize x and y to the same precision.

    Note that this is called a lot, so default-arg tricks are helpful.


    Args:
        x (FixedPoint): The first FixedPoint to normalize.
        y (float | str | FixedPoint): The second FixedPoint or number to
            normalize with.

    Returns:
        tuple[int, int, int]: A tuple containing the normalized numerator of
            `x`, the normalized numerator of `y`, and the common precision.
    """
    if not isinstance(x, FixedPoint):
        raise TypeError(f"first arg must be a FixedPoint: {x!r}")
    if not isinstance(y, FixedPoint):
        y = FixedPoint(y, x.p)
    xn, yn = x.n, y.n
    xp, yp = x.p, y.p
    if xp > yp:
        yn = yn * (10 ** (xp - yp))
        p = xp
    elif xp < yp:
        xn = xn * (10 ** (yp - xp))
        p = yp
    else:
        p = xp  # same as yp
    return xn, yn, p


def _make_fixed_point(n: int, p: int) -> FixedPoint:
    """Return a new :class:`FixedPoint` with the same value as `n` and `p`.

    Args:
        n (int): The numerator.
        p (int): The precision.

    Returns:
        FixedPoint: A new :class:`FixedPoint` with the same value as `n` and `p`.
    """
    f = FixedPoint()
    f.n = n
    f.p = p
    return f


# divide x by y, rounding to int via nearest-even
# y must be > 0
# XXX which rounding modes are useful?


def cmp(a: int, b: int) -> int:
    """Reimplemented Python 2.x cmp function.

    Args:
        a (int | float): A number.
        b (int | float): A number.

    Return:
        int: -1 or 1.
    """
    return (a > b) - (a < b)


def _roundquotient(x: int, y: int) -> int:
    """Divide x by y, rounding to int via nearest-even.

    Args:
        x (int): The numerator.
        y (int): The denominator, must be > 0.

    Returns:
        int: The rounded quotient of x and y.
    """
    if y <= 0:
        raise ValueError("y must be > 0")
    n, leftover = divmod(x, y)
    c = cmp(leftover << 1, y)
    # c < 0 <-> leftover < y/2, etc
    if c > 0 or (c == 0 and (n & 1) == 1):
        n = n + 1
    return n


# crud for parsing strings

# There's an optional sign at the start, and an optional exponent
# at the end.  The exponent has an optional sign and at least one
# digit.  In between, must have either at least one digit followed
# by an optional fraction, or a decimal point followed by at least
# one digit.  Yuck.

_parser = re.compile(
    r"""
    \s*
    (?P<sign>[-+])?
    (
        (?P<int>\d+) (\. (?P<frac>\d*))?
    |
        \. (?P<onlyfrac>\d+)
    )
    ([eE](?P<exp>[-+]? \d+))?
    \s* $
""",
    re.VERBOSE,
).match

del re


def _string2exact(s: str) -> tuple[int, int]:
    """Convert a string to an exact integer and exponent.

    Args:
        s (str): The string to convert.

    Returns:
        tupel[int, int]: A tuple containing the integer part and the exponent.
    """
    m = _parser(s)
    if m is None:
        raise ValueError(f"can't parse as number: {s!r}")

    exp = m.group("exp")
    exp = 0 if exp is None else int(exp)

    intpart = m.group("int")
    if intpart is None:
        intpart = "0"
        fracpart = m.group("onlyfrac")
    else:
        fracpart = m.group("frac")
        if fracpart is None or fracpart == "":
            fracpart = "0"

    if intpart is None:
        raise ValueError("intpart should not be None!")

    if fracpart is None:
        raise ValueError("fracpart should not be None!")

    i, f = int(intpart), int(fracpart)
    nfrac = len(fracpart)
    i = i * (10**nfrac) + f
    exp = exp - nfrac

    if m.group("sign") == "-":
        i = -i

    return i, exp


def get_all_keyboard_focusable_children(parent: wx.Control) -> list[wx.Control]:
    """Get all keyboard focusable children of parent.

    Args:
        parent (wx.Control): The parent control to search for children.

    Returns:
        list[wx.Control]: A list of all keyboard focusable children of the
            parent.
    """
    children = []
    try:
        iter(parent.Children)
        # Under Mac OS X panels may have non-iterator children
    except Exception:
        pass
    else:
        for child in parent.Children:
            if child.Enabled and child.IsShownOnScreen():
                if child.AcceptsFocusFromKeyboard() and (
                    not isinstance(child, wx.RadioButton) or child.Value
                ):
                    children.append(child)
                if child.Children:
                    children.extend(get_all_keyboard_focusable_children(child))
    return children


def focus_next_keyboard_focusable_control(control: wx.Control) -> None:
    """Focus the next control in tab order that can gain focus.

    If the shift key is held down, tab order is reversed.

    Args:
        control (wx.Control): The control to start from.
    """
    # Find the last panel in the hierarchy of parents
    parent = control.Parent
    focusparent = None
    while parent:
        if isinstance(parent, (wx.Panel, wx.PyPanel)):
            focusparent = parent
        parent = parent.Parent
    if not focusparent:
        return
    children = get_all_keyboard_focusable_children(focusparent)
    if wx.GetKeyState(wx.WXK_SHIFT):
        children = list(reversed(children))
    for i, child in enumerate(children):
        if child is not control:
            continue
        for next_child in children[i + 1 :] + children[:i]:
            if next_child is not child.Parent:
                next_child.SetFocus()
                break
        break
