#
# Urwid basic widget classes
#    Copyright (C) 2004-2012  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


from __future__ import annotations

import decimal
import re
import typing
import warnings

from urwid import Edit

if typing.TYPE_CHECKING:
    from collections.abc import Container, Hashable

    _TagMarkup = typing.Union[str, tuple[Hashable, typing.Union[str]], list["_TagMarkup"]]


class NumEdit(Edit):
    """NumEdit - edit numerical types

    based on the characters in 'allowed' different numerical types
    can be edited:
      + regular int: 0123456789
      + regular float: 0123456789.
      + regular oct: 01234567
      + regular hex: 0123456789abcdef
    """

    ALLOWED = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    def __init__(
        self,
        allowed: Container[str],
        caption: _TagMarkup,
        default: str,
        trimLeadingZeros: bool | None = None,
        *,
        trim_leading_zeros: bool = True,
        allow_negative: bool = False,
    ):
        super().__init__(caption, default)
        self._allowed = allowed
        self._trim_leading_zeros = trim_leading_zeros
        self._allow_negative = allow_negative

        if trimLeadingZeros is not None:
            warnings.warn(
                "'trimLeadingZeros' argument is deprecated. Use 'trim_leading_zeros' keyword argument",
                DeprecationWarning,
                stacklevel=3,
            )
            self._trim_leading_zeros = trimLeadingZeros

    def valid_char(self, ch: str) -> bool:
        """
        Return true for allowed characters.
        """
        if len(ch) == 1:
            if ch.upper() in self._allowed:
                return True

            return self._allow_negative and ch == "-" and self.edit_pos == 0 and "-" not in self.edit_text
        return False

    def keypress(
        self,
        size: tuple[int],
        key: str,
    ) -> str | None:
        """
        Handle editing keystrokes.  Remove leading zeros.

        >>> e, size = NumEdit("0123456789", "", "5002"), (10,)
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> assert e.edit_text == "002"
        >>> e.keypress(size, "end")
        >>> assert e.edit_text == "2"
        >>> # binary only
        >>> e, size = NumEdit("01", "", ""), (10,)
        >>> assert e.edit_text == ""
        >>> e.keypress(size, "1")
        >>> e.keypress(size, "0")
        >>> e.keypress(size, "1")
        >>> assert e.edit_text == "101"
        >>> e, size = NumEdit("0123456789", "", "", allow_negative=True), (10,)
        >>> e.keypress(size, "-")
        >>> e.keypress(size, "1")
        >>> e.edit_text
        '-1'
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> e.edit_text
        '1'
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "-")
        '-'
        >>> e.edit_text
        '1'
        """
        unhandled = super().keypress(size, key)

        if not unhandled and self._trim_leading_zeros:
            # trim leading zeros
            while self.edit_pos > 0 and self.edit_text[:1] == "0":
                self.set_edit_pos(self.edit_pos - 1)
                self.set_edit_text(self.edit_text[1:])

        return unhandled


class IntegerEdit(NumEdit):
    """Edit widget for integer values"""

    def __init__(
        self,
        caption: _TagMarkup = "",
        default: int | str | decimal.Decimal | None = None,
        base: int = 10,
        *,
        allow_negative: bool = False,
    ) -> None:
        """
        caption -- caption markup
        default -- default edit value

        >>> IntegerEdit("", 42)
        <IntegerEdit selectable flow widget '42' edit_pos=2>
        >>> e, size = IntegerEdit("", "5002"), (10,)
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> assert e.edit_text == "002"
        >>> e.keypress(size, "end")
        >>> assert e.edit_text == "2"
        >>> e.keypress(size, "9")
        >>> e.keypress(size, "0")
        >>> assert e.edit_text == "290"
        >>> e, size = IntegerEdit("", ""), (10,)
        >>> assert e.value() is None
        >>> # binary
        >>> e, size = IntegerEdit("", "1010", base=2), (10,)
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "1")
        >>> assert e.edit_text == "10101"
        >>> assert e.value() == decimal.Decimal("21")
        >>> # HEX
        >>> e, size = IntegerEdit("", "10", base=16), (10,)
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "F")
        >>> e.keypress(size, "f")
        >>> assert e.edit_text == "10Ff"
        >>> assert e.keypress(size, "G") == "G"  # unhandled key
        >>> assert e.edit_text == "10Ff"
        >>> # keep leading 0's when not base 10
        >>> e, size = IntegerEdit("", "10FF", base=16), (10,)
        >>> assert e.edit_text == "10FF"
        >>> assert e.value() == decimal.Decimal("4351")
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> e.keypress(size, "0")
        >>> assert e.edit_text == "00FF"
        >>> # test exception on incompatible value for base
        >>> e, size = IntegerEdit("", "10FG", base=16), (10,)
        Traceback (most recent call last):
            ...
        ValueError: invalid value: 10FG for base 16
        >>> # test exception on float init value
        >>> e, size = IntegerEdit("", 10.0), (10,)
        Traceback (most recent call last):
            ...
        ValueError: default: Only 'str', 'int', 'long' or Decimal input allowed
        >>> e, size = IntegerEdit("", decimal.Decimal("10.0")), (10,)
        Traceback (most recent call last):
            ...
        ValueError: not an 'integer Decimal' instance
        """
        self.base = base
        val = ""
        allowed_chars = self.ALLOWED[: self.base]
        if default is not None:
            if not isinstance(default, (int, str, decimal.Decimal)):
                raise ValueError("default: Only 'str', 'int' or Decimal input allowed")

            # convert to a long first, this will raise a ValueError
            # in case a float is passed or some other error
            if isinstance(default, str) and len(default):
                # check if it is a valid initial value
                validation_re = f"^[{allowed_chars}]+$"
                if not re.match(validation_re, str(default), re.IGNORECASE):
                    raise ValueError(f"invalid value: {default} for base {base}")

            elif isinstance(default, decimal.Decimal) and default.as_tuple()[2] != 0:
                # a Decimal instance with no fractional part
                raise ValueError("not an 'integer Decimal' instance")

            # convert possible int, long or Decimal to str
            val = str(default)

        super().__init__(
            allowed_chars,
            caption,
            val,
            trim_leading_zeros=(self.base == 10),
            allow_negative=allow_negative,
        )

    def value(self) -> decimal.Decimal | None:
        """
        Return the numeric value of self.edit_text.

        >>> e, size = IntegerEdit(), (10,)
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> assert e.value() == 51
        """
        if self.edit_text:
            return decimal.Decimal(int(self.edit_text, self.base))

        return None

    def __int__(self) -> int:
        """Enforced int value return.

        >>> e, size = IntegerEdit(allow_negative=True), (10,)
        >>> assert int(e) == 0
        >>> e.keypress(size, "-")
        >>> e.keypress(size, "4")
        >>> e.keypress(size, "2")
        >>> assert int(e) == -42
        """
        if self.edit_text:
            return int(self.edit_text, self.base)
        return 0


class FloatEdit(NumEdit):
    """Edit widget for float values."""

    def __init__(
        self,
        caption: _TagMarkup = "",
        default: str | int | decimal.Decimal | None = None,
        preserveSignificance: bool | None = None,
        decimalSeparator: str | None = None,
        *,
        preserve_significance: bool = True,
        decimal_separator: str = ".",
        allow_negative: bool = False,
    ) -> None:
        """
        caption -- caption markup
        default -- default edit value
        preserve_significance -- return value has the same signif. as default
        decimal_separator -- use '.' as separator by default, optionally a ','

        >>> FloatEdit("", "1.065434")
        <FloatEdit selectable flow widget '1.065434' edit_pos=8>
        >>> e, size = FloatEdit("", "1.065434"), (10,)
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> assert e.edit_text == ".065434"
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "backspace")
        >>> assert e.edit_text == ".06543"
        >>> e, size = FloatEdit(), (10,)
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> e.keypress(size, ".")
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> assert e.value() == decimal.Decimal("51.51"), e.value()
        >>> e, size = FloatEdit(decimal_separator=":"), (10,)
        Traceback (most recent call last):
            ...
        ValueError: invalid decimal separator: :
        >>> e, size = FloatEdit(decimal_separator=","), (10,)
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> e.keypress(size, ",")
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> assert e.edit_text == "51,51"
        >>> e, size = FloatEdit("", "3.1415", preserve_significance=True), (10,)
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "backspace")
        >>> e.keypress(size, "backspace")
        >>> assert e.edit_text == "3.14"
        >>> assert e.value() == decimal.Decimal("3.1400")
        >>> e.keypress(size, "1")
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "9")
        >>> assert e.value() == decimal.Decimal("3.1416"), e.value()
        >>> e, size = FloatEdit("", ""), (10,)
        >>> assert e.value() is None
        >>> e, size = FloatEdit("", 10.0), (10,)
        Traceback (most recent call last):
            ...
        ValueError: default: Only 'str', 'int', 'long' or Decimal input allowed
        """
        self.significance = None
        self._decimal_separator = decimal_separator
        if decimalSeparator is not None:
            warnings.warn(
                "'decimalSeparator' argument is deprecated. Use 'decimal_separator' keyword argument",
                DeprecationWarning,
                stacklevel=3,
            )
            self._decimal_separator = decimalSeparator

        if self._decimal_separator not in {".", ","}:
            raise ValueError(f"invalid decimal separator: {self._decimal_separator}")

        if preserveSignificance is not None:
            warnings.warn(
                "'preserveSignificance' argument is deprecated. Use 'preserve_significance' keyword argument",
                DeprecationWarning,
                stacklevel=3,
            )
            preserve_significance = preserveSignificance

        val = ""
        if default is not None and default != "":  # noqa: PLC1901,RUF100
            if not isinstance(default, (int, str, decimal.Decimal)):
                raise ValueError("default: Only 'str', 'int' or Decimal input allowed")

            if isinstance(default, str) and default:
                # check if it is a float, raises a ValueError otherwise
                float(default)
                default = decimal.Decimal(default)

            if preserve_significance and isinstance(default, decimal.Decimal):
                self.significance = default

            val = str(default)

        super().__init__(self.ALLOWED[0:10] + self._decimal_separator, caption, val, allow_negative=allow_negative)

    def value(self) -> decimal.Decimal | None:
        """
        Return the numeric value of self.edit_text.
        """
        if self.edit_text:
            normalized = decimal.Decimal(self.edit_text.replace(self._decimal_separator, "."))
            if self.significance is not None:
                return normalized.quantize(self.significance)
            return normalized

        return None

    def __float__(self) -> float:
        """Enforced float value return.

        >>> e, size = FloatEdit(allow_negative=True), (10,)
        >>> assert float(e) == 0.0
        >>> e.keypress(size, "-")
        >>> e.keypress(size, "4")
        >>> e.keypress(size, ".")
        >>> e.keypress(size, "2")
        >>> assert float(e) == -4.2
        """
        if self.edit_text:
            return float(self.edit_text.replace(self._decimal_separator, "."))
        return 0.0
