# Copyright (c) 2021-2022, NVIDIA CORPORATION.  All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#

__all__ = ['_make_registry']

from typing import Callable, Sequence


class _Factory:
    def __init__(self, factory: Callable, factory_id: str = None):
        self.__factory = factory
        self.__factory_id = factory_id if factory_id else factory.__class__.__name__

    def __call__(self, *args, **kwargs):
        return self.__factory(*args, **kwargs)

    def __repr__(self) -> str:
        return f'<class {self.__class__.__name__} {self.__factory_id}>'

    @property
    def factory_id(self):
        return self.__factory_id


def _make_registry():
    def invoke_load_callback(callback, recorded_known, factories):
        for fac_object in factories:
            if fac_object not in recorded_known:
                recorded_known.add(fac_object)
                callback(fac_object, True)

    class _Registry():
        """Class to register and de-register viewport factories"""
        __g_registered_factories = []
        __g_registered_callbacks = {}

        @classmethod
        def add_notifier(cls, callback):
            # Invoke the callback for all currently registered scenes
            cls.__g_registered_callbacks[callback] = set()
            invoke_load_callback(callback, cls.__g_registered_callbacks[callback], cls.__g_registered_factories)

        @classmethod
        def remove_notifier(cls, callback):
            try:
                del cls.__g_registered_callbacks[callback]
            except KeyError:
                pass

        @classmethod
        def ordered_factories(cls, order: Sequence[str], append_unkown: bool = True, ignore_unknown: Sequence[str] = tuple()):
            ordered = []
            known = set()

            def find_factory(factory_id):
                known.add(factory_id)
                for fact_obj in cls.__g_registered_factories:
                    if fact_obj.factory_id == factory_id:
                        return fact_obj
                return None

            for factory_id in order:
                ordered.append((factory_id, find_factory(factory_id)))
            if append_unkown:
                for fact_obj in cls.__g_registered_factories:
                    if (fact_obj.factory_id not in known) and (fact_obj.factory_id not in ignore_unknown):
                        ordered.append((fact_obj.factory_id, fact_obj))
            return ordered

        def __init__(self, factory, factory_id: str = None):
            # Save the types for de-registration
            self.__factory = _Factory(factory, factory_id)
            # Record them in the global-list for notifiers registered later
            self.__g_registered_factories.append(self.__factory)
            # Invoke all callbacks for new items
            for callback, recorded_known in self.__g_registered_callbacks.items():
                invoke_load_callback(callback, recorded_known, [self.__factory])

        def destroy(self):
            fac_object = self.__factory
            self.__factory = None
            try:
                self.__g_registered_factories.remove(fac_object)
            except (ValueError, KeyError):
                pass
            for callback, recorded_known in self.__g_registered_callbacks.items():
                if fac_object in recorded_known:
                    recorded_known.remove(fac_object)
                    try:
                        callback(fac_object, False)
                    except Exception:
                        import carb
                        import traceback
                        carb.log_error(f"Error unloading {fac_object.factory_id} with {callback}. Traceback:\n{traceback.format_exc()}")

        def __del__(self):
            self.destroy()

    return _Registry
