"""
  ______ .______   .______   .______    __  .__   __.  _______
 /      ||   _  \  |   _  \  |   _  \  |  | |  \ |  | |       \
|  ,----'|  |_)  | |  |_)  | |  |_)  | |  | |   \|  | |  .--.  |
|  |     |   ___/  |   ___/  |   _  <  |  | |  . `  | |  |  |  |
|  `----.|  |      |  |      |  |_)  | |  | |  |\   | |  '--'  |
 \______|| _|      | _|      |______/  |__| |__| \__| |_______/

This file is generated by cppbind on 06/23/2021-10:23.
Please do not change it manually.
"""

import functools
import importlib
import inspect

__all__ = ['CppBindMetaclass', 'CppBindEnumMetaclass']


class CppBindBaseMetaclass(type):
    """
    Base class for CppBindMetaclass and CppBindEnumMetaclass.
    """

    def __instancecheck__(self, instance):
        """
        This is responsible for correct instance check for cppbind class.
        As for wrapper's an instance of pybind class is returned then we need this to return true for wrapper instances.
        """
        # get pybind's corresponding type
        pybind_class = _load_pybind_type(self.__name__, self.__module__)
        # check if wrapper instance is instance of pybind type
        return isinstance(instance, pybind_class)


class CppBindMetaclass(CppBindBaseMetaclass):
    """
    Metaclass for all cppbind generated classes. It's responsible for replacing pybind's methods/properties with
    corresponding cppbind's methods/properties.
    Overrides __new__ and __init__ to create an instance of pybind class instead of cppbind's and changes
    base types for the types which are extended from cppbind types.
    """

    def __new__(mcs, future_class_name, future_class_parents, future_class_attrs, *args, **kwargs):
        """
        For user extended types it replaces base types which are cppbind generated types with original pybind types
        and overwrites cppbind's metaclass with pybind's metaclass.
        """
        pybind_type = _load_pybind_type(future_class_name, future_class_attrs['__module__'])
        # set metaclass for extended types to pybind metaclass
        meta_class = None
        if pybind_type is None:
            parents = []
            for parent in future_class_parents:
                if isinstance(parent, CppBindMetaclass):
                    # get pybind's corresponding type
                    pybind_class = _load_pybind_type(parent.__name__, parent.__dict__['__module__'])
                    parents.append(pybind_class)
                    if meta_class is None:
                        # set pybind metaclass for non cppbind generated types
                        meta_class = type(pybind_class)
                else:
                    parents.append(parent)
            future_class_parents = tuple(parents)
        meta_class = meta_class or mcs
        return super(CppBindMetaclass, mcs).__new__(meta_class, future_class_name, future_class_parents,
                                                    future_class_attrs, **kwargs)

    def __init__(cls, future_class_name, future_class_parents, future_class_attrs):
        """
        Replaces pybind's method/property with cppbind's method/property and attaches pybind's method/property as an
        attribute to cppbind method. Also overwrites __new__ for cppbind types to create an instance of pybind class.
        """
        # get pybind's corresponding type
        pybind_class = _load_pybind_type(future_class_name, future_class_attrs['__module__'])
        if pybind_class:
            originals = pybind_class.__dict__.copy()

            for attr in pybind_class.__dict__:
                if attr in future_class_attrs and attr != '__new__':
                    # add original pybind method/property to cppbind method/property as an attribute
                    if hasattr(future_class_attrs[attr], 'fn'):
                        # method decorated with @bind
                        setattr(future_class_attrs[attr].fn.original_function, attr, originals[attr])
                    elif inspect.isfunction(future_class_attrs[attr]):
                        # not decorated method
                        setattr(future_class_attrs[attr], attr, originals[attr])
                    elif isinstance(future_class_attrs[attr], property):
                        # properties
                        bound_getter = future_class_attrs[attr].fget
                        if bound_getter:
                            setattr(bound_getter.fn.original_function, attr, originals[attr].fget)
                        bound_setter = future_class_attrs[attr].fset
                        if bound_setter:
                            setattr(bound_setter.fn.original_function, attr, originals[attr].fset)
                    elif isinstance(future_class_attrs[attr], classmethod):
                        # static functions
                        bound_method = future_class_attrs[attr].__func__
                        setattr(bound_method.fn.original_function, attr,
                                originals[attr].__get__(future_class_attrs[attr]))

                    # replace pybind method with cppbind method
                    setattr(pybind_class, attr, future_class_attrs[attr])
            # there's no nesting in pybind using this to support nested types
            setattr(pybind_class, '__qualname__', future_class_attrs['__qualname__'])
            # overwrite __new__ to return create a pybind instance instead of cppbind instance
            setattr(cls, '__new__', functools.partial(_new_object, pybind_class))
        super(CppBindMetaclass, cls).__init__(future_class_name, future_class_parents, future_class_attrs)


class CppBindEnumMetaclass(CppBindBaseMetaclass):
    """
    Metaclass for all cppbind generated enums. It's responsible for adding user defined attributes to pybind enum type.
    Also overwrites cppbind enum's __new__ to create a pybind enum.
    """

    def __init__(cls, future_class_name, future_class_parents, future_class_attrs):
        """
        Adds cppbind generated enum attributes to corresponding pybind enum.
        """
        # get pybind's corresponding type
        pybind_enum = _load_pybind_type(future_class_name, future_class_attrs['__module__'])
        for attr in pybind_enum.__dict__:
            if attr in future_class_attrs and attr not in ('__doc__', '__qualname__', '__module__', '__new__'):
                # replace pybind method with wrapper method
                setattr(pybind_enum, attr, future_class_attrs[attr])
        # overwrite __new__ to create a pybind enum
        setattr(cls, '__new__', functools.partial(_new_object, pybind_enum))
        super(CppBindEnumMetaclass, cls).__init__(future_class_name, future_class_parents, future_class_attrs)


def _new_object(pybind_type, cls, *args, **kwargs):
    """Helper used to overwrite cppbind __new__ to create a pybind type."""
    return pybind_type(*args, **kwargs)


def _load_pybind_type(type_name, cppbind_module_name):
    """Loads and returns corresponding pybind module if it's available, otherwise None."""
    module = importlib.import_module(cppbind_module_name)
    file_name = cppbind_module_name.split('.')[-1]
    # cppbind adds an import of corresponding pybind module in cppbind module with convention = pybind_{cppbind_module_name}
    # then using this import loads pybind type
    pybind_module = getattr(module, f'pybind_{file_name}', None)
    if hasattr(pybind_module, type_name):
        return getattr(pybind_module, type_name)
    return None
