#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#

from __future__ import annotations

from typing import Any, Callable, ClassVar, Optional

from cproton import addressof, isnull, pn_incref, pn_decref, \
    pn_record_get_py

from ._exceptions import ProtonException


class Wrapper:
    """ Wrapper for python objects that need to be stored in event contexts and be retrieved again from them
        Quick note on how this works:
        The actual *python* object has only 2 attributes which redirect into the wrapped C objects:
        _impl   The wrapped C object itself
        _attrs  This is a special pn_record_t holding a PYCTX which is a python dict
                every attribute in the python object is actually looked up here

        Because the objects actual attributes are stored away they must be initialised *after* the wrapping. This is
        achieved by using the __new__ method of Wrapper to create the object with the actiual attributes before the
        __init__ method is called.

        In the case where an existing object is being wrapped, the __init__ method is called with the existing object
        but should not initialise the object. This is because the object is already initialised and the attributes
        are already set. Use the Uninitialised method to check if the object is already initialised.
    """

    constructor: ClassVar[Optional[Callable[[], Any]]] = None
    get_context: ClassVar[Callable[[Any], Any]]

    __slots__ = ["_impl", "_attrs"]

    @classmethod
    def wrap(cls, impl: Any) -> Optional[Wrapper]:
        if isnull(impl):
            return None
        else:
            return cls(impl)

    def __new__(cls, impl: Any = None) -> Wrapper:
        attrs = None
        try:
            if impl is None:
                # we are constructing a new object
                assert cls.constructor
                impl = cls.constructor()
                if impl is None:
                    raise ProtonException(
                        "Wrapper failed to create wrapped object. Check for file descriptor or memory exhaustion.")
            else:
                # we are wrapping an existing object
                pn_incref(impl)

            assert cls.get_context
            record = cls.get_context(impl)
            attrs = pn_record_get_py(record)
        finally:
            self = super().__new__(cls)
            self._impl = impl
            self._attrs = attrs
            return self

    def Uninitialized(self) -> bool:
        return self._attrs == {}

    def __getattr__(self, name: str) -> Any:
        attrs = self._attrs
        if attrs and name in attrs:
            return attrs[name]
        else:
            raise AttributeError(name + " not in _attrs")

    def __setattr__(self, name: str, value: Any) -> None:
        if hasattr(self.__class__, name):
            object.__setattr__(self, name, value)
        else:
            attrs = self._attrs
            if attrs is not None:
                attrs[name] = value

    def __delattr__(self, name: str) -> None:
        attrs = self._attrs
        if attrs:
            del attrs[name]

    def __hash__(self) -> int:
        return hash(addressof(self._impl))

    def __eq__(self, other: Any) -> bool:
        if isinstance(other, Wrapper):
            return addressof(self._impl) == addressof(other._impl)
        return False

    def __ne__(self, other: Any) -> bool:
        if isinstance(other, Wrapper):
            return addressof(self._impl) != addressof(other._impl)
        return True

    def __del__(self) -> None:
        pn_decref(self._impl)

    def __repr__(self) -> str:
        return '<%s.%s 0x%x ~ 0x%x>' % (self.__class__.__module__,
                                        self.__class__.__name__,
                                        id(self), addressof(self._impl))
