# 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.
"""Utilities shared across TVM IR packages."""

from typing import TypeVar

T = TypeVar("T")


def derived_object(cls: type[T]) -> type[T]:
    """A decorator to register derived subclasses for TVM objects.

    Parameters
    ----------
    cls : type
        The derived class to be registered.

    Returns
    -------
    cls : type
        The decorated TVM object.

    Example
    -------
    .. code-block:: python

        @register_object("s_tir.meta_schedule.PyRunner")
        class _PyRunner(meta_schedule.Runner):
            def __init__(self, f_run: Callable = None):
                self.__init_handle_by_constructor__(_ffi_api.RunnerPyRunner, f_run)

        class PyRunner:
            _tvm_metadata = {
                "cls": _PyRunner,
                "methods": ["run"]
            }
            def run(self, runner_inputs):
                raise NotImplementedError

        @derived_object
        class LocalRunner(PyRunner):
            def run(self, runner_inputs):
                ...
    """

    import functools  # pylint: disable=import-outside-toplevel
    import weakref  # pylint: disable=import-outside-toplevel

    def _extract(inst: type, name: str):
        """Extract function from intrinsic class."""

        def method(*args, **kwargs):
            return getattr(inst, name)(*args, **kwargs)

        for inherit_cls, base_cls in zip(cls.__mro__, cls.__mro__[1:]):
            # extract functions that differ from the base class
            if not hasattr(base_cls, name):
                continue
            if getattr(base_cls, name) is getattr(inherit_cls, name):
                continue
            return method

        # for task scheduler return None means calling default function
        # otherwise it will trigger a RuntimeError of method not implemented
        # on the c++ side when you call the method
        return None

    assert isinstance(cls.__base__, type)
    if hasattr(cls, "_type") and cls._type == "TVMDerivedObject":  # type: ignore
        raise TypeError(
            f"Inheritance from a decorated object `{cls.__name__}` is not allowed. "
            f"Please inherit from `{cls.__name__}._cls`."
        )
    assert hasattr(cls, "_tvm_metadata"), (
        "Please use the user-facing method overriding class, i.e., PyRunner."
    )

    base = cls.__base__
    metadata = getattr(base, "_tvm_metadata")
    fields = metadata.get("fields", [])
    methods = metadata.get("methods", [])

    base_cls = metadata["cls"]
    slots = []
    if getattr(base_cls, "__dictoffset__", 0) == 0:
        slots.append("__dict__")
    if getattr(base_cls, "__weakrefoffset__", 0) == 0:
        slots.append("__weakref__")

    class TVMDerivedObject(base_cls):  # type: ignore
        """The derived object to avoid cyclic dependency."""

        __slots__ = tuple(slots)

        _cls = cls
        _type = "TVMDerivedObject"

        def __init__(self, *args, **kwargs):
            """Constructor."""
            self._inst = cls(*args, **kwargs)

            super().__init__(
                # the constructor's parameters, builder, runner, etc.
                *[getattr(self._inst, name) for name in fields],
                # the function methods, init_with_tune_context, build, run, etc.
                *[_extract(self._inst, name) for name in methods],
            )

            # for task scheduler hybrid funcs in c++ & python side
            # using weakref to avoid cyclic dependency
            self._inst._outer = weakref.ref(self)

        def __getattr__(self, name):
            import inspect  # pylint: disable=import-outside-toplevel

            try:
                # fall back to instance attribute if there is not any
                # return self._inst.__getattribute__(name)
                result = self._inst.__getattribute__(name)
            except AttributeError:
                result = super().__getattr__(name)

            if inspect.ismethod(result):

                def method(*args, **kwargs):
                    return result(*args, **kwargs)

                # set __own__ to aviod implicit deconstruction
                setattr(method, "__own__", self)
                return method

            return result

        def __setattr__(self, name, value):
            if name not in ["_inst", "key", "handle"]:
                self._inst.__setattr__(name, value)
            else:
                super().__setattr__(name, value)

    functools.update_wrapper(TVMDerivedObject.__init__, cls.__init__)  # type: ignore
    TVMDerivedObject.__name__ = cls.__name__
    TVMDerivedObject.__doc__ = cls.__doc__
    TVMDerivedObject.__module__ = cls.__module__
    for key, value in cls.__dict__.items():
        if isinstance(value, classmethod | staticmethod):
            setattr(TVMDerivedObject, key, value)
    return TVMDerivedObject
