"""
frozen (immutable) data structures to support MultiIndexing

These are used for:

- .names (FrozenList)
- .levels & .codes (FrozenNDArray)

"""

import warnings

import numpy as np

from pandas.util._decorators import deprecate_kwarg

from pandas.core.dtypes.cast import coerce_indexer_dtype

from pandas.core.base import PandasObject

from pandas.io.formats.printing import pprint_thing


class FrozenList(PandasObject, list):

    """
    Container that doesn't allow setting item *but*
    because it's technically non-hashable, will be used
    for lookups, appropriately, etc.
    """

    # Side note: This has to be of type list. Otherwise,
    #            it messes up PyTables type checks.

    def union(self, other):
        """
        Returns a FrozenList with other concatenated to the end of self.

        Parameters
        ----------
        other : array-like
            The array-like whose elements we are concatenating.

        Returns
        -------
        diff : FrozenList
            The collection difference between self and other.
        """
        if isinstance(other, tuple):
            other = list(other)
        return type(self)(super().__add__(other))

    def difference(self, other):
        """
        Returns a FrozenList with elements from other removed from self.

        Parameters
        ----------
        other : array-like
            The array-like whose elements we are removing self.

        Returns
        -------
        diff : FrozenList
            The collection difference between self and other.
        """
        other = set(other)
        temp = [x for x in self if x not in other]
        return type(self)(temp)

    # TODO: Consider deprecating these in favor of `union` (xref gh-15506)
    __add__ = __iadd__ = union

    # Python 2 compat
    def __getslice__(self, i, j):
        return self.__class__(super().__getslice__(i, j))

    def __getitem__(self, n):
        # Python 3 compat
        if isinstance(n, slice):
            return self.__class__(super().__getitem__(n))
        return super().__getitem__(n)

    def __radd__(self, other):
        if isinstance(other, tuple):
            other = list(other)
        return self.__class__(other + list(self))

    def __eq__(self, other):
        if isinstance(other, (tuple, FrozenList)):
            other = list(other)
        return super().__eq__(other)

    __req__ = __eq__

    def __mul__(self, other):
        return self.__class__(super().__mul__(other))

    __imul__ = __mul__

    def __reduce__(self):
        return self.__class__, (list(self),)

    def __hash__(self):
        return hash(tuple(self))

    def _disabled(self, *args, **kwargs):
        """This method will not function because object is immutable."""
        raise TypeError(
            "'%s' does not support mutable operations." % self.__class__.__name__
        )

    def __str__(self):
        return pprint_thing(self, quote_strings=True, escape_chars=("\t", "\r", "\n"))

    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, str(self))

    __setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
    pop = append = extend = remove = sort = insert = _disabled


class FrozenNDArray(PandasObject, np.ndarray):

    # no __array_finalize__ for now because no metadata
    def __new__(cls, data, dtype=None, copy=False):
        warnings.warn(
            "\nFrozenNDArray is deprecated and will be removed in a "
            "future version.\nPlease use `numpy.ndarray` instead.\n",
            FutureWarning,
            stacklevel=2,
        )

        if copy is None:
            copy = not isinstance(data, FrozenNDArray)
        res = np.array(data, dtype=dtype, copy=copy).view(cls)
        return res

    def _disabled(self, *args, **kwargs):
        """This method will not function because object is immutable."""
        raise TypeError("'%s' does not support mutable operations." % self.__class__)

    __setitem__ = __setslice__ = __delitem__ = __delslice__ = _disabled
    put = itemset = fill = _disabled

    def _shallow_copy(self):
        return self.view()

    def values(self):
        """returns *copy* of underlying array"""
        arr = self.view(np.ndarray).copy()
        return arr

    def __repr__(self):
        """
        Return a string representation for this object.
        """
        prepr = pprint_thing(self, escape_chars=("\t", "\r", "\n"), quote_strings=True)
        return "%s(%s, dtype='%s')" % (type(self).__name__, prepr, self.dtype)

    @deprecate_kwarg(old_arg_name="v", new_arg_name="value")
    def searchsorted(self, value, side="left", sorter=None):
        """
        Find indices to insert `value` so as to maintain order.

        For full documentation, see `numpy.searchsorted`

        See Also
        --------
        numpy.searchsorted : Equivalent function.
        """

        # We are much more performant if the searched
        # indexer is the same type as the array.
        #
        # This doesn't matter for int64, but DOES
        # matter for smaller int dtypes.
        #
        # xref: https://github.com/numpy/numpy/issues/5370
        try:
            value = self.dtype.type(value)
        except ValueError:
            pass

        return super().searchsorted(value, side=side, sorter=sorter)


def _ensure_frozen(array_like, categories, copy=False):
    array_like = coerce_indexer_dtype(array_like, categories)
    array_like = array_like.view(FrozenNDArray)
    if copy:
        array_like = array_like.copy()
    return array_like
