from __future__ import annotations

from typing import TYPE_CHECKING
import weakref

from pandas.util._decorators import set_module

if TYPE_CHECKING:
    from pandas.core.generic import NDFrame


@set_module("pandas")
class Flags:
    """
    Flags that apply to pandas objects.

    “Flags” differ from “metadata”. Flags reflect properties of the pandas
    object (the Series or DataFrame). Metadata refer to properties of the
    dataset, and should be stored in DataFrame.attrs.

    Parameters
    ----------
    obj : Series or DataFrame
        The object these flags are associated with.
    allows_duplicate_labels : bool, default True
        Whether to allow duplicate labels in this object. By default,
        duplicate labels are permitted. Setting this to ``False`` will
        cause an :class:`errors.DuplicateLabelError` to be raised when
        `index` (or columns for DataFrame) is not unique, or any
        subsequent operation on introduces duplicates.
        See :ref:`duplicates.disallow` for more.

        .. warning::

           This is an experimental feature. Currently, many methods fail to
           propagate the ``allows_duplicate_labels`` value. In future versions
           it is expected that every method taking or returning one or more
           DataFrame or Series objects will propagate ``allows_duplicate_labels``.

    See Also
    --------
    DataFrame.attrs : Dictionary of global attributes of this dataset.
    Series.attrs : Dictionary of global attributes of this dataset.

    Examples
    --------
    Attributes can be set in two ways:

    >>> df = pd.DataFrame()
    >>> df.flags
    <Flags(allows_duplicate_labels=True)>
    >>> df.flags.allows_duplicate_labels = False
    >>> df.flags
    <Flags(allows_duplicate_labels=False)>

    >>> df.flags["allows_duplicate_labels"] = True
    >>> df.flags
    <Flags(allows_duplicate_labels=True)>
    """

    _keys: set[str] = {"allows_duplicate_labels"}

    def __init__(self, obj: NDFrame, *, allows_duplicate_labels: bool) -> None:
        self._allows_duplicate_labels = allows_duplicate_labels
        self._obj = weakref.ref(obj)

    @property
    def allows_duplicate_labels(self) -> bool:
        """
        Whether this object allows duplicate labels.

        Setting ``allows_duplicate_labels=False`` ensures that the
        index (and columns of a DataFrame) are unique. Most methods
        that accept and return a Series or DataFrame will propagate
        the value of ``allows_duplicate_labels``.

        See :ref:`duplicates` for more.

        See Also
        --------
        DataFrame.attrs : Set global metadata on this object.
        DataFrame.set_flags : Set global flags on this object.

        Examples
        --------
        >>> df = pd.DataFrame({"A": [1, 2]}, index=["a", "a"])
        >>> df.flags.allows_duplicate_labels
        True
        >>> df.flags.allows_duplicate_labels = False
        Traceback (most recent call last):
            ...
        pandas.errors.DuplicateLabelError: Index has duplicates.
              positions
        label
        a        [0, 1]
        """
        return self._allows_duplicate_labels

    @allows_duplicate_labels.setter
    def allows_duplicate_labels(self, value: bool) -> None:
        value = bool(value)
        obj = self._obj()
        if obj is None:
            raise ValueError("This flag's object has been deleted.")

        if not value:
            for ax in obj.axes:
                ax._maybe_check_unique()

        self._allows_duplicate_labels = value

    def __getitem__(self, key: str):
        if key not in self._keys:
            raise KeyError(key)

        return getattr(self, key)

    def __setitem__(self, key: str, value) -> None:
        if key not in self._keys:
            raise ValueError(f"Unknown flag {key}. Must be one of {self._keys}")
        setattr(self, key, value)

    def __repr__(self) -> str:
        return f"<Flags(allows_duplicate_labels={self.allows_duplicate_labels})>"

    def __eq__(self, other: object) -> bool:
        if isinstance(other, type(self)):
            return self.allows_duplicate_labels == other.allows_duplicate_labels
        return False
