import os
import re
import sys
import warnings
from collections.abc import Callable, Hashable, Iterator, Mapping
from typing import Any, Generic, TypeVar, Union

import wrapt

from napari.utils import misc
from napari.utils.translations import trans

_T = TypeVar('_T')
_K = TypeVar('_K', bound=Hashable)
_V = TypeVar('_V')


class ReadOnlyWrapper(wrapt.ObjectProxy):
    """
    Disable item and attribute setting with the exception of  ``__wrapped__``.
    """

    def __init__(self, wrapped: Any, exceptions: tuple[str, ...] = ()):
        super().__init__(wrapped)
        self._self_exceptions = exceptions

    def __setattr__(self, name: str, val: Any) -> None:
        if (
            name not in ('__wrapped__', '_self_exceptions')
            and name not in self._self_exceptions
        ):
            raise TypeError(
                trans._(
                    'cannot set attribute {name}',
                    deferred=True,
                    name=name,
                )
            )

        super().__setattr__(name, val)

    def __setitem__(self, name: str, val: Any) -> None:
        if name not in self._self_exceptions:
            raise TypeError(
                trans._('cannot set item {name}', deferred=True, name=name)
            )
        super().__setitem__(name, val)


_SUNDER = re.compile('^_[^_]')


class PublicOnlyProxy(wrapt.ObjectProxy, Generic[_T]):
    """Proxy to prevent private attribute and item access, recursively."""

    __wrapped__: _T

    @staticmethod
    def _is_private_attr(name: str) -> bool:
        return name.startswith('_') and not (
            name.startswith('__') and name.endswith('__')
        )

    @staticmethod
    def _private_attr_warning(name: str, typ: str) -> None:
        warnings.warn(
            trans._(
                "Private attribute access ('{typ}.{name}') in this context "
                '(e.g. inside a plugin widget or dock widget) is deprecated '
                'and will be unavailable in version 0.7.0',
                deferred=True,
                name=name,
                typ=typ,
            ),
            category=FutureWarning,
            stacklevel=3,
        )

        # This is code prepared for a moment where we want to block access to private attributes
        # raise AttributeError(
        #     trans._(
        #         "Private attribute set/access ('{typ}.{name}') not allowed in this context.",
        #         deferred=True,
        #         name=name,
        #         typ=typ,
        #     )
        # )

    @staticmethod
    def _is_called_from_napari() -> bool:
        """
        Check if the getter or setter is called from inner napari.
        """
        if hasattr(sys, '_getframe'):
            frame = sys._getframe(2)
            return frame.f_code.co_filename.startswith(misc.ROOT_DIR)
        return False

    def __getattr__(self, name: str) -> Any:
        if self._is_private_attr(name):
            # allow napari to access private attributes and get an non-proxy
            if self._is_called_from_napari():
                return super().__getattr__(name)

            typ = type(self.__wrapped__).__name__

            self._private_attr_warning(name, typ)
        with warnings.catch_warnings(record=True) as cx_manager:
            data = self.create(super().__getattr__(name))
        for warning in cx_manager:
            warnings.warn(warning.message, warning.category, stacklevel=2)

        return data

    def __setattr__(self, name: str, value: Any) -> None:
        if (
            os.environ.get('NAPARI_ENSURE_PLUGIN_MAIN_THREAD', '0')
            not in ('0', 'False')
        ) and not in_main_thread():
            raise RuntimeError(
                'Setting attributes on a napari object is only allowed from the main Qt thread.'
            )

        if self._is_private_attr(name):
            if self._is_called_from_napari():
                return super().__setattr__(name, value)

            typ = type(self.__wrapped__).__name__
            self._private_attr_warning(name, typ)

        if isinstance(value, PublicOnlyProxy):
            # if we want to set an attribute on a PublicOnlyProxy *and* the
            # value that we want to set is itself a PublicOnlyProxy, we unwrap
            # the value. This has two benefits:
            #
            # 1. Checking the attribute later will incur a significant
            # performance cost, because _is_called_from_napari() will be
            # checked on each attribute access and it involves inspecting the
            # calling frame, which is expensive.
            # 2. Certain equality checks fail when objects are
            # PublicOnlyProxies. Notably, equality checks fail when such
            # objects are included in a Qt data model. For example, plugins can
            # grab a layer from the viewer; this layer will be wrapped by the
            # PublicOnlyProxy, and then using this object to set the current
            # layer selection will not propagate the selection to the Viewer.
            # See https://github.com/napari/napari/issues/5767
            value = value.__wrapped__

        setattr(self.__wrapped__, name, value)
        return None

    def __getitem__(self, key: Any) -> Any:
        return self.create(super().__getitem__(key))

    def __repr__(self) -> str:
        return repr(self.__wrapped__)

    def __dir__(self) -> list[str]:
        return [x for x in dir(self.__wrapped__) if not _SUNDER.match(x)]

    @classmethod
    def create(cls, obj: Any) -> Union['PublicOnlyProxy', Any]:
        # restrict the scope of this proxy to napari objects
        if type(obj).__name__ == 'method':
            # If the given object is a method, we check the module *of the
            # object to which that method is bound*. Otherwise, the module of a
            # method is just builtins!
            mod = getattr(type(obj.__self__), '__module__', None) or ''
        else:
            # Otherwise, the module is of an object just given by the
            # __module__ attribute.
            mod = getattr(type(obj), '__module__', None) or ''
        if not mod.startswith('napari'):
            return obj
        if isinstance(obj, PublicOnlyProxy):
            return obj  # don't double-wrap
        if callable(obj):
            return CallablePublicOnlyProxy(obj)
        return PublicOnlyProxy(obj)


class CallablePublicOnlyProxy(PublicOnlyProxy[Callable]):
    def __call__(self, *args, **kwargs):  # type: ignore [no-untyped-def]
        # if a PublicOnlyProxy is callable, then when we call it we:
        # - unwrap the arguments, to avoid performance issues detailed in
        #   PublicOnlyProxy.__setattr__,
        # - call the unwrapped callable on the unwrapped arguments
        # - wrap the result in a PublicOnlyProxy
        args = tuple(
            arg.__wrapped__ if isinstance(arg, PublicOnlyProxy) else arg
            for arg in args
        )
        kwargs = {
            k: v.__wrapped__ if isinstance(v, PublicOnlyProxy) else v
            for k, v in kwargs.items()
        }
        return self.create(self.__wrapped__(*args, **kwargs))


class MappingProxy(Mapping[_K, _V]):
    def __init__(self, wrapped: Mapping[_K, _V]) -> None:
        """
        A read-only proxy for a mapping object.

        Parameters
        ----------
        wrapped : Mapping
            The mapping to wrap.
        """
        self._wrapped = wrapped

    def __getitem__(self, key: _K, /) -> _V:
        """
        Get an item from the wrapped mapping.

        Parameters
        ----------
        key : Any
            The key to get from the mapping.

        Returns
        -------
        value : Any
            The value associated with the key in the wrapped mapping.
        """
        return self._wrapped[key]

    def __len__(self) -> int:
        """
        Get the length of the wrapped mapping.

        Returns
        -------
        length : int
            The number of items in the wrapped mapping.
        """
        return len(self._wrapped)

    def __iter__(self) -> Iterator[_K]:
        """
        Get an iterator over the keys of the wrapped mapping.

        Returns
        -------
        iterator : Iterator
            An iterator over the keys of the wrapped mapping.
        """
        return iter(self._wrapped)

    def __contains__(self, item: Hashable) -> bool:
        """
        Check if the wrapped mapping contains a key.

        Parameters
        ----------
        item : Any
            The key to check for in the wrapped mapping.

        Returns
        -------
        contains : bool
            True if the key is in the wrapped mapping, False otherwise.
        """
        return item in self._wrapped


def in_main_thread_py() -> bool:
    """
    Check if caller is in main python thread.

    Returns
    -------
    thread_flag : bool
        True if we are in the main thread, False otherwise.
    """
    import threading

    return threading.current_thread() == threading.main_thread()


def _in_main_thread() -> bool:
    """
    General implementation of checking if we are in a proper thread.
    If Qt is available and Application is created then assign :py:func:`in_qt_main_thread` to `in_main_thread`.
    If Qt liba are not available then assign :py:func:`in_main_thread_py` to in_main_thread.
    IF Qt libs are available but there is no Application ti wil emmit warning and return result of in_main_thread_py.

    Returns
    -------
    thread_flag : bool
        True if we are in the main thread, False otherwise.
    """

    global in_main_thread
    try:
        from napari._qt.utils import in_qt_main_thread

        res = in_qt_main_thread()
        in_main_thread = in_qt_main_thread
    except ImportError:
        in_main_thread = in_main_thread_py
        return in_main_thread_py()
    except AttributeError:
        warnings.warn(
            'Qt libs are available but no QtApplication instance is created'
        )
        return in_main_thread_py()
    return res


in_main_thread = _in_main_thread
